@mingmaple/dsh-handover 0.9.12 → 0.9.13
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 +15 -10
- package/docs/releases/0.9.12.md +28 -0
- package/docs/releases/0.9.13.md +30 -0
- package/docs/troubleshoot.md +11 -3
- package/package.json +1 -1
- package/scripts/blank-dsh.mjs +6 -5
- package/scripts/blank-smoke.mjs +72 -23
- package/scripts/check-docs.mjs +23 -2
- package/scripts/office.mjs +81 -0
- package/src/client.js +25 -14
- package/src/index.mjs +47 -23
- package/src/lib/api.mjs +5 -4
- package/src/lib/consent.mjs +59 -20
- package/src/lib/facts.mjs +5 -4
- package/src/lib/guide.mjs +4 -1
- package/src/lib/manual.mjs +23 -12
- package/src/lib/names.mjs +2 -2
- package/src/lib/office.mjs +272 -0
- package/src/lib/phase.mjs +1 -1
- package/src/lib/role.mjs +4 -4
- package/src/lib/session-ops.mjs +147 -89
- package/src/lib/succession.mjs +244 -0
- package/src/lib/workspace.mjs +1 -1
- package/src/selftest.mjs +15 -0
- package/src/templates/Charter.skeleton.md +1 -1
- package/src/templates/Prompt_Templates.md +9 -17
- package/src/templates/Runtime_Manual.md +12 -9
- package/src/templates/rules/Handover_Rules.md +2 -2
- package/src/templates/rules/Misc_Task_Standard.md +3 -3
- package/src/tools/ho_office.mjs +59 -0
- package/src/tools/ho_succeed.mjs +24 -0
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ DeepSeek Harness 插件,用于管理长会话的上下文水位、交接文档
|
|
|
13
13
|
- 起始简报:水位、闸门、活跃与沉默、阶段建议,一条命令返回。
|
|
14
14
|
- 交接体检:检查交接文档的结构、字段与占位符,逐条列出问题。
|
|
15
15
|
- 压缩与续跑:由水位与剩余压缩次数共同判定时机,由用户授权,回合结束后执行;压缩后的去向由助手在登记时选择,继续工作时续跑,任务已收尾时静默等待;状态切换与失败原因写入结果。
|
|
16
|
+
- 在任登记与查询:每一类会话(全局、各个项目、各个杂项任务)都登记当前在任的会话与历代编号,记录保存在工作区内的 `Office.json`;按名字或旧编号查询现任,不启动会话也可用命令行查询。
|
|
17
|
+
- 换任由会话自行办理:在任会话在用户授权后自行检查交接、建立接任会话并登记在任,不再由全局会话转交。
|
|
16
18
|
- 会话清点与角色:查看会话水位与状态,设定角色,建立下一代会话,归档与解归档。
|
|
17
19
|
- 会话间通信:会话之间可直接对话,不必经全局转述;默认沉默,任务完成不向全局报告。
|
|
18
20
|
- 工作区迁移:预检、复制并逐文件校验、切换、改写登记、重建链接、清理旧副本。
|
|
@@ -66,7 +68,8 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
66
68
|
| 看整体状态 | 先出一份起始简报 |
|
|
67
69
|
| 查规则 | 把 Charter 第 7.2.3 节取出来 |
|
|
68
70
|
| 交接并压缩 | 先把交接文档写好,再压缩并继续 |
|
|
69
|
-
| 换任 |
|
|
71
|
+
| 换任 | 写好交接,请用户授权,由本会话自己换任 |
|
|
72
|
+
| 查在任 | 现在全局是谁在任 |
|
|
70
73
|
| 设置角色 | 把 Demo1 设成项目角色,并让它整理自己的文件 |
|
|
71
74
|
| 自检 | 检查插件当前状态 |
|
|
72
75
|
|
|
@@ -74,9 +77,9 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
74
77
|
|
|
75
78
|
| 角色 | 提示段 | 工具数 | 说明 |
|
|
76
79
|
|---|---|---|---|
|
|
77
|
-
| 全局 | 有 |
|
|
78
|
-
| 项目 | 有 |
|
|
79
|
-
| 杂项 | 有 |
|
|
80
|
+
| 全局 | 有 | 24 | 协调会话、维护规则与交接 |
|
|
81
|
+
| 项目 | 有 | 11 | 管理单个项目库,自己写骨架档案 |
|
|
82
|
+
| 杂项 | 有 | 11 | 管理单个杂项任务 |
|
|
80
83
|
| 普通 | 无 | 0 | 不注入提示、不加载工具 |
|
|
81
84
|
|
|
82
85
|
代理子会话不装配 `ho_` 与 `peer_` 工具。宿主逐步解析工具表,子会话在解析阶段即被排除,首轮即不可用。
|
|
@@ -88,7 +91,7 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
88
91
|
| 低于 32% | 继续 |
|
|
89
92
|
| 32% 至 35% | 可写交接;处于关键步骤时可暂缓 |
|
|
90
93
|
| 35% 至 85%,已压少于 3 次 | 写交接、原地压缩、读交接续跑 |
|
|
91
|
-
| 85% 及以上,已压少于 3 次 |
|
|
94
|
+
| 85% 及以上,已压少于 3 次 | 强制压缩 |
|
|
92
95
|
| 任意水位,已压满 3 次 | 写交接、建立新会话 |
|
|
93
96
|
|
|
94
97
|
压缩次数用尽(默认 3 次)的判定先于水位:额度用尽后,无论水位高低均建议换任;`force` 不能越过额度,`ho_sessions` 的压缩指令(`action="order", order="compact"`)同样会被拒绝。
|
|
@@ -97,6 +100,8 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
97
100
|
|
|
98
101
|
归档文件名为 `<原文件名>_<YYYYMMDD_HHMMSS>.md`,同一秒重复时追加 `-2`。全局与项目的归档位于 `_Archives/<范围>/Handover/`,杂项归档位于 `_Misc/tasks/<任务>/_archive/`。
|
|
99
102
|
|
|
103
|
+
换任由在任会话自行办理:写完交接后,由用户在聊天框发送 `/allow-succeed`,或在设置页「压缩授权」卡片内的「换任授权」区域点「允许换任」。授权一次有效,5 分钟内可用,按会话生效。随后会话调用 `ho_succeed`,插件依次校验授权与交接结构,按记录分配下一代编号,建立接任会话并写入角色,登记在任,投递启动提示词,并在旧交接末尾追加换任登记。任何一项校验不通过时,整体不做任何改动,并逐条列出缺项。全局的建会话与换任能力保留,作为自动流程失败时的备选方案。
|
|
104
|
+
|
|
100
105
|
### 设置页
|
|
101
106
|
|
|
102
107
|
设置页为单页卡片式,常用区直接展开,低频与长文段折叠。未接入工作区时,首次安装卡片置顶出现。删除旧副本须先输入「删除」两字才能解锁按钮。时间显示为 `YYYY-MM-DD HH:mm` 加本机时区偏移。
|
|
@@ -109,19 +114,19 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
109
114
|
|
|
110
115
|
## 故障处理
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
工具数量不符、工作区未接入、设置页刷新失败、压缩未生效、换任未执行、项目名或任务名不合法、新建会话找不到工作区、设置角色后工具未更新、迁移后原路径失效,各有对应处置。
|
|
113
118
|
|
|
114
119
|
详见 [常见故障处理](docs/troubleshoot.md)。
|
|
115
120
|
|
|
116
121
|
## 上下文开销
|
|
117
122
|
|
|
118
|
-
固定开销为工具 schema 与提示段。按 0.9.
|
|
123
|
+
固定开销为工具 schema 与提示段。按 0.9.13 实测,按角色载入后,全局提示段约 385 加权 token(长路径加迁移提示行约 435),项目与杂项约 370(最坏约 419),随项目名与路径长度波动;单条工具描述最宽约 56。以上为实测参考值,不设硬性上限:某条判定需要写清楚时,优先保证表达完整。
|
|
119
124
|
|
|
120
125
|
## 边界与限制
|
|
121
126
|
|
|
122
127
|
- 依赖 DSH 的 `ctx.compaction` 服务;未挂载时压缩相关工具明确报错。
|
|
123
128
|
- `ptc` 呈现模式下,插件工具只能经程序调用。
|
|
124
|
-
-
|
|
129
|
+
- 会话压力读取依赖宿主保存的会话状态文件布局;字段变化时用 `ho_diag` 排查。
|
|
125
130
|
- 迁移只搬文件与清单,不改写文件内容;写死的绝对路径以体检形式报出。
|
|
126
131
|
- 工具可见性按每一步计算:换角色在下一轮请求即生效,不必重启 DSH。插件重载后,已存在的会话沿用旧提示段文本,需要新文本时重启一次。
|
|
127
132
|
- 项目名与任务名按目录名校验:项目名不超过 24 字符,任务名不超过 40 字符;不含 `< > : " / \ | ? *` 与控制字符;不以点或空格结尾;不使用设备保留名。名字来自自由文本时同样校验,不合法则报错且不写入磁盘。
|
|
@@ -131,7 +136,7 @@ dsh plugin --profile web add @mingmaple/dsh-handover
|
|
|
131
136
|
|
|
132
137
|
```bash
|
|
133
138
|
npm test # 开发自测:逐个工具跑只读用例
|
|
134
|
-
npm run test:blank # 空白 DSH 验收:临时 HOME 与空目录跑
|
|
139
|
+
npm run test:blank # 空白 DSH 验收:临时 HOME 与空目录跑 24 工具及迁移四件
|
|
135
140
|
npm run test:smoke # 空白环境冒烟:工具装配、状态路由、浏览器半渲染、语体扫描
|
|
136
141
|
npm run test:migrate # 迁移与恢复端到端
|
|
137
142
|
npm run test:docs # 文档校验:工具名、旧数、链接、版本、语体与长度上限
|
|
@@ -141,7 +146,7 @@ npm run test:docs # 文档校验:工具名、旧数、链
|
|
|
141
146
|
|
|
142
147
|
## 版本
|
|
143
148
|
|
|
144
|
-
当前版本 0.9.
|
|
149
|
+
当前版本 0.9.13,首次公开版本为 0.9.6。内部版本史不随仓库发布。
|
|
145
150
|
|
|
146
151
|
支持的内核为 0.2.1-rc.2 至 0.3.0(不含 0.3.0 及其候选版),见 `package.json` 的 `engines.dsh`;0.2.2-rc.1 上的工具装配、角色裁剪与压缩调度已实测通过。
|
|
147
152
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 0.9.12
|
|
2
|
+
|
|
3
|
+
本次更新:首次安装改为一键部署;修「设置角色后必须重启 DSH」,并把角色装配收敛成一条主线;新建会话必须指明工作区。
|
|
4
|
+
|
|
5
|
+
## 新增与调整
|
|
6
|
+
|
|
7
|
+
- 一键部署:在设置页「首次安装」卡片粘贴绝对路径后点「一键部署」,一次完成三件事:建立 `_Workspace` 与整套文件结构,把该目录注册为「全局」工作区,新建一个「全局」会话并登记全局角色。「全局」会话的产生不再依赖手工新建标题含「全局」的会话。
|
|
8
|
+
- 补建入口:工作区已接入但还没有全局会话时,设置页单独出现「全局会话」卡片,可随时补建;只建会话,不建体系,不动既有文件。
|
|
9
|
+
- 新建会话必须指明工作区:`ho_sessions(action="new")` 未给 `workspace` 或 `cwd`、或项目没有对应工作区时直接报错,并列出已注册的工作区,不再静默落到调用者所在的工作区。传 `project` 且存在同名工作区时自动匹配;`cwd` 必须是已注册工作区的根。
|
|
10
|
+
- `ho_workspace(action="create")` 新增 `ensure`:目录不存在时自动创建再注册。仍是显式开关,默认不建目录。
|
|
11
|
+
|
|
12
|
+
## 修正
|
|
13
|
+
|
|
14
|
+
- 设置角色后必须重启 DSH 才生效:此前角色装配散在三套机制上——每步条件只排子会话、角色裁剪另用 `restrict`、角色表晚到再靠 1.5 秒与 4.5 秒的延迟复核。会话休眠期间改了角色,唤醒时宿主不保证重发创建事件,`restrict` 没有机会撤销,工具仍是旧的。现在收敛为一条主线:工具可见性由每步启用条件算,角色判定用 2 秒短缓存、写角色表即失效;提示段由创建、开轮前核对与重载补装三处对齐;只有不支持按步解析的旧内核才退回 `restrict`。
|
|
15
|
+
- 角色查找增加一层兜底:按会话 id 无法取到活体时,再按会话头的 id 在活体列表中查找,避免把仍在内存中的会话误判为已休眠。
|
|
16
|
+
- 删除不再使用的旧逻辑:延迟复核、一次性引导状态、重复的会话投递与建会话代码、未使用的辅助函数。会话投递与建会话各收敛成一个共用入口。
|
|
17
|
+
|
|
18
|
+
## 升级
|
|
19
|
+
|
|
20
|
+
- 从 npm 或 GitHub 安装后启用插件;已安装旧版时先停用再换版本,安装完成后启用并重启 DSH 一次。
|
|
21
|
+
- 以本地 `dist/` 挂载的安装方式:替换 `dist/` 后在插件列表停用再启用即换用新代码,不必重启 DSH。
|
|
22
|
+
- 升级后已有的角色与交接无需迁移。已存在的会话若仍显示旧提示段文本,重启 DSH 一次即可换成新文本,工具裁剪不受影响。
|
|
23
|
+
|
|
24
|
+
## 兼容
|
|
25
|
+
|
|
26
|
+
- 支持 DSH 内核 0.2.1-rc.2 至 0.3.0,不含 0.3.0 及其候选版,见 `package.json` 的 `engines.dsh`。
|
|
27
|
+
- 已在 DSH NEXT 2.0.22 与内核 0.2.2-rc.1 上实测:设置角色后按轮换装、新建会话的工作区校验与报错清单。
|
|
28
|
+
- 一键部署的建体系、注册工作区与建会话三步,在空白环境验证通过;首次实机安装建议先在空环境验证一次。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 0.9.13
|
|
2
|
+
|
|
3
|
+
本次更新新增两项能力:查询当前在任的会话,以及由会话在用户同意后自行完成换任。换任不再需要由全局会话代为办理;建立新会话时会同时写入在任记录。
|
|
4
|
+
|
|
5
|
+
## 新增
|
|
6
|
+
|
|
7
|
+
- 在任登记:每一类会话(全局、各个项目、各个杂项任务)都在 `_Control_Center/records/Office.json` 中记录当前在任的会话与历代编号;新的编号按该类已用的最大编号加一分配。`_Project_Meta/Registry.md` 中的「在役会话」一列由插件填写,只改这一格,其余文字不动。
|
|
8
|
+
- `ho_office`:查询某一类会话当前由哪个会话在任,返回名字与会话 id。可以输入不确定的名字(例如「全局3」会返回现任全局),多个名字用空格分隔,留空则列出全部;`json=true` 返回可供程序处理的结果。
|
|
9
|
+
- 命令行 `node scripts/office.mjs 全局 全局3`:与 `ho_office` 使用同一份实现与同一份记录,不启动会话即可查询;支持 `--root` 指定工作区根、`--all` 列出全部、`--json` 输出 JSON。
|
|
10
|
+
- `ho_succeed`:在任会话在用户授权后自行完成换任。依次校验授权、交接内容与在任身份,按记录分配新编号,建立接任会话并写入角色,登记在任,投递启动提示词,在旧交接末尾追加换任登记,最后注销本次授权。
|
|
11
|
+
- 换任授权:聊天框命令 `/allow-succeed`,或设置页「压缩授权」卡片内的「换任授权」区域。一次有效,5 分钟内可用,按会话生效;与压缩授权分别记录,互不影响。
|
|
12
|
+
|
|
13
|
+
## 改动
|
|
14
|
+
|
|
15
|
+
- 换任改由会话自行办理:由在任会话自己执行,不再由全局会话转交;全局的建会话与换任能力保留,作为自动流程失败时的备选方案。
|
|
16
|
+
- `ho_sessions(action="new")` 建立会话后自动写入在任记录,原在任者转为历史记录;全局手动建立会话不再漏记。
|
|
17
|
+
- 工具数量按角色调整:全局 24 个,项目 11 个,杂项 11 个,普通 0 个。
|
|
18
|
+
- 工作区规则同步:`Workspace_Charter.md` §7.2.8 为换任流程,§7.2.3 中的扫描与通知流程停止使用。
|
|
19
|
+
|
|
20
|
+
## 修复
|
|
21
|
+
|
|
22
|
+
- 修复换任需经全局会话转交、全局繁忙或休眠时换任停滞的问题。
|
|
23
|
+
- 修复全局手动建立会话后,在任记录与实际状态不一致的问题。
|
|
24
|
+
|
|
25
|
+
## 说明
|
|
26
|
+
|
|
27
|
+
- 换任有三项限制:只按会话自身已登记的角色确定它属于哪一类会话,不采用调用时传入的参数;只有该类会话当前在任者本人可以换任;已交接的旧会话不得再次换任。
|
|
28
|
+
- 交接内容检查只看结构:包含「下一步」一节且至少有一个条目,内容非空,且不含模板占位符。任何一项不通过时,整体不做任何改动,并逐条列出缺项。
|
|
29
|
+
- 换任授权与压缩授权分别记录,一种授权不能替代另一种;授权使用后即失效。
|
|
30
|
+
- 升级不需要迁移数据,已有交接无需改写。首次登记后,插件在工作区生成 `Office.json`。
|
package/docs/troubleshoot.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
## 工具缺失或数量不对
|
|
4
4
|
|
|
5
5
|
1. 在全局会话执行 `ho_diag`,看「角色判定与工具集」一节。
|
|
6
|
-
2. 全局应为
|
|
7
|
-
3.
|
|
6
|
+
2. 全局应为 24 个,项目与杂项 11 个,普通 0 个。
|
|
7
|
+
3. 新建会话时角色表中尚无该会话,插件会判为普通;角色写入后,该会话下一轮请求即按新角色装配。
|
|
8
8
|
4. 仍未装配时,在设置页「会话角色」卡片里重新设一次角色;仍无效则执行 `ho_diag` 查 `roleGating` 字段。
|
|
9
9
|
5. 改过源码但未生效:执行 `node scripts/build.mjs`,然后在插件列表停用并启用一次,不必重启 DSH。提示段文本不随重载替换,仍显示旧文本时重启 DSH。
|
|
10
10
|
6. 子会话不装配 `ho_` 与 `peer_` 工具,属正常:子会话在工具表解析阶段即被排除,与角色设置无关。
|
|
@@ -50,7 +50,15 @@ ho_init(parent="<已存在目录的绝对路径>", register=true)
|
|
|
50
50
|
1. 压缩须由用户在设置页点「压缩授权」,单次授权 5 分钟内有效,或在聊天框发送 `/allow-compact`。
|
|
51
51
|
2. `ho_checkpoint` 只登记,由调度器在回合结束后执行;目标必须空闲。
|
|
52
52
|
3. 交接体检不通过时拒绝排压缩,先用 `ho_handover(action=read)` 检查结构。
|
|
53
|
-
4. 已压满 3
|
|
53
|
+
4. 已压满 3 次后不再压缩,应换任:由本会话写好交接后调用 `ho_succeed` 自行换任,须用户先发送 `/allow-succeed` 授权。额度用尽的判定先于水位;低水位与 `force` 均不能越过,`ho_sessions` 的压缩指令(`action="order", order="compact"`)同样会被拒绝。
|
|
54
|
+
|
|
55
|
+
## 换任未执行
|
|
56
|
+
|
|
57
|
+
`ho_succeed` 报「换任未执行,未做任何写入」时,按报出的原因逐项处理:
|
|
58
|
+
|
|
59
|
+
1. 没有授权:由用户在聊天框发送 `/allow-succeed`(可留空,也可带 8 位短号授权别的会话),或在设置页「压缩授权」卡片的「换任授权」区域点「允许换任」。授权一次性、5 分钟有效、按会话生效;压缩授权不能顶替换任授权。
|
|
60
|
+
2. 不是在任者:只有登记表中该类会话的现任本人可以换任;已记过一次换任的旧会话也不得再次换任。先用 `ho_office` 确认现任的名字与会话 id,再由现任会话执行。
|
|
61
|
+
3. 交接体检不合格:报错逐条列出缺项——「下一步」一节缺失或没有条目、内容为空、仍有「待填」「TODO」一类占位符。补齐后用 `ho_handover(action=read)` 复核,再重新调用。
|
|
54
62
|
|
|
55
63
|
## 项目名或任务名不合法
|
|
56
64
|
|
package/package.json
CHANGED
package/scripts/blank-dsh.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 空白 DSH 验收:证明「移到一个没有任何旧全局文件的空 DSH 上,插件所有功能 100% 可用」。
|
|
3
|
-
* 做法:临时 HOME(无 ~/.dsh 状态)→ 只靠 dist/ 自带的东西跑完
|
|
3
|
+
* 做法:临时 HOME(无 ~/.dsh 状态)→ 只靠 dist/ 自带的东西跑完 24 个工具 + 迁移四件。
|
|
4
4
|
* 额外覆盖:7-Zip 不存在(强制 format=tar)、工作区规则文件被删光(必须退回内置模板)。
|
|
5
5
|
* 用法:node scripts/blank-dsh.mjs
|
|
6
6
|
*/
|
|
@@ -58,7 +58,7 @@ const ctx = {
|
|
|
58
58
|
apply(ctx, { packFormat: 'tar' }); // 强制内置归档器:模拟一台没有 7-Zip 的机器
|
|
59
59
|
|
|
60
60
|
section('装配');
|
|
61
|
-
ok(reg.tools.length ===
|
|
61
|
+
ok(reg.tools.length === 24, '注册 24 个工具', String(reg.tools.length));
|
|
62
62
|
ok(reg.tools.every((t) => t && t.name && t.description && typeof t.execute === 'function'), '每个工具都有 name/description/execute');
|
|
63
63
|
const names = reg.tools.map((t) => t.name).sort();
|
|
64
64
|
ok(names.indexOf('ho_pack') !== -1 && names.indexOf('peer_send') !== -1, '关键工具都在');
|
|
@@ -96,7 +96,7 @@ for (const rel of [
|
|
|
96
96
|
ok(fs.existsSync(path.join(WS, rel)), '铺好 ' + rel);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
section('
|
|
99
|
+
section('24 个工具在空白工作区逐个跑');
|
|
100
100
|
const calls = [
|
|
101
101
|
['ho_diag', {}],
|
|
102
102
|
['ho_brief', {}],
|
|
@@ -118,6 +118,7 @@ const calls = [
|
|
|
118
118
|
['peer_status', { session_id: 'session-blank-test' }],
|
|
119
119
|
['peer_peek', { session_id: 'session-blank-test', limit: 2 }],
|
|
120
120
|
['peer_rename', { session_id: 'session-blank-test', title: '空白验收' }],
|
|
121
|
+
['ho_succeed', {}],
|
|
121
122
|
];
|
|
122
123
|
for (const [tool, args] of calls) {
|
|
123
124
|
const r = await run(tool, args, WS, 'session-blank-test', '空白验收');
|
|
@@ -199,5 +200,5 @@ if (fails.length) {
|
|
|
199
200
|
for (const f of fails) console.log(' - ' + f);
|
|
200
201
|
process.exit(1);
|
|
201
202
|
}
|
|
202
|
-
console.log('BLANK-DSH OK(空白 DSH 上
|
|
203
|
-
|
|
203
|
+
console.log('BLANK-DSH OK(空白 DSH 上 24 工具 + 迁移四件全部可用)');
|
|
204
|
+
|
package/scripts/blank-smoke.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 空白环境冒烟:不依赖任何真实工作区,验「装上就能跑」。
|
|
3
|
-
* 1) 宿主半在假 ctx 上 apply:
|
|
3
|
+
* 1) 宿主半在假 ctx 上 apply:24 个工具、手册段、状态路由(200/404/405/HEAD)、只读工具不抛
|
|
4
4
|
* 2) 浏览器半在假 window/React 上装载:bundle 外形、settings.section 注册、成体视图能渲染
|
|
5
5
|
* 用法:node scripts/blank-smoke.mjs [--dist <dist 目录>]
|
|
6
6
|
*/
|
|
@@ -42,18 +42,21 @@ const ctx = {
|
|
|
42
42
|
};
|
|
43
43
|
apply(ctx, {});
|
|
44
44
|
|
|
45
|
-
ok(reg.tools.length ===
|
|
45
|
+
ok(reg.tools.length === 24, '工具数应为 24', String(reg.tools.length));
|
|
46
46
|
ok(reg.sections.length === 0, '未开启全局前置提示词时不得注册提示段', String(reg.sections.length));
|
|
47
47
|
ok(reg.tools.filter((t) => !t || !t.name || !t.description || !t.execute).length === 0, '每个工具都要有 name/description/execute');
|
|
48
|
-
//
|
|
48
|
+
// 上下文成本实测:工具描述是每次请求都要付的固定开销。参考值只用于展示,不作为通过条件。
|
|
49
49
|
const budget = await import(pathToFileURL(path.join(dist, 'lib', 'facts.mjs')).href);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
{
|
|
51
|
+
const rows = reg.tools.map((t) => ({ name: t.name, n: budget.estimateTokens(t.description) }));
|
|
52
|
+
const widest = rows.slice().sort((a, b) => b.n - a.n)[0];
|
|
53
|
+
console.log('工具描述 token 实测:最宽 ' + widest.name + ' ' + widest.n + '(参考值 ' + budget.TOKEN_BUDGET.toolDescTokens + ',不判失败)');
|
|
54
|
+
}
|
|
55
|
+
ok(reg.tools.filter((t) => t.name.startsWith('ho_')).length === 18, 'ho_* 应为 18 个', String(reg.tools.filter((t) => t.name.startsWith('ho_')).length));
|
|
53
56
|
ok(reg.tools.filter((t) => t.name.startsWith('peer_')).length === 6, 'peer_* 应为 6 个');
|
|
54
57
|
// 每步条件启用:内核 0.2.2 起工具按「步」解析,子会话在解析工具表这一步就要被排除,
|
|
55
|
-
// 否则它的首轮提示词会带着
|
|
56
|
-
ok((reg.toolOpts || []).length ===
|
|
58
|
+
// 否则它的首轮提示词会带着 24 个用不了的 schema(每步白付约 2.5k token)。
|
|
59
|
+
ok((reg.toolOpts || []).length === 24 && (reg.toolOpts || []).every((o) => o && typeof o.when === 'function'), '24 个工具都应带每步启用条件', String((reg.toolOpts || []).length));
|
|
57
60
|
|
|
58
61
|
/** 取某个工具的每步启用条件(0.2.2 起工具按步解析)。 */
|
|
59
62
|
const gateFor = (name) => {
|
|
@@ -146,11 +149,11 @@ let payload = null;
|
|
|
146
149
|
try { payload = JSON.parse(r1.body); } catch (e) { fails.push('返回不是 JSON:' + e.message); }
|
|
147
150
|
if (payload) {
|
|
148
151
|
ok(payload.plugin.version && payload.plugin.version !== '?', '应报出版本号', String(payload.plugin.version));
|
|
149
|
-
ok(payload.plugin.tools ===
|
|
152
|
+
ok(payload.plugin.tools === 24, 'payload 工具数应为 24', String(payload.plugin.tools));
|
|
150
153
|
ok(payload.workspace && payload.workspace.found === false, '空白 HOME 下应判定为没找到工作区', JSON.stringify(payload.workspace));
|
|
151
154
|
ok(Array.isArray(payload.notes) && payload.notes.length > 0, '没找到工作区时应给中文提示');
|
|
152
155
|
ok(payload.guide && Array.isArray(payload.guide.situations) && payload.guide.situations.length >= 8, 'payload 应带「怎么用」的时机表');
|
|
153
|
-
ok(payload.guide && payload.guide.groups.reduce((a, g) => a + g.tools.length, 0) ===
|
|
156
|
+
ok(payload.guide && payload.guide.groups.reduce((a, g) => a + g.tools.length, 0) === 24, '使用说明里应列全 24 个工具', payload.guide && String(payload.guide.groups.reduce((a, g) => a + g.tools.length, 0)));
|
|
154
157
|
ok(payload.plugin.initErrors && payload.plugin.initErrors.length === 0, '初始化错误应为空', JSON.stringify(payload.plugin.initErrors));
|
|
155
158
|
}
|
|
156
159
|
ok((await call('GET', '/dsh-handover/nope.json')).code === 404, '未知路径应 404');
|
|
@@ -284,6 +287,52 @@ fs.mkdirSync(path.join(WS, '_Control_Center'), { recursive: true });
|
|
|
284
287
|
fs.mkdirSync(path.join(WS, 'Projects', 'Demo', 'Demo库'), { recursive: true });
|
|
285
288
|
fs.mkdirSync(path.join(WS, '_Misc', 'tasks', '抢名额'), { recursive: true });
|
|
286
289
|
|
|
290
|
+
// ---------- 在任复用:工具层前置校验(任何写入之前返回) ----------
|
|
291
|
+
{
|
|
292
|
+
const { registerHolder } = await import(DIST('lib/office.mjs'));
|
|
293
|
+
const cacheDir = path.join(BLANK, '.dsh', 'storages', 'session_projcache', 'sessions');
|
|
294
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
295
|
+
fs.writeFileSync(path.join(cacheDir, 'session-hold.json'), JSON.stringify({
|
|
296
|
+
record: { rows: { contextPressure: { val: { pressureTokens: 16000, contextWindow: 160000 } } } },
|
|
297
|
+
}), 'utf8');
|
|
298
|
+
registerHolder(WS, { kind: 'project', project: 'Demo', name: 'Demo1', sessionId: 'session-hold', workspace: WS });
|
|
299
|
+
const sessToolR = reg.tools.find((t) => t.name === 'ho_sessions');
|
|
300
|
+
const makerExec = { name: 'ho_sessions', arguments: {}, agent: { session: { header: { id: 'session-maker', cwd: WS } } } };
|
|
301
|
+
const newErr = async (extra) => {
|
|
302
|
+
try {
|
|
303
|
+
await sessToolR.execute(Object.assign({ action: 'new', confirm: true, title: 'Demo2', project: 'Demo' }, extra), makerExec);
|
|
304
|
+
return '';
|
|
305
|
+
} catch (e) { return String(e && e.message ? e.message : e); }
|
|
306
|
+
};
|
|
307
|
+
const lowErr = await newErr({});
|
|
308
|
+
ok(/已有在任会话 Demo1/.test(lowErr) && /session-hold/.test(lowErr) && /peer_send/.test(lowErr) && /force=true/.test(lowErr), '项目类别有低水位在任时应拒绝新建并给出复用建议', lowErr);
|
|
309
|
+
const forcedErr = await newErr({ force: true });
|
|
310
|
+
ok(forcedErr.indexOf('已有在任会话') === -1, 'force=true 应越过复用判据', forcedErr);
|
|
311
|
+
|
|
312
|
+
// 换任:本类别现任就是本会话且水位未到线时拒绝,force=true 越过。
|
|
313
|
+
const handover = path.join(WS, '_Control_Center', 'records', 'Global_Handover.md');
|
|
314
|
+
fs.mkdirSync(path.dirname(handover), { recursive: true });
|
|
315
|
+
fs.writeFileSync(handover, '# 全局交接' + '\n\n## 下一步\n- 无\n', 'utf8');
|
|
316
|
+
registerHolder(WS, { kind: 'global', name: '全局1', sessionId: 'session-hold', workspace: WS });
|
|
317
|
+
const { grantConsent, consumedConsent } = await import(DIST('lib/consent.mjs'));
|
|
318
|
+
grantConsent('session-hold', 'blank-smoke', 'succeed');
|
|
319
|
+
const { runSuccession } = await import(DIST('lib/succession.mjs'));
|
|
320
|
+
const succEnv = {
|
|
321
|
+
cfg: resolveConfig({}),
|
|
322
|
+
roleOf: () => ({ role: 'global', project: '', task: '' }),
|
|
323
|
+
selfId: () => 'session-hold',
|
|
324
|
+
handoverFor: () => ({ path: handover, reason: '' }),
|
|
325
|
+
resolveWs: () => WS,
|
|
326
|
+
selfPct: () => 10,
|
|
327
|
+
registry: () => ({ list: () => [] }),
|
|
328
|
+
};
|
|
329
|
+
const succLow = await runSuccession(succEnv, null, {});
|
|
330
|
+
ok(/水位 10\.0%/.test(succLow) && /通常无需换任/.test(succLow) && /force=true/.test(succLow), '换任:本会话水位未到线时应拒绝并建议 force', succLow);
|
|
331
|
+
const succForce = await runSuccession(succEnv, null, { force: true });
|
|
332
|
+
ok(succForce.indexOf('尚未到线') === -1, '换任:force=true 应越过水位判据', succForce);
|
|
333
|
+
consumedConsent('session-hold', 'succeed');
|
|
334
|
+
}
|
|
335
|
+
|
|
287
336
|
const rTitle = resolveRole({ sessionId: 'session-t1', title: '全局9' });
|
|
288
337
|
ok(rTitle.role === 'global' && rTitle.source === 'title', '角色判定:标题含「全局」→ global', JSON.stringify(rTitle));
|
|
289
338
|
const rCwd = resolveRole({ sessionId: 'session-t2', title: 'Demo1', cwd: path.join(WS, 'Projects', 'Demo', 'Demo库') });
|
|
@@ -323,16 +372,16 @@ const hpU = handoverPathFor(WS, { role: 'plain' });
|
|
|
323
372
|
ok(hpU.path === null && !!hpU.reason, '普通会话不得给出交接路径(不退回全局)', JSON.stringify(hpU));
|
|
324
373
|
|
|
325
374
|
const tsG = toolsetFor('global');
|
|
326
|
-
ok(tsG.allow.length ===
|
|
375
|
+
ok(tsG.allow.length === 24 && tsG.deny.length === 0, '全局应可见全部 24 个工具', JSON.stringify([tsG.allow.length, tsG.deny.length]));
|
|
327
376
|
const tsP = toolsetFor('project');
|
|
328
|
-
ok(tsP.allow.length ===
|
|
377
|
+
ok(tsP.allow.length === 11, '项目角色应可见 11 个工具', String(tsP.allow.length));
|
|
329
378
|
ok(tsP.allow.indexOf('ho_handover') !== -1 && tsP.allow.indexOf('ho_checkpoint') !== -1, '项目角色必须能写自己的交接与压缩');
|
|
330
379
|
ok(tsP.allow.indexOf('ho_pack') === -1 && tsP.allow.indexOf('ho_sessions') === -1, '项目角色不该看见全局管理工具');
|
|
331
380
|
ok(tsP.deny.indexOf('ho_handover') === -1 && tsP.deny.indexOf('run_code') === -1, 'deny 名单不得含保留名或本角色已允许的工具');
|
|
332
381
|
const tsM = toolsetFor('misc');
|
|
333
|
-
ok(tsM.allow.length ===
|
|
382
|
+
ok(tsM.allow.length === 11 && tsM.allow.indexOf('peer_new') !== -1 && tsM.allow.indexOf('ho_gate') === -1, '杂项角色:11 个,含 peer_new、不含 ho_gate', String(tsM.allow.length));
|
|
334
383
|
const tsU = toolsetFor('plain');
|
|
335
|
-
ok(tsU.allow.length === 0 && tsU.deny.length ===
|
|
384
|
+
ok(tsU.allow.length === 0 && tsU.deny.length === 24, '普通会话不载入任何插件工具', JSON.stringify([tsU.allow.length, tsU.deny.length]));
|
|
336
385
|
const rSub = resolveRole({ sessionId: 'session-sub', title: 'Demo1', cwd: path.join(WS, 'Projects', 'Demo', 'Demo库'), subagent: true });
|
|
337
386
|
ok(rSub.role === 'plain' && rSub.source === 'subagent', '子会话默认按普通处理,不按 cwd 判项目', JSON.stringify(rSub));
|
|
338
387
|
|
|
@@ -463,15 +512,15 @@ ok(secNorm.indexOf('任务完成不报全局') !== -1, '项目提示段应写明
|
|
|
463
512
|
ok(secNorm.indexOf('任务达成') === -1, '项目提示段不得再出现任务达成');
|
|
464
513
|
ok(secNorm.indexOf('可直接 peer_send') !== -1, '项目提示段应允许会话之间直接对话', secNorm.slice(0, 400));
|
|
465
514
|
ok(secNorm.indexOf('resume=wait') !== -1, '项目提示段应说明收尾压缩不唤醒');
|
|
466
|
-
|
|
515
|
+
console.log('项目提示段 token 实测:' + budget.estimateTokens(calls.sections[0].text) + '(参考值 ' + budget.TOKEN_BUDGET.projectTokens + ',不判失败)');
|
|
467
516
|
ok(calls.sections[0].text.indexOf('子智能体') === -1, '项目段不得携带子智能体派发纪律(不属于本插件)');
|
|
468
|
-
// 真实工作区路径 +
|
|
517
|
+
// 真实工作区路径 + 迁移提示是最坏情形,参考值另设一档
|
|
469
518
|
{
|
|
470
519
|
const man = await import(pathToFileURL(path.join(dist, 'lib', 'manual.mjs')).href);
|
|
471
520
|
const longWs = path.join(os.tmpdir(), 'Project_Mate', '_Workspace');
|
|
472
521
|
const otherWs = path.join(os.tmpdir(), 'Project_Mate', '_Workspace_2');
|
|
473
522
|
const worst = man.scopedManual({ role: 'project', project: 'Demo', task: '' }, { ws: longWs, move: { phase: 'switched', dst: otherWs, src: longWs } });
|
|
474
|
-
|
|
523
|
+
console.log('项目最坏情形提示段 token 实测:' + budget.estimateTokens(worst) + '(参考值 ' + budget.TOKEN_BUDGET.projectWorstTokens + ',不判失败)');
|
|
475
524
|
}
|
|
476
525
|
ok(secNorm.indexOf('_Temp/Demo/') !== -1 && secNorm.indexOf('_Archives/Demo/') !== -1, '项目提示段应含临时与归档路径', secNorm.slice(0, 300));
|
|
477
526
|
// 回归锁:早期版本用 agent.inject({source:{kind:'plugin'}}) 投一次性引导,v4 格式准入拒绝该 source,
|
|
@@ -494,8 +543,8 @@ const globAgent = {
|
|
|
494
543
|
for (const cb of listeners) cb({ agent: globAgent, source: 'fresh' });
|
|
495
544
|
ok(gCalls.restrict === 0, '全局角色不应被裁剪', String(gCalls.restrict));
|
|
496
545
|
ok(gCalls.sections.length === 1 && gCalls.sections[0].name === 'dsh-handover:manual', '全局也应遮蔽兜底段');
|
|
497
|
-
//
|
|
498
|
-
|
|
546
|
+
// 常驻段只放每轮判定,一次性流程下沉手册;参考值按加权 token 计,只打印实测值。
|
|
547
|
+
console.log('全局提示段 token 实测:' + budget.estimateTokens(gCalls.sections[0].text) + '(参考值 ' + budget.TOKEN_BUDGET.globalTokens + ',不判失败)');
|
|
499
548
|
ok(gCalls.sections[0].text.indexOf('反模式') === -1, '全局段不得携带反模式纪律(不属于本插件)');
|
|
500
549
|
ok(gCalls.sections[0].text.indexOf('send_message 续用') === -1, '全局段不得携带子智能体续用纪律(不属于本插件)');
|
|
501
550
|
// 真机最坏情形:长路径 + 迁移提示行
|
|
@@ -504,7 +553,7 @@ ok(gCalls.sections[0].text.indexOf('send_message 续用') === -1, '全局段不
|
|
|
504
553
|
const longWs2 = path.join(os.tmpdir(), 'Project_Mate', '_Workspace');
|
|
505
554
|
const otherWs2 = path.join(os.tmpdir(), 'Project_Mate', '_Workspace_2');
|
|
506
555
|
const gWorst = man2.globalManual({ ws: longWs2, move: { phase: 'switched', dst: otherWs2, src: longWs2 } });
|
|
507
|
-
|
|
556
|
+
console.log('全局最坏情形提示段 token 实测:' + budget.estimateTokens(gWorst) + '(参考值 ' + budget.TOKEN_BUDGET.globalWorstTokens + ',不判失败)');
|
|
508
557
|
}
|
|
509
558
|
ok(gCalls.sections[0].text.indexOf('前置提示词') === -1, '插件角色段不得再携带前置提示词(由用户全局前置提示词注入)');
|
|
510
559
|
// 结构只保留会话定位、权限、节奏、交接与按需取用五节
|
|
@@ -576,7 +625,7 @@ writeRole({}, 'session-unr', { role: 'project', project: 'Demo', source: 'user'
|
|
|
576
625
|
for (const cb of ((reg.listeners && reg.listeners['agent/status']) || [])) cb({ agent: unrAgent, status: 'running' });
|
|
577
626
|
ok(!!unrCalls.section && /项目/.test(String(unrCalls.section.text)), '开轮前核对应按新角色补装提示段', JSON.stringify(unrCalls.section && unrCalls.section.name));
|
|
578
627
|
|
|
579
|
-
// 旧内核(没有 tools.resolve)才走 restrict:创建时按普通收窄
|
|
628
|
+
// 旧内核(没有 tools.resolve)才走 restrict:创建时按普通收窄 24 个,改角色后开轮前换成 13 个。
|
|
580
629
|
{
|
|
581
630
|
const legacyCalls = { restrict: 0, lastDeny: null, disposed: 0 };
|
|
582
631
|
const legacyListeners = {};
|
|
@@ -602,7 +651,7 @@ ok(!!unrCalls.section && /项目/.test(String(unrCalls.section.text)), '开轮
|
|
|
602
651
|
ctx: { tools: legacyTools, systemPrompt: legacyCtx.systemPrompt },
|
|
603
652
|
};
|
|
604
653
|
for (const cb of (legacyListeners['agent/created'] || [])) cb({ agent: legacyAgent, source: 'fresh' });
|
|
605
|
-
ok(legacyCalls.restrict === 1 && legacyCalls.lastDeny && legacyCalls.lastDeny.length ===
|
|
654
|
+
ok(legacyCalls.restrict === 1 && legacyCalls.lastDeny && legacyCalls.lastDeny.length === 24, '旧内核:创建时按普通收窄 24 个', JSON.stringify(legacyCalls.lastDeny && legacyCalls.lastDeny.length));
|
|
606
655
|
writeRole({}, 'session-legacy', { role: 'project', project: 'Demo', source: 'user' });
|
|
607
656
|
for (const cb of (legacyListeners['agent/status'] || [])) cb({ agent: legacyAgent, status: 'running' });
|
|
608
657
|
ok(legacyCalls.restrict === 2 && legacyCalls.lastDeny && legacyCalls.lastDeny.length === 13, '旧内核:开轮前换成项目角色的 13 个', JSON.stringify(legacyCalls.lastDeny && legacyCalls.lastDeny.length));
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 文档校验:工具名集合、旧数与旧术语、内部链接、版本、语体,以及版本说明的对外口吻。
|
|
3
3
|
* 只读,不做任何改写。退出码 0 表示全部通过。
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -14,6 +14,8 @@ const readme = path.join(root, 'README.md');
|
|
|
14
14
|
const files = [readme]
|
|
15
15
|
.concat(listFiles(path.join(root, 'docs'), ['.md']))
|
|
16
16
|
.filter((f) => fs.existsSync(f));
|
|
17
|
+
const releasesDir = path.join(root, 'docs', 'releases');
|
|
18
|
+
const releaseFiles = files.filter((f) => path.dirname(f) === releasesDir);
|
|
17
19
|
const problems = [];
|
|
18
20
|
|
|
19
21
|
// 1. README 长度(对外首页承载全部说明,上限放宽)
|
|
@@ -22,11 +24,30 @@ if (readmeLines > 200) problems.push('README 应不超过 200 行(当前 ' + r
|
|
|
22
24
|
|
|
23
25
|
// 2. 旧数与旧术语
|
|
24
26
|
const FORBIDDEN = ['21 个工具', '27 个工具', '未分类', '追加系统提示词', '4,028', '4.6 KB', '每库 24 行', 'CHANGELOG'];
|
|
27
|
+
// 审阅语境与内部用语:这些词只应出现在桌面审阅材料里,不能进对外文档与版本说明。
|
|
28
|
+
const REVIEW_WORDS = ['过目', '待验收', '待您', '待你', '请确认', '麻烦您', '麻烦你', '假宿主', '桩服务', '桩实现'];
|
|
25
29
|
for (const file of files) {
|
|
26
30
|
const text = fs.readFileSync(file, 'utf8');
|
|
27
31
|
for (const w of FORBIDDEN) {
|
|
28
32
|
if (text.indexOf(w) !== -1) problems.push(path.relative(root, file) + ' 仍含旧表述「' + w + '」');
|
|
29
33
|
}
|
|
34
|
+
for (const w of REVIEW_WORDS) {
|
|
35
|
+
if (text.indexOf(w) !== -1) problems.push(path.relative(root, file) + ' 含审阅语境或内部用语「' + w + '」');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2.1 版本说明是对外发布正文:不出现二人体,且每个已发布版本都要有一份。
|
|
40
|
+
for (const file of releaseFiles) {
|
|
41
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
42
|
+
const person = text.match(/[您你]/);
|
|
43
|
+
if (person) problems.push(path.relative(root, file) + ' 版本说明不应出现二人体(命中「' + person[0] + '」)');
|
|
44
|
+
if (!/^#\s+\S/.test(text)) problems.push(path.relative(root, file) + ' 版本说明应以一级标题开头');
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
const pkgVersion = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
48
|
+
if (!fs.existsSync(path.join(releasesDir, pkgVersion + '.md'))) {
|
|
49
|
+
problems.push('缺少版本说明 docs/releases/' + pkgVersion + '.md(Release 正文从这里取)');
|
|
50
|
+
}
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
// 3. 工具名:文档里出现的 ho_* 与 peer_* 必须在 names.mjs 中
|
|
@@ -70,5 +91,5 @@ if (problems.length) {
|
|
|
70
91
|
for (const p of problems) console.log(' - ' + p);
|
|
71
92
|
process.exitCode = 1;
|
|
72
93
|
} else {
|
|
73
|
-
console.log('CHECK-DOCS OK(' + files.length + '
|
|
94
|
+
console.log('CHECK-DOCS OK(' + files.length + ' 份文档,含版本说明 ' + releaseFiles.length + ' 份,工具名 ' + ALL_TOOL_NAMES.length + ' 个)');
|
|
74
95
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 命令行查在任登记表:`node scripts/office.mjs 全局 全局3 Mingent`
|
|
4
|
+
*
|
|
5
|
+
* 与工具 ho_office 同一份实现、同一份记录(_Control_Center/records/Office.json),
|
|
6
|
+
* 区别只是不起会话也能查。工作区根按 --root → DSH_OFFICE_ROOT → 从 cwd 向上找 _Control_Center 的顺序解析。
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { isDir } from '../src/lib/text.mjs';
|
|
10
|
+
import { loadOffice, resolveOffice, rowsOf, formatRow, officePath, KIND_LABEL } from '../src/lib/office.mjs';
|
|
11
|
+
|
|
12
|
+
function walkUp(start) {
|
|
13
|
+
let cur = path.resolve(start);
|
|
14
|
+
for (let i = 0; i < 8 && cur; i++) {
|
|
15
|
+
if (isDir(path.join(cur, '_Control_Center'))) return cur;
|
|
16
|
+
const up = path.dirname(cur);
|
|
17
|
+
if (up === cur) break;
|
|
18
|
+
cur = up;
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
let root = process.env.DSH_OFFICE_ROOT || process.env.DSH_HANDOVER_ROOT || '';
|
|
25
|
+
let json = false;
|
|
26
|
+
let all = false;
|
|
27
|
+
const queries = [];
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a === '--root') { root = argv[++i] || ''; continue; }
|
|
31
|
+
if (a === '--json') { json = true; continue; }
|
|
32
|
+
if (a === '--all' || a === '-a') { all = true; continue; }
|
|
33
|
+
if (a === '-h' || a === '--help') {
|
|
34
|
+
console.log('用法:node scripts/office.mjs [查询…] [--all] [--json] [--root <工作区根>]');
|
|
35
|
+
console.log('示例:node scripts/office.mjs 全局 全局3 Mingent');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
queries.push(a);
|
|
39
|
+
}
|
|
40
|
+
if (!root) root = walkUp(process.cwd());
|
|
41
|
+
if (!root) {
|
|
42
|
+
console.error('找不到工作区根(没有 _Control_Center):用 --root 指定,例如 --root E:\\_Project_Mate\\_Workspace');
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const store = loadOffice(root);
|
|
47
|
+
const rows = rowsOf(store);
|
|
48
|
+
if (!rows.length) {
|
|
49
|
+
console.error('在任登记表是空的:' + officePath(root));
|
|
50
|
+
console.log('提示:由全局会话用 ho_sessions / 换任流程登记一次在任会话,登记表随之生成。');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (all || !queries.length) {
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({ updatedAt: store.updatedAt || '', rows }, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
let lastKind = '';
|
|
59
|
+
for (const r of rows) {
|
|
60
|
+
if (r.kind !== lastKind) {
|
|
61
|
+
console.log((lastKind ? '\n' : '') + '【' + (KIND_LABEL[r.kind] || r.kind) + '】');
|
|
62
|
+
lastKind = r.kind;
|
|
63
|
+
}
|
|
64
|
+
console.log(formatRow(r));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let miss = 0;
|
|
71
|
+
const hits = [];
|
|
72
|
+
for (const q of queries) {
|
|
73
|
+
const r = resolveOffice(store, q);
|
|
74
|
+
if (!r.ok) { console.log(q + ' — ' + r.reason); miss++; continue; }
|
|
75
|
+
if (r.vacant) { console.log(q + ' — 类别「' + r.key + '」当前空缺(无人在任)'); continue; }
|
|
76
|
+
const row = { key: r.key, kind: r.lineage.kind, project: r.lineage.project, task: r.lineage.task, current: r.lineage.current };
|
|
77
|
+
if (!json) console.log(formatRow(row, q));
|
|
78
|
+
hits.push({ askedAs: q, via: r.via, key: r.key, kind: r.lineage.kind, name: r.lineage.current.name, sessionId: r.lineage.current.sessionId });
|
|
79
|
+
}
|
|
80
|
+
if (json) console.log(JSON.stringify({ updatedAt: store.updatedAt || '', hits }, null, 2));
|
|
81
|
+
process.exit(miss ? 1 : 0);
|