mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-20 03:23:21 +08:00
feat: 添加 HTTP API 功能,支持状态获取、设置应用和重启操作
This commit is contained in:
+243
-1
@@ -2694,4 +2694,246 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-settings {
|
||||
.api-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.api-status-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.api-status-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.api-status-value {
|
||||
font-size: 15px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
|
||||
&.ok {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-endpoint-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.api-endpoint-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.api-endpoint-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
code {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-method {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 42px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #047857;
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid rgba(16, 185, 129, 0.35);
|
||||
}
|
||||
|
||||
.api-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.input-inline-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 9999px;
|
||||
background: var(--bg-primary);
|
||||
padding: 0 6px 0 14px;
|
||||
min-height: 40px;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 8px 0;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.inline-icon-btn {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-setting {
|
||||
padding: 14px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
|
||||
.switch-setting-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.switch-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.switch-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.api-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.api-doc-link-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.api-switch {
|
||||
width: 44px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.off {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
&.on {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.api-switch-thumb {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s ease;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&.on .api-switch-thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
}
|
||||
|
||||
+312
-2
@@ -10,16 +10,17 @@ import {
|
||||
Eye, EyeOff, Key, FolderSearch, FolderOpen, Search,
|
||||
RotateCcw, Trash2, Save, Plug, X, Check, Sun, Moon, Monitor,
|
||||
Palette, Database, ImageIcon, Download, HardDrive, Info, RefreshCw, Shield, Clock, CheckCircle, AlertCircle, FileText, Mic,
|
||||
Zap, Layers, User, Sparkles, Github, Fingerprint, Lock, ShieldCheck, Minus, Plus, Smile
|
||||
Zap, Layers, User, Sparkles, Github, Fingerprint, Lock, ShieldCheck, Minus, Plus, Smile, Network, Copy
|
||||
} from 'lucide-react'
|
||||
import { useAuthStore } from '../stores/authStore'
|
||||
import './SettingsPage.scss'
|
||||
|
||||
type SettingsTab = 'appearance' | 'database' | 'stt' | 'ai' | 'data' | 'security' | 'activation' | 'about'
|
||||
type SettingsTab = 'appearance' | 'database' | 'api' | 'stt' | 'ai' | 'data' | 'security' | 'activation' | 'about'
|
||||
|
||||
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
||||
{ id: 'appearance', label: '外观', icon: Palette },
|
||||
{ id: 'database', label: '数据解密', icon: Database },
|
||||
{ id: 'api', label: '开放接口', icon: Network },
|
||||
{ id: 'security', label: '安全设置', icon: Lock },
|
||||
{ id: 'stt', label: '语音转文字', icon: Mic },
|
||||
{ id: 'ai', label: 'AI 摘要', icon: Sparkles },
|
||||
@@ -128,6 +129,57 @@ function SettingsPage() {
|
||||
const [autoUpdateMinInterval, setAutoUpdateMinInterval] = useState(1000) // 最小更新间隔(毫秒)
|
||||
const [autoUpdateDebounceTime, setAutoUpdateDebounceTime] = useState(500) // 防抖时间(毫秒)
|
||||
|
||||
// HTTP API 配置
|
||||
const [httpApiEnabled, setHttpApiEnabled] = useState(false)
|
||||
const [httpApiPort, setHttpApiPort] = useState(5031)
|
||||
const [httpApiToken, setHttpApiToken] = useState('')
|
||||
const [showHttpApiToken, setShowHttpApiToken] = useState(false)
|
||||
const [httpApiStatus, setHttpApiStatus] = useState<{
|
||||
running: boolean
|
||||
host: string
|
||||
port: number
|
||||
enabled: boolean
|
||||
startedAt: string
|
||||
uptimeMs: number
|
||||
tokenConfigured: boolean
|
||||
tokenPreview: string
|
||||
baseUrl: string
|
||||
endpoints: Array<{ method: string; path: string; desc: string }>
|
||||
lastError: string
|
||||
} | null>(null)
|
||||
const [isSavingHttpApi, setIsSavingHttpApi] = useState(false)
|
||||
const [isRefreshingHttpApi, setIsRefreshingHttpApi] = useState(false)
|
||||
const [nowTs, setNowTs] = useState(Date.now())
|
||||
|
||||
const copyText = async (text: string, successText: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showMessage(successText, true)
|
||||
} catch (e) {
|
||||
showMessage('复制失败,请手动复制', false)
|
||||
}
|
||||
}
|
||||
|
||||
const createRandomToken = () => {
|
||||
const randomPart = Math.random().toString(36).slice(2)
|
||||
const randomPart2 = Math.random().toString(36).slice(2)
|
||||
return `ct_${Date.now().toString(36)}_${randomPart}${randomPart2}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'api' || !httpApiStatus?.running) {
|
||||
return
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
setNowTs(Date.now())
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [activeTab, httpApiStatus?.running])
|
||||
|
||||
// AI 相关配置状态
|
||||
const [aiProvider, setAiProviderState] = useState('zhipu')
|
||||
const [aiApiKey, setAiApiKeyState] = useState('')
|
||||
@@ -190,9 +242,15 @@ function SettingsPage() {
|
||||
const savedCheckInterval = await configService.getAutoUpdateCheckInterval()
|
||||
const savedMinInterval = await configService.getAutoUpdateMinInterval()
|
||||
const savedDebounceTime = await configService.getAutoUpdateDebounceTime()
|
||||
const savedHttpApiEnabled = await configService.getHttpApiEnabled()
|
||||
const savedHttpApiPort = await configService.getHttpApiPort()
|
||||
const savedHttpApiToken = await configService.getHttpApiToken()
|
||||
setAutoUpdateCheckInterval(savedCheckInterval)
|
||||
setAutoUpdateMinInterval(savedMinInterval)
|
||||
setAutoUpdateDebounceTime(savedDebounceTime)
|
||||
setHttpApiEnabled(savedHttpApiEnabled)
|
||||
setHttpApiPort(savedHttpApiPort)
|
||||
setHttpApiToken(savedHttpApiToken)
|
||||
|
||||
const savedQuoteStyle = await configService.getQuoteStyle()
|
||||
setQuoteStyle(savedQuoteStyle)
|
||||
@@ -223,6 +281,11 @@ function SettingsPage() {
|
||||
setAiCustomSystemPromptState(savedAiCustomSystemPrompt)
|
||||
setAiEnableThinkingState(savedAiEnableThinking)
|
||||
setAiMessageLimitState(savedAiMessageLimit)
|
||||
|
||||
const statusResult = await window.electronAPI.httpApi.getStatus()
|
||||
if (statusResult.success && statusResult.status) {
|
||||
setHttpApiStatus(statusResult.status)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载配置失败:', e)
|
||||
}
|
||||
@@ -393,6 +456,67 @@ function SettingsPage() {
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
}
|
||||
|
||||
const refreshHttpApiStatus = async () => {
|
||||
setIsRefreshingHttpApi(true)
|
||||
try {
|
||||
const result = await window.electronAPI.httpApi.getStatus()
|
||||
if (result.success && result.status) {
|
||||
setHttpApiStatus(result.status)
|
||||
setHttpApiEnabled(result.status.enabled)
|
||||
setHttpApiPort(result.status.port)
|
||||
} else {
|
||||
showMessage(result.error || '获取接口状态失败', false)
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(`获取接口状态失败: ${e}`, false)
|
||||
} finally {
|
||||
setIsRefreshingHttpApi(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveHttpApiSettings = async () => {
|
||||
setIsSavingHttpApi(true)
|
||||
try {
|
||||
const result = await window.electronAPI.httpApi.applySettings({
|
||||
enabled: httpApiEnabled,
|
||||
port: httpApiPort,
|
||||
token: httpApiToken
|
||||
})
|
||||
|
||||
if (result.success && result.status) {
|
||||
setHttpApiStatus(result.status)
|
||||
setHttpApiPort(result.status.port)
|
||||
await configService.setHttpApiEnabled(httpApiEnabled)
|
||||
await configService.setHttpApiPort(result.status.port)
|
||||
await configService.setHttpApiToken(httpApiToken)
|
||||
showMessage('开放接口配置已保存并生效', true)
|
||||
} else {
|
||||
showMessage(result.error || '保存开放接口配置失败', false)
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(`保存开放接口配置失败: ${e}`, false)
|
||||
} finally {
|
||||
setIsSavingHttpApi(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestartHttpApi = async () => {
|
||||
setIsRefreshingHttpApi(true)
|
||||
try {
|
||||
const result = await window.electronAPI.httpApi.restart()
|
||||
if (result.success && result.status) {
|
||||
setHttpApiStatus(result.status)
|
||||
showMessage('接口服务已重启', true)
|
||||
} else {
|
||||
showMessage(result.error || '接口服务重启失败', false)
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(`接口服务重启失败: ${e}`, false)
|
||||
} finally {
|
||||
setIsRefreshingHttpApi(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearImages = () => {
|
||||
setShowClearDialog({
|
||||
type: 'images',
|
||||
@@ -1195,6 +1319,191 @@ function SettingsPage() {
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderApiTab = () => {
|
||||
const HTTP_API_DOC_URL = 'https://github.com/ILoveBingLu/miyu/blob/main/Docs/HTTP_API_%E5%BC%80%E5%8F%91%E8%A7%84%E5%88%92%E4%B8%8E%E6%8E%A5%E5%8F%A3%E6%A0%87%E5%87%86.md'
|
||||
const status = httpApiStatus
|
||||
const startedAtMs = status?.startedAt ? new Date(status.startedAt).getTime() : 0
|
||||
const uptime = status?.running && startedAtMs > 0
|
||||
? Math.max(0, nowTs - startedAtMs)
|
||||
: (status?.uptimeMs ?? 0)
|
||||
const uptimeText = uptime > 0
|
||||
? `${Math.floor(uptime / 1000)} 秒`
|
||||
: '0 秒'
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<section className="settings-section api-settings">
|
||||
<div className="api-title-row">
|
||||
<h3 className="section-title">开放接口(HTTP API)</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-icon-btn api-doc-link-btn"
|
||||
title="查看接口文档"
|
||||
aria-label="查看接口文档"
|
||||
onClick={() => window.electronAPI.shell.openExternal(HTTP_API_DOC_URL)}
|
||||
>
|
||||
<FileText size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="section-desc">用于给外部工具调用,默认仅监听本机地址 `127.0.0.1`。</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="switch-setting">
|
||||
<div className="switch-setting-main">
|
||||
<div>
|
||||
<div className="switch-title">启用 HTTP API</div>
|
||||
<div className="switch-desc">关闭后将停止监听端口,不再对外提供接口。</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`api-switch ${httpApiEnabled ? 'on' : 'off'}`}
|
||||
aria-label={httpApiEnabled ? '关闭 HTTP API' : '启用 HTTP API'}
|
||||
title={httpApiEnabled ? '点击关闭' : '点击启用'}
|
||||
onClick={() => setHttpApiEnabled((v) => !v)}
|
||||
>
|
||||
<span className="api-switch-thumb" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>监听端口</label>
|
||||
<span className="form-hint">建议保持默认 5031,范围 1-65535</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
value={httpApiPort}
|
||||
onChange={(e) => setHttpApiPort(Number(e.target.value || 5031))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>访问密钥(可选)</label>
|
||||
<span className="form-hint">令牌预设说明:留空表示不鉴权;设置后,`/v1/status` 等接口必须携带 `Authorization: Bearer <token>`。</span>
|
||||
<div className="input-inline-actions">
|
||||
<input
|
||||
type={showHttpApiToken ? 'text' : 'password'}
|
||||
value={httpApiToken}
|
||||
onChange={(e) => setHttpApiToken(e.target.value)}
|
||||
placeholder="留空表示不启用令牌鉴权"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-icon-btn"
|
||||
title={showHttpApiToken ? '隐藏密钥' : '显示密钥'}
|
||||
aria-label={showHttpApiToken ? '隐藏密钥' : '显示密钥'}
|
||||
onClick={() => setShowHttpApiToken((v) => !v)}
|
||||
>
|
||||
{showHttpApiToken ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-icon-btn"
|
||||
title="生成随机密钥"
|
||||
aria-label="生成随机密钥"
|
||||
onClick={() => setHttpApiToken(createRandomToken())}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="btn-row">
|
||||
<button className="btn btn-primary" onClick={handleSaveHttpApiSettings} disabled={isSavingHttpApi}>
|
||||
<Save size={16} /> {isSavingHttpApi ? '保存中...' : '保存并应用'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={refreshHttpApiStatus} disabled={isRefreshingHttpApi}>
|
||||
<RefreshCw size={16} className={isRefreshingHttpApi ? 'spin' : ''} /> 刷新状态
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleRestartHttpApi} disabled={isRefreshingHttpApi}>
|
||||
<RotateCcw size={16} /> 重启服务
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="divider" style={{ margin: '2rem 0', borderBottom: '1px solid var(--border-color)', opacity: 0.1 }} />
|
||||
|
||||
<section className="settings-section api-settings">
|
||||
<h3 className="section-title">接口状态与信息</h3>
|
||||
|
||||
{status ? (
|
||||
<>
|
||||
<div className="api-status-grid">
|
||||
<div className="api-status-card">
|
||||
<div className="api-status-label">运行状态</div>
|
||||
<div className={`api-status-value ${status.running ? 'ok' : 'error'}`}>{status.running ? '运行中' : '未运行'}</div>
|
||||
</div>
|
||||
<div className="api-status-card">
|
||||
<div className="api-status-label">监听地址</div>
|
||||
<div className="api-status-value mono">{status.host}:{status.port}</div>
|
||||
</div>
|
||||
<div className="api-status-card">
|
||||
<div className="api-status-label">运行时长</div>
|
||||
<div className="api-status-value">{uptimeText}</div>
|
||||
</div>
|
||||
<div className="api-status-card">
|
||||
<div className="api-status-label">鉴权状态</div>
|
||||
<div className="api-status-value">{status.tokenConfigured ? '已启用' : '未启用'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginTop: '16px' }}>
|
||||
<label>基础地址</label>
|
||||
<div className="input-inline-actions">
|
||||
<input type="text" value={status.baseUrl} readOnly />
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-icon-btn"
|
||||
title="复制基础地址"
|
||||
aria-label="复制基础地址"
|
||||
onClick={() => copyText(status.baseUrl, '基础地址已复制')}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>令牌预览</label>
|
||||
<div className="input-inline-actions">
|
||||
<input type="text" value={status.tokenConfigured ? status.tokenPreview : '未配置'} readOnly />
|
||||
<div className="inline-actions">
|
||||
{status.tokenConfigured && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-icon-btn"
|
||||
title="复制访问密钥"
|
||||
aria-label="复制访问密钥"
|
||||
onClick={() => copyText(httpApiToken, '访问密钥已复制')}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status.lastError && (
|
||||
<div className="unavailable-notice" style={{ background: 'var(--bg-secondary)', border: '1px solid var(--danger)' }}>
|
||||
<AlertCircle size={16} />
|
||||
<p>最近错误:{status.lastError}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>尚未读取到接口状态,请点击“刷新状态”。</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [isGettingImageKey, setIsGettingImageKey] = useState(false)
|
||||
const [imageKeyStatus, setImageKeyStatus] = useState('')
|
||||
|
||||
@@ -2621,6 +2930,7 @@ function SettingsPage() {
|
||||
<div className="settings-body">
|
||||
{activeTab === 'appearance' && renderAppearanceTab()}
|
||||
{activeTab === 'database' && renderDatabaseTab()}
|
||||
{activeTab === 'api' && renderApiTab()}
|
||||
{activeTab === 'security' && renderSecurityTab()}
|
||||
{activeTab === 'stt' && renderSttTab()}
|
||||
{activeTab === 'ai' && (
|
||||
|
||||
@@ -25,6 +25,9 @@ export const CONFIG_KEYS = {
|
||||
AUTO_UPDATE_CHECK_INTERVAL: 'autoUpdateCheckInterval', // 检查间隔(秒)
|
||||
AUTO_UPDATE_MIN_INTERVAL: 'autoUpdateMinInterval', // 最小更新间隔(毫秒)
|
||||
AUTO_UPDATE_DEBOUNCE_TIME: 'autoUpdateDebounceTime', // 防抖时间(毫秒)
|
||||
HTTP_API_ENABLED: 'httpApiEnabled',
|
||||
HTTP_API_PORT: 'httpApiPort',
|
||||
HTTP_API_TOKEN: 'httpApiToken',
|
||||
AUTH_ENABLED: 'authEnabled',
|
||||
AUTH_CREDENTIAL_ID: 'authCredentialId',
|
||||
AUTH_PASSWORD_HASH: 'authPasswordHash',
|
||||
@@ -82,6 +85,42 @@ export async function setAutoUpdateDebounceTime(ms: number): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AUTO_UPDATE_DEBOUNCE_TIME, Math.max(100, Math.min(5000, ms)))
|
||||
}
|
||||
|
||||
// --- HTTP API 配置 ---
|
||||
|
||||
// 获取是否启用 HTTP API
|
||||
export async function getHttpApiEnabled(): Promise<boolean> {
|
||||
const value = await config.get(CONFIG_KEYS.HTTP_API_ENABLED)
|
||||
return value !== undefined ? (value as boolean) : false
|
||||
}
|
||||
|
||||
// 设置是否启用 HTTP API
|
||||
export async function setHttpApiEnabled(enabled: boolean): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.HTTP_API_ENABLED, enabled)
|
||||
}
|
||||
|
||||
// 获取 HTTP API 端口
|
||||
export async function getHttpApiPort(): Promise<number> {
|
||||
const value = await config.get(CONFIG_KEYS.HTTP_API_PORT)
|
||||
return (value as number) || 5031
|
||||
}
|
||||
|
||||
// 设置 HTTP API 端口
|
||||
export async function setHttpApiPort(port: number): Promise<void> {
|
||||
const safePort = Number.isFinite(port) ? Math.max(1, Math.min(65535, Math.floor(port))) : 5031
|
||||
await config.set(CONFIG_KEYS.HTTP_API_PORT, safePort)
|
||||
}
|
||||
|
||||
// 获取 HTTP API 访问令牌
|
||||
export async function getHttpApiToken(): Promise<string> {
|
||||
const value = await config.get(CONFIG_KEYS.HTTP_API_TOKEN)
|
||||
return (value as string) || ''
|
||||
}
|
||||
|
||||
// 设置 HTTP API 访问令牌
|
||||
export async function setHttpApiToken(token: string): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.HTTP_API_TOKEN, token.trim())
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- AI 摘要配置 ---
|
||||
|
||||
Vendored
+53
@@ -71,6 +71,59 @@ export interface ElectronAPI {
|
||||
onDownloadProgress: (callback: (progress: number) => void) => () => void
|
||||
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void
|
||||
}
|
||||
httpApi: {
|
||||
getStatus: () => Promise<{
|
||||
success: boolean
|
||||
status?: {
|
||||
running: boolean
|
||||
host: string
|
||||
port: number
|
||||
enabled: boolean
|
||||
startedAt: string
|
||||
uptimeMs: number
|
||||
tokenConfigured: boolean
|
||||
tokenPreview: string
|
||||
baseUrl: string
|
||||
endpoints: Array<{ method: string; path: string; desc: string }>
|
||||
lastError: string
|
||||
}
|
||||
error?: string
|
||||
}>
|
||||
applySettings: (payload: { enabled: boolean; port: number; token: string }) => Promise<{
|
||||
success: boolean
|
||||
status?: {
|
||||
running: boolean
|
||||
host: string
|
||||
port: number
|
||||
enabled: boolean
|
||||
startedAt: string
|
||||
uptimeMs: number
|
||||
tokenConfigured: boolean
|
||||
tokenPreview: string
|
||||
baseUrl: string
|
||||
endpoints: Array<{ method: string; path: string; desc: string }>
|
||||
lastError: string
|
||||
}
|
||||
error?: string
|
||||
}>
|
||||
restart: () => Promise<{
|
||||
success: boolean
|
||||
status?: {
|
||||
running: boolean
|
||||
host: string
|
||||
port: number
|
||||
enabled: boolean
|
||||
startedAt: string
|
||||
uptimeMs: number
|
||||
tokenConfigured: boolean
|
||||
tokenPreview: string
|
||||
baseUrl: string
|
||||
endpoints: Array<{ method: string; path: string; desc: string }>
|
||||
lastError: string
|
||||
}
|
||||
error?: string
|
||||
}>
|
||||
}
|
||||
// Windows Hello 原生验证 (比 WebAuthn 更快)
|
||||
windowsHello: {
|
||||
/** 检查 Windows Hello 是否可用 */
|
||||
|
||||
Reference in New Issue
Block a user