@wjm200509/dsh-memory 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -2
- package/lib/client.js +374 -0
- package/lib/command.js +50 -0
- package/lib/dream.js +386 -0
- package/lib/fsutil.js +24 -0
- package/lib/health.js +66 -0
- package/lib/index.js +147 -0
- package/lib/inject.js +89 -0
- package/lib/paths.js +119 -0
- package/lib/prompt.js +133 -0
- package/lib/runner.js +583 -0
- package/lib/selection.js +15 -0
- package/lib/state.js +242 -0
- package/lib/status.js +55 -0
- package/lib/store.js +320 -0
- package/lib/tools.js +226 -0
- package/lib/trigger.js +97 -0
- package/package.json +59 -4
- package/patch.yml +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JiaMingWang-CN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**中文** · 跨会话记忆插件:让 DSH 的 agent 记住犯过的错、做成功的路子和验证过的规则,并在会话结束后自动 **dreaming**——对照会话记录纠错、合并、精简记忆。不修改 DSH 源码,卸载即恢复内置行为(记忆文件保留在原处)。
|
|
4
|
+
|
|
5
|
+
思路来自 Anthropic Engineering《The components of agent memory》:当下用会话记录忠实留痕,用克制的笔记捕捉环境给不了的判断,事后再以原始记录为依据离线整理。
|
|
6
|
+
|
|
7
|
+
## 它做什么
|
|
8
|
+
|
|
9
|
+
| 部分 | 行为 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 记忆笔记 | agent 用 `memory_list` / `memory_read` / `memory_write` / `memory_delete` / `memory_move`(改名或在分区间移动,一步完成)/ `memory_search`(在分区内搜文字)读写记忆;分**项目级**(当前工作目录,默认)和**用户级**(跨项目)两个分区 |
|
|
12
|
+
| 分区准入 | 工具要求声明:新建用户级笔记必须带 `basis: "user-stated"`(用户亲口说的偏好或工作方式)。这个声明由模型自己给出,工具只检查有没有,不核实真假,因此它是对模型的约束而不是安全边界;看似通用的教训先写进项目区并标 `candidate: user`,等跨项目整理确认至少两个项目都成立后再提升 |
|
|
13
|
+
| 大小限制 | 由工具强制:`INDEX.md`(文件名不分大小写)超过上限(默认 4 KB)或单行超过 240 字节时拒绝写入;笔记路径里的 `:`、`*`、`?`、结尾的点或空格、`CON` 这类设备名也会被拒绝;单篇笔记超过 3 KB 时照写,但提示拆分 |
|
|
14
|
+
| 索引注入 | 每个会话开始时注入两个分区的 `INDEX.md`,正文按需读取;会话内的改动下个会话生效 |
|
|
15
|
+
| dreaming | 会话最后一轮结束后空闲 10 分钟,或会话关闭时,在后台起一个隐藏的整理 agent:只给它记忆工具和本项目的会话记录,按"纠错 → 合并 → 索引瘦身 → 修悬空链接 → 补漏记的教训"的顺序整理,最后提交一份整理报告。悬空链接、没挂进索引的笔记、超限的 INDEX 和笔记由代码事先算好放进提示词,不让模型花步数去找 |
|
|
16
|
+
| 跨项目整理 | 唯一能同时看到所有分区的整理:把只属于某个项目的用户级笔记移回该项目,把两个以上项目都成立的教训提升到用户区,去重、拆分混杂的笔记。有项目整理过后每天自动跑一次,设置页「立即 dream」最后也会跑一次 |
|
|
17
|
+
| 设置页 | 设置侧栏「记忆」:总开关、自动 dreaming、空闲阈值、并发数、索引大小上限、dreaming 模型与思考强度、最近运行日志(点「详情」看摘要、改动了哪些笔记、遗留的问题和改动前的备份位置)、待处理数、「立即 dream」 |
|
|
18
|
+
| `/dream` | 立即整理当前项目的记忆,完成后返回摘要 |
|
|
19
|
+
|
|
20
|
+
记忆存放在 `<DSH home>/memory/`(默认 `~/.dsh/memory/`):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
memory/
|
|
24
|
+
user/ 跨项目:偏好、通用教训
|
|
25
|
+
projects/<项目>/ 本项目的规则、陷阱、做法
|
|
26
|
+
.state/ 插件状态:dreaming 队列与日志,以及 history/(每次整理前被改动笔记的原文件)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
都是普通 Markdown,可以直接查看、编辑或删除。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
# 从 npm
|
|
35
|
+
dsh plugin --profile web add -w "@wjm200509/dsh-memory"
|
|
36
|
+
|
|
37
|
+
# 从 GitHub(源码目录)
|
|
38
|
+
dsh plugin --profile web add -w "github:JiaMingWang-CN/dsh-bundles#path:dsh-memory"
|
|
39
|
+
|
|
40
|
+
# 本地目录
|
|
41
|
+
dsh plugin --profile web add -w C:\path\to\dsh-bundles\dsh-memory
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 费用与失败
|
|
45
|
+
|
|
46
|
+
- dreaming 会调用模型、消耗你自己的 token。默认用 DSH 的默认模型,可在设置页选另一个模型并调整思考强度(强度越高越仔细,也越费 token);不想自动运行就关掉「自动 dreaming」,改用 `/dream` 手动触发。
|
|
47
|
+
- **隐私**:整理时模型读到的是你的会话记录。选了别的 provider 的模型,所有项目的会话内容(含其中出现的代码、路径和粘贴的文本)都会发给那个 provider。整理 agent 被要求把会话记录和笔记当数据而非指令,也不得把密钥、令牌、口令等写进笔记,但这些主要是提示词里的约束;代码只拦截几种一眼可认的密钥(私钥块、AWS/GitHub/OpenAI 式 key、Slack 令牌、Google API key),其余没有拦截,请不要在会话里粘贴你不愿被记住的机密。
|
|
48
|
+
- 只有值得整理的会话才排队:至少两轮正常完成的对话,或一轮正常完成且调用过工具(记忆工具不算)。问候、被中止的会话不会触发;它们的活动会累计,等会话真正做了事再一起整理。
|
|
49
|
+
- 每次整理有步数上限(单个项目 40 步,跨项目 80 步)。到上限就停下,已做的改动保留,日志里记一条问题。没读到结尾的会话不会出队:水位线只推进到 agent **最后一次改笔记时**已连续读到的位置(光读不写不算整理,跳着读跳过的部分也不算已读),下一次整理从那里接着读(提示词里会写 `session-x (from seq N)`);连续三次没有任何进展的会话会被放弃并记入日志。为了不让 agent 把步数全花在阅读上,一次项目整理最多读 20 页转录,超出后 `transcript_read` 会提示先写笔记再收尾。已读完的会话不受影响。
|
|
50
|
+
- 长会话每次空闲后重新排队时,只整理水位线之后的新内容,不会从头重读。转录读取按事件分页,单行最多 2000 字符,用户粘贴的大段日志不会撑爆整理 agent 的上下文。
|
|
51
|
+
- 不同项目的 dreaming 并行执行,最多同时跑「并发数」个(默认 4,设置页可改,设为 1 即全局串行);同一项目始终一次只跑一个。并行的运行同时改同一篇用户级笔记时,后写的一方会遇到版本冲突,重新读取后在新内容上重做,不会覆盖对方。
|
|
52
|
+
- 日志区分三种结果:✔ 成功;⚠ 完成但有遗留问题(工具报错、整理 agent 报告没能核对或修好的内容、没提交报告、到了步数上限),批次照常出队;✖ 失败。改动按笔记计数:同一篇写几次只算一次。
|
|
53
|
+
- 失败按原因分开处理。**鉴权或额度类**(401/403、key 无效、额度不足)立即**暂停自动 dreaming**;**瞬时类**(429、5xx、超时、网络中断)不暂停,等 1 分钟、再等 3 分钟后自动重试,连续三波失败才暂停(并行的几个项目同时失败算一波);**手动运行**失败一律暂停。暂停时设置页显示原因和该怎么办,暂停期间结束的会话只排队、不调用模型。排查后点「立即 dream」或运行 `/dream`,成功后按结束顺序补完队列并恢复自动。
|
|
54
|
+
- 某个批次稳定地触发同一类失败(模型拒答 `blocked`、输出被截断 `max-tokens` 等)时,按会话计数;失败两次后该会话单独运行,第三次失败才把它放弃并记入日志,同批的其他会话不受牵连,也不会为此暂停。
|
|
55
|
+
- 项目目录暂时不可达(移动硬盘或网络盘没挂载)时,这次整理被跳过:会话继续排队,10 分钟后再看,既不记失败也不暂停;只有它所在的上级目录确实存在而项目目录没了,才按“项目已删除”处理(项目区只读,仅保留用户亲口说的偏好)。
|
|
56
|
+
- 关闭总开关或卸载插件时,正在跑的整理会被取消,不会继续消耗 token 或写记忆;它们的批次留在队列里。
|
|
57
|
+
- 一次错误之后的自动恢复不依赖你:状态文件损坏时会改名为 `state.json.corrupt-*` 并从空队列重新开始,不会让插件加载失败。
|
|
58
|
+
- 退出应用时正在排队或运行的会话会留在队列里,不会触发暂停。下次启动约 30 秒后会补登漏掉的结束事件,并**立即开始处理积压的队列**(自动 dreaming 开着时);在设置页打开总开关、打开自动 dreaming 或调大并发数,也会立刻开始处理已排队的会话。不想启动时消耗 token,就关掉「自动 dreaming」。
|
|
59
|
+
- 跨项目整理不会在第一次项目整理后立刻触发,而是从那时起算一天;自动触发时,若有笔记的项目少于 2 个且用户区没有笔记,就跳过(手动触发照常运行)。
|
|
60
|
+
- 整理会改动、删除笔记,改动前的原文件按运行保存在 `memory/.state/history/<整理会话 id>/`(保持原目录结构,保留最近 20 次),要撤销就把文件复制回去。设置页日志「详情」里能看到这次运行的摘要、改动的笔记和备份位置。
|
|
61
|
+
|
|
62
|
+
## 已知限制
|
|
63
|
+
|
|
64
|
+
- 只适配 DSH **0.2.2-rc.1**:依赖的会话、agent、提示词接口仍是 rc 版本,升级 DSH 后需要重新验证。
|
|
65
|
+
- 子代理会话(有工作目录的)也会注入两份记忆索引,但换成一小段只读指引,并且 `memory_write` / `memory_delete` / `memory_move` 对它们直接拒绝:子代理的会话不进入整理队列,没人复核它写的笔记,也判断不了分区准入。值得记的东西由它在结果里告诉父代理,由父代理决定。
|
|
66
|
+
- 笔记目录下的空文件夹会在删除笔记时顺手清掉,崩溃遗留的临时文件启动时清理;已删除项目的分区目录不会自动删除(里面可能有你想保留的内容),可自行删除 `memory/projects/` 下对应目录。
|
|
67
|
+
- 提示和界面文案是中文,没有多语言切换。
|
|
68
|
+
- dreaming 使用的隐藏会话保存在 `~/.dsh/sessions/_no-cwd/`,标题固定为「记忆整理 · 项目名」,不会出现在会话列表和搜索里。插件按磁盘上的会话内容识别自己的整理会话,只保留最近 50 个;重装插件、状态重置后留下的旧会话也会被清理,其他隐藏会话不受影响。
|
|
69
|
+
- 启动时只为最近 7 天内**创建**的会话补登漏掉的结束事件;更早创建、仍在使用的会话,若宿主崩溃时有未整理的内容,要等它下次结束或运行 `/dream` 才会处理。
|
|
70
|
+
|
|
71
|
+
## 开发
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
cd dsh-memory
|
|
75
|
+
pnpm install
|
|
76
|
+
pnpm test
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
建议 Node.js 22.15 或更新(`zlib` 才有 zstd);更低版本能加载,只是跳过对 zstd 压缩的整理会话的清理。仓库里的 GitHub Actions 会在每次推送时跑这套测试。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-memory",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
const React = react;
|
|
9
|
+
//#region lib/client.js
|
|
10
|
+
/**
|
|
11
|
+
* dsh-memory — client half: the 记忆 settings page.
|
|
12
|
+
*
|
|
13
|
+
* Every value lives in the `memory` settings form (all fields volatile).
|
|
14
|
+
* The page edits the user-facing fields, shows the host-written
|
|
15
|
+
* `status` (pause, running project, queue, recent runs) and asks for a
|
|
16
|
+
* manual run by writing a fresh `dreamRequest` nonce. Styling follows
|
|
17
|
+
* DSH's own settings pages (alias tokens, settings-card surfaces).
|
|
18
|
+
*/
|
|
19
|
+
const CSS = [
|
|
20
|
+
".dsh-mem{max-width:720px;display:flex;flex-direction:column;gap:20px;color:var(--dsw-alias-label-primary)}",
|
|
21
|
+
".dsh-mem-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}",
|
|
22
|
+
".dsh-mem-h1{margin:0;font-size:16px;font-weight:500;line-height:24px}",
|
|
23
|
+
".dsh-mem-intro{margin:4px 0 0;font-size:14px;line-height:22px;color:var(--dsw-alias-label-tertiary)}",
|
|
24
|
+
".dsh-mem-section{display:flex;flex-direction:column;gap:8px}",
|
|
25
|
+
".dsh-mem-h2{margin:0;font-size:13px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-secondary)}",
|
|
26
|
+
".dsh-mem-card{border:.5px solid var(--dsw-alias-settings-card-stroke);background:var(--dsw-alias-settings-card-fill);border-radius:var(--dsw-radius-xl);padding:4px 14px;display:flex;flex-direction:column}",
|
|
27
|
+
".dsh-mem-card.off{opacity:.5;pointer-events:none}",
|
|
28
|
+
".dsh-mem-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:12px 0}",
|
|
29
|
+
".dsh-mem-row+.dsh-mem-row{border-top:.5px solid var(--dsw-alias-border-l2)}",
|
|
30
|
+
".dsh-mem-label{min-width:0}",
|
|
31
|
+
".dsh-mem-title{font-size:14px;font-weight:500;line-height:22px}",
|
|
32
|
+
".dsh-mem-desc{margin-top:2px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
33
|
+
".dsh-mem-switch{position:relative;box-sizing:border-box;width:36px;height:20px;border-radius:10px;padding:0;cursor:pointer;flex:none;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border:.5px solid var(--dsw-alias-border-l3);transition:background .15s,border-color .15s}",
|
|
34
|
+
".dsh-mem-switch.on{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));border-color:transparent}",
|
|
35
|
+
".dsh-mem-knob{position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}",
|
|
36
|
+
".dsh-mem-switch.on .dsh-mem-knob{transform:translateX(16px)}",
|
|
37
|
+
".dsh-mem-field{box-sizing:border-box;height:32px;padding:0 10px;border-radius:var(--dsw-radius-md);font:inherit;font-size:13px;background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);outline:none;transition:border-color .15s}",
|
|
38
|
+
".dsh-mem-field:focus{border-color:var(--dsw-alias-brand-primary)}",
|
|
39
|
+
".dsh-mem-field:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}",
|
|
40
|
+
".dsh-mem-field.invalid{border-color:var(--dsw-alias-state-error-primary)}",
|
|
41
|
+
"select.dsh-mem-field{width:260px;cursor:pointer}",
|
|
42
|
+
".dsh-mem-number{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-tertiary)}",
|
|
43
|
+
".dsh-mem-number .dsh-mem-field{width:76px;text-align:center}",
|
|
44
|
+
".dsh-mem-status{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 0}",
|
|
45
|
+
".dsh-mem-status-main{display:flex;align-items:center;gap:10px;min-width:0}",
|
|
46
|
+
".dsh-mem-pill{display:inline-flex;align-items:center;gap:6px;flex:none;padding:2px 8px;border-radius:var(--dsw-radius-sm);font-size:12px;line-height:18px;border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary)}",
|
|
47
|
+
".dsh-mem-pill.running{color:var(--dsw-alias-state-business-primary)}",
|
|
48
|
+
".dsh-mem-pill.paused{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}",
|
|
49
|
+
".dsh-mem-dot{width:7px;height:7px;border-radius:50%;flex:none;background:currentColor}",
|
|
50
|
+
".dsh-mem-pill.idle .dsh-mem-dot{background:var(--dsw-alias-state-success-primary)}",
|
|
51
|
+
".dsh-mem-meta{font-size:13px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
52
|
+
".dsh-mem-btn{box-sizing:border-box;height:32px;padding:0 14px;border-radius:var(--dsw-radius-md);font:inherit;font-size:13px;cursor:pointer;flex:none;display:inline-flex;align-items:center;border:.5px solid var(--dsw-alias-border-l3);background:transparent;color:var(--dsw-alias-label-primary);transition:background .15s}",
|
|
53
|
+
".dsh-mem-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
54
|
+
".dsh-mem-btn.primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
|
|
55
|
+
".dsh-mem-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}",
|
|
56
|
+
".dsh-mem-btn.small{height:26px;padding:0 10px;font-size:12px;border-radius:var(--dsw-radius-sm)}",
|
|
57
|
+
".dsh-mem-btn:disabled{opacity:.45;cursor:default}",
|
|
58
|
+
".dsh-mem-banner{margin:0 0 12px;padding:8px 12px;border-radius:var(--dsw-radius-md);font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));background:var(--dsw-alias-state-warn-tertiary,transparent);border:.5px solid var(--dsw-alias-state-warn-secondary,var(--dsw-alias-border-l3))}",
|
|
59
|
+
".dsh-mem-log{display:flex;align-items:center;gap:10px;padding:10px 0;font-size:13px;line-height:20px}",
|
|
60
|
+
".dsh-mem-log+.dsh-mem-log,.dsh-mem-logitem+.dsh-mem-logitem{border-top:.5px solid var(--dsw-alias-border-l2)}",
|
|
61
|
+
".dsh-mem-detail{margin:0 0 10px;padding:8px 12px;border-radius:var(--dsw-radius-md);font-size:12px;line-height:19px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;gap:4px;word-break:break-all}",
|
|
62
|
+
".dsh-mem-detail code{font-family:var(--dsw-font-mono,ui-monospace,monospace);font-size:11.5px}",
|
|
63
|
+
".dsh-mem-log .dsh-mem-dot.ok{background:var(--dsw-alias-state-success-primary)}",
|
|
64
|
+
".dsh-mem-log .dsh-mem-dot.fail{background:var(--dsw-alias-state-error-primary)}",
|
|
65
|
+
".dsh-mem-time{flex:none;width:84px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}",
|
|
66
|
+
".dsh-mem-project{flex:none;max-width:160px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
67
|
+
".dsh-mem-count{flex:none;color:var(--dsw-alias-label-tertiary)}",
|
|
68
|
+
".dsh-mem-result{flex:1;min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
69
|
+
".dsh-mem-result.fail{color:var(--dsw-alias-state-error-primary)}",
|
|
70
|
+
".dsh-mem-log .dsh-mem-dot.warn{background:var(--dsw-alias-state-warn-primary)}",
|
|
71
|
+
".dsh-mem-result.warn{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}",
|
|
72
|
+
".dsh-mem-empty{padding:14px 0;font-size:13px;color:var(--dsw-alias-label-tertiary)}",
|
|
73
|
+
".dsh-mem-path{flex:1;min-width:0;font-family:var(--dsw-font-mono,ui-monospace,monospace);font-size:12px;color:var(--dsw-alias-label-secondary);word-break:break-all}",
|
|
74
|
+
".dsh-mem-pathlabel{flex:none;width:64px;font-size:13px}"
|
|
75
|
+
].join("");
|
|
76
|
+
/** Stable Cordis plugin name; also the settings namespace. */
|
|
77
|
+
const name = "memory";
|
|
78
|
+
const inject = ["slots", "configForms", "remote", "remote.session"];
|
|
79
|
+
|
|
80
|
+
/** Stable select value for one provider/model route ("" is the default model). */
|
|
81
|
+
function modelKey(provider, model) {
|
|
82
|
+
return provider === "" && model === "" ? "" : JSON.stringify([provider, model]);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Model select options: the default model first, then every catalog
|
|
86
|
+
* model, plus the saved route when the catalog no longer offers it.
|
|
87
|
+
* @param {Array<{ id: string, name: string, models: Array<{ id: string, name: string }> }>} groups - `session.modelCatalog` groups.
|
|
88
|
+
*/
|
|
89
|
+
function modelOptions(groups, provider, model) {
|
|
90
|
+
const options = [{ value: "", label: "默认模型(跟随 DSH 默认设置)" }];
|
|
91
|
+
for (const group of groups) {
|
|
92
|
+
for (const entry of group.models) options.push({ value: modelKey(group.id, entry.id), label: `${group.name || group.id} · ${entry.name || entry.id}` });
|
|
93
|
+
}
|
|
94
|
+
const saved = modelKey(provider, model);
|
|
95
|
+
if (saved !== "" && !options.some((option) => option.value === saved)) options.push({ value: saved, label: `不可用:${provider} / ${model}` });
|
|
96
|
+
return options;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reasoning-effort options for the chosen model, from the catalog's
|
|
100
|
+
* `reasoning: { efforts, defaultEffort? }`. Disabled when the default
|
|
101
|
+
* model is used (DSH's default effort applies) or the model has none.
|
|
102
|
+
* @returns {{ disabled: boolean, options: Array<{ value: string, label: string }> }}
|
|
103
|
+
*/
|
|
104
|
+
function effortOptions(groups, provider, model, effort) {
|
|
105
|
+
if (provider === "" || model === "") return { disabled: true, options: [{ value: "", label: "跟随默认模型" }] };
|
|
106
|
+
const reasoning = groups.find((group) => group.id === provider)?.models.find((entry) => entry.id === model)?.reasoning;
|
|
107
|
+
const options = reasoning === undefined
|
|
108
|
+
? [{ value: "", label: "该模型不支持调节" }]
|
|
109
|
+
: [
|
|
110
|
+
{ value: "", label: reasoning.defaultEffort === undefined ? "模型默认" : `模型默认(${reasoning.efforts.find((level) => level.id === reasoning.defaultEffort)?.name ?? reasoning.defaultEffort})` },
|
|
111
|
+
...reasoning.efforts.map((level) => ({ value: level.id, label: level.name || level.id }))
|
|
112
|
+
];
|
|
113
|
+
if (effort !== "" && !options.some((option) => option.value === effort)) options.push({ value: effort, label: `不可用:${effort}` });
|
|
114
|
+
return { disabled: reasoning === undefined && effort === "", options };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What the user should do about a paused state, by why it paused. */
|
|
118
|
+
function pausedHint(paused) {
|
|
119
|
+
if (paused.kind === "auth") return "修好模型或密钥后点「立即 dream」补跑,成功后自动恢复。";
|
|
120
|
+
if (paused.kind === "repeated") return "连续多次失败,多半是网络或服务暂时不可用;稳定后点「立即 dream」补跑,成功后自动恢复。";
|
|
121
|
+
return "排查原因后点「立即 dream」补跑,成功后自动恢复。";
|
|
122
|
+
}
|
|
123
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
124
|
+
/** `MM-DD HH:mm` in local time. */
|
|
125
|
+
function formatTime(ms) {
|
|
126
|
+
const d = new Date(ms);
|
|
127
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
128
|
+
}
|
|
129
|
+
/** Last path segment of a project directory. */
|
|
130
|
+
function projectName(cwd) {
|
|
131
|
+
const trimmed = String(cwd).replace(/[\\/]+$/, "");
|
|
132
|
+
return trimmed.split(/[\\/]/).pop() || trimmed;
|
|
133
|
+
}
|
|
134
|
+
const joinPath = (root, leaf) => `${root}${root.includes("\\") ? "\\" : "/"}${leaf}`;
|
|
135
|
+
const h = React.createElement;
|
|
136
|
+
|
|
137
|
+
function Switch(props) {
|
|
138
|
+
return h("button", {
|
|
139
|
+
type: "button",
|
|
140
|
+
className: "dsh-mem-switch" + (props.on ? " on" : ""),
|
|
141
|
+
role: "switch",
|
|
142
|
+
"aria-checked": props.on ? "true" : "false",
|
|
143
|
+
"aria-label": props.label,
|
|
144
|
+
onClick: props.onToggle
|
|
145
|
+
}, h("span", { className: "dsh-mem-knob" }));
|
|
146
|
+
}
|
|
147
|
+
function Row(props) {
|
|
148
|
+
return h("div", { className: "dsh-mem-row" },
|
|
149
|
+
h("div", { className: "dsh-mem-label" },
|
|
150
|
+
h("div", { className: "dsh-mem-title" }, props.title),
|
|
151
|
+
props.desc ? h("div", { className: "dsh-mem-desc" }, props.desc) : null),
|
|
152
|
+
props.control);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Integer input that shows the stored value (also when it arrives after the first render), saves only valid
|
|
156
|
+
* values, marks invalid input and falls back to the stored value on blur.
|
|
157
|
+
*/
|
|
158
|
+
function NumberField(props) {
|
|
159
|
+
const [text, setText] = React.useState(String(props.value));
|
|
160
|
+
React.useEffect(() => setText(String(props.value)), [props.value]);
|
|
161
|
+
const parse = (raw) => {
|
|
162
|
+
const n = Number(raw);
|
|
163
|
+
return raw.trim() !== "" && Number.isInteger(n) && n >= props.min && n <= props.max ? n : null;
|
|
164
|
+
};
|
|
165
|
+
const invalid = parse(text) === null;
|
|
166
|
+
return h("input", {
|
|
167
|
+
type: "number", min: props.min, max: props.max, className: "dsh-mem-field" + (invalid ? " invalid" : ""), "aria-label": props.label, "aria-invalid": invalid ? "true" : "false",
|
|
168
|
+
title: invalid ? `请输入 ${props.min}–${props.max} 之间的整数` : undefined, value: text,
|
|
169
|
+
onChange: (event) => {
|
|
170
|
+
setText(event.target.value);
|
|
171
|
+
const n = parse(event.target.value);
|
|
172
|
+
if (n !== null) props.onCommit(n);
|
|
173
|
+
},
|
|
174
|
+
onBlur: () => setText(String(props.value))
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function Select(props) {
|
|
178
|
+
return h("select", { className: "dsh-mem-field", "aria-label": props.label, value: props.value, disabled: props.disabled, onChange: props.onChange },
|
|
179
|
+
props.options.map((option) => h("option", { key: option.value, value: option.value }, option.label)));
|
|
180
|
+
}
|
|
181
|
+
/** A run's or running entry's target: a project name, or 跨项目 for the cross-project run. */
|
|
182
|
+
function targetName(run) {
|
|
183
|
+
return run.scope === "cross" ? "跨项目" : projectName(run.cwd);
|
|
184
|
+
}
|
|
185
|
+
/** Expanded run: what it said, which notes it touched, what it left open, and where the pre-change copies are. */
|
|
186
|
+
function RunDetail(props) {
|
|
187
|
+
const run = props.run;
|
|
188
|
+
const paths = run.changes?.paths ?? {};
|
|
189
|
+
const touched = [["新增", paths.created], ["修改", paths.modified], ["删除", paths.deleted]].filter(([, list]) => list && list.length > 0);
|
|
190
|
+
const backup = run.dreamSessionId && props.root && touched.length > 0 ? joinPath(joinPath(joinPath(props.root, ".state"), "history"), run.dreamSessionId) : null;
|
|
191
|
+
return h("div", { className: "dsh-mem-detail" },
|
|
192
|
+
run.summary ? h("div", null, run.summary) : null,
|
|
193
|
+
!run.ok && run.error ? h("div", null, `错误:${run.error}`) : null,
|
|
194
|
+
touched.map(([label, list]) => h("div", { key: label }, `${label}:`, h("code", null, list.join("、")))),
|
|
195
|
+
(run.ok ? run.issues ?? [] : []).map((issue, index) => h("div", { key: "i" + index }, `问题:${issue}`)),
|
|
196
|
+
backup ? h("div", null, "改动前的原文件:", h("code", null, backup)) : null,
|
|
197
|
+
!run.summary && !run.error && touched.length === 0 && !(run.issues ?? []).length ? h("div", null, "这次运行没有改动笔记。") : null);
|
|
198
|
+
}
|
|
199
|
+
function LogRow(props) {
|
|
200
|
+
const run = props.run;
|
|
201
|
+
const [open, setOpen] = React.useState(false);
|
|
202
|
+
const issues = run.ok ? run.issues ?? [] : [];
|
|
203
|
+
const changes = `新增 ${run.changes?.created ?? 0} / 修改 ${run.changes?.modified ?? 0} / 删除 ${run.changes?.deleted ?? 0}`;
|
|
204
|
+
return h("div", { className: "dsh-mem-logitem" }, h("div", { className: "dsh-mem-log" },
|
|
205
|
+
h("span", { className: "dsh-mem-dot " + (!run.ok ? "fail" : issues.length > 0 ? "warn" : "ok") }),
|
|
206
|
+
h("span", { className: "dsh-mem-time" }, formatTime(run.at)),
|
|
207
|
+
h("span", { className: "dsh-mem-project", title: run.cwd ?? "" }, targetName(run)),
|
|
208
|
+
h("span", { className: "dsh-mem-count" }, run.scope === "cross" ? "全部分区" : `会话 ${run.sessionIds.length} 个`),
|
|
209
|
+
!run.ok
|
|
210
|
+
? h("span", { className: "dsh-mem-result fail", title: run.error }, `${run.skipped ? "已跳过" : "失败"}:${run.error ?? "未知错误"}`)
|
|
211
|
+
: issues.length > 0
|
|
212
|
+
? h("span", { className: "dsh-mem-result warn", title: issues.join("\n") }, `${changes} · 有 ${issues.length} 个问题:${issues[0]}`)
|
|
213
|
+
: h("span", { className: "dsh-mem-result" }, changes),
|
|
214
|
+
h("button", { type: "button", className: "dsh-mem-btn small", "aria-expanded": open ? "true" : "false", onClick: () => setOpen(!open) }, open ? "收起" : "详情"),
|
|
215
|
+
run.ok || run.skipped ? null : h("button", { type: "button", className: "dsh-mem-btn small", disabled: props.busy || !props.enabled, onClick: props.onRetry }, "重试")),
|
|
216
|
+
open ? h(RunDetail, { run, root: props.root }) : null);
|
|
217
|
+
}
|
|
218
|
+
function PathRow(props) {
|
|
219
|
+
const copy = () => { try { navigator.clipboard.writeText(props.path); } catch { /* clipboard unavailable */ } };
|
|
220
|
+
return h("div", { className: "dsh-mem-log" },
|
|
221
|
+
h("span", { className: "dsh-mem-pathlabel" }, props.label),
|
|
222
|
+
h("span", { className: "dsh-mem-path" }, props.path),
|
|
223
|
+
h("button", { type: "button", className: "dsh-mem-btn small", onClick: copy }, "复制"));
|
|
224
|
+
}
|
|
225
|
+
function StatusCard(props) {
|
|
226
|
+
const status = props.status;
|
|
227
|
+
const running = status.running != null;
|
|
228
|
+
const runningNames = running ? [].concat(status.running).map(targetName).join("、") : "";
|
|
229
|
+
const paused = status.paused != null;
|
|
230
|
+
const pill = running
|
|
231
|
+
? h("span", { className: "dsh-mem-pill running" }, h("span", { className: "dsh-mem-dot" }), "整理中")
|
|
232
|
+
: paused
|
|
233
|
+
? h("span", { className: "dsh-mem-pill paused" }, h("span", { className: "dsh-mem-dot" }), "已暂停")
|
|
234
|
+
: h("span", { className: "dsh-mem-pill idle" }, h("span", { className: "dsh-mem-dot" }), "空闲");
|
|
235
|
+
return h("div", { className: "dsh-mem-card" },
|
|
236
|
+
h("div", { className: "dsh-mem-status" },
|
|
237
|
+
h("div", { className: "dsh-mem-status-main" },
|
|
238
|
+
pill,
|
|
239
|
+
h("span", { className: "dsh-mem-meta" }, running ? `正在整理:${runningNames}` : status.notice ? status.notice.text : status.retryAt ? `暂时失败,将于 ${formatTime(status.retryAt)} 自动重试(待处理 ${status.pendingCount ?? 0} 个)` : `待处理:${status.pendingCount ?? 0} 个会话`)),
|
|
240
|
+
h("button", { type: "button", className: "dsh-mem-btn primary", disabled: running || !props.enabled, title: props.enabled ? undefined : "记忆已关闭", onClick: props.onDream }, "立即 dream")),
|
|
241
|
+
paused ? h("div", { className: "dsh-mem-banner" }, `自动 dreaming 已暂停(${formatTime(status.paused.since)}):${status.paused.reason}。${pausedHint(status.paused)}`) : null);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The 记忆 settings page. Displayed values follow the settings form;
|
|
246
|
+
* writes go through the form, never a local echo.
|
|
247
|
+
*/
|
|
248
|
+
function SettingsPage(props) {
|
|
249
|
+
const host = props.host;
|
|
250
|
+
const read = () => host.getSnapshot().value ?? {};
|
|
251
|
+
const [value, setValue] = React.useState(read);
|
|
252
|
+
React.useEffect(() => host.subscribe(() => setValue(read())), [host]);
|
|
253
|
+
const [groups, setGroups] = React.useState([]);
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
let live = true;
|
|
256
|
+
Promise.resolve(props.loadCatalog?.()).then((response) => {
|
|
257
|
+
if (live && response?.ok) setGroups(response.value.groups);
|
|
258
|
+
}, () => {});
|
|
259
|
+
return () => { live = false; };
|
|
260
|
+
}, [props.loadCatalog]);
|
|
261
|
+
|
|
262
|
+
const enabled = value.enabled !== false;
|
|
263
|
+
const status = value.status ?? {};
|
|
264
|
+
const busy = status.running != null;
|
|
265
|
+
const provider = value.dreamProvider ?? "";
|
|
266
|
+
const model = value.dreamModel ?? "";
|
|
267
|
+
const effort = value.dreamEffort ?? "";
|
|
268
|
+
const efforts = effortOptions(groups, provider, model, effort);
|
|
269
|
+
const log = [...(status.log ?? [])].reverse();
|
|
270
|
+
|
|
271
|
+
const requestDream = () => host.set("dreamRequest", String(Date.now()));
|
|
272
|
+
/* A model change resets the effort: levels differ between models. */
|
|
273
|
+
const chooseModel = (event) => {
|
|
274
|
+
const [nextProvider, nextModel] = event.target.value === "" ? ["", ""] : JSON.parse(event.target.value);
|
|
275
|
+
host.mutate([
|
|
276
|
+
{ op: "set", path: ["dreamProvider"], value: nextProvider },
|
|
277
|
+
{ op: "set", path: ["dreamModel"], value: nextModel },
|
|
278
|
+
{ op: "set", path: ["dreamEffort"], value: "" }
|
|
279
|
+
]);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return h("div", { className: "dsh-mem" },
|
|
283
|
+
h("div", { className: "dsh-mem-head" },
|
|
284
|
+
h("div", null,
|
|
285
|
+
h("h2", { className: "dsh-mem-h1" }, "记忆"),
|
|
286
|
+
h("p", { className: "dsh-mem-intro" }, "让 agent 跨会话记住教训、做法与验证过的规则。关闭后不再注入记忆,也不提供记忆工具。")),
|
|
287
|
+
h(Switch, { label: "记忆", on: enabled, onToggle: () => host.set("enabled", !enabled) })),
|
|
288
|
+
|
|
289
|
+
h("div", { className: "dsh-mem-section" },
|
|
290
|
+
h("h3", { className: "dsh-mem-h2" }, "自动整理(dreaming)"),
|
|
291
|
+
h("div", { className: "dsh-mem-card" + (enabled ? "" : " off") },
|
|
292
|
+
h(Row, {
|
|
293
|
+
title: "自动 dreaming",
|
|
294
|
+
desc: "会话结束后,对照会话记录自动纠错、合并、精简记忆;会消耗模型 token。",
|
|
295
|
+
control: h(Switch, { label: "自动 dreaming", on: value.autoDream !== false, onToggle: () => host.set("autoDream", value.autoDream === false) })
|
|
296
|
+
}),
|
|
297
|
+
h(Row, {
|
|
298
|
+
title: "空闲阈值",
|
|
299
|
+
desc: "会话最后一轮结束后空闲这么久,或会话被关闭,视为会话结束。",
|
|
300
|
+
control: h("span", { className: "dsh-mem-number" },
|
|
301
|
+
h(NumberField, { label: "空闲阈值(分钟)", min: 1, max: 1440, value: value.idleMinutes ?? 10, onCommit: (n) => host.set("idleMinutes", n) }),
|
|
302
|
+
"分钟")
|
|
303
|
+
}),
|
|
304
|
+
h(Row, {
|
|
305
|
+
title: "并发数",
|
|
306
|
+
desc: "最多同时整理几个项目;同一项目始终一次只跑一个。越大越快,同一时刻消耗的 token 也越多。",
|
|
307
|
+
control: h("span", { className: "dsh-mem-number" },
|
|
308
|
+
h(NumberField, { label: "并发数", min: 1, max: 16, value: value.concurrency ?? 4, onCommit: (n) => host.set("concurrency", n) }),
|
|
309
|
+
"个")
|
|
310
|
+
}),
|
|
311
|
+
h(Row, {
|
|
312
|
+
title: "索引大小上限",
|
|
313
|
+
desc: "每个分区的 INDEX.md 允许的最大字节数,也是每次会话开头注入的上限;越大越占上下文。",
|
|
314
|
+
control: h("span", { className: "dsh-mem-number" },
|
|
315
|
+
h(NumberField, { label: "索引大小上限(字节)", min: 512, max: 65536, value: value.indexMaxBytes ?? 4096, onCommit: (n) => host.set("indexMaxBytes", n) }),
|
|
316
|
+
"字节")
|
|
317
|
+
}),
|
|
318
|
+
h(Row, {
|
|
319
|
+
title: "整理模型",
|
|
320
|
+
desc: "可选更便宜的模型以节省 token。",
|
|
321
|
+
control: h(Select, { label: "dreaming 模型", value: modelKey(provider, model), options: modelOptions(groups, provider, model), onChange: chooseModel })
|
|
322
|
+
}),
|
|
323
|
+
h(Row, {
|
|
324
|
+
title: "思考强度",
|
|
325
|
+
desc: "整理需要对照证据做判断,强度越高越仔细,也越费 token。",
|
|
326
|
+
control: h(Select, { label: "思考强度", value: effort, disabled: efforts.disabled, options: efforts.options, onChange: (event) => host.set("dreamEffort", event.target.value) })
|
|
327
|
+
}))),
|
|
328
|
+
|
|
329
|
+
h("div", { className: "dsh-mem-section" },
|
|
330
|
+
h("h3", { className: "dsh-mem-h2" }, "状态"),
|
|
331
|
+
h(StatusCard, { status, enabled, onDream: requestDream })),
|
|
332
|
+
|
|
333
|
+
h("div", { className: "dsh-mem-section" },
|
|
334
|
+
h("h3", { className: "dsh-mem-h2" }, "最近运行"),
|
|
335
|
+
h("div", { className: "dsh-mem-card" },
|
|
336
|
+
log.length === 0
|
|
337
|
+
? h("div", { className: "dsh-mem-empty" }, "还没有运行记录。会话结束后会在这里看到每次整理的结果。")
|
|
338
|
+
: log.map((run, index) => h(LogRow, { key: `${run.at}-${index}`, run, busy, enabled, root: status.root, onRetry: requestDream })))),
|
|
339
|
+
|
|
340
|
+
status.root ? h("div", { className: "dsh-mem-section" },
|
|
341
|
+
h("h3", { className: "dsh-mem-h2" }, "记忆文件"),
|
|
342
|
+
h("div", { className: "dsh-mem-card" },
|
|
343
|
+
h(PathRow, { label: "项目记忆", path: joinPath(status.root, "projects") }),
|
|
344
|
+
h(PathRow, { label: "用户记忆", path: joinPath(status.root, "user") }))) : null);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Client plugin body: stylesheet plus the settings.section page.
|
|
349
|
+
* @param ctx - client root context.
|
|
350
|
+
*/
|
|
351
|
+
function apply(ctx) {
|
|
352
|
+
const host = ctx.configForms.get(name);
|
|
353
|
+
ctx.effect(() => {
|
|
354
|
+
const tag = document.createElement("style");
|
|
355
|
+
tag.textContent = CSS;
|
|
356
|
+
document.head.append(tag);
|
|
357
|
+
return () => tag.remove();
|
|
358
|
+
}, "memory: styles");
|
|
359
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
360
|
+
{ name: "settings.section", id: "memory", order: 26, label: "记忆" },
|
|
361
|
+
() => h(SettingsPage, { host, loadCatalog: () => ctx.remote.session.modelCatalog() })
|
|
362
|
+
));
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
exports.apply = apply;
|
|
366
|
+
exports.inject = inject;
|
|
367
|
+
exports.name = name;
|
|
368
|
+
exports.SettingsPage = SettingsPage;
|
|
369
|
+
exports.modelOptions = modelOptions;
|
|
370
|
+
exports.modelKey = modelKey;
|
|
371
|
+
exports.effortOptions = effortOptions;
|
|
372
|
+
return module.exports;
|
|
373
|
+
}
|
|
374
|
+
});
|
package/lib/command.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/dream`: consolidate the current project's memory now.
|
|
3
|
+
*
|
|
4
|
+
* Scope (design §5.5): the project's queued sessions plus ended sessions not
|
|
5
|
+
* consolidated yet, never the session typing the command. The handler waits
|
|
6
|
+
* for the run and returns its summary. While auto dreaming is paused, a
|
|
7
|
+
* successful run also drains every other project's queue and resumes auto.
|
|
8
|
+
*/
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
|
|
11
|
+
const projectName = (cwd) => basename(cwd.replace(/[\\/]+$/, "")) || cwd;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* User-facing summary of a manual run.
|
|
15
|
+
* @param {import("./runner.js").RunReport} report
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function formatReport(report) {
|
|
19
|
+
if (report.runs.length === 0) return "没有需要整理的会话。";
|
|
20
|
+
const lines = report.runs.flatMap((run) => {
|
|
21
|
+
const issues = run.ok ? run.issues ?? [] : [];
|
|
22
|
+
const target = run.scope === "cross" ? "跨项目" : `${projectName(run.cwd)} · 会话 ${run.sessionIds.length} 个`;
|
|
23
|
+
const head = `${!run.ok ? "✖" : issues.length > 0 ? "⚠" : "✔"} ${target} · `;
|
|
24
|
+
if (!run.ok) return [`${head}失败:${run.error}`];
|
|
25
|
+
return [`${head}新增 ${run.changes.created} / 修改 ${run.changes.modified} / 删除 ${run.changes.deleted}${issues.length > 0 ? ` · 有 ${issues.length} 个问题` : ""}`, ...issues.map((issue) => ` - ${issue}`)];
|
|
26
|
+
});
|
|
27
|
+
/* A batch that was set aside is a failed run too, but it does not pause anything. */
|
|
28
|
+
if (!report.ok && report.runs.some((run) => !run.ok && !run.skipped)) lines.push("自动 dreaming 已暂停。排查原因(模型、密钥或网络)后再运行 /dream 补跑。");
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param ctx - host plugin context (`commands`).
|
|
34
|
+
* @param {{ config: object, runner: { runManual: Function }, reconcile: (scope: { cwd: string, excludeSessionId: string }) => Promise<unknown> }} deps
|
|
35
|
+
*/
|
|
36
|
+
export function installDreamCommand(ctx, { config, runner, reconcile }) {
|
|
37
|
+
ctx.commands.register({
|
|
38
|
+
name: "dream",
|
|
39
|
+
description: "整理当前项目的记忆(对照会话记录纠错、合并、瘦身)",
|
|
40
|
+
async handler({ agent }) {
|
|
41
|
+
if (!config.enabled.get()) return { kind: "error", text: "记忆功能已关闭,可在设置的「记忆」页打开。" };
|
|
42
|
+
const header = agent?.session?.header;
|
|
43
|
+
if (typeof header?.cwd !== "string") return { kind: "error", text: "当前会话没有工作目录,无法确定要整理哪个项目的记忆。" };
|
|
44
|
+
const scope = { cwd: header.cwd, excludeSessionId: header.id };
|
|
45
|
+
await reconcile(scope);
|
|
46
|
+
const report = await runner.runManual(scope);
|
|
47
|
+
return { kind: report.ok ? "success" : "error", text: formatReport(report) };
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|