@shgroup/dsh-serenity-hooks 1.79.2 → 1.79.4

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
@@ -1,56 +1,42 @@
1
1
  # 宁静号 ACC —— 给 DeepSeek Harness 装一个「AI 工作区」
2
2
 
3
- > **一句话**:本插件为 AI 指定一个普通目录作为其工作区,
4
- > AI 在其中工作时即获得**记忆、纪律、工具和边界**——中途更换模型、重启电脑、次日再来,均可继续。
5
- >
6
- > 适配 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(下称 DSH)的**三条发布线**:
7
- > `0.1.7-rc.1` / `0.2.0-rc.1` / `0.2.1-rc.1`(声明为 `^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1`,三档并行支持)。
8
- > 设计动机(为何称"认知容器")见 [docs/cognitive-container-theory.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/cognitive-container-theory.md);本 README 只述**能做什么、怎么用**。
3
+ > **一句话**:本插件把一个普通目录变成 AI 的**工作区**。进去之后,它自带**记忆、纪律、工具和边界**——
4
+ > 中途换模型、重启机器、次日再来,都从上次停下的地方接着干。
9
5
 
10
- **术语定义**(后文沿用下列术语,不再重复解释):
6
+ ## 1 它是什么 / 不是什么
11
7
 
12
- | 术语 | 释义 |
8
+ | | |
13
9
  |---|---|
14
- | **工作区 / CCC** | 一个目录,内含一个空的 `.serenity` 标记文件。DSH 在其它目录运行时,本插件不介入;进入此类目录后自动生效 |
15
- | **插件 / ACC** | = 本仓库(npm 包 `@shgroup/dsh-serenity-hooks`)。为 DSH 增加工具、约束与记忆 |
16
- | **MSM** | 编写在工作区内的可执行小工具(一个脚本 + 一行注册)。AI 通过 `msm("名字", ["参数"])` 调用它们 |
17
- | **SESSION.md** | 工作区内的"工作日志"。AI 将目标、决定、进度写入其中;中断后再启动即从此处继续 |
10
+ | **是** | 一个 **DSH 插件**(npm `@shgroup/dsh-serenity-hooks`):给宿主加工具、加拦截缝、加装配文本 |
11
+ | **是** | 一份**标准**的一个实现 —— 标准在 [serenity-acc-specs](https://github.com/tellmewhattodo/serenity-acc-specs),本仓是它在 DSH 上的适配层 |
12
+ | **不是** | Agent 框架。不定义 agent loop、不选模型、不做 prompt 编排 —— 那是宿主的事 |
13
+ | **不是** | DSH 的替代或分支。**本插件从不修改宿主本体** |
14
+ | **不是** | 模型路由 / 网关。不代理模型请求,也不替你决定用哪个模型 |
15
+ | **不是** | 某个工作区的私有产物。换一个符合标准的实现,同一个工作区照常工作 |
18
16
 
19
- ---
17
+ **支持的宿主档位** = 包内 `package.json` 的 `peerDependencies`(现为 `^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1` 三档并行;加档减档以该文件为准)。
18
+ **当前版本 / 工具数 / 测试规模**:本文一律不写死 —— 版本看 [CHANGELOG.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/CHANGELOG.md),运行态看 `dashboard health`。
20
19
 
21
- ## 1. 它解决什么问题
20
+ ## 2 快速开始(2 分钟)
22
21
 
23
- 直接给出四个日常场景的对照:
24
-
25
- | 场景 | 未安装 | 安装后 |
26
- |---|---|---|
27
- | **AI 中途遗忘自身目标** | 上下文一满,先前的目标与决定全部丢失,须重新交代一遍 | 每个工作区有一本工作日志,AI 每推进一段即写入;上下文满则"更换载体"重来,日志留在原地,工作继续 |
28
- | **AI 越界读取与修改文件** | 它可读取整台机器的文件,包括密钥 | 工作区有围墙:墙内可用,墙外一律拒绝;密钥文件连"读"都不可达 |
29
- | **外出使用** | 只能坐在那台电脑前 | 自带带登录的网页入口(密码或手机验证码二选一),手机可用 |
30
- | **家人经微信提问** | 须教他们安装软件、开启电脑 | 扫一次码接入微信后,家人在微信中发言,AI 以配置的人格回话 |
31
-
32
- ---
33
-
34
- ## 2. 快速开始(2 分钟)
35
-
36
- 需要 Node ≥ 20(或 bun),以及 DSH 0.1.7 或 0.2.0 任一发布线。
22
+ 需要 Node ≥ 20(或 bun),以及一条受支持的 DSH 发布线。
37
23
 
38
24
  ```bash
39
- # 1. 装插件(自动加入 DSH 的 web profile)
25
+ # 1 装插件(自动加入 DSH 的 web profile)
40
26
  dsh plugin --profile web add @shgroup/dsh-serenity-hooks
41
27
 
42
- # 2. 重启 dsh web(插件和网页端界面一起生效)
28
+ # 2 重启 dsh web(插件与网页端界面一起生效)
43
29
  dsh web
44
30
 
45
- # 3. 验证:进到带 .serenity 标记的目录里开一个会话
46
- # · 会话开头会自动带上这个插件的身份说明和技能目录
47
- # · 网页端会话标题旁出现一个状态胶囊(绿点常亮 + SAFE 滑块)
48
- # · 输入 dashboard health,看到工作区三项检查全通过
31
+ # 3 验证:进到带 .serenity 标记的目录里开一个会话
32
+ # · 会话开头自带容器身份与技能目录
33
+ # · 标题旁出现状态胶囊(绿点 + SAFE 滑块)
34
+ # · 输入 dashboard health:P1 / P2 / 配置 三项全通过
49
35
  ```
50
36
 
51
37
  卸载:`dsh plugin --profile web remove @shgroup/dsh-serenity-hooks`
52
38
 
53
- 从源码安装(改代码时使用):
39
+ 从源码安装(改代码时用):
54
40
 
55
41
  ```bash
56
42
  git clone https://github.com/tellmewhattodo/dsh-serenity-plugin.git
@@ -58,402 +44,129 @@ cd dsh-serenity-plugin
58
44
  dsh plugin --profile web add link:$(pwd)/hooks/dsh-serenity-hooks
59
45
  ```
60
46
 
61
- > ⚠️ 不得使用 `dsh plugin add github:...`:该地址指向仓库根目录,非插件包本身,安装后不生效。**改用 npm 或上面的 `link:` 方式。**
62
-
63
- **安全模式**:拨动网页端胶囊中的 SAFE 滑块后,`bash` 即从 AI 的工具清单中**移除**——AI 看不到该工具,也无法调用。开关归使用者:AI 看不见、也打不开。(机制细节见 [§3.2](#32-机械约束ai-绕不过去)。)
64
-
65
- ---
66
-
67
- ## 3. 安装后新增的能力
68
-
69
- ### 3.1 十四个工具(其中三个按条件出现)
70
-
71
- | 工具 | 功能 | 使用场景 |
72
- |---|---|---|
73
- | `container_fs` | 在工作区内管理文件:列目录、查找、复制、移动、新建、追加、在文件管理器中打开 | 需要查看或整理工作区内文件时 |
74
- | `cognitive_trajectory` | **一条认知轨迹**:其身体(SESSION.md)+ 其时间轴。除建/看/切换/原地重建外,还可登记**未来唤醒**与**即时递话** | 任何多步骤工作,第一步即调用它;需要 AI 在未来某刻自动继续时也用它 |
75
- | `dashboard` | 仪表盘:工作区健康检查(三项)、当前时间、等待 | 进入工作区后先行自查;等待外部服务时使用 |
76
- | `container_git` | git 操作:status / commit / push / log / pull / diff | 提交与推送代码;它绝不自动强推 |
77
- | `msm` | 小工具执行入口:`msm("名字", ["参数"])`;名字记不全时返回候选;`inspect=true` 查看用法 | 调用工作区内已注册的任何小工具 |
78
- | `praxis` | 按需向 AI 注入四套"做事方法":输出自检(eap)、设计对齐(neat)、认知连续性(cce)、本体工程(ontology) | 需要它把话说清楚 / 先对齐再动手 / 动手前先把领域结构显式化时 |
79
- | `ontology` | 本体工程**引擎**(可执行者):读取 `.ttl` 本体文件,执行结构校验、生成摘要、检查"声明与实况是否一致"、输出语义审查提示词、输出合规清单,并可写入一份新本体模板 | 需要为某个领域建立或校验本体时。⚠️ 与 `praxis ontology` 是**同一领域的两个面**:前者提供**方法文本**(如何建模),后者**实际执行检查**。路径必须在工作区内 |
80
- | `serenity-skill-creator` | 增强型技能标准(serenity-skill)的创建器:`guide` 编写方法 / `create` 生成骨架(基础链路径由它算)/ `check` 结构检查(三态可分辨:已建模通过 · 普通技能 · 建模了但检查未执行)/ `review` 逐条合规问题清单 / `index` 预览**模型实际会看到**的参考件清单 / `status` 全容器一览 | 新建或改造技能、为技能建本体、需要确认"模型究竟能否看到指定参考件"时 |
81
- | `cheap-worker` | 杂工:委派一个低成本模型的助手执行任务,取回其最终文本。一次调用一个串行子代理;`jobs` 可一次扇出多个并行(上限 10)。**不循环、不校验完成码、不写进度文件** | 大批量、重复性的工作(扫描数十个技能、逐用例回归) |
82
- | `localstore` | 存储密钥与配置(凭据、偏好两个命名空间) | API key、密码集中放一处,不进 git |
83
- | `container_admin` | 机务舱:`config` 域(说明目录/按章详情 + 轨迹约束的 guide/validate/apply/list + 看当前配置)/`msm` 域(小工具注册表 + 手册) | 定义轨迹约束、注册新小工具、查配置怎么填时 |
84
- | `im-bridge` | 向 IM 联系人发消息(目前为微信):发文本、发文件、查已配置联系人、查通道状态。每次成功发送自动写入工作区的消息记录 | 需要 AI 主动给家人/同事发消息时(见 §6.2)。**条件出现**:仅在工作区配置了微信桥时可见 |
85
- | `acc-diag` | ACC 运行态诊断:一次调用输出全报告——当前活跃会话数、各自所属工作区、唤醒时钟的武装态与 tick 次数、唤醒登记表的每一条 | **ACC 维护者专用**。**条件出现**:默认对所有工作区隐藏,仅在工作区配置中点名(`exclusiveTools`)时出现 |
86
- | `diagram` | 绘图:AI 给出一段**标签式源码**,插件**在进程内**将其绘制为 PNG 并**贴入对话**——不启动浏览器、不截图、正常路径不落文件。坐标 = 画布像素、y 向下;写错的图元逐条报错并跳过,其余照常绘制;自带四项检查(块重叠 / 标签溢出 / 悬空端点 / 线压字),**只告警不拦图** | 需要把关系图/示意图直接放进对话时。**条件出现**:实验性 demo,**默认关**(关闭时该工具**根本不注册**,AI 看不到它、不占上下文);开关在设置面板「工具」组,拨动即时生效。中英文与常用符号均可绘制,因字体随插件内置(约 5 MB) |
87
-
88
- > **更名记录**(旧名已彻底停用,无兼容别名;旧会话中出现旧名时,**一律取所在那一组的末项**):
89
- > `cc_fs` → `container_fs` · `cc_git` → `container_git` · `session`+`session_rebuild` → `logbook` → `trajectory` → **`cognitive_trajectory`**(今名) ·
90
- > `acc_kit` → `dashboard` · `acc_msm` → `msm`(执行)+ `container_admin`(管理) · `eap`/`neat`/`cce` → `praxis` ·
91
- > `skiff_admin` → `container_admin role` · `autopilot-trajectory` → `trajectory` → **`cognitive_trajectory`**(今名)。
92
-
93
- ### 3.2 机械约束(AI 绕不过去)
94
-
95
- 这些不依赖提示词劝阻,而是**机制上无法执行**:
96
-
97
- | 约束 | 可观察到的效果 | 设计理由 |
98
- |---|---|---|
99
- | **安全模式** | bash 从工具列表里消失(每一步都同步一次),即使调用也会被兜底拦下 | 走已注册的小工具,比让 AI 自行拼接 shell 命令可靠 |
100
- | **工作区围墙** | 墙内可用,墙外一律拒绝(路径无法解析) | AI 不应接触工作区之外的内容 |
101
- | **黑名单 / 治理文件** | 黑名单可配置;`.serenity` 等治理文件禁止 AI 写入 | 防止 AI 破坏自身所在的"地基" |
102
- | **密钥文件守卫** | `localstore.json` 对**所有**工具拒绝(包括 read/grep/glob) | 密钥值在结构上无法外流 |
103
- | **对外输出守卫** | 对外向会话(受限轨迹 / 重建会话)若答出敏感词,会被打回重答,并告知命中的词与修改方向 | 外部使用者不应看到内部机制 |
104
- | **轨迹提醒** | 持续一段时间后会提醒 AI"把进度写回工作日志",并要求它回一个确认码 | 提醒是机制,不依赖自觉 |
105
- | **工作日志体积提醒** | 工作日志(SESSION.md)超过 200KB 时提醒 AI"停下重写",并附四条重写原则(保留骨架 / 细节移至附件文件 / 可合并的合并 / 其余自行判断) | 日志持续增厚则无人读得完;重写成本低于堆积。阈值按工作区可调(`sessionKeeper.sessionMdMaxKB`,0 = 关) |
106
- | **重建前交接** | 重建上下文前,要求 AI 先把"手头做到哪了 / 还差什么 / 下一步做什么"写入**本轨迹目录下的 `REBUILD-TODO.md`**(固定文件名);重建后的 AI 首先读取该文件并继续 | 上下文被清空,但在办的事不能丢。该便签是**只用一次的交界材料**:无格式义务、随时可丢;超过 10 KB 会提醒清理(`SCRATCH PRUNE`,整份删除亦可) |
107
-
108
- ### 3.3 对外的入口
109
-
110
- | 入口 | 默认端口 | 使用者 |
111
- |---|---|---|
112
- | DSH 主界面 | 3080 | 本机自用(插件不碰这个端口,也不起这个监听器) |
113
- | **网页登录入口** | 3081 | 外部/手机访问完整界面:登录后反向代理到 3080,可配工作区白名单 |
114
- | **微信主动发送入口** | 3082(只绑 127.0.0.1) | 工作区外的脚本/集成用它给微信发消息(公网不可达,所以不需要密钥)。工作区内的 AI 不走这个端口,直接使用 `im-bridge` 工具 |
115
- | **微信桥** | 无需端口(出站长轮询) | 家人在微信里直接和 AI 说话 |
116
-
117
- > 3081 **默认关**(须自行开启);3082 **默认开**(微信桥启用时才有意义)。默认只监听 127.0.0.1 的入口,如需暴露到公网,由使用者自行决定方式(隧道 / 反代 / 端口映射都行),插件不绑定任何特定做法。
118
- >
119
- > 原 3099「受限轨迹调试页」已随 v1.73.0 整条退役 —— 轨迹约束的配置现由 `container_admin config` 直接读写。
47
+ ⚠️ 不要用 `dsh plugin add github:…`:那个地址指向**仓库根**而不是插件包,装了不生效。
120
48
 
121
- > 🔴 **已退役:原「ACP + 对外问答页」(3100)**。该面是本插件早期自造的一套私有 HTTP+HTML 协议外壳
122
- > (无客户端生态),已于 2026-10-15 **整体移除**(连同 `acp-core` / `acp-http` 与相关开关、问答页 key)。
123
- > **要程序化或从手机/远程接入,走宿主自带的官方 ACP server**:`dsh --profile acp` 起一个纯 stdio、
124
- > 零认证的 ACP 服务,再由任一现成 ACP 客户端(编辑器如 Zed;手机端如 Agmente、ACP UI)连接。
125
- > 认证落在**接入层**(隧道 / 反代 / Cloudflare Access),不再由本插件发放 key。
49
+ **工作区 = 一个目录 + 一个标记文件**:
126
50
 
127
- ### 3.4 自动配置:用 opencode 的模型
128
-
129
- 要在 DSH 里用 opencode 的网关(`opencode.ai/zen`),本来除了配路由还得手抄一串请求头——
130
- 这些头是 opencode 用来做**会话亲和路由**的,少写一个 `x-opencode-session`,`/zen/go` 面就直接 400 拒绝。
131
- **插件装好就自动配**,无需手工操作:
132
-
133
- | 使用情况 | 插件行为 |
134
- |---|---|
135
- | 已经配了 opencode 路由,但缺头 | 只补缺的那几个;**手写过的值一律不动**(头名不分大小写,`X-Title` 和 `x-title` 一样算数) |
136
- | 一个 opencode 路由都没有,但环境里有 `OPENCODE_API_KEY` | 自动建 `opencode-go` 路由并带全头(没有 key 的人不受影响,不会平白多出一组模型) |
137
-
138
- - **只管"会话族"的头**:`x-opencode-session` / `X-Session-ID` / `x-opencode-client` / `x-opencode-project`。
139
- 前两个名字是**同一个值的两个别名**——写了其中一个,即以**该值**补上另一个,不会出现两个互相冲突的会话 id
140
- - **注入范围以接口要求为界**:`/zen/go` 缺会话族头即返回 400,故补齐;
141
- `X-Title` / `HTTP-Referer` 属客户端身份头,不在接口要求之内,故不注入。
142
- `User-Agent` 是 DSH 的保留名,想注也注不了
143
- - **会话标识是固定的**(`dsh-serenity`):DSH 的请求头在路由解析时一次算好,只能给静态值。
144
- 优势是同一台机器的请求稳定落同一个上游(对缓存友好);代价是无法做到"按会话分区"
145
- - **写入的是使用者自己的文件**:写入 DSH 的 settings(`llm-pi-ai.providers.<路由>.headers`),随时可改可删。
146
- 不需要这个自动配置时,关闭本插件的 `opencodeProvider.autoConfigure`(关闭后一切回到手抄)
147
- - **不接触密钥**:插件只补路由和头,`OPENCODE_API_KEY` 放环境变量即可
148
-
149
- ---
150
-
151
- ## 4. 一个工作区长什么样
152
-
153
- 工作区是一个普通目录,外加一个标记文件:
154
-
155
- ```
156
- my-workspace/ ← 工作区根目录(放一个 .serenity 就成)
157
- ├── .serenity ← 标记:这个目录是一个工作区
158
- ├── .opencode/
159
- │ ├── serenity.json ← 工作区级配置:助手模型白名单 / 日志阈值 / 子角色
160
- │ └── skills/ ← 领域技能(每个技能 = 一个领域的知识 + 可能有小工具)
161
- │ ├── home-media/ ← 例如:媒体(找片源 / 做字幕 / 推送)
162
- │ ├── home-wealth/ ← 例如:家庭财务
163
- │ └── …(每个技能可以自带脚本)
164
- ├── AGENT_SESSIONS/ ← 工作日志库:每个目录一本 SESSION.md
165
- │ └── 2026-09-08--S142--xxx/
166
- │ ├── SESSION.md ← 目标 / 决定 / 进度(永远留在这里,不会被搬走)
167
- │ └── REBUILD-TODO.md ← 重建前的交接便签(只在需要时存在,可随时丢)
168
- └── _tmp/ ← 运行时落盘:你粘贴的图片和文件
169
- ├── images_from_user/
170
- └── files_from_user/
171
51
  ```
172
-
173
- > 🔴 **技能的两层披露**:助手看到的 skill 目录**那一行即带参考件文件名**(插件按文件树即时生成 ⇒ 新增一个文件它自己就更新,**不用手写**);
174
- > 真要用时它才去 `read` 打开正文 —— **正文不进上下文,除非助手自己去取**。
175
-
176
- ---
177
-
178
- ## 5. 12 个真实用例
179
-
180
- > 以下用例都在真实部署里运行。地址、账号、路径均作了泛化。
181
-
182
- | # | 目标 | 实际路径 |
183
- |---|---|---|
184
- | 1 | **长期项目不断线** | `cognitive_trajectory create` 建轨迹 → 每推进一段写进去 → 中断后 `cognitive_trajectory use` 接上 → 上下文满了 `cognitive_trajectory rebuild` 原地重建并自动继续 |
185
- | 2 | **批量同步代码** | 当前仓库 `container_git commit/push`;多个子仓库一条命令全同步(自动提交 + 推送) |
186
- | 3 | **做一集字幕** | 搜片源 → 下载 → Whisper 转写 → 翻译 → 双语 SRT → 机械质检(7 项)→ 推送订阅/邮件 |
187
- | 4 | **服务器巡检** | 一条命令输出 CPU/内存/GPU/容器/服务报告;重启容器也在同一条白名单通道里 |
188
- | 5 | **内网服务定位** | 仓库全景(分类/技术栈/关联)+ 设备端口扫描 |
189
- | 6 | **家庭财务** | 结构化记录资产/负债/收支/预算,随时查询汇总;房贷利率对比这类宏观跟踪 |
190
- | 7 | **家人档案** | 成员资料统一维护,工作区是唯一真相源 |
191
- | 8 | **想法随手记** | 想到什么就聊,AI 访谈式问清 → 结构化归档 → 定期回顾思考模式 |
192
- | 9 | **手机/外出使用** | 浏览器打开 `http://内网地址:3081` → 输密码或 6 位验证码 → 直接用完整界面 |
193
- | 10 | **粘贴资料自动处理** | 粘图片 → 自动落盘 → 视觉模型识别(快递单/截图/图表);粘 PDF/压缩包 → 自动落盘 → 提取文本/解压/读表格 |
194
- | 11 | **微信里用 AI** | 面板扫一次码 → 家人在微信发消息(文字/语音/图片/文件)→ 路由到指定子角色 → 回复回到微信 |
195
- | 12 | **定时自动工作** | 给某条轨迹放一段 `continuous-re-occurrence.ts`(CRO,见 §6.5)→ 调度器每 5 分钟问它一次"现在该不该醒、醒了说什么"→ 由**程序**决定何时接着干。也可以让轨迹在每轮结束时自己排下一轮 |
196
-
197
- **典型一天**:
198
-
52
+ my-workspace/
53
+ ├── .serenity ← 标记:这个目录是一个工作区(没有它,插件完全不介入)
54
+ ├── .opencode/skills/ ← 领域技能:一个领域一份知识,可自带小工具
55
+ ├── AGENT_SESSIONS/ ← 库房:每条认知轨迹一个目录,SESSION.md 是它的身体
56
+ └── _tmp/ ← 粘进对话的图片与文件落在这里
199
57
  ```
200
- 早上 服务器巡检(一条命令)→ 一切正常
201
- 上午 同步昨天的代码 → 子仓库全部推送
202
- 午间 收到 PDF 账单 → 粘进对话 → 自动落盘 + 表格提取 → 记进财务
203
- 下午 做一集视频字幕(转写 → 翻译 → 双语 SRT → 质检)→ 推送订阅
204
- 晚间 手机登录 3081 处理运维(验证码验证)
205
- 全程 每段工作都落在 SESSION.md 里 → 轨迹连续,随时换人/换模型/换机器接着干
206
- ```
207
-
208
- ---
209
58
 
210
- ## 6. 对外入口详解
59
+ **安全模式**:拨动胶囊里的 SAFE 滑块 ⇒ `bash` 从 AI 的工具清单里**移除** —— 它看不见、也调不动。开关只在人这一侧。
211
60
 
212
- ### 6.1 网页登录入口(3081)
61
+ ## 3 装完多了什么
213
62
 
214
- 插件自行启动第二个监听器,请求流程如下:
215
-
216
- ```
217
- 外部浏览器 → http://内网IP:3081
218
- → 没登录 → 极简登录页(用户名 + 密码,或 6 位动态验证码,二选一;手机端适配)
219
- → 提交 → scrypt 校验 / TOTP 校验 + CSRF 校验 + 账号失败锁定
220
- → 通过 → 下发 HttpOnly cookie(SameSite=Strict,24 小时滑动续期)→ 302 跳转
221
- → 已登录 → 反向代理到 127.0.0.1:3080(改写过 Host/Origin,作为信任栅栏)
222
- → 工作区列表按白名单过滤 + 新建工作区做校验
223
- → WebSocket 升级也转发(101 回写 + 双向错误监听,防止连接被压垮)
224
- ```
225
-
226
- ### 6.2 微信桥
227
-
228
- 工作区级配置(`.opencode/serenity.json` 的 `weixin` 段),**凭据放在工作区的 `localstore.json`**(不以明文进入 git)。
229
- 一个 DSH 进程可以同时承载多个工作区,每个工作区各自对接自己的微信。
230
-
231
- - **扫码绑定**:设置面板 → 微信桥 → 选工作区 → 扫码(手机微信确认)→ 机器人 token 自动写入凭据
232
- - **多账号**:每个账号独立扫码、独立移除
233
- - **接收内容**:文字、语音(微信服务端自带转写,无需额外识别)、图片、文件
234
- (图片和文件会从微信 CDN 下载并解密,落到 `_tmp/weixin-inbound/`,随后把路径告知 AI)
235
- - **正在输入**:处理期间微信会显示"正在输入…"
236
- - **回复精简**:自动剥掉思考过程,微信只看到正文
237
- - **会话保持**:同一条轨迹对应固定会话,重启后恢复历史,不会"失忆"(身份挂在**轨迹**上,不挂在人上)
238
- - **路由**:微信号 → **认知轨迹编码**(精确匹配优先,`*` 兜底)
239
- —— `routes: [{ "user": "*", "trajectory": "qa" }]`
240
- - **主动发消息**(v1.31.0):用 `im-bridge` 工具,AI 自己就能发——
241
- `im-bridge({channel:"weixin", action:"send", user:"yh", text:"内容"})`
242
- (发文件:`action:"send-file"` + `file:"<工作区内的路径>"`,可加 `caption`)。
243
- 没有"目标工作区"参数,因此它**发不错容器**;发出的消息会自动被记录(可见性见 §3.1)。
244
- - **由 AI 自行决定回复方式**(v1.30.10):配置 `"weixin": { "autoReplyWithLastMessage": false }`
245
- 后,插件不再自动把 AI 最后那段话转给用户,而是每轮告知它"你必须自己发",并附上一条可直接照抄的 `im-bridge` 调用。
246
- 适用于需要过程汇报、想分多条发、或者该安静就安静的角色。默认 `true`(保持原行为)。
247
- 该发却没发时,插件会**打回提醒**(最多 2 次);还是不发送,可以再开
248
- `"fallbackOnNoSend": true` 让插件兜底把那轮的话转给用户(记录为 `source: "reply-fallback"`),
249
- 保证"消息不丢"。
250
- - **消息记录**:配一个 `weixin.hook` 脚本,每收/发一条消息就把事件(JSON)交给它,存储位置自行决定。
251
- 记录里 `source: "reply"` 表示"回复用户",`source: "proactive"` 表示"AI 主动发起",
252
- `source: "reply-fallback"` 表示"AI 没发、插件兜底发的"。
253
- - **排障**:`msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"])`
254
-
255
- ### 6.3 轨迹声明(trajectory declaration)
256
-
257
- **轨迹的"声明"只有一个文件**:`trajectory.json`。**目录里有它 ⇒ 这条轨迹受约束**;删掉它,当场恢复成普通轨迹。
258
-
259
- ```jsonc
260
- // AGENT_SESSIONS/2026-10-19--qa--问答/ ← 轨迹目录(编码 = 目录名固定位置那一段)
261
- // ├─ SESSION.md ← 它自己的身体(工作台就是它,不另建)
262
- // ├─ PROMPT.md ← 它的章程(推荐;所有轨迹都适用,不限于受约束的轨迹)
263
- // ├─ trajectory.json ← 声明面(这个文件的存在,就是判据)
264
- // └─ cro-wake-log.log ← CRO 流水(纯文本;只有真的唤起过才会有)
265
- {
266
- "model": "provider/model", // 这条轨迹单独用哪个模型
267
- "msms": ["web-search", "vlm-describe"], // 它能调哪些小工具(MSM)
268
- "tools": ["read", "grep", "glob"], // 它能用哪些平台工具
269
- "cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // 缺省即全 true
270
- "uid": "my-handle", // 可选:旧的只读标识(身份已归轨迹编码)
271
- "cro": { "host": "<机器名>" } // 可选:本轨迹的 CRO 程序只许哪台机器跑
272
- }
273
- ```
274
-
275
- - 两份白名单分开配(小工具 / 平台工具),**没列出来的一律从工具清单里移除**(不是"看得见调不动")
276
- - **`cognitiveTrajectory` 缺省全 `true`**:工作台就是它自己的 `SESSION.md`,关掉等于把它的身体摘了
277
- - 章程写在轨迹目录的 **`PROMPT.md`** 里(跟着轨迹走,归档时一起搬);旧的 `systemPromptFile` 仍被读取,但 `PROMPT.md` 优先
278
- - `container_admin config validate` 校验,`apply` 才真正生效;两者都**实时读盘**,改完下一轮即生效
279
- - 微信路由也指向它:`routes: [{ "user": "*", "trajectory": "qa" }]`(**user → 轨迹编码**)
280
- - 🔴 **旧文件名仍被读取**:`trajectory-constraint.json`(v1.74)/ `skiff.json`(v1.71)都在**读取族**里,
281
- **写只写 `trajectory.json`**,旧文件**永不自动删**。判据 = **认族不认名**(上游改名、下游写死旧名
282
- 是本仓反复发生的失效形态)。详见 [`docs/trajectory-declaration-file.md`](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/trajectory-declaration-file.md)。
283
-
284
- ### 6.4 对外接入(官方 ACP)
63
+ ### 3.1 十四个工具(其中三个按条件出现)
285
64
 
286
- **本插件不提供 ACP 面** —— 既不提供当年那种自造 HTTP 面,也不提供跑在 `dsh web` 进程内的
287
- 那一版(后者已在 v1.69.1 整面删除)。要程序化或远程使用,用宿主自带的官方 ACP server:
65
+ > 这份清单**只在这里**(用户视角:能做什么)。**契约语义**(每个动作必须怎样)在标准的 §4,本文不复制。
288
66
 
289
- | 步 | 做什么 |
67
+ | 工具 | 能做什么 |
290
68
  |---|---|
291
- | 1 | `dsh --profile acp` —— 起官方 ACP 服务(**纯 stdio、零认证**;首次运行自动建 profile) |
292
- | 2 | 用任一 ACP 客户端连它(编辑器内直接起子进程;手机/远程经桥 + 隧道) |
293
- | 3 | **认证放在接入层**(隧道 / 反代 / Cloudflare Access)——官方 server 自身不做认证 |
294
-
295
- > **为什么不自造**:原 3100 面是一个**没有客户端生态**的私有外壳,而官方件已发布并随宿主同版本演进。
296
- > **为什么连进程内那版也删掉**:`dsh-acp` 没有 `session/load`,且 `session/resume` 不回放历史
297
- > ⇒ 客户端每次连上只能新建会话、看不到既有对话。对一个以「长期认知轨迹」为核心的系统,
298
- > 那是结构性障碍,不是体验问题。⇒ 详见 CHANGELOG 的 v1.69.1。
69
+ | `container_fs` | 工作区内的文件操作:列目录 / 树 / 查找 / 复制 / 移动 / 新建 / 追加 / 在文件管理器中打开。往工作区外一步都出不去 |
70
+ | `cognitive_trajectory` | **一条认知轨迹**:它的身体(SESSION.md)+ 时间轴。建 / 看 / 切换 / 原地重建,以及**未来某刻唤醒**(`send-later`)与**即时递话**(`send-now`) |
71
+ | `dashboard` | 常开仪表:`health`(三项自检)/ `time` / `wait` |
72
+ | `container_git` | git:status / commit / push / log / pull / diff。**绝不自动强推** |
73
+ | `msm` | 小工具单入口:`msm("名字", ["参数"])`;记不全给候选;`inspect=true` 看用法 |
74
+ | `praxis` | 按需注入四套方法:`eap`(把话说清)/ `neat`(先对齐再动手)/ `cce`(连续性)/ `ontology`(把领域结构显式化) |
75
+ | `ontology` | 本体工程**引擎**:读 `.ttl`,做结构校验与语义审查、写模板。与 `praxis ontology` 一个说、一个跑 |
76
+ | `serenity-skill-creator` | 增强型技能的创建器:`guide` / `create` / `check` / `review` / `index` / `status` |
77
+ | `cheap-worker` | 杂工:把任务交给低成本模型的子代理并取回最终文本;`jobs` 一次扇出(上限 10) |
78
+ | `localstore` | 凭据与本地偏好:工作区根的 `localstore.json`,默认拒绝入库 |
79
+ | `container_admin` | 机务舱:`config`(说明目录 / 轨迹约束 / 当前配置)+ `msm`(注册表管理) |
80
+ | `im-bridge` ● | 给联系人发消息(微信 / 飞书):文本、文件、联系人、通道状态 |
81
+ | `acc-diag` ● | 运行态诊断:活跃会话 / 唤醒时钟 / 唤醒注册表的全报告。**只读** |
82
+ | `diagram` ● | 标签源码 → 图,**进程内**光栅化并贴进对话(不起浏览器、不落文件) |
83
+
84
+ ● = **按条件出现**。条件不满足时**根本不注册**(AI 连看都看不到,也不占上下文):`im-bridge` 要有已启用的 IM 通道;`acc-diag` 要在工作区配置里逐字点名(`exclusiveTools`,维护者专用);`diagram` 是实验性功能,**默认关**,开关在设置面板「工具」组。
85
+
86
+ > **改名一律硬切、无兼容别名**:`cc_fs` → `container_fs`、`logbook` / `trajectory` → **`cognitive_trajectory`**、`eap` / `neat` / `cce` → `praxis`。读到旧名时按此表换算。
299
87
 
300
- ### 6.5 认知轨迹与定时唤醒(cognitive trajectory)
301
-
302
- `cognitive trajectory`(认知轨迹)是一等概念:一个工作区里可以有任意多条认知轨迹并行。
303
-
304
- **定时唤醒**:用 `cognitive_trajectory send-later` 登记一条"唤醒" = 未来某时刻 + 一条消息——可以给自己预约,也可以唤醒别的轨迹;落在工作区内的 `AGENT_SESSIONS/wake-registry.json`(可读可审计),到点由中心调度器投递,**不阻塞、不等待、也不回执**。
305
-
306
- **即时投递**:用 `cognitive_trajectory send-now` 把一条消息**现在**就递过去,并且**不排队**——目标在内存里且**正在跑轮次**,就 `steer` **当场注入当前轮**;目标空闲就**立即起一轮**;目标不在内存里就**冷载入后投递**(**等效于直接唤醒**,但**不等调度器的 5 分钟节拍**)。它**有同步回执**(告知走了 live 还是冷载入),**不落注册表**(那张表专管"未来时刻")。
307
-
308
- - 两者只差两处:**时刻**(现在 / 未来)与**回执**(有 / 无)——"预约"天生是 fire-and-forget(不可回收、不回执),"递话"则是一次调用一次答复。各自语义干净,不把两种语义塞进同一个动作(🔴 更名:原名 `wake-later` / `send-message`,现名 **`send-later` / `send-now`**——族名取共享词干 `send-`,轴取 `-later`/`-now`)
309
- - ⚠️ **回执只到"已注入 / 已起轮"**:它**不表示**目标已经执行或答复。要确证"目标真的动了",须查看它自己的 `SESSION.md`(**不得凭回执结案**)
310
- - ⚠️ **唯一的例外路径**:`steer` 不可用时**退回排队**(fail-safe:**宁可排队,不可静默丢消息**)——此时回执文案写明"steer 不可用 ⇒ 退回 followup"。**`send-later` 不受影响**:预约的本职是"到点唤起",在跑时排队不打断当前轮,这一行为已经过实证验收
311
- - **目标没打开也能唤醒**:冷载入不了则条目留在登记表里并记下原因,**不静默丢弃**
312
- - **"周期"归工作区自己**:要一轮接一轮,就由**收到唤醒的那条轨迹**在每轮结束时再排一次下一轮——ACC 只提供"到点投递一条消息"这个原语,不替工作区决定节拍(焦点、节拍、偏见都由工作区自定;任意条轨迹并行、互不干扰)
313
- - **轨迹可以带上 skill**(两种写法,**取并集**:容器级在前、轨迹级追加、同名去重):① **一处声明、全容器生效**——在 `.opencode/serenity.json` 写 `cognitiveTrajectory.skills: [名字, …]`(底座),**本工作区的每条轨迹**被绑定期间都注入这些 skill 的全文(**连子角色会话也算**);② **只给这一条**——在它的 `SESSION.md` 顶部 frontmatter 写 `skills: [名字, …]`(这条额外的)。⚠️ 是"**绑定期间一直供给**"而非"`use` 时注入一次"——改了配置或 frontmatter **立即生效**(每请求重新求值,不缓存),也不随对话压缩消失;而 `create` 只新建、**不夺走当前绑定** ⇒ 新轨迹要**显式 `use`** 才挂上。skill 名来自工作区数据,**先过安全校验才会用于拼路径**,找不到会在提示里明说"缺失"而**不静默跳过**;⚠️ **重写 `SESSION.md` 时务必原样保留顶部 frontmatter**(抹掉 = 静默撤销该轨迹的声明)
314
-
315
- #### CRO —— 由轨迹**自行判断何时该被叫醒**
316
-
317
- **问题**:上面那两条都要求**先算好一个时刻**("3 点叫我")。但**该不该醒往往不由时间决定**:某条轨迹应该在"天亮 + 家里有人 + 非高峰"才醒;已经在工作就不该再叫一次;日志快满了,下次叫它时该**要求它先整理**。
88
+ ### 3.2 机械约束(AI 绕不过去)
318
89
 
319
- **做法**:**让一条轨迹自己带一段程序**,由 ACC 在每次检查时运行它,**由这段程序决定"现在该不该叫我、叫我的时候说什么"**。
90
+ > 这是本插件的主要卖点:下面这些**不靠提示词劝阻**,靠机制。每一条你都能在自己那一侧观察到。
320
91
 
321
- | | 内容 |
92
+ | 约束 | 你会看到什么 |
322
93
  |---|---|
323
- | **程序放哪** | `<工作区根>/AGENT_SESSIONS/<轨迹目录>/continuous-re-occurrence.ts`(放**轨迹自己目录**里 ⇒ 跟随轨迹跨载体存活、天然进 git) |
324
- | **开关** | ① **逐轨迹**:**文件在 = 启用,文件不在 = 禁用**(无 enabled 字段、无注册表)② 🔴 **总闸**:设置面板「**CRO(轨迹自编程唤起)**」(`croEnabled`,缺省**开**)——关掉只停 CRO,不影响 `send-later` / `send-now` / 唤醒表投递 |
325
- | **谁跑它** | ACC 的唤醒调度器(既有 **5 分钟** tick) |
326
- | **给它什么** | 一条 stdin 进来的 **JSON 快照**:身份 / 时间 / 轨迹身体(SESSION.md 体积与 mtime、`references/` 清单)/ 绑定与载体(绑定的、live 的、🔴 **正在跑轮次的**)/ 调度面(本轨迹在办唤醒、调度器状态) |
327
- | **它给我什么** | stdout **一行 JSON**:`{"wake":true,"prompt":"…","reason":"…"}` 或 `{"wake":false}`(**缺省 = 不打扰**) |
328
- | **写程序前先读** | `cognitive_trajectory cro-guide` —— **指南 + 一份可直接拿去自测的样例快照** |
94
+ | **工作区围墙** | 墙内可用,墙外一律拒绝 —— 路径连解析都不做 |
95
+ | **安全模式** | `bash` 从工具清单里消失(每一步都同步一次);即使强行调用也被兜底拒绝 |
96
+ | **治理文件与黑名单** | `.serenity` 等治理文件禁止 AI 写入;其余路径黑名单可配置 |
97
+ | **凭据守卫** | `localstore.json` 对**所有**工具拒绝(含 read / grep / glob)⇒ 密钥值在结构上出不去 |
98
+ | **对外输出守卫** | 对外向的会话(受限轨迹 / 重建会话)若答出敏感词 ⇒ **打回重答**,并告知命中的词与修改方向 |
99
+ | **进度提醒** | 做久了提醒 AI「把进度写回工作日志」,并要求它回一个确认码 —— 提醒是机制,不依赖自觉 |
100
+ | **日志体积治理** | `SESSION.md` 超过阈值 ⇒ 提醒重写(保留骨架 / 细节外移 / 可并则并)。阈值 `sessionKeeper.sessionMdMaxKB`,0 = 关 |
101
+ | **重建前交接** | 重建上下文前,先把「做到哪 / 差什么 / 下一步」写进轨迹目录的 `REBUILD-TODO.md`;重建后的自己先读它 |
102
+
103
+ ## 4 对外入口(可选)
104
+
105
+ | 入口 | 默认 | 用途 |
106
+ |---|---|---|
107
+ | DSH 主界面 | 3080 | 本机自用。**插件不碰这个端口**,也不起这个监听器 |
108
+ | 网页登录入口 | 3081(**默认关**) | 手机 / 外网访问完整界面:密码或 6 位动态验证码二选一,按账号失败锁定,登录后反代到 3080 |
109
+ | 微信主动发送口 | 3082(只绑 `127.0.0.1`) | 工作区**外**的脚本用它发微信;工作区内的 AI 不用它,直接用 `im-bridge` |
110
+ | 微信桥 | 无端口(出站长轮询) | 家人在微信里直接跟 AI 说话:扫码绑定,收文字 / 语音 / 图片 / 文件 |
329
111
 
330
- **几条设计上的硬约束**(都不是随便定的):
112
+ - **会话挂在轨迹上**:同一条轨迹固定同一个会话,重启后历史还在(身份在人这一层之外)。
113
+ - **路由 = 微信号 → 轨迹编码**(精确匹配优先,`*` 兜底)。
114
+ - 默认只监听 `127.0.0.1`;要暴露到公网由你决定方式(隧道 / 反代 / 端口映射都行),插件不绑定任何一种。
115
+ - **程序化或远程接入走宿主自带的官方 ACP server**(`dsh --profile acp`,纯 stdio):本插件不提供 ACP 面,认证放在接入层。
331
116
 
332
- - 🔴 **ACC 只"起进程",从不 import 用户程序**——进程边界同时挡掉两件事:ACC 不必依赖工作区的**源码路径**(装机版在别处,两条路径 = 两个真相源),以及**一个用户程序的语法错会放倒整个容器**
333
- - 🔴 **半成品报错就行**:程序报错 / 超时(60 秒硬超时,超了 kill)/ 输出非法 ⇒ **记一行 + 跳过本轮**——任何失败都不影响既有机制(这条由实测用例钉住,不是口头承诺)
334
- - 🔴 **`reason` 强烈建议填**:改成程序判定之后,"当时为什么叫了"**不再能从时间表重建**(原因在程序内部);不写,以后出事无法复现
335
- - **程序想要记住"上次判了什么",得自己记**——写在**自己轨迹目录**里的状态文件(那是它自己的进程状态,ACC 物理上拿不到)。所以**防抖也归程序**:想"不要叫太频繁"就自己记时间戳
336
- - ⚠️ **不自带自测**:流程为**先用开发名编写**(如 `continuous-re-occurrence.dev.ts`,不会被启用)→ 自测通过 → **改为正式名**("文件在即启用",**改名即上线动作**)
117
+ ## 5 常见问题
337
118
 
338
- > ⚠️ **历史**:ACC 曾自带一套"周期自唤醒 autopilot",**v1.35.0 起整段退场**(机制、时钟、`container_admin autopilot` 域、面板开关一并删除)。
339
- > 旧会话 / 旧文档里看到 `container_admin autopilot`、`autopilot-trajectory`、`--auto` 目录后缀、`[Autopilot Trajectory · 唤起]` 等字样,均为**已删除的机制**。
340
- > CRO 不是它的回归:退场删掉的是**判据内容**(周期节拍 + 提示词),留下的是**调度能力**;CRO 补的是"**在没人醒着的时候判断该不该醒**"。
119
+ **装完没反应?**
120
+ 先确认你进的是带 `.serenity` 的目录 —— 不是工作区,插件完全不介入。进去后输入 `dashboard health` 看三项自检。
341
121
 
342
- ### 6.6 安全模型
122
+ **`bash` 怎么不见了?**
123
+ 安全模式开着。这是设计,不是故障:关掉胶囊里的 SAFE 滑块就回来。
343
124
 
344
- | 层面 | 做了什么 |
345
- |---|---|
346
- | 登录 | scrypt 密码哈希 + 常量时间比较 + 256-bit token + 24 小时滑动有效期 + 审计日志 |
347
- | 双因素 | TOTP(兼容 Authenticator),扫码绑定;密码和验证码二选一 |
348
- | 防爆破 | 按账号锁定:连续失败 5 次 → 锁 15 分钟,且指数退避 |
349
- | 防 CSRF | 登录双提交 + 配置写入校验 Origin + 服务端 token 集合(多标签页不冲突) |
350
- | 凭据 | 集中放 `localstore.json`(默认禁止提交到 git);密钥文件对工具结构性隔离 |
351
- | 对外输出 | 敏感词检测 → 打回重答,并告知命中词和规避方向 |
125
+ **3081 登录被锁了?**
126
+ 连续失败 5 次锁 15 分钟起(指数退避)。等锁过期,或检查账号的验证码绑定状态。
352
127
 
353
- ---
128
+ **上下文快满了怎么办?**
129
+ 先让 AI 把进度写回 `SESSION.md`,再按提示做原地重建(`cognitive_trajectory rebuild`)——**载体换掉,轨迹不动**,不用手动开新会话。
354
130
 
355
- ## 7. 配置分四层
131
+ **怎么从手机 / 别的编辑器接进来?**
132
+ 走宿主官方 ACP:`dsh --profile acp` 起服务,用任一 ACP 客户端连。认证与暴露范围由你那一侧的隧道 / 反代 / Access 决定,插件不发放 key。
356
133
 
357
- | 层 | 位置 | 放什么 |
358
- |---|---|---|
359
- | DSH 原生设置 | DSH 的 settings.yaml(即设置面板那几项) | 开关与阈值:网关总开关、重建阈值、子角色开关与调试端口、**CRO 总闸(`croEnabled`,默认为开)**、**图工具总闸(默认关)** |
360
- | 插件全局文件 | `~/.dsh/serenity-hooks.json`(权限 0600) | 网关账号(scrypt + TOTP)、监听地址与端口、工作区白名单、cookie 安全开关 |
361
- | 工作区配置 | `.opencode/serenity.json` | 助手模型白名单、日志阈值、安全模式黑名单、子角色、**认知轨迹级 skill 供给(`cognitiveTrajectory.skills`)**、微信(账号/路由/开关) |
362
- | 工作区凭据 | `localstore.json` | 密钥与本地偏好;微信机器人 token 也在这一层 |
134
+ **我在 DSH 里用 opencode 的模型,要手抄一串请求头吗?**
135
+ 不用。插件只补 `x-opencode-session` 这一族**缺的**头,**你写过的值一律不动**;环境里有 `OPENCODE_API_KEY` 而一个 opencode 路由都没有时会自动建一条。不需要就关 `opencodeProvider.autoConfigure`。
363
136
 
364
- > 原则:**插件是全局的,工作区是具体的**——账号、开关、阈值归插件层;角色、凭据、本地偏好归工作区层。
365
- > 「重建总开关」与「周期自唤醒」这两个旧开关**已砍掉**(超限重建恒开;周期节拍改由工作区/ CRO 自己决定),旧配置里若还留着它们,会被**静默忽略**。
137
+ **微信里 AI 的回复是怎么发出去的?**
138
+ 默认由插件把它最后一段话转给用户;配 `autoReplyWithLastMessage: false` 后改由 AI 自己发(`im-bridge`),该发没发会被打回提醒(最多 2 次),再开 `fallbackOnNoSend` 可让插件兜底转达。
366
139
 
367
- ---
140
+ **主动发的消息会被记录吗?**
141
+ 会,跟回复走同一条记录通道,事件里用 `source` 区分 `reply` / `proactive` / `reply-fallback`。
368
142
 
369
- ## 8. 上下文将满时的处理
143
+ **轨迹声明、定时唤醒、CRO 怎么配?**
144
+ 本文不展开,各归其位:**声明面**(`trajectory.json` 能写什么、缺省是什么)在标准 §3.0.1;**工具自带手册**是 `container_admin config docs trajectory-self`(描述自己)与 `config docs trajectory-constraint`(限制自己);**CRO 编写指南**是 `cognitive_trajectory cro-guide`。
370
145
 
371
- AI 一次能"记住"的内容有上限。满了不用手动开新会话:
146
+ ## 6 延伸阅读
372
147
 
373
- | 机制 | 说明 |
148
+ | 要什么 | 去哪 |
374
149
  |---|---|
375
- | **工作日志(SESSION.md)** | AI 的"笔记本",永远留在原地。目标、决定、进度都写这儿 |
376
- | **原地重建(cognitive_trajectory rebuild)** | 快满时它会提示 AI 主动重建:把这一轮对话清空,但重新注入"你是谁 + 继续 S### 的工作"——**载体换了,活儿接着干**。重建前它会把"手头做到哪了"写进同目录的 `REBUILD-TODO.md`,重建后的它先去读那张便签 |
377
- | **进度提醒** | 做久了会按计分提醒 AI 把进度写回日志,并要求它回确认码 |
378
- | **沉淀纪律** | 重建前如果产生了有价值的认知,先把它写进相关技能(而不是丢掉) |
379
-
380
- ---
150
+ | **标准**:六条不变量、工具契约面、装配内容、拦截缝语义、验收清单 | [serenity-acc-specs](https://github.com/tellmewhattodo/serenity-acc-specs) |
151
+ | **改了什么、为什么**(含当前版本号) | [CHANGELOG.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/CHANGELOG.md) |
152
+ | 为什么叫「认知容器」/「认知轨迹」 | [docs/cognitive-container-theory.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/cognitive-container-theory.md) |
153
+ | 轨迹声明面的完整判据 | [docs/trajectory-declaration-file.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/trajectory-declaration-file.md) |
154
+ | 代码地图 | [docs/codebase-overview-v1.22.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/codebase-overview-v1.22.md) |
155
+ | 维护者的判据库(本仓的维护纪律) | 维护技能 `dsh-serenity-plugin-development` |
381
156
 
382
- ## 9. 面向插件开发者
157
+ ### 面向插件开发者
383
158
 
384
159
  ```bash
385
- pnpm typecheck # 类型检查(node + 浏览器端两套)
386
- pnpm test # 全量测试(vitest;文件数与用例数看运行时输出,本文不写死)
387
- pnpm build # 打包(lib/index.js + client.js)
160
+ pnpm typecheck # 类型检查(node + 浏览器端两套)
161
+ pnpm test # 全量测试(vitest)
162
+ pnpm build # 打包(lib/index.js + client.js)
388
163
  ```
389
164
 
390
- - **开发用小工具**:`scripts/dsh-develop.ts`——typecheck / test / build / status / commit / push / version / bump / deploy / **lockfile** / **host-upgrade** / restart-web / publish / pack-check / github-push 一条龙。
391
- `lockfile` = 重生成锁文件 + 用 `--frozen-lockfile` 自检(CI 的 `Install (hooks)` 同款判定);
392
- `pack-check` 会在发布前核对打包产物是否完整(曾出现"发到 npm 少了文件"的情形);`scripts/dsh-crash-investigate.ts` 用来查崩溃(只读)。
393
- ⚠️ **改完 `package.json` 依赖后必须跑 `lockfile` 并提交锁文件** —— 曾漏过这一步,CI 从此一直红(红在"测试根本没跑",见 CHANGELOG)
394
- - **宿主类型基准 = devDependencies**:`tsconfig.json` / `client/tsconfig.json` 的 `paths` 指向**仓库内** `node_modules/@deepseek-ai/*`,由精确钉版的 devDependencies 提供 → **新增 paths 条目必须同时加 devDependency**(否则 tsc 静默回落 node_modules = 假绿,CI 的 typecheck 也就形同虚设)。机械闸门见 `tests/compliance.test.ts`;升级宿主时只改 `package.json` 一处 + 重跑 `lockfile`
395
- - **升级宿主**:`dsh-develop host-upgrade <版本|dist-tag>` 全局升级 DSH CLI(包名硬编码 `@deepseek-ai/dsh`、默认官方源、`--dry-run` 预览)→ 把 `package.json` 的 peer/devDeps 基准抬到同一版本 + 重跑 `lockfile`(`compliance.test.ts` 与 `host-manifest.test.ts` 会强制各声明面同步,漏一处必红)→ `restart-web` → `dashboard health` 看 `dshVersion`
396
- - **诊断会话打不开**:`dsh-develop session-doctor` —— 会话日志体检(**只读**)。
397
- `--probe` 走宿主**真实读取路径**判定(`msm("dsh-develop", ["session-doctor","--probe","--summary"])` 可全量跑)。
398
- ⚠️ 两个易误读点:① 存储层读**不走格式迁移** ⇒ 它对历史世代报 "no upgrade path" 属**正常现象**,不是损坏证据;判断"用户能不能打开"要看应用面 ② 本工具修的两个真实缺陷见 CHANGELOG(`rebuild` 写的事件缺字段 ⇒ 会话**永久打不开**;会话文件清理不认某些后缀 ⇒ 静默失效)
399
- - **架构**:Cordis 原生插件,用 DSH 的正式接口注册工具和拦截点——**从不修改 DSH 本体**
400
- - **代码地图**:[docs/codebase-overview-v1.22.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/codebase-overview-v1.22.md)
401
- - **设计决策**:见 [CHANGELOG.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/CHANGELOG.md) 和维护技能 `dsh-serenity-plugin-development`
402
- - **发布**:npm `@shgroup/dsh-serenity-hooks` + GitHub 双仓库同步推送
403
-
404
- ---
405
-
406
- ## 10. 和 opencode 版是什么关系
407
-
408
- | | opencode-serenity-plugin | dsh-serenity-plugin(本仓库) |
409
- |---|---|---|
410
- | 跑在 | OpenCode | DeepSeek Harness |
411
- | 实现 | 独立 | **独立**(不复用源码,但遵循同一套标准) |
412
- | 系统提示词 | `system.transform` | `systemPrompt.section`,平台无关的部分逐字对齐 |
413
- | 工具 | msm / container_fs / logbook 等 | container_fs / cognitive_trajectory / dashboard / container_git / msm / praxis / ontology / serenity-skill-creator / cheap-worker / localstore / container_admin + 三个条件出现的(`im-bridge` / `acc-diag` / `diagram`) |
414
-
415
- **同一个工作区可以随时换运行时**:`.serenity` 标记、`.opencode/skills/`、配置、`AGENT_SESSIONS/` 的文件格式都一致;
416
- 差别只在平台层(工具名、注入方式),换过去以后 AI 收到的约束是一样的。
417
-
418
- ---
419
-
420
- ## 11. 常见问题
421
-
422
- **Q:装完没反应?**
423
- 先确认进入的是带 `.serenity` 标记的目录。不是工作区的话,插件完全不介入。进去后输入 `dashboard health` 看三项检查。
424
-
425
- **Q:bash 怎么不见了?**
426
- 安全模式开着——这是设计,不是 bug。关掉胶囊里的 SAFE 滑块就回来了(机制见 §3.2)。
427
-
428
- **Q:3081 登录被锁了?**
429
- 连续失败 5 次锁 15 分钟起(指数退避,上限 4 小时)。等锁过期,或检查账号的验证码绑定状态。
430
-
431
- **Q:上下文快满了怎么办?**
432
- 先让 AI 把进度写回 SESSION.md,然后按提示调用 `cognitive_trajectory rebuild`。轨迹会自动接续,不用手动开新会话。
433
-
434
- **Q:对外问答页会返回内部信息吗?**
435
- 该面已于 2026-10-15 移除(改用官方 ACP,见 §6.4)。走官方 ACP 时,**认证与暴露范围由接入层决定**——
436
- 官方 server 只跑在 stdio 上、不认识网络,把它接到哪里、谁能连,取决于你怎么架隧道。
437
-
438
- **Q:微信桥里,AI 的回复是怎么发出去的?**
439
- 默认由插件自动把它的最后一段话转发给用户。如果配了 `autoReplyWithLastMessage: false`,插件就不转了,改由 AI 自己发——所以这时候它如果没发,用户就收不到消息(除非另外开了 `fallbackOnNoSend` 让插件兜底)。
440
-
441
- **Q:主动发的微信消息会被记录吗?**
442
- 会。和工作区里配的 `weixin.hook` 记录脚本走同一条路,事件里标 `source: "proactive"`;自动回复标 `source: "reply"`。
443
-
444
- ---
445
-
446
- ## 12. 延伸阅读
447
-
448
- - **理论来源**:[docs/cognitive-container-theory.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/docs/cognitive-container-theory.md)——认知容器是什么、认知的"发生/存储/再发生"、轨迹与载体
449
- - **权威标准**:[serenity-acc-specs](https://github.com/tellmewhattodo/serenity-acc-specs)——理论根基、注入规范、不变量
450
- - **改了什么**:[CHANGELOG.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/CHANGELOG.md)——每个版本做了什么、为什么这么做,**当前版本号也看这里**
451
-
452
- ---
165
+ - 一条龙开发脚本 = `scripts/dsh-develop.ts`(typecheck / test / build / commit / push / version / bump / deploy / lockfile / host-upgrade / restart-web / publish / pack-check / github-push),子命令看 `--help`。
166
+ - 🔴 **改完 `package.json` 依赖必须跑 `lockfile` 并提交锁文件** —— 漏过一次,CI 从此一直红在「测试根本没跑」。
167
+ - **宿主类型基准 = devDependencies**:两份 `tsconfig` 的 `paths` 指向仓库内 `node_modules/@deepseek-ai/*`,由钉版 devDeps 提供 ⇒ **新增 `paths` 条目必须同时加 devDependency**;闸门是 `tests/compliance.test.ts`。
168
+ - **包内 README 是生成物**:仓根 `README.md` → `hooks/dsh-serenity-hooks/README.md`(`dsh-develop readme-sync`,相对链接转绝对 URL)。**不要手改包内那份。**
453
169
 
454
170
  ## 许可
455
171
 
456
172
  MIT(见 [LICENSE](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/LICENSE))
457
-
458
- > **前置**:DSH `^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1`(三档)| Node ≥ 20 或 bun。
459
- > **当前版本 / 工具数 / 测试规模**均不写死在此 —— 版本看 [CHANGELOG.md](https://github.com/tellmewhattodo/dsh-serenity-plugin/blob/master/CHANGELOG.md),运行态看设置面板与 `dashboard health`。
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.79.2",
3
+ "version": "1.79.4",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
227
227
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
228
228
  let rolesOf = null;
229
229
  if (opts.withRoles === true) {
230
- const { listTrajectoryConstraints } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
230
+ const { listTrajectoryConstraints } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
- import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-D3ty1C8P.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-DAwz9Li1.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
- import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-C7BuyZK7.js";
4
+ import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-5TQ_C1xi.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
- import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-BJO5q-rD.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-B4eZD3Hv.js";
6
+ import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-DK2CnXlh.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-BYC1uWi0.js";
8
8
  import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
- import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-Bb4OnEdI.js";
9
+ import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-B-vn8j4g.js";
10
10
  import { REGISTERED_TOOLS } from "./invariant.js";
11
11
  import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-Dr0SFD8A.js";
12
12
  import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
@@ -819,6 +819,7 @@ function runGit(root, args) {
819
819
  if (!scoped && !hasChanges(root)) return "(nothing to commit — working tree clean)";
820
820
  const addR = git(root, scoped ? [
821
821
  "add",
822
+ "-A",
822
823
  "--",
823
824
  ...paths
824
825
  ] : ["add", "-A"]);
@@ -14077,7 +14078,7 @@ function registerStatusApi(ctx, opts = {}) {
14077
14078
  });
14078
14079
  }
14079
14080
  const { saveWeixinRoutes } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14080
- const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
14081
+ const { findDirNameByCode } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
14081
14082
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
14082
14083
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
14083
14084
  return;
@@ -19074,7 +19075,7 @@ const MEDIA_MAX_BYTES = 20971520;
19074
19075
  */
19075
19076
  async function resolveCccRootByAlias(ctx, alias) {
19076
19077
  try {
19077
- const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
19078
+ const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
19078
19079
  const entries = await listCccs(ctx);
19079
19080
  const byName = entries.find((e) => e.name === alias);
19080
19081
  if (byName) return byName.root;
@@ -19142,7 +19143,7 @@ async function resolveInboundTarget(ctx, input) {
19142
19143
  });
19143
19144
  return null;
19144
19145
  }
19145
- const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
19146
+ const { findDirNameByCode } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
19146
19147
  if (findDirNameByCode(root, target.role) === null) {
19147
19148
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
19148
19149
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -19982,7 +19983,7 @@ function syncHumanChannel(ctx) {
19982
19983
  (async () => {
19983
19984
  try {
19984
19985
  stopMachineLoops();
19985
- const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
19986
+ const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
19986
19987
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
19987
19988
  } catch {}
19988
19989
  })();
@@ -20246,7 +20247,7 @@ function matchCcc(input, candidates) {
20246
20247
  }
20247
20248
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
20248
20249
  async function collectCandidates(ctx) {
20249
- const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
20250
+ const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
20250
20251
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
20251
20252
  const seen = /* @__PURE__ */ new Set();
20252
20253
  const out = [];
@@ -1,6 +1,6 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-D3ty1C8P.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-DAwz9Li1.js";
2
2
  import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
3
- import { A as sessionEvents, E as parseTrajectoryCode, b as getActiveSessionInfo, j as sessionsRoot, x as getLastActiveSessionInfo } from "./profile-C7BuyZK7.js";
3
+ import { A as sessionEvents, E as parseTrajectoryCode, b as getActiveSessionInfo, j as sessionsRoot, x as getLastActiveSessionInfo } from "./profile-5TQ_C1xi.js";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -271,6 +271,32 @@ const ARCHIVED_DIR = "_archived";
271
271
  function sessionsRoot(root) {
272
272
  return join(root, "AGENT_SESSIONS");
273
273
  }
274
+ /** 归档区绝对路径(`<CCC>/AGENT_SESSIONS/_archived`) */
275
+ function archivedSessionsDir(sessionsDir) {
276
+ return join(sessionsDir, ARCHIVED_DIR);
277
+ }
278
+ /**
279
+ * **编码是否已被占用**(活跃区 + 归档区,**全仓**)。
280
+ *
281
+ * 🔴 为什么必须扫归档区(2026-10-20,S222 报告 / owner 转 S142 处置):
282
+ * 编码是**终身身份**(AXIOM 3),而归档只改位置、不改身份 ⇒
283
+ * 若分配器只看活跃区,归档过的码会被**重新发出**,于是同一个身份挂在两个载体上
284
+ * (实测:`S222` 活跃+归档并存;更早 `S140` 被两条轨迹共用,08-27 就登记过、一直没判据)。
285
+ * 一个可复用的身份码会让历史 `send-now` / 唤醒条目 / 文档引用**静默指向另一个对象**。
286
+ *
287
+ * ⚠️ 与 `readAllSessions` 的分工:那个函数回答"**眼下有哪些活跃轨迹**"(该跳 `_` 前缀),
288
+ * 本函数回答"**这个码有没有人用过**"(必须看全仓)。**两个问题,两个扫描域** ——
289
+ * 把它们合并成一个才是错的。
290
+ *
291
+ * @returns 撞车时返回**占用者的完整路径**(活跃区优先说明);无冲突 ⇒ `null`
292
+ */
293
+ function findCodeCollision(sessionsDir, code) {
294
+ for (const dir of [sessionsDir, archivedSessionsDir(sessionsDir)]) {
295
+ const hit = readAllSessions(dir).find((s) => parseTrajectoryCode(s.dirName) === code);
296
+ if (hit) return hit.path;
297
+ }
298
+ return null;
299
+ }
274
300
  /**
275
301
  * 解析 SESSION.md 存在性。
276
302
  *
@@ -572,10 +598,14 @@ function createSession(opts) {
572
598
  if (desc.length > 200) throw new Error(`description too long: ${desc.length} chars (max 200)`);
573
599
  const sessions = readAllSessions(sessionsDir);
574
600
  let code;
575
- if (requestedId !== void 0 && requestedId !== "") code = requestedId;
576
- else {
601
+ if (requestedId !== void 0 && requestedId !== "") {
602
+ code = requestedId;
603
+ const dup = findCodeCollision(sessionsDir, code);
604
+ if (dup !== null) throw new Error(`编码「${code}」已被占用:${dup}\n认知轨迹的编码是**终身身份**(AXIOM 3:identity = 编码),归档不会释放它 —— 归档只改位置,不改身份。
605
+ ⇒ 换一个编码(\`create --id <新码>\`),或用 \`create\` 的自动分配(会跳过全部已用码)。`);
606
+ } else {
577
607
  let maxId = 0;
578
- for (const s of sessions) {
608
+ for (const s of [...sessions, ...readAllSessions(archivedSessionsDir(sessionsDir))]) {
579
609
  const m = parseTrajectoryCode(s.dirName)?.match(/^S(\d{3,})$/);
580
610
  if (m) {
581
611
  const num = parseInt(m[1], 10);
@@ -1,8 +1,8 @@
1
- import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-D3ty1C8P.js";
1
+ import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-DAwz9Li1.js";
2
2
  import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
3
- import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-C7BuyZK7.js";
3
+ import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-5TQ_C1xi.js";
4
4
  import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
5
- import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-BJO5q-rD.js";
5
+ import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-DK2CnXlh.js";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -1,4 +1,4 @@
1
- import { f as hostSettings } from "./ccc-roots-D3ty1C8P.js";
1
+ import { f as hostSettings } from "./ccc-roots-DAwz9Li1.js";
2
2
  //#region src/surface/settings.ts
3
3
  /**
4
4
  * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
@@ -79,6 +79,24 @@ export declare function sessionEvents<T = unknown>(session: unknown): readonly T
79
79
  /** 归档区目录名(`_` 前缀 ⇒ 不是轨迹,见 readAllSessions 的跳过判据) */
80
80
  export declare const ARCHIVED_DIR = "_archived";
81
81
  export declare function sessionsRoot(root: string): string;
82
+ /** 归档区绝对路径(`<CCC>/AGENT_SESSIONS/_archived`) */
83
+ export declare function archivedSessionsDir(sessionsDir: string): string;
84
+ /**
85
+ * **编码是否已被占用**(活跃区 + 归档区,**全仓**)。
86
+ *
87
+ * 🔴 为什么必须扫归档区(2026-10-20,S222 报告 / owner 转 S142 处置):
88
+ * 编码是**终身身份**(AXIOM 3),而归档只改位置、不改身份 ⇒
89
+ * 若分配器只看活跃区,归档过的码会被**重新发出**,于是同一个身份挂在两个载体上
90
+ * (实测:`S222` 活跃+归档并存;更早 `S140` 被两条轨迹共用,08-27 就登记过、一直没判据)。
91
+ * 一个可复用的身份码会让历史 `send-now` / 唤醒条目 / 文档引用**静默指向另一个对象**。
92
+ *
93
+ * ⚠️ 与 `readAllSessions` 的分工:那个函数回答"**眼下有哪些活跃轨迹**"(该跳 `_` 前缀),
94
+ * 本函数回答"**这个码有没有人用过**"(必须看全仓)。**两个问题,两个扫描域** ——
95
+ * 把它们合并成一个才是错的。
96
+ *
97
+ * @returns 撞车时返回**占用者的完整路径**(活跃区优先说明);无冲突 ⇒ `null`
98
+ */
99
+ export declare function findCodeCollision(sessionsDir: string, code: string): string | null;
82
100
  /**
83
101
  * 轨迹的**存在形态**(🔴 **2026-10-18 起不再是"生命周期"**)。
84
102
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.79.2",
3
+ "version": "1.79.4",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {