lume-dsh-plugin 0.5.0 → 0.6.1

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 CHANGED
@@ -14,12 +14,13 @@
14
14
  - **真实工具结果验证**——监听工具成败与结果未知;失败或未知不允许报完成,交付时区分「已验证 / 未验证 / 推测有效」
15
15
  - **会话内自愈与复盘回环**——相同请求连续失败自动注入归因纠偏;跨会话聚合反思评分,对持续低分的维度定向提醒
16
16
  - **上下文压缩感知**——压缩发生后重锚状态,提醒模型摘要不是完整历史,细节依赖先确认
17
+ - **文档能力感知**——按当前环境探测文档工具:有就要求先读后写、交付前回读验证;没有就如实说明边界,不用文本读取或脚本硬解二进制办公文件
17
18
 
18
19
  ### 能力二:人设系统(人设即人)
19
20
 
20
21
  塑造「以何种风格表达」:具名角色、长期记忆、随对话演进:
21
22
 
22
- - **聊天记录蒸馏**——从微信/QQ 导出记录蒸馏角色卡,语气、口头禅与回复篇幅锚定真实对话统计
23
+ - **素材蒸馏**——从聊天记录、小说、剧本、人物设定文档蒸馏角色卡,语气、口头禅与回复篇幅锚定真实素材统计
23
24
  - **长期记忆与生命周期**——事件记忆 + 故事记忆;相对时间的临时记忆自动过期,长期事实不受影响
24
25
  - **双向反馈闭环**——负面反馈自动转成风格约定,被认可的回复摘录为语料,语气随使用收敛
25
26
  - **记忆星图与角色卡导入导出**——可视化记忆、行内编辑,卡片可分享迁移
@@ -27,7 +28,7 @@
27
28
  人设只影响自然语言表达,不介入任务执行,也不影响代码、命令与工具调用的结果。
28
29
 
29
30
  [![CI](https://github.com/cayan0x/Lume/actions/workflows/ci.yml/badge.svg)](https://github.com/cayan0x/Lume/actions/workflows/ci.yml)
30
- [![Version](https://img.shields.io/badge/version-0.5.0-blue)](./CHANGELOG.md)
31
+ [![Version](https://img.shields.io/badge/version-0.6.1-blue)](./CHANGELOG.md)
31
32
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
32
33
 
33
34
  *人设系统:内置角色卡、蒸馏与管理入口,以及记忆星图*
@@ -77,6 +78,14 @@
77
78
 
78
79
  这三层共同形成“常规协议 → 检测问题 → 临时强化 → 成功解除 → 跨会话复盘”的闭环,在需要深度处理时增加约束,在简单问题上控制 Token 消耗。
79
80
 
81
+ ### 文档能力感知(与文档工具插件协作)
82
+
83
+ DSH 本身不带 Office / PDF 读写能力:附件只接受图片,工具名册里没有任何文档工具。模型面对 `.docx`、`.xlsx` 这类二进制容器时,只能在“当文本读”“现场解压 zip”“手写解析脚本”之间瞎试——慢,而且几乎必然出错。
84
+
85
+ 微光不重复造这套工具,而是**做能力探测与约束**:注入前查询宿主的工具注册表,按工具名的能力族前缀(`word_*` / `excel_*` / `ppt_*` / `pdf_*` 等)判断当前环境具备哪些文档能力,再据此分叉——**有工具**时要求走工具而不是自己解析、先读后写、交付前回读验证;**没有工具**时要求第一轮就如实说明边界、不要静默硬解二进制,并给出替代交付方式。用户明确说「就用脚本自己试」时仍然照做,但要先说明代价。
86
+
87
+ 两条都只在**文档任务轮注入**:闲聊、代码、排查类请求零成本,只有请求里出现文件后缀、Word / Excel / PPT / PDF 等格式名,或“写一份报告”这类产物请求时才生效;中文里“查一下官方文档”这种泛称不会被误判成文档任务。判据取自冻结的意图文本,因此在一轮之内稳定,不会像工具结果那样在轮中作废前缀缓存。探测按能力族前缀而非插件名,因此不绑定任何第三方实现——`dsh-office-tools`、`dsh-excel-chat`、`dsh-ppt` 装了哪个都能识别,一个都不装就退化为边界声明模式。
88
+
80
89
  ### 优化清单
81
90
 
82
91
  - [x] 问答、查找、讨论、诊断、执行五类请求路由
@@ -92,6 +101,8 @@
92
101
  - [x] 记忆生命周期:相对时间记忆标记为临时记忆,30 天后自动失效;旧记忆无感兼容
93
102
  - [x] 反思日志跨会话反馈、旧字段迁移和模型感知协议
94
103
  - [x] 上下文压缩感知:识别宿主压缩检查点,压缩后重锚状态并提醒“摘要不是完整历史”
104
+ - [x] 文档能力感知:探测文档工具并按需注入——有工具要求先读后写与回读验证,没工具要求如实说明边界、不硬解二进制
105
+ - [x] 系统提示词轮内稳定:意图冻结(只认真实用户消息)+ 会变的内容走 runtime-context 通道,一轮只产生一份系统提示词
95
106
  - [x] 角色卡算法自动升级且保留记忆、风格和认可语料
96
107
 
97
108
  ### 为什么不接管宿主的历史压缩
@@ -178,7 +189,7 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
178
189
 
179
190
  ## 六、反思日志
180
191
 
181
- 会话结束时,插件在空闲时间跑一次小模型调用,对整段对话的 Codex 工作协议执行情况进行复盘:上下文管理、计划与门控、验证与失败处理、结果复核,各打 0-2 分并附一句中文备注,写入 `lume_reflection` 域。
192
+ 会话结束时,插件在空闲时间跑一次小模型调用,对整段对话的任务执行协议执行情况进行复盘:上下文管理、计划与门控、验证与失败处理、结果复核,各打 0-2 分并附一句中文备注,写入 `lume_reflection` 域。
182
193
 
183
194
  升级到 0.4.0 时,旧版反思日志会在域打开后自动从旧字段迁移到新字段;迁移幂等,不影响角色卡、记忆或会话。
184
195
 
@@ -198,13 +209,14 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
198
209
 
199
210
  | 注入段 | 无优化 | 优化后 | 使用的算法 |
200
211
  |---|---|---|---|
201
- | Codex 工作协议 | ~500 | 闲聊约 100;任务约 500 | 普通闲聊短版注入;代码/复杂任务自动切换完整版 |
212
+ | 任务执行协议 | ~500 | 闲聊约 100;任务约 500 | 普通闲聊短版注入;代码/复杂任务自动切换完整版 |
202
213
  | 人设契约 | ~350 | ~250 | 契约精简 |
203
214
  | 语料示例 | 6 条 ~600 | 稳态 2 条 ~200 | 少样本衰减 `max(2, 6−轮数)` |
204
215
  | 工具定义 ×3 | ~600 | ~450 | description 精简 |
205
216
  | 记忆 | 15 条 ~350 | core + top5 ~120 | 相关性检索(本地分词 + mini-IDF,零成本) |
206
217
  | 风格层 | 10 条 ~250 | top5 ~120 | 同上 |
207
218
  | 身份 | ~80 | ~80 | 恒注入 |
219
+ | 文档能力指引 | 常驻 ~120 | 文档任务轮 ~120,其余 0 | 工具能力探测 + 按轮触发 |
208
220
 
209
221
  - **成熟态稳态约 1,570 tok/请求,较无优化降低 39%**;缓存友好分层(静态内容前置于易变内容)叠加前缀缓存后,有效成本可再降约一个数量级
210
222
  - 相比 v0.2.0(约 1,400 tok),v0.3.0 全部新功能的稳态净增仅约 **170 tok/请求**
@@ -222,7 +234,7 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
222
234
  | `extractionCooldownMs` | 600000 | 被动提取冷却(毫秒) |
223
235
  | `extractionProvider` / `extractionModel` | 回落主对话 | 提取专用模型档(可仅配置其一) |
224
236
  | `distillProvider` / `distillModel` | 回落主对话 | 蒸馏专用模型档(可仅配置其一) |
225
- | `reflectionEnabled` | `true` | 会话结束时运行 Codex 工作协议反思评估,写入 `lume_reflection` 域 |
237
+ | `reflectionEnabled` | `true` | 会话结束时运行任务执行协议反思评估,写入 `lume_reflection` 域 |
226
238
 
227
239
  ## 存储
228
240
 
@@ -249,7 +261,7 @@ dsh plugin add lume-dsh-plugin
249
261
  若网络无法访问 npm,也可直接从仓库安装:
250
262
 
251
263
  ```bash
252
- dsh plugin add github:cayan0x/Lume#v0.4.4
264
+ dsh plugin add github:cayan0x/Lume#v0.6.1
253
265
  ```
254
266
 
255
267
  安装后需**完全重启 DSH(包含托盘进程)**方可加载;启动日志中出现 `lume: 已加载(builtins=loli,senpai,butler,tsundere,none)` 即表示加载成功。构建产物随仓库发布,两种路径都不需要本地构建。
@@ -261,7 +273,7 @@ dsh plugin add github:cayan0x/Lume#v0.4.4
261
273
  ```bash
262
274
  dsh plugin add lume-dsh-plugin # npm(推荐)
263
275
  # 或
264
- dsh plugin add github:cayan0x/Lume#v0.4.4 # GitHub(备选)
276
+ dsh plugin add github:cayan0x/Lume#v0.6.1 # GitHub(备选)
265
277
  ```
266
278
 
267
279
  人设选择、记忆与风格数据存放在 `storages/` 目录,升级不会丢失。
@@ -271,13 +283,13 @@ dsh plugin add github:cayan0x/Lume#v0.4.4 # GitHub(备选)
271
283
  ### 指定其他版本
272
284
 
273
285
  ```bash
274
- dsh plugin add lume-dsh-plugin@0.4.4 # npm 指定版本
286
+ dsh plugin add lume-dsh-plugin@0.6.1 # npm 指定版本
275
287
  dsh plugin add lume-dsh-plugin@latest # npm 最新
276
288
  dsh plugin add github:cayan0x/Lume # GitHub 最新 main
277
- dsh plugin add github:cayan0x/Lume#v0.3.0 # GitHub 任意历史标签
289
+ dsh plugin add github:cayan0x/Lume#v0.6.0 # GitHub 任意历史标签
278
290
  ```
279
291
 
280
- 标签与版本的对应关系见 [CHANGELOG](./CHANGELOG.md)。0.3.6 新增聊天记录蒸馏;0.3.5 导出弹窗 UI 整洁化;0.3.4 反思日志、记忆星图、人设卡片导出/导入,建议始终使用最新标签。
292
+ 标签与版本的对应关系见 [CHANGELOG](./CHANGELOG.md),建议始终使用最新标签。
281
293
 
282
294
  ## 开发
283
295
 
@@ -293,7 +305,7 @@ npm run watch # 客户端 bundle 增量构建
293
305
  ```
294
306
  src/index.ts 宿主入口:注入 + RPC + 工具 + 事件接线
295
307
  src/core/ 纯逻辑:种子采样、检索打分、衰减、对话挖掘、manifest 解析、文本组装
296
- src/host/ 存储(选择/身份)、蒸馏管线、提取器、工具、RPC、注册表
308
+ src/host/ 存储(选择/身份)、蒸馏管线、提取器、工具、协议与文档能力、RPC、注册表
297
309
  src/client/ 前端:人设菜单、蒸馏弹窗、管理弹窗(插槽 conversation.input.left)
298
310
  lib/ 构建产物(随仓库提交,GitHub 安装路径依赖它)
299
311
  test/ vitest 单元测试 + storage 栈集成测试(含带数据重开域回归)
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 文档能力感知:把「这一轮是不是在做办公文档」与「当前环境有没有文档工具」
3
+ * 合成一条按需注入的指引。
4
+ *
5
+ * 为什么需要它:DSH 本身不带文档读写能力——附件只接受光栅图片,工具名册里
6
+ * 没有任何 Office / PDF 工具。模型面对 .docx / .xlsx 这类二进制容器时,只能
7
+ * 在「当文本读」、「现场解压 zip」、「手写解析脚本」之间瞎试,慢且几乎必然出错;
8
+ * 而第三方文档工具插件(dsh-office-tools、dsh-excel-chat、dsh-ppt 等)装与
9
+ * 不装由用户自由决定,插件不能假定它存在。所以指引按探测结果分叉:
10
+ *
11
+ * - 有工具:约束「走工具、先读后写、交付前回读验证」;
12
+ * - 没工具:约束「如实说明能力边界,不要静默硬解二进制」,并给出替代交付方式。
13
+ *
14
+ * 两条都在「这一轮看起来是文档任务」时才注入,闲聊与非文档轮次零成本——与长
15
+ * 会话护栏、压缩提示同属按需注入族。
16
+ *
17
+ * 探测按工具名的能力族前缀匹配,不按插件名,因此不绑定任何第三方实现:
18
+ * 用户装哪一个文档插件都能被识别;一个都不装就退化为边界声明。
19
+ */
20
+ /**
21
+ * 能力族 → 工具名前缀。用前缀而非白名单,是为了让新出现的文档插件无需改动
22
+ * 这里就能被识别(例如 word_* / excel_* / ppt_* / pptd_* / slides_* / pdf_*)。
23
+ */
24
+ const FAMILY_PREFIXES = [
25
+ { family: "word", re: /^(?:word|docx?|doc)_/i },
26
+ { family: "excel", re: /^(?:excel|xlsx?|sheet|spreadsheet)_/i },
27
+ { family: "slides", re: /^(?:pptx?|pptd?|slide|slides|deck|presentation)_/i },
28
+ { family: "pdf", re: /^pdf_/i },
29
+ ];
30
+ const FAMILY_LABEL = {
31
+ word: "Word",
32
+ excel: "Excel",
33
+ slides: "PPT",
34
+ pdf: "PDF",
35
+ };
36
+ /** 明确的办公文件后缀:出现即视为文档任务,误判率最低。 */
37
+ const DOC_FILE_RE = /\.(?:docx?|xlsx?|pptx?|pdf)\b/i;
38
+ /** 办公套件 / 格式名:用户点名了产出格式(含不带点的 docx / xlsx / pptx)。 */
39
+ const DOC_SUITE_RE = /\bword\b|\bexcel\b|\bppt\b|\bpptx\b|\bpdf\b|\bdocx\b|\bxlsx\b|电子表格|工作簿|幻灯片|演示文稿|spreadsheet|presentation/i;
40
+ /** 产生或改动文件的动作。 */
41
+ const DOC_ACTION_RE = /写|撰写|生成|制作|做一?[份个张]|创建|新建|导出|输出|保存为|另存为|整理成|汇总成|排版|转换|转成|转格式|填表|填写/;
42
+ /** 以文件形态交付的产物名。 */
43
+ const DOC_ARTIFACT_RE = /文档|文件|报告|汇报|合同|简历|论文|纪要|表格|表单|报表|标书|方案书|提案|周报|月报|季报/;
44
+ export function detectDocumentCapabilities(toolNames) {
45
+ const caps = { word: false, excel: false, slides: false, pdf: false };
46
+ for (const raw of toolNames) {
47
+ const name = String(raw ?? "").trim();
48
+ if (!name)
49
+ continue;
50
+ for (const { family, re } of FAMILY_PREFIXES) {
51
+ if (re.test(name))
52
+ caps[family] = true;
53
+ }
54
+ }
55
+ return caps;
56
+ }
57
+ export function hasDocumentCapability(caps) {
58
+ return caps.word || caps.excel || caps.slides || caps.pdf;
59
+ }
60
+ function probeNames(tools, scope) {
61
+ try {
62
+ const schemas = tools?.schemas?.(scope);
63
+ if (!Array.isArray(schemas))
64
+ return detectDocumentCapabilities([]);
65
+ return detectDocumentCapabilities(schemas.map((schema) => String(schema?.name ?? "")));
66
+ }
67
+ catch {
68
+ return detectDocumentCapabilities([]);
69
+ }
70
+ }
71
+ /**
72
+ * 探测当前环境可见的文档工具。
73
+ *
74
+ * 先按调用方作用域探测(尊重单个 agent 的工具限制),为空时退回全局视图:作用域
75
+ * 参数在宿主版本漂移时可能被忽略,退回可以避免「明明有工具却报告没有」这种最坏的
76
+ * 误判——那等于给模型一条与事实相反的边界声明。任何异常均按「没有文档工具」处理:
77
+ * 宁可保守地如实说明边界,也不凭空承诺一项不存在的能力。
78
+ */
79
+ export function probeDocumentCapabilities(tools, scope) {
80
+ const scoped = probeNames(tools, scope);
81
+ if (hasDocumentCapability(scoped) || scope === undefined)
82
+ return scoped;
83
+ return probeNames(tools, undefined);
84
+ }
85
+ function familyList(caps) {
86
+ return Object.keys(FAMILY_LABEL)
87
+ .filter((key) => caps[key])
88
+ .map((key) => FAMILY_LABEL[key])
89
+ .join(" / ");
90
+ }
91
+ function buildRouting(caps) {
92
+ return `〔文档任务〕本轮要产出或改动办公文档(本会话可用:${familyList(caps)})。这类文件是二进制容器,不要用文本读取或 shell 命令去解析内容,也不要现场解压 zip、手写解析脚本——直接调用已经存在的文档工具。
93
+ 顺序是先读后写:先读出目标文件的现有内容和结构,再最小范围地创建或更新;没有读过目标文件就不要整份覆盖。
94
+ 交付前必须回读一次生成结果,确认关键内容确实写进去了。没有回读证据不要说“已生成 / 已完成”,也不要声称做过未执行的检查(例如用 Office 逐页打开核对)。`;
95
+ }
96
+ function buildBoundary() {
97
+ return `〔文档任务·能力边界〕本轮涉及办公文档(Word / Excel / PPT / PDF),但当前环境没有任何文档读写工具:DSH 的附件只接受图片,工具名册里也没有 Office / PDF 工具。这些格式是二进制容器,用文本读取、解压 zip 或手写解析脚本去取内容既慢又几乎必然出错——不要静默尝试。
98
+ 正确做法是第一轮就说明这个边界,并给出可行选项:改用 Markdown / CSV / HTML 等纯文本格式交付(用户可自行另存为所需格式),或由用户安装文档工具插件后重开会话再直接产出文件。
99
+ 如果用户明确要求「就用命令行或脚本自己试」,可以照做,但先说明代价与不确定性,再动手,并如实报告结果。`;
100
+ }
101
+ /**
102
+ * 注入判据刻意保守,两个信号分叉:
103
+ *
104
+ * - **点名了格式或文件**(`.docx`、Word、Excel、PPT、PDF…):无论有没有工具都值得
105
+ * 说话——有工具就走工具,没工具就如实说明边界。
106
+ * - **只是模糊的产物请求**(「写一份季度报告」):只有存在工具时才提示走工具;
107
+ * 没有工具时不注入,因为退回纯文本交付本来就是正确结果,多说一句反而误导。
108
+ *
109
+ * 反过来,「查一下官方文档」这类常见的「文档」泛称不会被误判——中文里的「文档」
110
+ * 绝大多数指技术文档,误注入会让每一轮问答都平白多背一段无关约束。
111
+ */
112
+ export function buildDocumentDirective(input) {
113
+ const text = String(input.query ?? "").trim();
114
+ if (!text)
115
+ return null;
116
+ const officeIntent = DOC_FILE_RE.test(text) || DOC_SUITE_RE.test(text);
117
+ if (!hasDocumentCapability(input.capabilities))
118
+ return officeIntent ? buildBoundary() : null;
119
+ const artifactIntent = officeIntent || (DOC_ACTION_RE.test(text) && DOC_ARTIFACT_RE.test(text));
120
+ return artifactIntent ? buildRouting(input.capabilities) : null;
121
+ }
@@ -5,8 +5,15 @@ const MODE_RULES = {
5
5
  diagnosis: "当前模式:诊断。先说明现象、证据、可能根因和验证办法;除非用户明确要求修复,不越权修复,不要越过诊断边界动手。",
6
6
  execute: "当前模式:执行。先确认目标和完成标准,再做最小变更;交付时明确列出“已完成、已验证、未验证、残留副作用”,不要用动作完成冒充目标达成。",
7
7
  };
8
- const EXECUTE_RE = /(?:请|帮我|直接|开始|去|把|给我|完成|实现|修改|修复|优化|写入|删除|创建|运行|执行|部署|安装|迁移|提交|发布).*(?:做|改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布)|^(?:做|改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布)/i;
9
- const DIAGNOSIS_RE = /为什么|原因|问题在哪|哪里不对|诊断|排查|分析一下|评估一下|是不是.*问题|能不能解释/i;
8
+ // 显式请求标记后必须紧跟一个动作动词,且限制在同一小句内(旧版用 `.*` 贪婪跨越
9
+ // 整句,导致「是什么驱动你去这么做的」也被判成执行)。句首祈使不再放行「做」:
10
+ // 「做一件事…」这类名词化表述是讨论而非执行。
11
+ const EXECUTE_RE = new RegExp([
12
+ "(?:请|帮我|帮忙|直接|把|给我|替我|麻烦|需要你)\\s*[^,。!?;\\n]{0,24}?(?:做|改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布|检查|核对|补|替换|重命名|合并|回滚|加上)",
13
+ "^(?:改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布|检查|补|替换|重命名|合并|回滚)",
14
+ "\\b(?:add|commit|push|pull|merge|rebase|fix|build|rebuild|install|uninstall|deploy|migrate|refactor|rename|update|upgrade|write|create|delete|remove|revert|rollback)\\b",
15
+ ].join("|"), "i");
16
+ const DIAGNOSIS_RE = /为什么|为啥|原因|问题在哪|哪里不对|诊断|排查|分析一下|评估一下|是不是.*问题|能不能解释|怎么会|是什么驱动/i;
10
17
  const DISCUSSION_RE = /讨论|聊聊|怎么看|你觉得|比较一下|方案|取舍|利弊|可能性|有没有更好|先别做|探讨/i;
11
18
  const RESEARCH_RE = /查一下|查找|搜索|检索|资料|文档|来源|证据|最新|核对|确认事实|看一下.*是否/i;
12
19
  /**
@@ -26,12 +33,42 @@ export function classifyInteraction(text) {
26
33
  return "research";
27
34
  return "question";
28
35
  }
36
+ /**
37
+ * 判定一条消息是否出自真实用户。
38
+ *
39
+ * 宿主的 `user/message` 通道混着大量非用户消息:运行时快照
40
+ * (`plugin:@deepseek-ai/dsh-system-prompt`)、工作区指令(`agent-instructions`)、
41
+ * 技能目录(`skill-catalog`)。它们都带 `role: "user"`,只靠角色无法区分——实测
42
+ * 曾被当成“用户当前说的话”,覆盖真实请求并清零工具计数。
43
+ * `source.kind` 缺失时放行,避免在不上报来源的宿主版本上把意图彻底丢掉。
44
+ */
45
+ export function isUserAuthored(message) {
46
+ const m = message;
47
+ if (m?.role !== "user")
48
+ return false;
49
+ const kind = m.source?.kind;
50
+ return kind === "user" || kind === undefined;
51
+ }
29
52
  export function buildInteractionDirective(mode) {
30
53
  return `〔当前请求路由〕${MODE_RULES[mode]}`;
31
54
  }
32
55
  export function taskPhaseForMode(mode) {
33
56
  return mode === "research" ? "research" : mode === "discussion" ? "discuss" : mode === "diagnosis" ? "diagnose" : mode === "execute" ? "execute" : "answer";
34
57
  }
58
+ /**
59
+ * 阶段只前进,不回退到初始的「回答」。
60
+ *
61
+ * 一轮内阶段若被重置回 answer,系统提示词会在轮内变化——宿主的 `request/header`
62
+ * 因内容变化而重新记录,聊天界面每次渲染一行「系统提示词」,前缀缓存也随之作废。
63
+ * 失败后回到 diagnose 是合法回退(不属于「重置为初始态」),因此只拦截 answer。
64
+ */
65
+ export function advancePhase(current, next) {
66
+ if (current === "answer")
67
+ return next;
68
+ if (next === "answer")
69
+ return current;
70
+ return next;
71
+ }
35
72
  export function buildTaskPhaseDirective(phase) {
36
73
  const rules = {
37
74
  answer: "当前阶段:回答。直接处理当前问题,不把普通问答扩张成任务执行。",
@@ -44,13 +81,19 @@ export function buildTaskPhaseDirective(phase) {
44
81
  };
45
82
  return `〔任务阶段〕${rules[phase]}`;
46
83
  }
47
- export function buildToolEvidenceDirective(input) {
48
- if (input.calls === 0)
84
+ /**
85
+ * 工具证据提示:只在出现失败或结果未知时给出,且不带计数。
86
+ *
87
+ * 旧版把「本轮已调用 N 次工具」写进系统提示词段落,N 每步递增——于是每一轮对话
88
+ * 里系统提示词被改写数十次(实测一轮 37 次工具调用产生 28 份不同的系统提示词),
89
+ * 前缀缓存几乎每步作废。计数对模型没有增量信息(工具结果本身就在上下文里),
90
+ * 真正需要提醒的只有「失败/未知不等于完成」。判定改为常量文本后,一轮内至多变
91
+ * 一次,且注册在 runtime-context 通道(不进 system 串、不作废前缀)。
92
+ */
93
+ export function buildToolFailureNotice(input) {
94
+ if (input.failures === 0 && input.unknown === 0)
49
95
  return null;
50
- if (input.failures > 0 || input.unknown > 0) {
51
- return `〔工具证据〕本轮已调用 ${input.calls} 次工具,其中成功 ${input.successes} 次、失败 ${input.failures} 次、结果未知 ${input.unknown} 次。失败或未知结果不能当成完成;先归因或检查实际状态,再决定是否重试。`;
52
- }
53
- return `〔工具证据〕本轮已调用 ${input.calls} 次工具且均返回成功,但工具成功只证明动作执行成功,不等于用户目标已经达成;仍需检查实际结果、兼容性和副作用。`;
96
+ return "〔工具证据〕本轮有工具调用失败或结果未知。失败或未知结果不能当成完成:先归因或检查实际状态,再决定是否重试。";
54
97
  }
55
98
  /**
56
99
  * 长会话不重述整段历史,只提醒模型以最新状态为准。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 反思日志:会话结束时评估对话是否遵守 Codex 风格任务执行协议,写入本地存储。
2
+ * 反思日志:会话结束时评估对话是否遵守任务执行协议,写入本地存储。
3
3
  *
4
4
  * 零用户感知 token:会话结束后(session/disposed)在空闲时间跑一次小模型调用,
5
5
  * 读完对话片段后给四条规则各打 0-2 分并附一句备注,写到 `lume_reflection` 域。
@@ -33,7 +33,7 @@ export class ReflectionStore {
33
33
  await this.#table.put(sessionId, entry);
34
34
  this.#feedbackCache = null; // 新日志可能改变反馈结论,缓存失效
35
35
  }
36
- /** 把旧版 p0~p3 日志迁移为公开的 Codex 协议字段;幂等且只处理旧记录。 */
36
+ /** 把旧版 p0~p3 日志迁移为公开的协议字段;幂等且只处理旧记录。 */
37
37
  async migrateLegacy() {
38
38
  let migrated = 0;
39
39
  for (const key of this.#table.keys()) {
@@ -83,7 +83,7 @@ export class ReflectionStore {
83
83
  }
84
84
  export const REFLECTION_SYSTEM = [
85
85
  "你是一个冷静的复盘评估器。下面会给你一段与用户对话的片段。",
86
- "请评估其中的助手是否遵守了 Codex 风格任务执行协议,每项打 0/1/2 分(0=明显违反,1=一般,2=良好):",
86
+ "请评估其中的助手是否遵守了任务执行协议,每项打 0/1/2 分(0=明显违反,1=一般,2=良好):",
87
87
  "",
88
88
  "上下文管理:是否理解并保留目标、约束、状态、关键决策和已排除假设",
89
89
  "计划与门控:是否拆解任务、先调研再执行,并按风险自适应投入",
@@ -21,6 +21,8 @@ function defaultRuntime() {
21
21
  lastFailureQuery: null,
22
22
  failureStreak: 0,
23
23
  interactionMode: "question",
24
+ intent: null,
25
+ personaCache: null,
24
26
  alignmentCorrection: null,
25
27
  recentUserQueries: [],
26
28
  postTurnReview: null,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 任务执行协议文本(Codex 风格)与分流判据。
2
+ * 任务执行协议文本与分流判据。
3
3
  *
4
4
  * 从 index.ts 抽出:协议是行为约束的核心资产,独立成模块才能被测试直接锁定
5
5
  * (此前埋在插件入口里,任何措辞回归都只能靠人眼发现)。
@@ -10,7 +10,7 @@
10
10
  * - THINKING_REASONING_TEXT 精简协议:推理型模型的任务轮,省掉它天生具备的
11
11
  * 计划/分解条款,只保留行为约束、证据纪律与事实边界
12
12
  */
13
- /** Codex 风格任务执行协议:完整版。 */
13
+ /** 任务执行协议:完整版。 */
14
14
  export const THINKING_TEXT = `[任务执行协议]
15
15
 
16
16
  你应遵循以下公开的工程工作协议。它约束任务如何被完成,不要求输出隐藏的逐步思考过程;对外只给出必要的结论、计划、变更和验证结果。
package/lib/index.js CHANGED
@@ -34,7 +34,8 @@ import { SessionRuntimeStore } from "./host/session-runtime.js";
34
34
  import { isCompactionCheckpoint } from "./host/compaction.js";
35
35
  import { LUME_REFLECTION_SPEC, ReflectionStore, buildReflectionPrompt, parseReflectionScore } from "./host/reflection.js";
36
36
  import { appendLumeLog } from "./host/diag.js";
37
- import { buildAlignmentCorrection, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolEvidenceDirective, classifyInteraction, taskPhaseForMode } from "./host/protocol.js";
37
+ import { advancePhase, buildAlignmentCorrection, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyInteraction, isUserAuthored, taskPhaseForMode } from "./host/protocol.js";
38
+ import { buildDocumentDirective, probeDocumentCapabilities } from "./host/documents.js";
38
39
  import { REASONING_MODEL_RE, TASK_SIGNAL_RE, selectThinkingProtocol } from "./host/thinking.js";
39
40
  /** schemastery → domainTable 形参的桥接(与 identity.ts 同款)。 */
40
41
  const recordSchema = zodLike;
@@ -63,6 +64,10 @@ const LUME_THINKING_ORDER = 1;
63
64
  const LUME_PERSONA_ORDER = 10000;
64
65
  const LUME_BOUNDARY_SECTION = "lume:boundary";
65
66
  const LUME_BOUNDARY_ORDER = 10100;
67
+ /** 工具失败提示:走 runtime-context 通道(渲染成对话尾部的一条消息),
68
+ * 不进 system 串——避免会变的内容每步作废前缀缓存。 */
69
+ const LUME_TOOL_NOTICE_CONTEXT = "lume:tool-notice";
70
+ const LUME_TOOL_NOTICE_ORDER = 10150;
66
71
  const MAX_SESSIONS = 200;
67
72
  const SWITCH_BOUNDARY_TURNS = 2;
68
73
  /** Cordis 插件名 */
@@ -434,19 +439,17 @@ export function apply(ctx, config = {}) {
434
439
  appendLumeLog(`[${sid}] 检测到上下文压缩检查点(第 ${st.turnIndex} 轮)`);
435
440
  break;
436
441
  }
442
+ // 只有真实用户消息能定义本轮意图。宿主快照(@deepseek-ai/dsh-system-prompt)、
443
+ // 工作区指令(agent-instructions)、技能目录(skill-catalog)都经这条通道投递,
444
+ // 曾被当成用户发言:覆盖真实请求,并把模式从「执行」冲成「问答」。
445
+ if (!isUserAuthored(event.data))
446
+ break;
437
447
  const text = messageText(event.data);
438
448
  if (text) {
439
449
  const normalized = text.trim().replace(/\s+/g, " ").slice(0, 240);
440
450
  const explicitCorrection = /不是这个意思|不是我说的|你理解错|答非所问|听不懂|我说的是|我指的是|不对|错了|别这样|重新来/i.test(text);
441
451
  const repeatedRequest = normalized.length >= 5 && st.recentUserQueries.includes(normalized);
442
452
  st.userText = text;
443
- st.lastQuery = text;
444
- st.interactionMode = classifyInteraction(text);
445
- st.taskPhase = taskPhaseForMode(st.interactionMode);
446
- st.toolCalls = 0;
447
- st.toolSuccesses = 0;
448
- st.toolFailures = 0;
449
- st.toolUnknown = 0;
450
453
  st.alignmentCorrection = explicitCorrection
451
454
  ? buildAlignmentCorrection("user-correction")
452
455
  : repeatedRequest
@@ -458,6 +461,8 @@ export function apply(ctx, config = {}) {
458
461
  st.recentTurns.push(`用户: ${text.slice(0, 300)}`);
459
462
  if (st.recentTurns.length > 12)
460
463
  st.recentTurns.shift();
464
+ // 模式/阶段的冻结统一由 resolveIntent(组装时读会话权威历史)负责,
465
+ // 这里只记账:两处都写会让「同一条消息」被判定为不同轮而反复重算。
461
466
  }
462
467
  break;
463
468
  }
@@ -474,7 +479,7 @@ export function apply(ctx, config = {}) {
474
479
  case "tool/call": {
475
480
  st.toolCalls++;
476
481
  if (st.interactionMode === "execute")
477
- st.taskPhase = "execute";
482
+ st.taskPhase = advancePhase(st.taskPhase, "execute");
478
483
  break;
479
484
  }
480
485
  case "tool/result": {
@@ -489,7 +494,7 @@ export function apply(ctx, config = {}) {
489
494
  else
490
495
  st.toolSuccesses++;
491
496
  if (st.interactionMode === "execute")
492
- st.taskPhase = unknownResult || explicitError ? "diagnose" : "verify";
497
+ st.taskPhase = advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
493
498
  break;
494
499
  }
495
500
  case "compaction/summary": {
@@ -525,7 +530,7 @@ export function apply(ctx, config = {}) {
525
530
  ? "〔上轮交付复核〕上一轮执行回复没有给出可见的验证证据。本轮若继续处理同一任务,先确认上轮变更是否真实生效,再继续扩大范围。"
526
531
  : null;
527
532
  if (st.interactionMode === "execute")
528
- st.taskPhase = st.toolFailures > 0 || st.toolUnknown > 0 ? "diagnose" : claimsVerification ? "deliver" : "verify";
533
+ st.taskPhase = advancePhase(st.taskPhase, st.toolFailures > 0 || st.toolUnknown > 0 ? "diagnose" : claimsVerification ? "deliver" : "verify");
529
534
  // 即时对齐只影响当前轮;下一轮重新根据用户消息判断,避免纠偏条款滞留。
530
535
  st.alignmentCorrection = null;
531
536
  // 风格泄漏检测挂在 turn/end(该事件已被窗口机制验证可靠;assistant/message
@@ -654,7 +659,50 @@ export function apply(ctx, config = {}) {
654
659
  }));
655
660
  }, "lume: persona tools");
656
661
  // ── 人设五段式注入 + 切换播报 ──
657
- function buildSessionText(sid) {
662
+ /**
663
+ * 权威意图解析:与宿主会话历史对账,而不是只信事件缓存。
664
+ *
665
+ * 事件投递与首次提示词组装存在时序差——实测同一轮的 step 1 仍带上一轮的模式,
666
+ * step 2 才切到本轮分类,系统提示词因此在轮内变化、前缀缓存每步作废。这里直接
667
+ * 读会话投影出的消息(`deriveMessages`,宿主侧带增量缓存),取最后一条真实用户
668
+ * 消息作为本轮意图;messageId 未变时零成本短路,保证一轮内只冻结一次。
669
+ */
670
+ function resolveIntent(context, st) {
671
+ const session = context?.agent?.session;
672
+ const messages = typeof session?.deriveMessages === "function" ? session.deriveMessages() : [];
673
+ let text = null;
674
+ let messageId = "";
675
+ for (let i = messages.length - 1; i >= 0; i--) {
676
+ const msg = messages[i];
677
+ if (!isUserAuthored(msg))
678
+ continue;
679
+ const candidate = messageText(msg);
680
+ if (!candidate)
681
+ continue;
682
+ text = candidate;
683
+ messageId = String(msg.id ?? "");
684
+ break;
685
+ }
686
+ // 权威会话历史不可用(旧宿主、非标准 session 对象)时退回事件记录的用户文本:
687
+ // 否则意图会永远停在默认的「问答」,比改动前更差。
688
+ if (text === null && st.userText) {
689
+ text = st.userText;
690
+ messageId = `text:${st.userText.slice(0, 120)}`;
691
+ }
692
+ if (text !== null && (st.intent === null || st.intent.messageId !== messageId)) {
693
+ const mode = classifyInteraction(text);
694
+ st.intent = { turnIndex: st.turnIndex, messageId, text };
695
+ st.lastQuery = text;
696
+ st.interactionMode = mode;
697
+ st.taskPhase = taskPhaseForMode(mode);
698
+ st.toolCalls = 0;
699
+ st.toolSuccesses = 0;
700
+ st.toolFailures = 0;
701
+ st.toolUnknown = 0;
702
+ }
703
+ return { text: st.intent?.text ?? st.lastQuery ?? "", mode: st.interactionMode };
704
+ }
705
+ function buildSessionText(sid, context) {
658
706
  if (!currentStore)
659
707
  return "";
660
708
  const st = runtime.get(sid);
@@ -680,18 +728,49 @@ export function apply(ctx, config = {}) {
680
728
  : null;
681
729
  // 播报改由独立的尾部 section 渲染(LUME_BOUNDARY_SECTION),人设段不再内联
682
730
  st.activeBoundary = boundaryText;
683
- const text = buildPersonaSection({
684
- persona,
685
- profileName: personaName ? registry.profileNameOf(personaName) : null,
686
- memories: personaName ? identity?.getMemory(personaName) ?? [] : [],
687
- styleRules: personaName ? identity?.getStyleRules(personaName) ?? [] : [],
688
- corpusPins: personaName ? identity?.getCorpusPins(personaName) ?? [] : [],
689
- query: st.lastQuery,
690
- turnIndex: st.turnIndex,
691
- sessionKey: sid,
692
- boundaryText: null,
693
- config: { sampleCount, sampleMin, memoryInject, styleInject, strategy },
694
- });
731
+ const memories = personaName ? identity?.getMemory(personaName) ?? [] : [];
732
+ const styleRules = personaName ? identity?.getStyleRules(personaName) ?? [] : [];
733
+ const corpusPins = personaName ? identity?.getCorpusPins(personaName) ?? [] : [];
734
+ const { text: query } = resolveIntent(context, st);
735
+ // 一轮一算:注入段按查询做 top-k 检索,查询或记忆每步抖动会让整段改写,
736
+ // 系统提示词随之在轮内变化(实测一轮产生 28 份),前缀缓存每步作废。
737
+ // 指纹覆盖 persona、查询、记忆/风格/语料与注入配置;人设切换、边界窗口、
738
+ // 记忆写入都会改变指纹而自动失效。
739
+ const last = (list, field) => list.length > 0 ? list[list.length - 1]?.[field] : "";
740
+ const key = [
741
+ String(personaName),
742
+ String(persona?.name ?? ""),
743
+ query,
744
+ String(st.turnIndex),
745
+ String(memories.length),
746
+ String(last(memories, "text")),
747
+ String(styleRules.length),
748
+ String(last(styleRules, "rule")),
749
+ String(corpusPins.length),
750
+ String(last(corpusPins, "assistant")),
751
+ String(boundaryText ?? ""),
752
+ String(sampleCount),
753
+ String(sampleMin),
754
+ String(memoryInject),
755
+ String(styleInject),
756
+ strategy,
757
+ ].join("\u0001");
758
+ let text = st.personaCache !== null && st.personaCache.key === key ? st.personaCache.text : null;
759
+ if (text === null) {
760
+ text = buildPersonaSection({
761
+ persona,
762
+ profileName: personaName ? registry.profileNameOf(personaName) : null,
763
+ memories,
764
+ styleRules,
765
+ corpusPins,
766
+ query,
767
+ turnIndex: st.turnIndex,
768
+ sessionKey: sid,
769
+ boundaryText: null,
770
+ config: { sampleCount, sampleMin, memoryInject, styleInject, strategy },
771
+ });
772
+ st.personaCache = { turnIndex: st.turnIndex, key, text };
773
+ }
695
774
  st.lastInjected = personaName;
696
775
  if (greeting)
697
776
  st.switchGreetingPending = false;
@@ -732,7 +811,7 @@ export function apply(ctx, config = {}) {
732
811
  order: personaOrder,
733
812
  text: (context) => {
734
813
  const sid = context.agent?.session?.id ?? context.agent?.id;
735
- return sid ? buildSessionText(String(sid)) : "";
814
+ return sid ? buildSessionText(String(sid), context) : "";
736
815
  },
737
816
  }), "lume.persona-section()");
738
817
  ctx.effect(() => ctx.systemPrompt.section({
@@ -751,7 +830,10 @@ export function apply(ctx, config = {}) {
751
830
  text: (context) => {
752
831
  const sid = context.agent?.session?.id ?? context.agent?.id;
753
832
  const st = sid ? runtime.get(String(sid)) : null;
754
- const query = st?.lastQuery ?? "";
833
+ // 意图取自会话权威历史(resolveIntent),不再读事件缓存里的最新文本:
834
+ // 后者会被注入消息覆盖,且首步可能还带着上一轮的值,导致模式在轮内漂移。
835
+ const intent = st !== null && sid ? resolveIntent(context, st) : null;
836
+ const query = intent?.text ?? "";
755
837
  // 已知推理型模型具备计划能力,任务轮只保留变更、验证、归因和复核约束;
756
838
  // 路由未知时使用完整版,避免误判造成能力退化。
757
839
  const base = selectThinkingProtocol({
@@ -762,17 +844,46 @@ export function apply(ctx, config = {}) {
762
844
  // 才能在「不使用人设」的纯任务会话里也生效。
763
845
  const correction = st?.protocolCorrection;
764
846
  const reflectionHint = reflectionStore?.getFeedback() ?? null;
765
- const route = buildInteractionDirective(st?.interactionMode ?? "question");
847
+ const mode = intent?.mode ?? "question";
848
+ const route = buildInteractionDirective(mode);
766
849
  const longSession = buildLongSessionGuard(st?.turnIndex ?? 0);
767
- const anchor = buildSessionAnchor(st?.turnIndex ?? 0, st?.interactionMode ?? "question", query, st?.recentTurns ?? []);
850
+ const anchor = buildSessionAnchor(st?.turnIndex ?? 0, mode, query, st?.recentTurns ?? []);
768
851
  const alignment = st?.alignmentCorrection;
769
852
  const postTurnReview = st?.postTurnReview;
770
853
  const phase = buildTaskPhaseDirective(st?.taskPhase ?? "answer");
771
- const toolEvidence = st ? buildToolEvidenceDirective({ calls: st.toolCalls, successes: st.toolSuccesses, failures: st.toolFailures, unknown: st.toolUnknown }) : null;
854
+ // 文档能力感知:DSH 本身不带 Office/PDF 读写能力,第三方文档工具插件
855
+ // 装与不装由用户决定。按探测结果分叉——有工具就约束走工具与回读验证,
856
+ // 没工具就约束「如实说明边界、不要硬解二进制」。只在文档任务轮注入,
857
+ // 且判据取自冻结的意图文本,因此轮内稳定,不会在轮中作废前缀缓存。
858
+ const document = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.tools, context.agent) });
859
+ // 工具失败提示已移到 runtime-context 通道(见 lume.tool-notice-context):
860
+ // 它一出现就固定不变,但会变的内容不该待在 system 串里——那会作废前缀缓存。
772
861
  // 压缩重锚:宿主的 preset 隔离域负责压缩,Lume 只能观察事件;
773
862
  // 在压缩后一轮提醒「摘要不是完整历史」。
774
863
  const compactionNotice = st?.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null;
775
- return [base, route, phase, longSession, anchor, alignment, postTurnReview, toolEvidence, compactionNotice, correction, reflectionHint].filter(Boolean).join("\n\n");
864
+ return [base, route, phase, document, longSession, anchor, alignment, postTurnReview, compactionNotice, correction, reflectionHint].filter(Boolean).join("\n\n");
776
865
  },
777
866
  }), "lume.thinking-section()");
867
+ // 工具失败提示走 runtime-context 通道:宿主把它渲染成对话尾部的一条消息,
868
+ // 而不是拼进 system 串。system 串只要变化就会写一条新的 request/header,
869
+ // 既在界面上多出一行「系统提示词」,也让前缀缓存从系统提示词处整段失效。
870
+ // `systemPrompt.context` 是宿主较新版本才有的 API,缺失时静默跳过(旧宿主下
871
+ // 只是失去这条提示,不影响其余功能)。
872
+ ctx.effect(() => {
873
+ if (typeof ctx.systemPrompt?.context !== "function") {
874
+ ctx.logger?.warn?.("lume: 当前宿主不支持 systemPrompt.context,工具失败提示已跳过(不影响其余功能)");
875
+ return;
876
+ }
877
+ return ctx.systemPrompt.context({
878
+ name: LUME_TOOL_NOTICE_CONTEXT,
879
+ order: LUME_TOOL_NOTICE_ORDER,
880
+ text: (context) => {
881
+ const sid = context.agent?.session?.id ?? context.agent?.id;
882
+ const st = sid ? runtime.get(String(sid)) : null;
883
+ if (!st)
884
+ return "";
885
+ return buildToolFailureNotice({ failures: st.toolFailures, unknown: st.toolUnknown }) ?? "";
886
+ },
887
+ });
888
+ }, "lume.tool-notice-context()");
778
889
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lume-dsh-plugin",
3
- "description": "微光 (Lume) — DSH Desktop 增强插件:Codex 风格自适应任务执行协议 + 人设系统(聊天记录蒸馏具名角色、长期记忆、风格纠偏自动捕获)",
4
- "version": "0.5.0",
3
+ "description": "微光 (Lume) — DSH Desktop 增强插件:给会话装上工程纪律与真实关系。纪律层约束「如何正确完成任务」——意图路由、阶段门控、真实工具证据、交付前复核、文档能力感知;人设层塑造「以何种风格表达」——从聊天记录、小说、剧本、设定文档蒸馏具名角色,长期记忆与风格随对话演进。约束按需注入,闲聊不额外付 token。",
4
+ "version": "0.6.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -44,7 +44,12 @@
44
44
  "persona",
45
45
  "personality",
46
46
  "identity",
47
- "memory"
47
+ "memory",
48
+ "protocol",
49
+ "task-execution",
50
+ "distillation",
51
+ "人设",
52
+ "记忆"
48
53
  ],
49
54
  "engines": {
50
55
  "node": ">=20"