diff --git a/PROJECT.md b/PROJECT.md index 1635319..158e15d 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -10,4 +10,4 @@ - `npx tsc -b` — 类型检查;`npm run build` — 产物 dist/(Dev Makefile 拷贝至 backend web/dist 内嵌)。 - `npm test` — vitest:契约形状测试(Core 黄金样本驱动的判别联合/事件信封/HITL impact/工具三清单断言)。 - 开发:`npm run dev`(Vite 代理 /api → 127.0.0.1:8787)。 -- 依赖刻意最小:react + marked;无 UI 框架(设计令牌手写,见 styles.css)。 +- 依赖刻意最小:react + marked + dompurify(模型输出与文档预览先清洗再写入页面);无 UI 框架(设计令牌手写,见 styles.css)。 diff --git a/package-lock.json b/package-lock.json index 982c1fb..630fced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "docx-preview": "^0.4.1", + "dompurify": "^3.4.16", "jszip": "^3.10.2", "mammoth": "^1.13.0", "marked": "^15.0.7", @@ -1725,6 +1726,13 @@ "@types/react": "^19.3.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2094,6 +2102,15 @@ "jszip": ">=3.0.0" } }, + "node_modules/dompurify": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/duck": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", diff --git a/package.json b/package.json index e1e48bf..3943559 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "docx-preview": "^0.4.1", + "dompurify": "^3.4.16", "jszip": "^3.10.2", "mammoth": "^1.13.0", "marked": "^15.0.7", diff --git a/src/api.ts b/src/api.ts index 5fe8429..1b5bb77 100644 --- a/src/api.ts +++ b/src/api.ts @@ -110,6 +110,20 @@ export const api = { listPlugins: () => auth.fetch(`${BASE}/api/plugins`).then(r => r.json()), reloadPlugins: () => auth.fetch(`${BASE}/api/plugins/reload`, { method: 'POST' }).then(r => r.json()), listCommands: () => auth.fetch(`${BASE}/api/commands`).then(r => r.json()) as Promise<{ name?: string; id?: string; title?: string; description?: string; danger?: string }[]>, + setSessionNote: (id: string, sessionNote: string) => auth.fetch(`${BASE}/api/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionNote }) }).then(r => { if (!r.ok) throw new Error(`${r.status}`) }), + excludeMessage: (id: string, excluded: boolean) => auth.fetch(`${BASE}/api/messages/${id}/exclude`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ excluded }) }).then(r => { if (!r.ok) throw new Error(`${r.status}`) }), + selectVariant: (id: string) => auth.fetch(`${BASE}/api/messages/${id}/variant`, { method: 'POST' }).then(r => { if (!r.ok) throw new Error(`${r.status}`) }), + regenerate: (convId: string, messageId: string) => auth.fetch(`${BASE}/api/conversations/${convId}/regenerate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messageId }) }).then(r => { if (!r.ok) throw new Error(`${r.status}: ${r.statusText}`) }), + listSecrets: () => auth.fetch(`${BASE}/api/secrets`).then(r => json<{ id: string; service: string; label: string; active: boolean; mask: string }[]>(r)), + createSecret: (service: string, label: string, value: string) => auth.fetch(`${BASE}/api/secrets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service, label, value }) }).then(r => json(r)), + activateSecret: (id: string) => auth.fetch(`${BASE}/api/secrets/${id}/activate`, { method: 'POST' }), + deleteSecret: (id: string) => auth.fetch(`${BASE}/api/secrets/${id}`, { method: 'DELETE' }), + listContextEntries: () => auth.fetch(`${BASE}/api/context-entries`).then(r => json<{ id: string; keys: string[]; secondary?: string; logic: string; content: string; constant: boolean; scanDepth: number; budgetChars: number; enabled: boolean }[]>(r)), + saveContextEntry: (body: Record) => auth.fetch(`${BASE}/api/context-entries`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => json(r)), + deleteContextEntry: (id: string) => auth.fetch(`${BASE}/api/context-entries/${id}`, { method: 'DELETE' }), + listPromptCommands: () => auth.fetch(`${BASE}/api/prompt-commands`).then(r => json<{ id: string; name: string; title: string; template: string }[]>(r)), + savePromptCommand: (body: { name: string; title: string; template: string }) => auth.fetch(`${BASE}/api/prompt-commands`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => json(r)), + deletePromptCommand: (id: string) => auth.fetch(`${BASE}/api/prompt-commands/${id}`, { method: 'DELETE' }), listWorkspaces: () => auth.fetch(`${BASE}/api/workspaces`).then(r => r.json()) as Promise<{ id: string; title: string; icon?: string; shortcut?: string; badgeTool?: string; body?: any[]; refreshTool?: string }[]>, } diff --git a/src/components/BorrowPanels.tsx b/src/components/BorrowPanels.tsx new file mode 100644 index 0000000..d8f8bca --- /dev/null +++ b/src/components/BorrowPanels.tsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from 'react' +import { Btn, SettingRow, SettingsGroup } from '@ui' +import { api } from '../api' + +const services = [ + ['models.chat', '主模型'], + ['models.lightweight', '轻量模型'], + ['models.embedding', 'Embedding'], + ['models.rerank', 'Reranker'], + ['tools.tavily', 'Tavily'], +] as const + +type Secret = { id: string; service: string; label: string; active: boolean; mask: string } +type Entry = { id: string; keys: string[]; content: string; constant: boolean; logic: string } +type Cmd = { id: string; name: string; title: string; template: string } + +export function BorrowPanels() { + const [secrets, setSecrets] = useState([]) + const [entries, setEntries] = useState([]) + const [cmds, setCmds] = useState([]) + const [service, setService] = useState(services[0][0]) + const [label, setLabel] = useState('') + const [value, setValue] = useState('') + const [keys, setKeys] = useState('') + const [content, setContent] = useState('') + const [constant, setConstant] = useState(false) + const [cmdName, setCmdName] = useState('/summary') + const [cmdTitle, setCmdTitle] = useState('总结') + const [cmdBody, setCmdBody] = useState('请总结以上内容。\n{{input}}') + + const reload = () => { + api.listSecrets().then(setSecrets).catch(() => undefined) + api.listContextEntries().then(setEntries).catch(() => undefined) + api.listPromptCommands().then(setCmds).catch(() => undefined) + } + useEffect(() => { reload() }, []) + + return ( + <> + + + + + + setLabel(e.target.value)} /> + setValue(e.target.value)} /> + { + if (!value.trim()) return + await api.createSecret(service, label || '未命名', value.trim()) + setValue(''); setLabel(''); reload() + }}>添加 + + {secrets.map(s => ( + + {!s.active && { await api.activateSecret(s.id); reload() }}>启用} + { await api.deleteSecret(s.id); reload() }}>删除 + + ))} + + + + setKeys(e.target.value)} /> + + + +