@yfwu2020/dsh-selection-explain 0.10.4 → 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/cordis.patch.yml +1 -1
- package/lib/client.js +10698 -12424
- package/lib/client.js.map +7 -0
- package/lib/config-values.js +55 -0
- package/lib/config-values.js.map +1 -0
- package/lib/host/analyze.js +795 -0
- package/lib/host/analyze.js.map +1 -0
- package/lib/host/config.js +81 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/constants.js +25 -0
- package/lib/host/constants.js.map +1 -0
- package/lib/host/context.js +237 -0
- package/lib/host/context.js.map +1 -0
- package/lib/host/design-skill.js +42 -0
- package/lib/host/design-skill.js.map +1 -0
- package/lib/host/effort.js +239 -0
- package/lib/host/effort.js.map +1 -0
- package/lib/host/filters.js +132 -0
- package/lib/host/filters.js.map +1 -0
- package/lib/host/history-store.js +404 -0
- package/lib/host/history-store.js.map +1 -0
- package/lib/host/http.js +46 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/messages.js +14 -0
- package/lib/host/messages.js.map +1 -0
- package/lib/host/mic.js +246 -0
- package/lib/host/mic.js.map +1 -0
- package/lib/host/models.js +246 -0
- package/lib/host/models.js.map +1 -0
- package/lib/host/plugin.js +41 -0
- package/lib/host/plugin.js.map +1 -0
- package/lib/host/promote.js +219 -0
- package/lib/host/promote.js.map +1 -0
- package/lib/host/routes.js +237 -0
- package/lib/host/routes.js.map +1 -0
- package/lib/host/sessions.js +181 -0
- package/lib/host/sessions.js.map +1 -0
- package/lib/host/settings-store.js +140 -0
- package/lib/host/settings-store.js.map +1 -0
- package/lib/host/speech.js +320 -0
- package/lib/host/speech.js.map +1 -0
- package/lib/host/tools.js +179 -0
- package/lib/host/tools.js.map +1 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/lib/index.js +10 -3765
- package/lib/index.js.map +1 -1
- package/lib/prompts.js +1 -1
- package/lib/settings.js +17 -27
- package/lib/settings.js.map +1 -1
- package/lib/types/config-values.d.ts +175 -0
- package/lib/types/host/analyze.d.ts +10 -0
- package/lib/types/host/config.d.ts +147 -0
- package/lib/types/host/constants.d.ts +24 -0
- package/lib/types/host/context.d.ts +41 -0
- package/lib/types/host/design-skill.d.ts +1 -0
- package/lib/types/host/effort.d.ts +61 -0
- package/lib/types/host/filters.d.ts +20 -0
- package/lib/types/host/history-store.d.ts +13 -0
- package/lib/types/host/http.d.ts +8 -0
- package/lib/types/host/messages.d.ts +13 -0
- package/lib/types/host/mic.d.ts +17 -0
- package/lib/types/host/models.d.ts +57 -0
- package/lib/types/host/plugin.d.ts +3 -0
- package/lib/types/host/promote.d.ts +9 -0
- package/lib/types/host/routes.d.ts +14 -0
- package/lib/types/host/sessions.d.ts +14 -0
- package/lib/types/host/settings-store.d.ts +8 -0
- package/lib/types/host/speech.d.ts +23 -0
- package/lib/types/host/tools.d.ts +39 -0
- package/lib/types/host/types.d.ts +89 -0
- package/lib/types/index.d.ts +12 -441
- package/lib/types/prompts.d.ts +1 -1
- package/lib/types/settings.d.ts +1 -1
- package/package.json +31 -21
- package/assets/code.png +0 -0
- package/assets/detail.png +0 -0
- package/assets/quotes.png +0 -0
- package/assets/stage1.png +0 -0
- package/assets/stage2.png +0 -0
- package/assets/translation.png +0 -0
- package/assets/voice-card-light.png +0 -0
- package/assets/voice-inserted.png +0 -0
- package/assets/voice-settings.png +0 -0
- package/assets/voice.png +0 -0
- package/assets/web.png +0 -0
- package/scripts/build-demo-pages.mjs +0 -350
- package/scripts/build-vcard-lab.mjs +0 -834
- package/scripts/build.sh +0 -147
- package/scripts/crop-png.mjs +0 -142
- package/scripts/dump-prompt.mjs +0 -143
- package/scripts/fixtures/fake-mic-probe.sh +0 -9
- package/scripts/gen-star-path.mjs +0 -71
- package/scripts/measure-cost.mjs +0 -251
- package/scripts/smoke-bridge.mjs +0 -365
- package/scripts/smoke-panel.mjs +0 -1903
- package/scripts/smoke-voice.mjs +0 -774
- package/scripts/test-bridge.mjs +0 -412
- package/scripts/test-client.mjs +0 -6175
- package/scripts/test-effort-map.mjs +0 -129
- package/scripts/test-effort-retry.mjs +0 -109
- package/scripts/test-escape-focus.mjs +0 -101
- package/scripts/test-filters.mjs +0 -146
- package/scripts/test-format.mjs +0 -93
- package/scripts/test-guard.mjs +0 -48
- package/scripts/test-mic-build.mjs +0 -146
- package/scripts/test-mic-idle.mjs +0 -158
- package/scripts/test-model-follow.mjs +0 -193
- package/scripts/test-prompt.mjs +0 -193
- package/scripts/test-route-cache.mjs +0 -138
- package/scripts/test-settings-behavior.mjs +0 -188
- package/scripts/test-settings-host.mjs +0 -48
- package/scripts/test-settings.mjs +0 -329
- package/scripts/test-speech.mjs +0 -220
- package/scripts/test-transcript.mjs +0 -151
- package/src/client/index.js +0 -12580
- package/src/index.ts +0 -4254
- package/src/prompts.ts +0 -144
- package/src/settings.ts +0 -313
package/src/prompts.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @yfwu2020/dsh-selection-explain — 提示词与写作规则(纯文本常量,无逻辑)。
|
|
3
|
-
*
|
|
4
|
-
* 从 src/index.ts 抽出来的原因:那是 2500+ 行的 host 全量逻辑(路由 / LLM 调用 /
|
|
5
|
-
* 会话背景 / 历史落盘 / 过滤器),提示词占其中约 20%,混在一起既难找也容易误改。
|
|
6
|
-
*
|
|
7
|
-
* 这里只放**字符串常量**,不 import 任何东西(避免与 index.ts 形成循环依赖)。
|
|
8
|
-
* 组装逻辑仍在 src/index.ts 里(按 stage / kind 选择用哪一套,见 chooseSystemPrompt)。
|
|
9
|
-
*
|
|
10
|
-
* 改动提示词的注意事项:
|
|
11
|
-
* · 结构契约由客户端解析(按 `## 翻译` / `## 详解` 标题切分),改标题会同时影响渲染;
|
|
12
|
-
* · scripts/test-prompt.mjs 里有"提示词契约测试"——删掉关键约束会直接 FAIL;
|
|
13
|
-
* · 想知道模型**实际收到**什么(含 harness 注入与会话背景拼接结果),跑
|
|
14
|
-
* `node scripts/dump-prompt.mjs --all`(需要 DSH 正在运行)。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** 系统提示词:固定文本,两节结构,前端按标题切分。 */
|
|
18
|
-
/** 公共前言与尾部规则(两套提示词共用,避免重复维护)。 */
|
|
19
|
-
export const PROMPT_HEAD = [
|
|
20
|
-
'你是一位跨领域的资深解读专家,覆盖中英双语语言学、软件工程与系统架构、数据与算法、产品与交互设计。',
|
|
21
|
-
'先判断【选中文字】实际属于哪个领域,再按该领域资深从业者的标准解读:',
|
|
22
|
-
'术语取该领域的准确定义(必要时补一句通用含义做对照),关注点也对齐该领域的关注点——',
|
|
23
|
-
'代码看行为、边界与副作用,架构看约束与取舍,设计看目标与权衡,语言看语域、搭配与言外之意。',
|
|
24
|
-
'领域判断是内部动作:**不要把判断过程写出来**,也不要写「这属于工程领域」这类交代,直接给解读。',
|
|
25
|
-
'',
|
|
26
|
-
'用户消息可能是:先给一段【会话背景】(可选,最前面),最后给出要解读的【选中文字】及其上下文。',
|
|
27
|
-
].join('\n')
|
|
28
|
-
|
|
29
|
-
/** 只输出「翻译」一节时用。 */
|
|
30
|
-
export const TRANSLATION_PROMPT = [
|
|
31
|
-
PROMPT_HEAD,
|
|
32
|
-
'',
|
|
33
|
-
'**本轮只输出一个章节**(标题见下),不要输出其它章节,不要前言与结语。',
|
|
34
|
-
'先看【选中文字】里**有没有需要翻译的英文**,据此决定标题与内容(判断过程不要写出来):',
|
|
35
|
-
'',
|
|
36
|
-
'### 情形一:选中文字里有英文(含中英混排)→ 标题写 `## 翻译`',
|
|
37
|
-
'- **以中文为主的句子**(可能夹着英文词):中文部分**一律不翻译**、也不要复述原句;',
|
|
38
|
-
' 只把【选中文字】**自己**含着的**英文**片段(由拉丁字母组成的单词、词组或整句)挑出来逐条译成',
|
|
39
|
-
' 中文,一个片段一条,且**层级分明**:',
|
|
40
|
-
' · 是**单词** → 父项写「词头 + 音标」(IPA,斜杠包起来),词性与义项**缩进两个空格**作为子项,',
|
|
41
|
-
' 每行以 `**n.**`、`**v.**` 这样的词性标记开头(**词性必须标**,一词多义就多行):',
|
|
42
|
-
' 形如 `- **migration** /maɪˈɡreɪʃn/`,下一行缩进写 `- **n.** 迁移;移居`;',
|
|
43
|
-
' · 是**词组或整句** → 一行 `- **原英文**:中文意思` 即可(不标音标、不拆词性)。',
|
|
44
|
-
' 音标拿不准就不写(缩写、专有名词尤其常见);宁可不标也不要编。',
|
|
45
|
-
' 中文词句即使有对应的英文说法(如「缓存层」「灰度」)也**不要列**,不要到上下文或标题里去找英文;',
|
|
46
|
-
' 写完自查:列出的每一条都必须①由英文字母构成、②能在【选中文字】里逐字找到,否则删掉。',
|
|
47
|
-
'- **以英文为主**(整句、词组或单词都算):',
|
|
48
|
-
' · 句子或词组 → 译文**单独成段**,段落以译文的第一个字开头(长句可拆成 2~3 行;',
|
|
49
|
-
' 句中的中文词原样保留,关键术语用括号注明原词);',
|
|
50
|
-
' · 单词 → **两级结构,层级要看得出来**:',
|
|
51
|
-
' 第一行是**词头 + 音标**(IPA,斜杠包起来),它是父项:`- **migrate** /maɪˈɡreɪt/`;',
|
|
52
|
-
' 英式与美式读音不同就都写(`/英 …/ `/美 …/` 并列,标上「英」「美」),相同只写一个;拿不准就不写,不要编。',
|
|
53
|
-
' 下面**每个词性义项各占一行、缩进两个空格**作为子项,行首是 `**v.**` 这样的词性标记,',
|
|
54
|
-
' 后面跟中文义项,常用义项在前;词头行本身**不要再写义项**。',
|
|
55
|
-
' 形如:父项 `- **long** /英 lɒŋ/ /美 lɔːŋ/`,子项缩进两格 `- **adj.** 长的;远的`、`- **v.** 渴望,极想`。',
|
|
56
|
-
'- **以中英之外的语言为主**(日、韩、法、德等):译成中文。',
|
|
57
|
-
'',
|
|
58
|
-
'### 情形二:选中文字**全是中文**(纯汉字的词、短语或句子)→ 标题写 `## 解读`',
|
|
59
|
-
'- **不要翻译**:不译成英文,不给英文说法,也不列义项、不做词典式解释;',
|
|
60
|
-
'- 只是把这段中文**在当前上下文里是什么意思**讲清楚,一两句就够,直接说结论。',
|
|
61
|
-
'',
|
|
62
|
-
'两种情形的**最后一行都固定**写成引用行,作为本节的总结(**这一行是本节的重点**):',
|
|
63
|
-
'> 在本句中:<它在这段上下文里到底是什么意思>',
|
|
64
|
-
'- 一行说清:用日常话讲明白,专业上的准确说法和必要限定顺在里面带出来即可,',
|
|
65
|
-
' 不用套固定句式、也不用刻意分两截;',
|
|
66
|
-
'- **不要照抄原句**,不要写「可能」「取决于」:给确定答案;',
|
|
67
|
-
'- 说清楚为止:该长就长、该短就短,不卡字数;也不要为凑字数把一行写成一段。',
|
|
68
|
-
'',
|
|
69
|
-
'规则:',
|
|
70
|
-
'- 只处理选中文字,不要整段翻译「上下文」,也不要复述指令。',
|
|
71
|
-
'- 正文里不要出现「翻译:」「中文句子:」这类分类标签,直接给内容。',
|
|
72
|
-
'- 用简洁 Markdown:要点可以用「- 」,但别把什么都做成列表;强调用 **加粗**,别整段加粗;',
|
|
73
|
-
' 整段输出不要用代码块包裹。',
|
|
74
|
-
'- 不要出现「以下是」「希望对你有所帮助」之类的客套话。',
|
|
75
|
-
'- **不要叙述查资料的过程**("让我先搜一下""我再确认一下"这类一律不写);查到什么直接给结论,',
|
|
76
|
-
' 有链接就自然带在结论里。',
|
|
77
|
-
].join('\n')
|
|
78
|
-
|
|
79
|
-
/** 选中文字是代码时用:代码块原样呈现 + 逐句注释 + 小结(首轮,只要快)。 */
|
|
80
|
-
export const CODE_PROMPT = [
|
|
81
|
-
PROMPT_HEAD,
|
|
82
|
-
'',
|
|
83
|
-
'**本轮只输出「## 注释」这一节**,不要输出「## 详解」或其它章节,不要前言与结语:',
|
|
84
|
-
'',
|
|
85
|
-
'## 注释',
|
|
86
|
-
'**整节只有一个代码块**(用 ``` 围起来,尽可能标上语言),**注释就写在代码块里面**,',
|
|
87
|
-
'贴着它解释的那条语句,看起来像一份"带批注的代码清单":',
|
|
88
|
-
'',
|
|
89
|
-
'```',
|
|
90
|
-
'// 这条语句在做什么(用该语言的注释符号)',
|
|
91
|
-
'<原代码这一句>',
|
|
92
|
-
'// 下一条语句在做什么',
|
|
93
|
-
'<原代码下一句>',
|
|
94
|
-
'```',
|
|
95
|
-
'',
|
|
96
|
-
'块内规则:',
|
|
97
|
-
'- 原代码**逐字不变**:缩进、空行、注释、变量名全部保留;不要补全、不要精简、不要加行号、不要改写成伪代码。',
|
|
98
|
-
'- 在**每一条语句/逻辑块之前**加一行注释,用**该语言的注释符号**(`//`、`#`、`--`、`<!-- -->`、`<!--` 等),',
|
|
99
|
-
' 注释紧贴代码、中间不插空行;注释的缩进跟着它解释的那行代码走。',
|
|
100
|
-
'- **每一句都要覆盖**,不要跳句;讲「这行为什么这么写、和整体什么关系、有什么副作用或坑」,',
|
|
101
|
-
' 不要复述语言基础语法("这是一个 if 语句"这种不要写)。',
|
|
102
|
-
'- 太长的语句可以在其后补一行注释,但不要撕裂代码本身。',
|
|
103
|
-
'- 每条注释尽量一两句说完;说不完就换行再起一行注释(小窗很窄,一条注释太长会被折成好几行),不要写成一大段。',
|
|
104
|
-
'',
|
|
105
|
-
'代码块**外面**只留最后一行,固定写成引用行作为总结:',
|
|
106
|
-
'> 小结:<这段代码整体在干什么、值得点出的地方>',
|
|
107
|
-
'- 一行说清,话要顺:用日常话讲它在做什么,顺带点出关键路径、复杂度、副作用或坑;',
|
|
108
|
-
' 没什么可点就只说在做什么,别硬凑,也别把这一行拆成好几段;',
|
|
109
|
-
'- 用自己的话讲,不要照抄代码,也不要复述块内的逐句注释;',
|
|
110
|
-
'- 说清楚为止:该长就长、该短就短,不卡字数;也不要为整齐而拉长。',
|
|
111
|
-
'',
|
|
112
|
-
'规则:',
|
|
113
|
-
'- 只处理【选中文字】这段代码;上下文只用来理解它,不要注释上下文里的其它代码。',
|
|
114
|
-
'- 除了最后那一行小结,**不要在代码块外再写逐句注释**(注释已经在块内了)。',
|
|
115
|
-
'- 用中文讲解;术语、API 名保留原文。',
|
|
116
|
-
'- 不要出现「以下是」「希望对你有所帮助」之类的客套话。',
|
|
117
|
-
].join('\n')
|
|
118
|
-
|
|
119
|
-
/** 只输出「详解」一节时用(第二阶段,带完整会话背景)。 */
|
|
120
|
-
export const DETAIL_PROMPT = [
|
|
121
|
-
PROMPT_HEAD,
|
|
122
|
-
'',
|
|
123
|
-
'**本轮只输出「## 详解」这一节**,不要输出「## 翻译」或其它章节,不要前言与结语:',
|
|
124
|
-
'',
|
|
125
|
-
'## 详解',
|
|
126
|
-
'选中的这段文字是什么意思?把意思讲透——既专业,也让人看得懂;该长就长、该短就短。',
|
|
127
|
-
'(先看一眼它是怎么来的:真实对话/正文,还是示例、测试用例、演示输出、引用别人的话;',
|
|
128
|
-
'是后者就先说一句,再按它实际的意思解释。)',
|
|
129
|
-
'选中文字是单个英文单词时,带上音标(IPA;英式美式不同就都写,拿不准就不写)。',
|
|
130
|
-
].join('\n')
|
|
131
|
-
|
|
132
|
-
/** 追问模式(对话小窗)的系统提示词:不再要求两节结构,就是围绕这段文字聊天。 */
|
|
133
|
-
export const CHAT_SYSTEM_PROMPT = [
|
|
134
|
-
'你是 DeepSeek Harness 中「划词解读」小窗里的助手,正在辅助用户对一段【选中文字】及其上下文进行理解。',
|
|
135
|
-
'用户问什么就答什么:可以问这段文字本身,也可以顺着它问到别的,不必把话题拉回选中文字。',
|
|
136
|
-
'回答简洁、准确、具体;前情里已经给过的翻译/详解不用重复,引用时一句话带过即可。',
|
|
137
|
-
'用户消息里可能带【引用 N】块:那是用户**特意挑出来**附给这条提问的材料(可能来自小窗里的某段回答,',
|
|
138
|
-
'也可能来自主界面)。它就是被问的对象,当成提问的一部分来答;别把它认成你上一轮的回答,也别复述一遍。',
|
|
139
|
-
'引用块里可能带【引用处上下文】(引用文字当时所在的对话,被引用的部分用【】标出)——那是判断',
|
|
140
|
-
'"它在这里什么意思"的依据。只写出处(「小窗第 2 轮回答」「小窗「翻译」节」这类)时,说明那段材料',
|
|
141
|
-
'本来就在本轮上下文里,找到它、按引用的要求作答即可。',
|
|
142
|
-
'要画图、做可视化或写页面时,直接给**完整的单文件 HTML**(放进 ```html 代码块,样式和脚本都内联),',
|
|
143
|
-
'小窗会把它当场渲染成可交互的网页;不要只给片段,也不要说"你可以把它保存成 .html 再打开"。',
|
|
144
|
-
].join('\n')
|
package/src/settings.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 划词解读 · 设置面板的**单一事实来源**。
|
|
3
|
-
*
|
|
4
|
-
* 为什么单独成模块:
|
|
5
|
-
* · 小窗设置页要渲染 20+ 个字段,字段的标题 / 说明 / 单位 / 边界**必须**与 host 的
|
|
6
|
-
* 校验规则是同一份数据 —— 分两处写迟早会漂(UI 说 1~600,host 却按 1~200 截断)。
|
|
7
|
-
* 所以 GET /settings 直接把这份 spec 发给客户端,客户端只负责画,不做业务判断。
|
|
8
|
-
* · 纯数据 + 纯函数,不 import cordis / node,单测可以直接跑(见 scripts/test-settings.mjs)。
|
|
9
|
-
*
|
|
10
|
-
* 分组是**导航结构**,不是装饰:20+ 项平铺会淹没用户,按"用户想去哪类事"分 6 组。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** 字段控件类型。 */
|
|
14
|
-
export type SettingKind = 'switch' | 'number' | 'text' | 'seg' | 'model'
|
|
15
|
-
|
|
16
|
-
/** 一个设置项的声明。 */
|
|
17
|
-
export interface SettingSpec {
|
|
18
|
-
/** 对应 Config 的字段名(也是 POST body 里的键)。 */
|
|
19
|
-
key: string
|
|
20
|
-
kind: SettingKind
|
|
21
|
-
/** 行标题。 */
|
|
22
|
-
label: string
|
|
23
|
-
/** 一行说明:说清"改了会怎样",包括代价。 */
|
|
24
|
-
hint: string
|
|
25
|
-
/** 关闭此开关时禁用当前字段,但保留其配置值。 */
|
|
26
|
-
enabledBy?: string
|
|
27
|
-
/** number 的单位(显示在输入框右侧)。 */
|
|
28
|
-
unit?: string
|
|
29
|
-
/** number 的边界(与 host 侧 clamp 用同一组值)。 */
|
|
30
|
-
min?: number
|
|
31
|
-
max?: number
|
|
32
|
-
/** number 的步进。 */
|
|
33
|
-
step?: number
|
|
34
|
-
/** seg 的候选项。 */
|
|
35
|
-
options?: string[]
|
|
36
|
-
/** 留空时的语义说明(text 用,放进 placeholder)。 */
|
|
37
|
-
placeholder?: string
|
|
38
|
-
/**
|
|
39
|
-
* **复合字段**:一个控件写多个 Config 字段(当前只有模型选择 —— 一个下拉同时定 provider 与 model)。
|
|
40
|
-
* 有它就说明 `key` 是个**虚拟键**、本身不写进配置,真正写的是这里列出的字段。
|
|
41
|
-
*/
|
|
42
|
-
keys?: string[]
|
|
43
|
-
/**
|
|
44
|
-
* 显示单位与配置单位的**倍率**:显示值 = 配置值 / scale。
|
|
45
|
-
*
|
|
46
|
-
* 配置里时间一律是毫秒(schema 与 yml 的历史值都是毫秒,不动它),
|
|
47
|
-
* 但界面上"秒"更好读(用户提的要求)。所以 min/max 仍然按**配置单位**声明,
|
|
48
|
-
* 由客户端换算出显示用的边界 —— 边界的唯一事实来源始终是 Config schema。
|
|
49
|
-
*/
|
|
50
|
-
scale?: number
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 一个分组。 */
|
|
54
|
-
export interface SettingGroup {
|
|
55
|
-
id: string
|
|
56
|
-
/** 左栏分类名。 */
|
|
57
|
-
label: string
|
|
58
|
-
items: SettingSpec[]
|
|
59
|
-
sections: Array<{ label: string; keys: string[]; hint?: string; note?: string }>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 分组顺序 = 左栏从上到下的顺序。
|
|
64
|
-
* 「模型」放第一:27 项里唯一大多数人有理由改的一类。
|
|
65
|
-
*/
|
|
66
|
-
export const SETTINGS_GROUPS: SettingGroup[] = [
|
|
67
|
-
{
|
|
68
|
-
id: 'model', label: '模型',
|
|
69
|
-
sections: [
|
|
70
|
-
{"label": "模型", "keys": ["modelChoice"]},
|
|
71
|
-
{"label": "思考强度", "keys": ["translationReasoningEffort", "reasoningEffort", "chatReasoningEffort"], "note": "关闭思考仍会生成回答。"},
|
|
72
|
-
],
|
|
73
|
-
items: [
|
|
74
|
-
{"key": "modelChoice", "kind": "model", "label": "模型", "hint": "", "keys": ["provider", "model"]},
|
|
75
|
-
{"key": "translationReasoningEffort", "kind": "seg", "label": "首轮", "hint": "翻译 / 解读 / 注释", "options": ["off", "low", "high", "max"]},
|
|
76
|
-
{"key": "reasoningEffort", "kind": "seg", "label": "次轮", "hint": "点击「展开详解」", "options": ["off", "low", "high", "max"]},
|
|
77
|
-
{"key": "chatReasoningEffort", "kind": "seg", "label": "追问", "hint": "在小窗中继续提问", "options": ["off", "low", "high", "max"]},
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 'read', label: '解读',
|
|
82
|
-
sections: [
|
|
83
|
-
{"label": "文本范围", "keys": ["maxSelectionChars", "maxContextChars"]},
|
|
84
|
-
{"label": "频率与时限", "keys": ["maxRequestsPerMinute", "timeoutMs"]},
|
|
85
|
-
],
|
|
86
|
-
items: [
|
|
87
|
-
{"key": "maxSelectionChars", "kind": "number", "label": "选中文字上限", "hint": "超出此长度时,不进行解读。", "unit": "字符", "min": 20, "max": 20000, "step": 1},
|
|
88
|
-
{"key": "maxContextChars", "kind": "number", "label": "局部前后文上限", "hint": "不含会话背景,超出部分截断。", "unit": "字符", "min": 200, "max": 60000, "step": 1},
|
|
89
|
-
{"key": "maxRequestsPerMinute", "kind": "number", "label": "每分钟请求上限", "hint": "所有阶段合计,超限需稍后重试。", "unit": "次", "min": 1, "max": 600, "step": 1},
|
|
90
|
-
{"key": "timeoutMs", "kind": "number", "label": "生成时限", "hint": "含检索耗时,超时后停止生成。", "unit": "秒", "min": 5000, "max": 600000, "step": 1000, "scale": 1000},
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'ctx', label: '背景',
|
|
95
|
-
sections: [
|
|
96
|
-
{"label": "解读", "keys": ["sessionContext", "sessionContextFastMessages", "sessionContextMaxMessages", "sessionContextMaxChars"]},
|
|
97
|
-
{"label": "引用", "keys": ["quoteContextRounds", "quoteContextMaxCharsPerTurn", "quoteContextMaxChars"], "hint": "将引用所在轮及其上下文随提问一起发给模型。", "note": "一轮包含一次提问及其回答。"},
|
|
98
|
-
],
|
|
99
|
-
items: [
|
|
100
|
-
{"key": "sessionContext", "kind": "switch", "label": "附带会话消息", "hint": "将选中文字所在消息及之前的消息一并发给模型。"},
|
|
101
|
-
{"key": "sessionContextFastMessages", "kind": "number", "label": "首轮上下文消息数", "hint": "每条提问或回答计一条。", "unit": "条", "min": 1, "max": 200, "step": 1, "enabledBy": "sessionContext"},
|
|
102
|
-
{"key": "sessionContextMaxMessages", "kind": "number", "label": "次轮与追问上下文消息数", "hint": "", "unit": "条", "min": 1, "max": 200, "step": 1, "enabledBy": "sessionContext"},
|
|
103
|
-
{"key": "sessionContextMaxChars", "kind": "number", "label": "上下文总字符上限", "hint": "0 为不限;超出时优先保留近期消息。", "unit": "字符", "min": 0, "max": 2000000, "step": 1000, "enabledBy": "sessionContext"},
|
|
104
|
-
{"key": "quoteContextRounds", "kind": "number", "label": "上下文轮数", "hint": "引用前后各取指定轮数;0 仅取所在轮。", "unit": "轮", "min": 0, "max": 10, "step": 1},
|
|
105
|
-
{"key": "quoteContextMaxCharsPerTurn", "kind": "number", "label": "单条消息字符上限", "hint": "长消息优先保留引用附近内容。", "unit": "字符", "min": 200, "max": 20000, "step": 100},
|
|
106
|
-
{"key": "quoteContextMaxChars", "kind": "number", "label": "上下文总字符上限", "hint": "超出时减少上下文,保留引用所在消息。", "unit": "字符", "min": 400, "max": 100000, "step": 100},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: 'tools', label: '联网',
|
|
111
|
-
sections: [
|
|
112
|
-
{"label": "工具调用", "keys": ["tools", "maxToolRounds", "toolResultMaxChars"]},
|
|
113
|
-
{"label": "工具范围", "keys": ["toolNames", "fallbackToolNames", "toolReadRoot"]},
|
|
114
|
-
],
|
|
115
|
-
items: [
|
|
116
|
-
{"key": "tools", "kind": "switch", "label": "允许调用工具", "hint": "模型可按需调用下方指定的工具。"},
|
|
117
|
-
{"key": "maxToolRounds", "kind": "number", "label": "调用轮数", "hint": "0 为不调用工具;同一轮可调用多个工具。", "unit": "轮", "min": 0, "max": 8, "step": 1, "enabledBy": "tools"},
|
|
118
|
-
{"key": "toolResultMaxChars", "kind": "number", "label": "单次结果字符上限", "hint": "工具返回的文本超出时截断。", "unit": "字符", "min": 200, "max": 20000, "step": 100, "enabledBy": "tools"},
|
|
119
|
-
{"key": "toolNames", "kind": "text", "label": "允许的工具", "hint": "工具名以逗号分隔。", "placeholder": "advanced_search,platform_search,read,grep,glob", "enabledBy": "tools"},
|
|
120
|
-
{"key": "fallbackToolNames", "kind": "text", "label": "备用搜索工具", "hint": "找不到允许列表中的搜索工具时补充。", "placeholder": "web_search", "enabledBy": "tools"},
|
|
121
|
-
{"key": "toolReadRoot", "kind": "text", "label": "文件读取目录", "hint": "留空取会话工作目录;* 为不限。", "placeholder": "会话工作目录", "enabledBy": "tools"},
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: 'ui', label: '界面',
|
|
126
|
-
sections: [
|
|
127
|
-
{"label": "划词入口", "keys": ["bridgeSidebarPreview"]},
|
|
128
|
-
{"label": "悬浮入口", "keys": ["pillEnabled", "pillIdleMs"], "note": "费用胶囊显示时保持展开。"},
|
|
129
|
-
{"label": "语音输入", "keys": ["voiceCancelOnSilence", "voiceAutoSend"], "note": "结束语音输入后不会立刻动作:先等输入法把字定下来(它还会补标点、改错词;约半秒,最长 4 秒)。"},
|
|
130
|
-
],
|
|
131
|
-
items: [
|
|
132
|
-
{"key": "bridgeSidebarPreview", "kind": "switch", "label": "侧边栏网页划词", "hint": "选中文字后显示「解读」按钮。"},
|
|
133
|
-
{"key": "pillEnabled", "kind": "switch", "label": "显示状态胶囊", "hint": "显示最近一次解读状态,点击打开或收起小窗。"},
|
|
134
|
-
{"key": "pillIdleMs", "kind": "number", "label": "自动收起时间", "hint": "空闲达到此时长后缩成小球。", "unit": "秒", "min": 2000, "max": 600000, "step": 1000, "scale": 1000, "enabledBy": "pillEnabled"},
|
|
135
|
-
{"key": "voiceCancelOnSilence", "kind": "switch", "label": "空录音时收起", "hint": "结束语音输入后,输入框里没字就收起卡片(等于按 Esc)。"},
|
|
136
|
-
{"key": "voiceAutoSend", "kind": "switch", "label": "说完直接发送", "hint": "结束语音输入后,输入框里有字就直接发送(等于按发送键)。"},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
id: 'data', label: '数据',
|
|
141
|
-
sections: [
|
|
142
|
-
{"label": "历史记录", "keys": ["historyMaxEntries"], "note": "0 为不保存,已有对话保留。超出时移除最久未更新的对话,已升格的保留。"},
|
|
143
|
-
],
|
|
144
|
-
items: [
|
|
145
|
-
{"key": "historyMaxEntries", "kind": "number", "label": "保留最近对话数量", "hint": "每条包含所选文字、模型回答及后续追问。", "unit": "条", "min": 0, "max": 500, "step": 1},
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
/** spec 里的全部条目(含虚拟键)。 */
|
|
151
|
-
export const SETTINGS_ITEMS: SettingSpec[] = SETTINGS_GROUPS.flatMap((group) => group.items)
|
|
152
|
-
|
|
153
|
-
/** key → spec 的索引(渲染与校验按 key 找规则)。 */
|
|
154
|
-
export const SETTINGS_BY_KEY: Map<string, SettingSpec> = new Map(
|
|
155
|
-
SETTINGS_ITEMS.map((item) => [item.key, item] as const),
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 真正会写进配置的字段全集。
|
|
160
|
-
*
|
|
161
|
-
* 与「spec 的 key 集合」**不是一回事**:模型下拉的 key 是虚拟的 `modelChoice`,
|
|
162
|
-
* 它写的是 `provider` + `model` 两个真实字段(见 SettingSpec.keys)。
|
|
163
|
-
* 校验、应用覆盖、组装当前值,全都用这一份。
|
|
164
|
-
*/
|
|
165
|
-
export const SETTINGS_KEYS: string[] = SETTINGS_ITEMS.flatMap((item) => item.keys ?? [item.key])
|
|
166
|
-
|
|
167
|
-
/** 只读的虚拟键(客户端可以渲染,但不能直接 POST 写它)。 */
|
|
168
|
-
export const SETTINGS_VIRTUAL_KEYS: string[] = SETTINGS_ITEMS
|
|
169
|
-
.filter((item) => Array.isArray(item.keys))
|
|
170
|
-
.map((item) => item.key)
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* **配置键** → 声明它的 spec(复合字段按 keys 展开)。
|
|
174
|
-
*
|
|
175
|
-
* 校验走这一份而不是 SETTINGS_BY_KEY:POST 上来的是配置键(`provider` / `model`),
|
|
176
|
-
* 而 spec 里那条的 key 是虚拟的 `modelChoice` —— 只有展开过才找得到规则。
|
|
177
|
-
*/
|
|
178
|
-
export const SETTINGS_WRITABLE_BY_KEY: Map<string, SettingSpec> = new Map(
|
|
179
|
-
SETTINGS_ITEMS.flatMap((item) =>
|
|
180
|
-
(item.keys ?? [item.key]).map((key) => [key, item] as const),
|
|
181
|
-
),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
/** 校验结果:通过则 values 是**只含合法键**的补丁。 */
|
|
185
|
-
export interface NormalizeResult {
|
|
186
|
-
ok: boolean
|
|
187
|
-
/** 被接受的键值对(已按 spec 归一)。 */
|
|
188
|
-
values: Record<string, unknown>
|
|
189
|
-
/** 被拒绝的键及原因(给客户端提示用,不阻断其它键)。 */
|
|
190
|
-
rejected: Array<{ key: string; reason: string }>
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* 把一个客户端补丁归一成"可直接写进配置"的对象。
|
|
195
|
-
*
|
|
196
|
-
* 规则:
|
|
197
|
-
* · 不认识的 key 一律拒绝(不能靠 POST 往配置里塞任意字段);
|
|
198
|
-
* · number 做 clamp(超界不报错,夹到边界 —— 用户拖滑块时不该看到红色报错);
|
|
199
|
-
* · seg 只接受 options 里的值;
|
|
200
|
-
* · switch 只接受真正的布尔(字符串 'true' 之类的也别猜,直接拒);
|
|
201
|
-
* · text 去掉首尾空白,长到离谱的直接拒(防止把配置文件写爆)。
|
|
202
|
-
*
|
|
203
|
-
* 有意**不抛异常**:一个键写错不该让整次保存失败。
|
|
204
|
-
*/
|
|
205
|
-
export function normalizeSettingsPatch(input: unknown): NormalizeResult {
|
|
206
|
-
const values: Record<string, unknown> = {}
|
|
207
|
-
const rejected: Array<{ key: string; reason: string }> = []
|
|
208
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
209
|
-
return { ok: false, values, rejected: [{ key: '', reason: '请求体不是对象' }] }
|
|
210
|
-
}
|
|
211
|
-
for (const [key, raw] of Object.entries(input as Record<string, unknown>)) {
|
|
212
|
-
const spec = SETTINGS_WRITABLE_BY_KEY.get(key)
|
|
213
|
-
if (!spec) {
|
|
214
|
-
// 虚拟键(如 modelChoice)不直接写配置:客户端要把它展开成 provider + model 再发上来。
|
|
215
|
-
// 单独认一下是为了给一句能照着做的错误,而不是笼统的"未知设置项"。
|
|
216
|
-
const virtual = SETTINGS_BY_KEY.get(key)
|
|
217
|
-
rejected.push({
|
|
218
|
-
key,
|
|
219
|
-
reason: virtual && Array.isArray(virtual.keys)
|
|
220
|
-
? `该字段由 ${virtual.keys.join(' + ')} 组成,请分别提交`
|
|
221
|
-
: '未知的设置项',
|
|
222
|
-
})
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
// 复合字段的子键一律按文本校验:目前唯一的复合字段是模型选择,
|
|
226
|
-
// provider 与 model 都是字符串(新增复合字段时在这里补类型判断)。
|
|
227
|
-
if (Array.isArray(spec.keys)) {
|
|
228
|
-
if (typeof raw !== 'string') {
|
|
229
|
-
rejected.push({ key, reason: '需要字符串' })
|
|
230
|
-
continue
|
|
231
|
-
}
|
|
232
|
-
if (raw.trim().length > 500) {
|
|
233
|
-
rejected.push({ key, reason: '太长(上限 500 字符)' })
|
|
234
|
-
continue
|
|
235
|
-
}
|
|
236
|
-
values[key] = raw.trim()
|
|
237
|
-
continue
|
|
238
|
-
}
|
|
239
|
-
if (spec.kind === 'switch') {
|
|
240
|
-
if (typeof raw !== 'boolean') {
|
|
241
|
-
rejected.push({ key, reason: '需要布尔值' })
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
244
|
-
values[key] = raw
|
|
245
|
-
continue
|
|
246
|
-
}
|
|
247
|
-
if (spec.kind === 'number') {
|
|
248
|
-
const num = typeof raw === 'number' ? raw : Number(raw)
|
|
249
|
-
if (!Number.isFinite(num)) {
|
|
250
|
-
rejected.push({ key, reason: '需要数字' })
|
|
251
|
-
continue
|
|
252
|
-
}
|
|
253
|
-
const min = spec.min ?? -Infinity
|
|
254
|
-
const max = spec.max ?? Infinity
|
|
255
|
-
values[key] = Math.min(max, Math.max(min, num))
|
|
256
|
-
continue
|
|
257
|
-
}
|
|
258
|
-
if (spec.kind === 'seg') {
|
|
259
|
-
if (typeof raw !== 'string' || !(spec.options ?? []).includes(raw)) {
|
|
260
|
-
rejected.push({ key, reason: `可选:${(spec.options ?? []).join(' / ')}` })
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
values[key] = raw
|
|
264
|
-
continue
|
|
265
|
-
}
|
|
266
|
-
// text
|
|
267
|
-
if (typeof raw !== 'string') {
|
|
268
|
-
rejected.push({ key, reason: '需要字符串' })
|
|
269
|
-
continue
|
|
270
|
-
}
|
|
271
|
-
const text = raw.trim()
|
|
272
|
-
if (text.length > 2000) {
|
|
273
|
-
rejected.push({ key, reason: '太长(上限 2000 字符)' })
|
|
274
|
-
continue
|
|
275
|
-
}
|
|
276
|
-
values[key] = text
|
|
277
|
-
}
|
|
278
|
-
return { ok: rejected.length === 0, values, rejected }
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** 设置页需要的完整载荷。 */
|
|
282
|
-
export interface SettingsPayload {
|
|
283
|
-
groups: SettingGroup[]
|
|
284
|
-
/** 当前生效值。 */
|
|
285
|
-
values: Record<string, unknown>
|
|
286
|
-
/** 出厂默认值(「恢复默认」和"是否改过"的判断都靠它)。 */
|
|
287
|
-
defaults: Record<string, unknown>
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** 组装 GET /settings 的响应体。 */
|
|
291
|
-
export function buildSettingsPayload(
|
|
292
|
-
values: Record<string, unknown>,
|
|
293
|
-
defaults: Record<string, unknown>,
|
|
294
|
-
): SettingsPayload {
|
|
295
|
-
return { groups: SETTINGS_GROUPS, values, defaults }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** 旧设置文件的底层配置继续生效,但不再暴露为设置控件或接受 UI 写入。 */
|
|
299
|
-
export const PERSISTED_SETTING_KEYS = ['maxTokens', 'temperature', 'resultCacheTtlMs']
|
|
300
|
-
export function normalizeStoredSettings(input: unknown): Record<string, unknown> {
|
|
301
|
-
const values = normalizeSettingsPatch(input).values
|
|
302
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) return values
|
|
303
|
-
const raw = input as Record<string, unknown>
|
|
304
|
-
const bounds: Record<string, [number, number]> = { maxTokens: [0, 32000], temperature: [-1, 2], resultCacheTtlMs: [0, 86400000] }
|
|
305
|
-
for (const key of PERSISTED_SETTING_KEYS) {
|
|
306
|
-
const value = raw[key]
|
|
307
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
|
308
|
-
const [min, max] = bounds[key]!
|
|
309
|
-
values[key] = Math.min(max, Math.max(min, value))
|
|
310
|
-
}
|
|
311
|
-
// chatProvider/chatModel 已退役,不迁入统一的 provider/model。
|
|
312
|
-
return values
|
|
313
|
-
}
|