@shgroup/dsh-serenity-hooks 1.57.2 → 1.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/lib/acp-face.d.ts +149 -0
- package/lib/cro.d.ts +90 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +497 -4
- package/lib/ports.d.ts +5 -3
- package/lib/{settings-section-DQIVsTqE.js → settings-section-DGUQZo5j.js} +28 -2
- package/lib/settings-section.d.ts +22 -0
- package/package.json +11 -3
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.58.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1 与 0.2.0-rc.1(双档,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acp-face.ts — **面 E:官方 ACP 面**(2026-10-16,owner 令「跑在 dsh web 进程内」)
|
|
3
|
+
*
|
|
4
|
+
* ## 它是什么
|
|
5
|
+
*
|
|
6
|
+
* 把**官方** `@deepseek-ai/dsh-acp` 挂进**本进程**(宿主 `dsh web` 那条 Cordis 树),
|
|
7
|
+
* 让标准 ACP 客户端(Agmente 等)经 **WebSocket** 登入本 CCC。
|
|
8
|
+
*
|
|
9
|
+
* ## 🔴 为什么是"自持面"而不是借宿主 `webServer`(本模块的核心设计决策)
|
|
10
|
+
*
|
|
11
|
+
* 两条路,**选乙**:
|
|
12
|
+
* · 甲:`ctx.webServer.registerUpgrade({path})` —— 探针(P0′)走的那条;
|
|
13
|
+
* · 乙:**本插件自己起一个 listener**(面宿主统一范式),在它上面挂 `'upgrade'` ← **本模块**
|
|
14
|
+
*
|
|
15
|
+
* 选乙的理由(按强度):
|
|
16
|
+
* ① 🔴 **不需要往 `HostWebServer` 本地接口补 `registerUpgrade`** —— 面宿主是**我们自己的**,
|
|
17
|
+
* `FaceHandle.server` 就是原始 `node:http` server(`face-host.ts:81` 注释逐字指出
|
|
18
|
+
* *"面若需挂自己的 server 级事件(gateway 的 `'upgrade'`)在此挂"*)⇒ **少动一处宿主缝**;
|
|
19
|
+
* ② ✅ **先例在生产里跑着**:`gateway.ts:454` 的 `handle.server.on('upgrade', …)` 就是这条路;
|
|
20
|
+
* ③ ✅ **与三个既有面同范式**(A 宿主监听/B C D 插件监听)⇒ **零新概念**;
|
|
21
|
+
* ④ ✅ **端口独立**(不动宿主 `webServer.host/port`)—— 而 owner Q3 恰恰说
|
|
22
|
+
* 「**先 `0.0.0.0` 监听即可**」,自持面**可直接绑 `0.0.0.0`**(面自己的 `host`)⇒ 更贴那条裁决;
|
|
23
|
+
* ⑤ ✅ **拆卸免费**(`stopAllFaces` 是"插件卸载/HMR 拆卸自起 listener 的唯一入口")。
|
|
24
|
+
*
|
|
25
|
+
* ## 🔴 为什么不走 `AcpServer` 而直接调 `dsh-acp` 的 `apply`(P0′ 实测结论)
|
|
26
|
+
*
|
|
27
|
+
* `AcpServer` 的入参是 `{ agent: AgentConnector }`,而 **`dsh-acp` 不导出它的 connector**
|
|
28
|
+
* (它在 `apply` 内部自建)⇒ 走 `AcpServer` 就得**重写 `dsh-acp` 全部 onRequest 接线**。
|
|
29
|
+
* ⇒ 最短路 = 自己把 socket 适配成 ACP `Stream`,交给 `apply` 的 `config.stream`
|
|
30
|
+
* (其 JSDoc 逐字:*"Runtime-only transport override; production uses stdio"*)。
|
|
31
|
+
*
|
|
32
|
+
* ## ⚠️ 三个**已实测**的坑(照做,别重新发明)
|
|
33
|
+
*
|
|
34
|
+
* ① **直接调 `apply` 时,`inject` 要替 `dsh-acp` 声明它需要的全部服务** ——
|
|
35
|
+
* 实测逐字失败过:`cannot get property "sessionPersistence" without inject`
|
|
36
|
+
* (抛自 `dsh-acp/lib/index.js:1074`)。故本模块 `inject` 含
|
|
37
|
+
* `agents` / `llm` / `sessionPersistence` / `sessions`。
|
|
38
|
+
* ② **`dsh-acp` 的 `Config` schema 里没有 `stream` 字段** ⇒ 只能**程序化传入**,不可经 YAML。
|
|
39
|
+
* ③ **`wsToAcpStream` 的元素是"已解析的 JSON-RPC 消息"**(不是字节)——
|
|
40
|
+
* 官方 `ndJsonStream` 干的就是"字节 ↔ 消息"的分帧,我们照它的语义自己分帧(按行)。
|
|
41
|
+
*
|
|
42
|
+
* ## 🔴 安全边界(不许含混)
|
|
43
|
+
*
|
|
44
|
+
* **ACP 协议本身不做认证**(`dsh-acp` 的 `authenticate` 恒成功),而本面按 owner 裁决绑
|
|
45
|
+
* **`0.0.0.0`** ⇒ **必须有外部接入层兜底**(现为 Cloudflare Access)。
|
|
46
|
+
* ⛔ 「隧道不是 ACC 该关心的事」(Q3 裁决)**成立**,但「因此不需要认证层」**不成立** ——
|
|
47
|
+
* 故本模块的启动日志与 `status()` **都显式带这条前提**。
|
|
48
|
+
*/
|
|
49
|
+
import type { Context } from 'cordis';
|
|
50
|
+
import { type FaceHandle } from './face-host.js';
|
|
51
|
+
export declare function acpFaceActive(): boolean;
|
|
52
|
+
export declare function acpFacePort(): number | null;
|
|
53
|
+
/**
|
|
54
|
+
* 面是否在监听(意图读取)。
|
|
55
|
+
* ⚠️ 与 `startFace` 的关系同其余各面:**启动本身不读它**(`startFace` 是机械入口),
|
|
56
|
+
* 由装配层用它决定"起还是停"。
|
|
57
|
+
*/
|
|
58
|
+
export declare function acpFaceEnabled(): boolean;
|
|
59
|
+
/** 面的运行统计(`dashboard` / 诊断面用;**只读快照**) */
|
|
60
|
+
export interface AcpFaceStatus {
|
|
61
|
+
active: boolean;
|
|
62
|
+
port: number | null;
|
|
63
|
+
host: string;
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
connections: number;
|
|
66
|
+
applied: number;
|
|
67
|
+
errors: Array<{
|
|
68
|
+
at: number;
|
|
69
|
+
applied: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** 🔴 本面**没有内建认证** —— 这条随状态一起出,避免"看到 active 就以为它是安全的" */
|
|
73
|
+
authNote: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function acpFaceStatus(): AcpFaceStatus;
|
|
76
|
+
/**
|
|
77
|
+
* 启动面 E(幂等;重复调用只起一次)。
|
|
78
|
+
*
|
|
79
|
+
* 🔴 **失败语义**:本面**默认关**且是**可选面** ⇒ 起不来**不得**影响插件其余部分
|
|
80
|
+
* (调用方负责 catch;本函数把失败原样抛出,由装配层记日志)。
|
|
81
|
+
*/
|
|
82
|
+
export declare function startAcpFace(ctx: Context): Promise<FaceHandle>;
|
|
83
|
+
/**
|
|
84
|
+
* 停面 E(幂等)。
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ **与其余各面同款注意**:`stopFace` **不用** `closeAllConnections()`(那会掐断在飞的请求);
|
|
87
|
+
* WS 连接由 `wss.close()` 与 socket 自身生命周期收束。
|
|
88
|
+
*/
|
|
89
|
+
export declare function stopAcpFace(): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* 解析 ACP 会话该用的 `{provider, model}`。
|
|
92
|
+
*
|
|
93
|
+
* ## 🔴 为什么必须有这个函数(2026-10-16 容器实测抓出的**真缺陷**)
|
|
94
|
+
*
|
|
95
|
+
* 早先我们是**裸传** `provider: readAcpProvider(), model: readAcpModel()` —— 而它们读的是
|
|
96
|
+
* **环境变量**,默认**未设** ⇒ 传下去就是 `undefined`。后果**不是"用默认模型"**:
|
|
97
|
+
* 上游 `dsh-acp` 的 `agentOptions()`(`lib/index.js:1356-1361`)**只在字段非 undefined 时才带上**:
|
|
98
|
+
* ```js
|
|
99
|
+
* ...config.provider !== void 0 ? { provider: config.provider } : {},
|
|
100
|
+
* ...config.model !== void 0 ? { model: config.model } : {}
|
|
101
|
+
* ```
|
|
102
|
+
* ⇒ **我们传 undefined,agent 就真的没有模型** ⇒ 第一条 `session/prompt` 必失败,逐字报:
|
|
103
|
+
* `Internal error: turn failed: prompt variable "{{model}}" has no value for this assembly
|
|
104
|
+
* (section "deployment:persona-prefix")`。
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ **网页端不中招**(它经 `agent-default-model` 层拿到默认值)⇒ **只有 ACP 这条入口坏** ——
|
|
107
|
+
* 也就是说:**手机上第一条消息必然失败,而本机怎么试都正常**。
|
|
108
|
+
* 🔴 这个缺陷 **typecheck / 2812 条单测 / deploy 预检 全绿**(它是运行时合成形态,
|
|
109
|
+
* 静态面结构性地看不见)⇒ **只有"容器 + 真模型"那一跑能抓到它**。
|
|
110
|
+
*
|
|
111
|
+
* ## 修法 = **读宿主自己的默认模型服务**(不另造真相源)
|
|
112
|
+
*
|
|
113
|
+
* 宿主暴露服务 `ctx.agentDefaultModel.currentSelection()`,其契约逐字写着
|
|
114
|
+
* *"Default model selection for Agents created **without an explicit model**"*
|
|
115
|
+
* (`dsh-agent-default-model/lib/types/index.d.ts`)—— 这正是"没显式指定时该用什么"的**权威出处**。
|
|
116
|
+
* ⇒ 我们只是**把它转交给 ACP**,不引入第二套缺省。
|
|
117
|
+
*
|
|
118
|
+
* 优先级(**显式 > 宿主默认**):`SERENITY_ACP_PROVIDER/MODEL` 环境变量(调试用)> 宿主默认。
|
|
119
|
+
* ⚠️ **一处如实标注的弱化**:这里**不打日志**(每次连接都解析一次,打日志会刷屏);
|
|
120
|
+
* 若宿主服务缺失,**退回"两个都 undefined"=旧行为**(即那条已知会失败的形态)——
|
|
121
|
+
* 而不是静默编一个模型。⇒ 副作用是"没有默认模型时仍会失败",但那**与宿主自身缺配置时的行为一致**。
|
|
122
|
+
*
|
|
123
|
+
* 🔵 **公开导出**(而非模块私有):它是一个**纯函数 + 有独立契约**的单元(本仓同类先例 =
|
|
124
|
+
* `host/access.ts` 的一组读取器),且**那个真缺陷正是在它这一层**(裸传 undefined)——
|
|
125
|
+
* 回归钉必须能**直接钉住它**,而不是隔着 WS 握手去间接观察(那样测不出"取到了什么")。
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveAcpSelection(ctx: Context): {
|
|
128
|
+
provider?: string;
|
|
129
|
+
model?: string;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* 装配(`index.ts` 的 `apply` 调用):按**面板/部署键**启停面 E,配置变化热同步。
|
|
133
|
+
*
|
|
134
|
+
* 🔴 **W3 接线**(2026-10-16):本轮之前 `startAcpFace` 虽已实现却**没有任何调用点**
|
|
135
|
+
* —— 即"面写好了但从不起"。本函数补上它,形式与两个兄弟面一致
|
|
136
|
+
* (`registerWeixinSendApi` / `registerDiagramTool`):
|
|
137
|
+
*
|
|
138
|
+
* ① **反应式**:面板拨开关**只改配置、不重跑 `apply`**(v1.51.1 实测过的那条铁律)
|
|
139
|
+
* ⇒ 必须监听事件才会"拨了就有反应";
|
|
140
|
+
* ② 监听**宿主的真事件** `settings/document-updated`(按 ns 过滤),
|
|
141
|
+
* ⛔ **不监听** `serenity/settings-changed` —— 那个事件**全仓没有任何 `emit` 点**
|
|
142
|
+
* (=死通道;见 `registerDiagramTool` 的注释),照它接线等于接了个永远不响的铃;
|
|
143
|
+
* ③ 再挂 `agent/created`/`session/created` 兜底(宿主事件面若换名,至少还有这条能自愈);
|
|
144
|
+
* ④ 启动时同步一次(`apply` 时按当前配置定初始态)。
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ **失败语义**:本面**默认关**且**依赖可选**(`dsh-acp` 是 peer)⇒ 起不来**只记日志**,
|
|
147
|
+
* 绝不抛出(抛出会让整个插件加载失败,而它只是一个可选面)。
|
|
148
|
+
*/
|
|
149
|
+
export declare function registerAcpFace(ctx: Context): void;
|
package/lib/cro.d.ts
CHANGED
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
*/
|
|
46
46
|
/** 入口文件名(**全写**,所有者令 2026-09-19;不改缩写) */
|
|
47
47
|
export declare const CRO_FILENAME = "continuous-re-occurrence.ts";
|
|
48
|
+
/**
|
|
49
|
+
* 机器绑定文件名(**全写**,与 `CRO_FILENAME` 同风格)。
|
|
50
|
+
*
|
|
51
|
+
* 🔴 **它随 CCC git 走是设计意图,不是缺陷**:
|
|
52
|
+
* 绑定声明必须跟着**轨迹**走,才能在第二台机器上被读到、并被判为"不是我的"。
|
|
53
|
+
* (这正是机器绑定的整个目的 —— 见 `resolveCroHostBinding` 的头注。)
|
|
54
|
+
*/
|
|
55
|
+
export declare const CRO_HOST_FILENAME = "cro-host-binding";
|
|
48
56
|
/** 硬超时(沿用 `biasProvider` 先例的 60s;设计 §5 情形 2) */
|
|
49
57
|
export declare const CRO_TIMEOUT_MS = 60000;
|
|
50
58
|
/**
|
|
@@ -223,6 +231,88 @@ export type CroRunner = (scriptAbs: string, stdinJson: string, timeoutMs: number
|
|
|
223
231
|
export declare function croScriptPath(root: string, dirName: string): string;
|
|
224
232
|
/** CRO 是否启用(**判据 = 文件在不在**;设计 §2.1:无 enabled 字段、无注册表) */
|
|
225
233
|
export declare function isCroEnabled(root: string, dirName: string): boolean;
|
|
234
|
+
/**
|
|
235
|
+
* 机器绑定判定结果(**永不抛**)。
|
|
236
|
+
*
|
|
237
|
+
* 三态而非布尔:**「没绑」与「绑给别人」必须分得开**(见下"为什么三态")。
|
|
238
|
+
*/
|
|
239
|
+
export interface CroHostVerdict {
|
|
240
|
+
/** 这台机器可以跑吗(唯一决定投递与否的字段) */
|
|
241
|
+
allowed: boolean;
|
|
242
|
+
/** 绑定文件里写的机器名(未绑/读不到 ⇒ `null`) */
|
|
243
|
+
bound: string | null;
|
|
244
|
+
/** 本机机器名 */
|
|
245
|
+
self: string;
|
|
246
|
+
/**
|
|
247
|
+
* 机器可读的判定原因(稳定码,供测试与日志断言)。
|
|
248
|
+
* · `bound-to-self` —— 已绑且是本机 ⇒ 跑
|
|
249
|
+
* · `not-bound` —— **没有绑定文件**(或文件是空的)⇒ **不跑**(fail-closed)
|
|
250
|
+
* · `bound-to-other` —— 绑给了别的机器 ⇒ 不跑
|
|
251
|
+
* · `read-error` —— 文件在但读不了 ⇒ 不跑(fail-closed)
|
|
252
|
+
*/
|
|
253
|
+
code: 'bound-to-self' | 'not-bound' | 'bound-to-other' | 'read-error';
|
|
254
|
+
/** 人读说明(进 tick 日志) */
|
|
255
|
+
detail: string;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 解析机器绑定 —— 「**这条轨迹由哪台机器负责自唤醒**」(所有者令 2026-10-15)。
|
|
259
|
+
*
|
|
260
|
+
* ## 所有者裁定的语义(逐字,D135)
|
|
261
|
+
*
|
|
262
|
+
* 「**CRO 必须指定绑定机器,否则不运行,唤醒器 检查这个**」⇒ 三条同时成立:
|
|
263
|
+
* ① **只许这一台**(白名单,不是"排除这一台");
|
|
264
|
+
* ② **fail-closed** —— **没绑 = 不跑**(不是"没绑 = 随便跑");
|
|
265
|
+
* ③ **检查者在唤醒器**(本函数由 `runCroPhase` 调用,不是程序自己判)。
|
|
266
|
+
*
|
|
267
|
+
* ## 绑定键 = 机器名(所有者 2026-10-15 裁「用 hostname 吧,比较合理」)
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ **已接受的取舍**:本机机器名是 QEMU 机型派生的默认形
|
|
270
|
+
* (`yh-Standard-PC-Q35-ICH9-2009`)⇒ 同法新建的同名用户 VM 会取到同一个名字。
|
|
271
|
+
* 所有者 2026-10-15 明确「**不改名,家里就这几台机器没必要**」⇒ **风险由所有者接受**。
|
|
272
|
+
*
|
|
273
|
+
* ## 🔴 为什么必须是三态,「没绑」不能并进「绑给别人」
|
|
274
|
+
*
|
|
275
|
+
* 2026-10-15 已发生过**同族事故**:`send-now` 失败时报的是「无绑定会话记录」,
|
|
276
|
+
* 而真因是"那条轨迹不可寻址"—— **文案相同、原因不同**,害得排查方向错了一次
|
|
277
|
+
* (S142 §日志 续 129 / D134)。
|
|
278
|
+
* ⇒ 绑定这一层**不许再造同样的坑**:「你没绑」与「你绑给了别人」是**两件不同的事**,
|
|
279
|
+
* 日志必须分开写 —— 否则将来"为什么这条轨迹不跑了"会又一次指错方向。
|
|
280
|
+
*
|
|
281
|
+
* ## 文件形态(刻意的极简)
|
|
282
|
+
*
|
|
283
|
+
* · 位置 = `<轨迹目录>/cro-host-binding`(与 CRO 程序**同级**)
|
|
284
|
+
* · 内容 = **一行机器名**(首尾空白裁掉;只取第一行有效内容)
|
|
285
|
+
* · 不用 JSON —— 它只有一个值,JSON 只会多一种写错的方式
|
|
286
|
+
* · 支持 `#` 注释行与空行(方便人写、方便留痕)
|
|
287
|
+
*
|
|
288
|
+
* 🔴 **它必须随 git 入库**(不是"运行态"):绑定声明要**跟着轨迹走到第二台机器**,
|
|
289
|
+
* 在那里被判成「绑给了别人」而**正确地不跑** —— 那就是本机制在工作。
|
|
290
|
+
* ⇒ ⛔ **不要把它加进 `.gitignore`**(那会让第二台机器读到"没绑"而同样不跑,
|
|
291
|
+
* 表面上结果一样,但**判据从"明确绑给别人"退化成"什么都没说"**,失去可诊断性)。
|
|
292
|
+
*
|
|
293
|
+
* ## 为什么读文件而不是读快照字段
|
|
294
|
+
*
|
|
295
|
+
* 快照(`CroSnapshot`)是喂给**程序**的;而本判定发生在**程序被 spawn 之前**
|
|
296
|
+
* —— 程序还没跑,没有快照可读。⇒ 只能读文件。(这也是"机制属 ACC"的体现:
|
|
297
|
+
* ACC 在调用用户程序前先判定自己该不该调用。)
|
|
298
|
+
*
|
|
299
|
+
* @param root CCC 根
|
|
300
|
+
* @param dirName 轨迹目录名
|
|
301
|
+
* @param selfHost 本机机器名(**注入** ⇒ 可测;生产调用方传 `hostname()`)
|
|
302
|
+
* @returns 判定结果(**永不抛** —— 旁路容忍铁律)
|
|
303
|
+
*/
|
|
304
|
+
export declare function resolveCroHostBinding(root: string, dirName: string, selfHost: string): CroHostVerdict;
|
|
305
|
+
/**
|
|
306
|
+
* 解析绑定文件正文 ⇒ 机器名(取不到 ⇒ `null`)。
|
|
307
|
+
*
|
|
308
|
+
* 规则(刻意宽松:**只认一行,其余都当注释**):
|
|
309
|
+
* · 逐行扫;跳过空行与 `#` 开头的行;
|
|
310
|
+
* · **第一条有效行** = 机器名(裁首尾空白);
|
|
311
|
+
* · 无有效行 ⇒ `null`。
|
|
312
|
+
* @param raw 文件正文
|
|
313
|
+
* @returns 机器名;无有效内容 ⇒ `null`
|
|
314
|
+
*/
|
|
315
|
+
export declare function parseCroHostBinding(raw: string): string | null;
|
|
226
316
|
/**
|
|
227
317
|
* 列出**启用了 CRO 的轨迹目录名**(供调度器每 tick 扫描,设计 §6.1)。
|
|
228
318
|
*
|
package/lib/index.d.ts
CHANGED
|
@@ -174,6 +174,16 @@ export interface Config {
|
|
|
174
174
|
unattendedEnabled?: Volatile<boolean | undefined>;
|
|
175
175
|
/** **diagram 工具总闸**(面板层;部署层同义键 = `diagram.enabled`;缺省关;D104) */
|
|
176
176
|
diagramEnabled?: Volatile<boolean | undefined>;
|
|
177
|
+
/**
|
|
178
|
+
* 🔴 **面 E:官方 ACP 总闸**(面板层;部署层同义键 = `acpFace.enabled`;**缺省关**;2026-10-16)。
|
|
179
|
+
*
|
|
180
|
+
* ⚠️ **键名刻意不叫 `acpEnabled`** —— 那是**旧面 E(3100 自造问答面)**的键,
|
|
181
|
+
* 已随 owner「砍掉外部问答」裁决退役、现被**静默忽略**。复用同名会让"当年那份旧配置"
|
|
182
|
+
* 在新面上复活(判据:**退役的是功能,名字仍须留作"无人读"**,否则退役不彻底)。
|
|
183
|
+
*/
|
|
184
|
+
acpFaceEnabled?: Volatile<boolean | undefined>;
|
|
185
|
+
/** 面 E 端口(面板层;部署层同义键 = `acpFace.port`;缺省 3200) */
|
|
186
|
+
acpFacePort?: Volatile<number | undefined>;
|
|
177
187
|
}
|
|
178
188
|
/** schemastery 的 schema 类型(别名只为可读;**名字取自全局命名空间**,见上方 TS2742 说明) */
|
|
179
189
|
type SchemaOf<S, T = S> = Schemastery<S, T>;
|
|
@@ -230,6 +240,13 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
230
240
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
231
241
|
enabled: z<boolean, boolean, "defined">;
|
|
232
242
|
}>>, "plain">;
|
|
243
|
+
acpFace: z<Schemastery.ObjectS<NoInfer<{
|
|
244
|
+
enabled: z<boolean, boolean, "defined">;
|
|
245
|
+
port: z<number, number, "defined">;
|
|
246
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
247
|
+
enabled: z<boolean, boolean, "defined">;
|
|
248
|
+
port: z<number, number, "defined">;
|
|
249
|
+
}>>, "plain">;
|
|
233
250
|
humanChannel: z<Schemastery.ObjectS<NoInfer<{
|
|
234
251
|
accounts: z<HumanChannelAccountConfig[], HumanChannelAccountConfig[], "defined">;
|
|
235
252
|
subscriptions: z<HumanChannelSubscriptionConfig[], HumanChannelSubscriptionConfig[], "defined">;
|
|
@@ -264,6 +281,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
264
281
|
croEnabled: z<boolean, boolean, "volatile">;
|
|
265
282
|
unattendedEnabled: z<boolean, boolean, "volatile">;
|
|
266
283
|
diagramEnabled: z<boolean, boolean, "volatile">;
|
|
284
|
+
acpFaceEnabled: z<boolean, boolean, "volatile">;
|
|
285
|
+
acpFacePort: z<number, number, "volatile">;
|
|
267
286
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
268
287
|
serenityConfigPaths: z<string[], string[], "defined">;
|
|
269
288
|
tools: z<boolean, boolean, "defined">;
|
|
@@ -313,6 +332,13 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
313
332
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
314
333
|
enabled: z<boolean, boolean, "defined">;
|
|
315
334
|
}>>, "plain">;
|
|
335
|
+
acpFace: z<Schemastery.ObjectS<NoInfer<{
|
|
336
|
+
enabled: z<boolean, boolean, "defined">;
|
|
337
|
+
port: z<number, number, "defined">;
|
|
338
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
339
|
+
enabled: z<boolean, boolean, "defined">;
|
|
340
|
+
port: z<number, number, "defined">;
|
|
341
|
+
}>>, "plain">;
|
|
316
342
|
humanChannel: z<Schemastery.ObjectS<NoInfer<{
|
|
317
343
|
accounts: z<HumanChannelAccountConfig[], HumanChannelAccountConfig[], "defined">;
|
|
318
344
|
subscriptions: z<HumanChannelSubscriptionConfig[], HumanChannelSubscriptionConfig[], "defined">;
|
|
@@ -347,6 +373,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
347
373
|
croEnabled: z<boolean, boolean, "volatile">;
|
|
348
374
|
unattendedEnabled: z<boolean, boolean, "volatile">;
|
|
349
375
|
diagramEnabled: z<boolean, boolean, "volatile">;
|
|
376
|
+
acpFaceEnabled: z<boolean, boolean, "volatile">;
|
|
377
|
+
acpFacePort: z<number, number, "volatile">;
|
|
350
378
|
}>>, "plain">;
|
|
351
379
|
export declare function apply(ctx: Context, config: Config): void;
|
|
352
380
|
export {};
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebSe
|
|
|
3
3
|
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
|
|
4
4
|
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-B4P3Uw6D.js";
|
|
5
5
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B9gU1YXz.js";
|
|
6
|
-
import { a as
|
|
6
|
+
import { a as FACE_PORTS, c as MECHANISM_PORTS, d as registerSkiffSession$1, f as skiffRoleFor$1, h as ACC_VERSION, i as ACP_PORT, l as SKIFF_DEBUG_PORT, m as unregisterSkiffSession$1, n as readSimpleSettings, o as GATEWAY_PORT, p as skiffSessionInfo$1, r as registerSettingsSection, s as MAIN_WEB_PORT, u as WEIXIN_SEND_PORT } from "./settings-section-DGUQZo5j.js";
|
|
7
7
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
9
9
|
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
|
|
@@ -13,7 +13,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
13
13
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
14
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
16
|
-
import { homedir, platform } from "node:os";
|
|
16
|
+
import { homedir, hostname, platform } from "node:os";
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
18
|
import { platform as platform$1 } from "node:process";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -9971,10 +9971,20 @@ function listWakes(root, opts = {}) {
|
|
|
9971
9971
|
*/
|
|
9972
9972
|
/** 入口文件名(**全写**,所有者令 2026-09-19;不改缩写) */
|
|
9973
9973
|
const CRO_FILENAME = "continuous-re-occurrence.ts";
|
|
9974
|
+
/**
|
|
9975
|
+
* 机器绑定文件名(**全写**,与 `CRO_FILENAME` 同风格)。
|
|
9976
|
+
*
|
|
9977
|
+
* 🔴 **它随 CCC git 走是设计意图,不是缺陷**:
|
|
9978
|
+
* 绑定声明必须跟着**轨迹**走,才能在第二台机器上被读到、并被判为"不是我的"。
|
|
9979
|
+
* (这正是机器绑定的整个目的 —— 见 `resolveCroHostBinding` 的头注。)
|
|
9980
|
+
*/
|
|
9981
|
+
const CRO_HOST_FILENAME = "cro-host-binding";
|
|
9974
9982
|
/** 硬超时(沿用 `biasProvider` 先例的 60s;设计 §5 情形 2) */
|
|
9975
9983
|
const CRO_TIMEOUT_MS = 6e4;
|
|
9976
9984
|
/** stdout 上限(防一个坏程序把日志撑爆;沿用 hook 截断思路) */
|
|
9977
9985
|
const CRO_STDOUT_MAX = 65536;
|
|
9986
|
+
/** 绑定文件体积上限(它只是个机器名 ⇒ 超过这个数一定是写错了,不读) */
|
|
9987
|
+
const CRO_HOST_FILE_MAX = 1024;
|
|
9978
9988
|
/** SESSION.md 文件名(与 `trajectory-ops.ts` 同值;此处本地常量避免反向依赖) */
|
|
9979
9989
|
const SESSION_MD = "SESSION.md";
|
|
9980
9990
|
/**
|
|
@@ -10109,6 +10119,135 @@ function croScriptPath(root, dirName) {
|
|
|
10109
10119
|
return resolveInside(root, join("AGENT_SESSIONS", dirName, CRO_FILENAME));
|
|
10110
10120
|
}
|
|
10111
10121
|
/**
|
|
10122
|
+
* 解析机器绑定 —— 「**这条轨迹由哪台机器负责自唤醒**」(所有者令 2026-10-15)。
|
|
10123
|
+
*
|
|
10124
|
+
* ## 所有者裁定的语义(逐字,D135)
|
|
10125
|
+
*
|
|
10126
|
+
* 「**CRO 必须指定绑定机器,否则不运行,唤醒器 检查这个**」⇒ 三条同时成立:
|
|
10127
|
+
* ① **只许这一台**(白名单,不是"排除这一台");
|
|
10128
|
+
* ② **fail-closed** —— **没绑 = 不跑**(不是"没绑 = 随便跑");
|
|
10129
|
+
* ③ **检查者在唤醒器**(本函数由 `runCroPhase` 调用,不是程序自己判)。
|
|
10130
|
+
*
|
|
10131
|
+
* ## 绑定键 = 机器名(所有者 2026-10-15 裁「用 hostname 吧,比较合理」)
|
|
10132
|
+
*
|
|
10133
|
+
* ⚠️ **已接受的取舍**:本机机器名是 QEMU 机型派生的默认形
|
|
10134
|
+
* (`yh-Standard-PC-Q35-ICH9-2009`)⇒ 同法新建的同名用户 VM 会取到同一个名字。
|
|
10135
|
+
* 所有者 2026-10-15 明确「**不改名,家里就这几台机器没必要**」⇒ **风险由所有者接受**。
|
|
10136
|
+
*
|
|
10137
|
+
* ## 🔴 为什么必须是三态,「没绑」不能并进「绑给别人」
|
|
10138
|
+
*
|
|
10139
|
+
* 2026-10-15 已发生过**同族事故**:`send-now` 失败时报的是「无绑定会话记录」,
|
|
10140
|
+
* 而真因是"那条轨迹不可寻址"—— **文案相同、原因不同**,害得排查方向错了一次
|
|
10141
|
+
* (S142 §日志 续 129 / D134)。
|
|
10142
|
+
* ⇒ 绑定这一层**不许再造同样的坑**:「你没绑」与「你绑给了别人」是**两件不同的事**,
|
|
10143
|
+
* 日志必须分开写 —— 否则将来"为什么这条轨迹不跑了"会又一次指错方向。
|
|
10144
|
+
*
|
|
10145
|
+
* ## 文件形态(刻意的极简)
|
|
10146
|
+
*
|
|
10147
|
+
* · 位置 = `<轨迹目录>/cro-host-binding`(与 CRO 程序**同级**)
|
|
10148
|
+
* · 内容 = **一行机器名**(首尾空白裁掉;只取第一行有效内容)
|
|
10149
|
+
* · 不用 JSON —— 它只有一个值,JSON 只会多一种写错的方式
|
|
10150
|
+
* · 支持 `#` 注释行与空行(方便人写、方便留痕)
|
|
10151
|
+
*
|
|
10152
|
+
* 🔴 **它必须随 git 入库**(不是"运行态"):绑定声明要**跟着轨迹走到第二台机器**,
|
|
10153
|
+
* 在那里被判成「绑给了别人」而**正确地不跑** —— 那就是本机制在工作。
|
|
10154
|
+
* ⇒ ⛔ **不要把它加进 `.gitignore`**(那会让第二台机器读到"没绑"而同样不跑,
|
|
10155
|
+
* 表面上结果一样,但**判据从"明确绑给别人"退化成"什么都没说"**,失去可诊断性)。
|
|
10156
|
+
*
|
|
10157
|
+
* ## 为什么读文件而不是读快照字段
|
|
10158
|
+
*
|
|
10159
|
+
* 快照(`CroSnapshot`)是喂给**程序**的;而本判定发生在**程序被 spawn 之前**
|
|
10160
|
+
* —— 程序还没跑,没有快照可读。⇒ 只能读文件。(这也是"机制属 ACC"的体现:
|
|
10161
|
+
* ACC 在调用用户程序前先判定自己该不该调用。)
|
|
10162
|
+
*
|
|
10163
|
+
* @param root CCC 根
|
|
10164
|
+
* @param dirName 轨迹目录名
|
|
10165
|
+
* @param selfHost 本机机器名(**注入** ⇒ 可测;生产调用方传 `hostname()`)
|
|
10166
|
+
* @returns 判定结果(**永不抛** —— 旁路容忍铁律)
|
|
10167
|
+
*/
|
|
10168
|
+
function resolveCroHostBinding(root, dirName, selfHost) {
|
|
10169
|
+
const self = String(selfHost ?? "").trim();
|
|
10170
|
+
let fileAbs;
|
|
10171
|
+
try {
|
|
10172
|
+
fileAbs = resolveInside(root, join("AGENT_SESSIONS", dirName, CRO_HOST_FILENAME));
|
|
10173
|
+
} catch (err) {
|
|
10174
|
+
return {
|
|
10175
|
+
allowed: false,
|
|
10176
|
+
bound: null,
|
|
10177
|
+
self,
|
|
10178
|
+
code: "read-error",
|
|
10179
|
+
detail: `绑定文件路径非法(已忽略):${String(err?.message ?? err)}`
|
|
10180
|
+
};
|
|
10181
|
+
}
|
|
10182
|
+
let raw;
|
|
10183
|
+
try {
|
|
10184
|
+
if (!existsSync(fileAbs)) return {
|
|
10185
|
+
allowed: false,
|
|
10186
|
+
bound: null,
|
|
10187
|
+
self,
|
|
10188
|
+
code: "not-bound",
|
|
10189
|
+
detail: `未绑定机器(缺 ${CRO_HOST_FILENAME})⇒ 按"必须指定"不运行`
|
|
10190
|
+
};
|
|
10191
|
+
const st = statSync(fileAbs);
|
|
10192
|
+
if (st.size > CRO_HOST_FILE_MAX) return {
|
|
10193
|
+
allowed: false,
|
|
10194
|
+
bound: null,
|
|
10195
|
+
self,
|
|
10196
|
+
code: "read-error",
|
|
10197
|
+
detail: `绑定文件过大(${st.size} B > ${CRO_HOST_FILE_MAX} B)⇒ 判为写错,不运行`
|
|
10198
|
+
};
|
|
10199
|
+
raw = readFileSync(fileAbs, "utf-8");
|
|
10200
|
+
} catch (err) {
|
|
10201
|
+
return {
|
|
10202
|
+
allowed: false,
|
|
10203
|
+
bound: null,
|
|
10204
|
+
self,
|
|
10205
|
+
code: "read-error",
|
|
10206
|
+
detail: `绑定文件读取失败 ⇒ 不运行:${String(err?.message ?? err)}`
|
|
10207
|
+
};
|
|
10208
|
+
}
|
|
10209
|
+
const bound = parseCroHostBinding(raw);
|
|
10210
|
+
if (bound === null) return {
|
|
10211
|
+
allowed: false,
|
|
10212
|
+
bound: null,
|
|
10213
|
+
self,
|
|
10214
|
+
code: "not-bound",
|
|
10215
|
+
detail: `绑定文件为空(只有注释/空行)⇒ 按"必须指定"不运行`
|
|
10216
|
+
};
|
|
10217
|
+
if (bound === self) return {
|
|
10218
|
+
allowed: true,
|
|
10219
|
+
bound,
|
|
10220
|
+
self,
|
|
10221
|
+
code: "bound-to-self",
|
|
10222
|
+
detail: `已绑定本机(${bound})`
|
|
10223
|
+
};
|
|
10224
|
+
return {
|
|
10225
|
+
allowed: false,
|
|
10226
|
+
bound,
|
|
10227
|
+
self,
|
|
10228
|
+
code: "bound-to-other",
|
|
10229
|
+
detail: `绑定给别的机器(绑定=${bound},本机=${self})⇒ 本机不运行`
|
|
10230
|
+
};
|
|
10231
|
+
}
|
|
10232
|
+
/**
|
|
10233
|
+
* 解析绑定文件正文 ⇒ 机器名(取不到 ⇒ `null`)。
|
|
10234
|
+
*
|
|
10235
|
+
* 规则(刻意宽松:**只认一行,其余都当注释**):
|
|
10236
|
+
* · 逐行扫;跳过空行与 `#` 开头的行;
|
|
10237
|
+
* · **第一条有效行** = 机器名(裁首尾空白);
|
|
10238
|
+
* · 无有效行 ⇒ `null`。
|
|
10239
|
+
* @param raw 文件正文
|
|
10240
|
+
* @returns 机器名;无有效内容 ⇒ `null`
|
|
10241
|
+
*/
|
|
10242
|
+
function parseCroHostBinding(raw) {
|
|
10243
|
+
for (const line of String(raw ?? "").split(/\r?\n/)) {
|
|
10244
|
+
const t = line.trim();
|
|
10245
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
10246
|
+
return t;
|
|
10247
|
+
}
|
|
10248
|
+
return null;
|
|
10249
|
+
}
|
|
10250
|
+
/**
|
|
10112
10251
|
* 列出**启用了 CRO 的轨迹目录名**(供调度器每 tick 扫描,设计 §6.1)。
|
|
10113
10252
|
*
|
|
10114
10253
|
* ## 判据与形态(R↓)
|
|
@@ -11441,10 +11580,18 @@ async function runCroPhase(ctx, root, registry, log) {
|
|
|
11441
11580
|
lastSkipReason: s.lastSkipReason
|
|
11442
11581
|
};
|
|
11443
11582
|
const agents = hostAgents(ctx);
|
|
11583
|
+
const selfHost = safeHostname();
|
|
11584
|
+
let hostSkips = 0;
|
|
11444
11585
|
let wakes = 0;
|
|
11445
11586
|
let quiet = 0;
|
|
11446
11587
|
let skips = 0;
|
|
11447
11588
|
for (const dirName of dirs) try {
|
|
11589
|
+
const verdict = resolveCroHostBinding(root, dirName, selfHost);
|
|
11590
|
+
if (!verdict.allowed) {
|
|
11591
|
+
hostSkips += 1;
|
|
11592
|
+
log.push(`· ${root}: ${dirName}: CRO 机器绑定未通过(${verdict.code}):${verdict.detail}`);
|
|
11593
|
+
continue;
|
|
11594
|
+
}
|
|
11448
11595
|
const bound = listBoundSessionIds(root, dirName);
|
|
11449
11596
|
const running = runningCarriersOf(bound, nowMs);
|
|
11450
11597
|
const outcome = await evaluateCro(root, dirName, readCroSnapshotInput(root, dirName, nowMs, {
|
|
@@ -11481,7 +11628,22 @@ async function runCroPhase(ctx, root, registry, log) {
|
|
|
11481
11628
|
skips += 1;
|
|
11482
11629
|
log.push(`· ${root}: ${dirName}: CRO 内部异常(已忽略,不影响既有链路): ${String(err?.message ?? err)}`);
|
|
11483
11630
|
}
|
|
11484
|
-
log.push(`· ${root}: CRO 评估 ${dirs.length} 条(唤起 ${wakes},不唤起 ${quiet},跳过 ${skips})`);
|
|
11631
|
+
log.push(`· ${root}: CRO 评估 ${dirs.length} 条(唤起 ${wakes},不唤起 ${quiet},跳过 ${skips}${hostSkips > 0 ? `,非本机绑定 ${hostSkips}` : ""})`);
|
|
11632
|
+
}
|
|
11633
|
+
/**
|
|
11634
|
+
* 取本机机器名(**永不抛**)。
|
|
11635
|
+
*
|
|
11636
|
+
* `os.hostname()` 理论上可能抛(极少数环境)⇒ 这里兜底成空串。
|
|
11637
|
+
* ⚠️ **空串的后果是"全部不通过"**(`bound === self` 永不成立)⇒ 即 **fail-closed**,
|
|
11638
|
+
* 与所有者裁定同向(宁可都不跑,也不会"名字取不到就随便跑")。
|
|
11639
|
+
* @returns 本机机器名;取不到 ⇒ `''`
|
|
11640
|
+
*/
|
|
11641
|
+
function safeHostname() {
|
|
11642
|
+
try {
|
|
11643
|
+
return hostname();
|
|
11644
|
+
} catch {
|
|
11645
|
+
return "";
|
|
11646
|
+
}
|
|
11485
11647
|
}
|
|
11486
11648
|
/** 一次调度 tick:遍历已知 CCC → 到期项串行投递 / 超窗项置 missed;返回人读摘要 */
|
|
11487
11649
|
async function runWakeTick(ctx) {
|
|
@@ -11641,6 +11803,36 @@ const CRO_GUIDE = `CRO(Continuous Re-Occurrence · 持续再发生)编写指
|
|
|
11641
11803
|
(tick 日志里会记一行跳过理由 —— 这是为了让你事后能分辨
|
|
11642
11804
|
「**总闸关着没跑**」和「**跑了、程序自己判了不叫**」这两件完全不同的事)。
|
|
11643
11805
|
|
|
11806
|
+
## 2b. 🔴 机器绑定(**必须指定,否则不运行** —— 所有者令 2026-10-15)
|
|
11807
|
+
|
|
11808
|
+
同目录再放一个文件:
|
|
11809
|
+
|
|
11810
|
+
<CCC 根>/AGENT_SESSIONS/<轨迹目录>/cro-host-binding
|
|
11811
|
+
|
|
11812
|
+
内容 = **一行机器名**(就是本机 \`hostname\`)。也支持 \`#\` 注释行,方便留痕:
|
|
11813
|
+
|
|
11814
|
+
# 这条轨迹的自唤醒只在 CCC 宿主机上跑
|
|
11815
|
+
yh-Standard-PC-Q35-ICH9-2009
|
|
11816
|
+
|
|
11817
|
+
**语义(三条,缺一不可)**:
|
|
11818
|
+
· **只许这一台** —— 文件名里写的那台;别处**不跑**;
|
|
11819
|
+
· 🔴 **fail-closed** —— **没写这个文件 ⇒ 不跑**(不是"没写就随便跑");
|
|
11820
|
+
· **检查者是唤醒器**(ACC),**不是**你的程序 —— 你的程序**根本不会被调用**。
|
|
11821
|
+
|
|
11822
|
+
**为什么要有它**:CRO 程序本身**随 git 走**(它在你轨迹目录里,会跟着 CCC 到第二台机器)。
|
|
11823
|
+
没有这道门,**同一段程序会在两台机器各跑一次**(同一轨迹被叫两次)。
|
|
11824
|
+
有了它,第二台机器读到"绑给了别人"就**正确地不跑**。
|
|
11825
|
+
|
|
11826
|
+
⚠️ **两条与它有关的纪律**:
|
|
11827
|
+
1. 🔴 **不要把这个文件加进 \`.gitignore\`** —— 它**必须**跟着仓走。
|
|
11828
|
+
否则第二台机器读到的是"没绑",结果**碰巧一样**(也不跑),
|
|
11829
|
+
但你的判据从「**明确绑给了别人**」退化成「**什么都没说**」⇒ 排查时看不出真因。
|
|
11830
|
+
2. 🔴 **tick 日志会区分两种跳过**:
|
|
11831
|
+
「\`not-bound\` 你没绑」与「\`bound-to-other\` 你绑给了别人」是**两件事**,文案不同。
|
|
11832
|
+
(2026-10-15 有过一次教训:两种不同的原因报同一句话,害排查指错方向。)
|
|
11833
|
+
|
|
11834
|
+
**改绑 / 解除**:直接改这个文件(或删掉 ⇒ 变回"没绑 ⇒ 不跑")。**没有额外命令**。
|
|
11835
|
+
|
|
11644
11836
|
## 3. 我给你什么(输入)
|
|
11645
11837
|
ACC 把这条轨迹的状态做成一个 **JSON 快照**,从 **stdin** 喂给你的程序。
|
|
11646
11838
|
字段分四组:identity(身份)/ time(时间)/ body(轨迹身体)/ binding + scheduling(绑定与调度)。
|
|
@@ -21488,6 +21680,300 @@ function registerWeixinSendApi(ctx) {
|
|
|
21488
21680
|
sync();
|
|
21489
21681
|
}
|
|
21490
21682
|
//#endregion
|
|
21683
|
+
//#region src/acp-face.ts
|
|
21684
|
+
/** 面名(面宿主幂等键;与 FACE_PORTS 的 id 同名,便于对账) */
|
|
21685
|
+
const FACE_ACP = "acp";
|
|
21686
|
+
/** 本面绑定的地址(取自集中端口表 —— **单一真相源**,不在本文件硬写) */
|
|
21687
|
+
const ACP_HOST = FACE_PORTS.acp.host ?? "127.0.0.1";
|
|
21688
|
+
/**
|
|
21689
|
+
* `dsh-acp` 需要的服务(**替它声明**,见文件头坑 ①)。
|
|
21690
|
+
*
|
|
21691
|
+
* 逐字出处 = `dsh-acp/lib/index.js` 的 `const inject = ["agents","llm","sessionPersistence","sessions"]`。
|
|
21692
|
+
* ⚠️ **少一个就在它内部抛错**(实测那条是 `sessionPersistence`)⇒ 这里是**照抄它的声明**,
|
|
21693
|
+
* 不是"我们觉得它需要什么"。⇒ 它升版改了 `inject`,这里要跟着改(列为**跨版本注意点**)。
|
|
21694
|
+
*/
|
|
21695
|
+
const ACP_REQUIRED_SERVICES = [
|
|
21696
|
+
"agents",
|
|
21697
|
+
"llm",
|
|
21698
|
+
"sessionPersistence",
|
|
21699
|
+
"sessions"
|
|
21700
|
+
];
|
|
21701
|
+
function acpFaceActive() {
|
|
21702
|
+
return faceActive(FACE_ACP);
|
|
21703
|
+
}
|
|
21704
|
+
/**
|
|
21705
|
+
* 面是否在监听(意图读取)。
|
|
21706
|
+
* ⚠️ 与 `startFace` 的关系同其余各面:**启动本身不读它**(`startFace` 是机械入口),
|
|
21707
|
+
* 由装配层用它决定"起还是停"。
|
|
21708
|
+
*/
|
|
21709
|
+
function acpFaceEnabled() {
|
|
21710
|
+
return readSimpleSettings().acpFaceEnabled;
|
|
21711
|
+
}
|
|
21712
|
+
/**
|
|
21713
|
+
* 把一条裸 WS socket 适配成 ACP `Stream`。
|
|
21714
|
+
*
|
|
21715
|
+
* 🔴 与官方 `ndJsonStream` **同语义**:我们自己做「消息 ↔ 文本帧」的分帧(按行)。
|
|
21716
|
+
* 实测注意(P0′):`dsh-acp` 写出的每条消息**自带换行**,客户端同样 ⇒ 按行切分即正确。
|
|
21717
|
+
* ⚠️ **空行要丢**;**非 JSON 帧要容忍**(桥/客户端可能插入控制帧)。
|
|
21718
|
+
*/
|
|
21719
|
+
function wsToAcpStream(ws, onWarn) {
|
|
21720
|
+
return {
|
|
21721
|
+
readable: new ReadableStream({ start(controller) {
|
|
21722
|
+
let buf = "";
|
|
21723
|
+
ws.on("message", (data) => {
|
|
21724
|
+
buf += typeof data === "string" ? data : String(data);
|
|
21725
|
+
let i;
|
|
21726
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
21727
|
+
const line = buf.slice(0, i);
|
|
21728
|
+
buf = buf.slice(i + 1);
|
|
21729
|
+
if (!line.trim()) continue;
|
|
21730
|
+
try {
|
|
21731
|
+
controller.enqueue(JSON.parse(line));
|
|
21732
|
+
} catch {
|
|
21733
|
+
onWarn(line.slice(0, 120));
|
|
21734
|
+
}
|
|
21735
|
+
}
|
|
21736
|
+
});
|
|
21737
|
+
ws.on("close", () => {
|
|
21738
|
+
try {
|
|
21739
|
+
controller.close();
|
|
21740
|
+
} catch {}
|
|
21741
|
+
});
|
|
21742
|
+
ws.on("error", () => {
|
|
21743
|
+
try {
|
|
21744
|
+
controller.close();
|
|
21745
|
+
} catch {}
|
|
21746
|
+
});
|
|
21747
|
+
} }),
|
|
21748
|
+
writable: new WritableStream({ write(msg) {
|
|
21749
|
+
ws.send(`${JSON.stringify(msg)}\n`);
|
|
21750
|
+
} })
|
|
21751
|
+
};
|
|
21752
|
+
}
|
|
21753
|
+
function newState() {
|
|
21754
|
+
return {
|
|
21755
|
+
handle: null,
|
|
21756
|
+
wss: null,
|
|
21757
|
+
connections: 0,
|
|
21758
|
+
applied: 0,
|
|
21759
|
+
lastErrors: [],
|
|
21760
|
+
disposers: []
|
|
21761
|
+
};
|
|
21762
|
+
}
|
|
21763
|
+
/** 当前面状态(供 `status()` / 诊断读;无监听时 `handle` 为 null) */
|
|
21764
|
+
let state = newState();
|
|
21765
|
+
/** 保留最近若干条连接错误(诊断窗口;不无限增长) */
|
|
21766
|
+
const MAX_ERR_KEEP = 8;
|
|
21767
|
+
/**
|
|
21768
|
+
* 启动面 E(幂等;重复调用只起一次)。
|
|
21769
|
+
*
|
|
21770
|
+
* 🔴 **失败语义**:本面**默认关**且是**可选面** ⇒ 起不来**不得**影响插件其余部分
|
|
21771
|
+
* (调用方负责 catch;本函数把失败原样抛出,由装配层记日志)。
|
|
21772
|
+
*/
|
|
21773
|
+
async function startAcpFace(ctx) {
|
|
21774
|
+
const port = readSimpleSettings().acpFacePort;
|
|
21775
|
+
let dshAcp;
|
|
21776
|
+
let wsMod;
|
|
21777
|
+
try {
|
|
21778
|
+
dshAcp = await import("@deepseek-ai/dsh-acp");
|
|
21779
|
+
wsMod = await import("ws");
|
|
21780
|
+
} catch (err) {
|
|
21781
|
+
const msg = String(err?.message ?? err);
|
|
21782
|
+
throw new Error(`面 E 依赖不可用(${msg})—— 需要宿主提供 @deepseek-ai/dsh-acp,且本插件能 resolve 到 ws`);
|
|
21783
|
+
}
|
|
21784
|
+
if (typeof dshAcp.apply !== "function") throw new Error("面 E:@deepseek-ai/dsh-acp 未导出 apply(版本不匹配?)");
|
|
21785
|
+
const wss = new wsMod.WebSocketServer({ noServer: true });
|
|
21786
|
+
const local = newState();
|
|
21787
|
+
local.wss = wss;
|
|
21788
|
+
const handle = await startFace({
|
|
21789
|
+
name: FACE_ACP,
|
|
21790
|
+
port,
|
|
21791
|
+
host: ACP_HOST,
|
|
21792
|
+
enabled: () => readSimpleSettings().acpFaceEnabled,
|
|
21793
|
+
handler: (_req, res) => {
|
|
21794
|
+
res.writeHead(426, {
|
|
21795
|
+
"content-type": "text/plain; charset=utf-8",
|
|
21796
|
+
"upgrade": "websocket",
|
|
21797
|
+
"cache-control": "no-store"
|
|
21798
|
+
});
|
|
21799
|
+
res.end("This is an ACP (Agent Client Protocol) endpoint over WebSocket. Use an ACP client.\n");
|
|
21800
|
+
}
|
|
21801
|
+
});
|
|
21802
|
+
local.handle = handle;
|
|
21803
|
+
handle.server.on("upgrade", (req, socket, head) => {
|
|
21804
|
+
local.connections += 1;
|
|
21805
|
+
try {
|
|
21806
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
21807
|
+
try {
|
|
21808
|
+
const stream = wsToAcpStream(ws, (line) => {
|
|
21809
|
+
console.log(`[serenity-hooks] 面 E:丢弃一条非 JSON 帧(前 120 字): ${line}`);
|
|
21810
|
+
});
|
|
21811
|
+
const fiber = ctx.plugin({
|
|
21812
|
+
inject: ACP_REQUIRED_SERVICES,
|
|
21813
|
+
apply: (scoped) => {
|
|
21814
|
+
const sel = resolveAcpSelection(scoped);
|
|
21815
|
+
dshAcp.apply(scoped, {
|
|
21816
|
+
stream,
|
|
21817
|
+
provider: sel.provider,
|
|
21818
|
+
model: sel.model
|
|
21819
|
+
});
|
|
21820
|
+
}
|
|
21821
|
+
});
|
|
21822
|
+
local.disposers.push(() => {
|
|
21823
|
+
try {
|
|
21824
|
+
fiber?.dispose?.();
|
|
21825
|
+
} catch {}
|
|
21826
|
+
});
|
|
21827
|
+
local.applied += 1;
|
|
21828
|
+
} catch (err) {
|
|
21829
|
+
const msg = String(err?.message ?? err).slice(0, 200);
|
|
21830
|
+
local.lastErrors.push({
|
|
21831
|
+
at: Date.now(),
|
|
21832
|
+
applied: false,
|
|
21833
|
+
error: msg
|
|
21834
|
+
});
|
|
21835
|
+
if (local.lastErrors.length > MAX_ERR_KEEP) local.lastErrors.shift();
|
|
21836
|
+
console.warn(`[serenity-hooks] ✗ 面 E:dsh-acp 接线失败: ${msg}`);
|
|
21837
|
+
try {
|
|
21838
|
+
ws.close(1011, "acp bridge failed");
|
|
21839
|
+
} catch {}
|
|
21840
|
+
}
|
|
21841
|
+
});
|
|
21842
|
+
} catch (err) {
|
|
21843
|
+
console.warn(`[serenity-hooks] ✗ 面 E:WebSocket 握手失败: ${String(err?.message ?? err)}`);
|
|
21844
|
+
try {
|
|
21845
|
+
socket.destroy();
|
|
21846
|
+
} catch {}
|
|
21847
|
+
}
|
|
21848
|
+
});
|
|
21849
|
+
state = local;
|
|
21850
|
+
console.log(`[serenity-hooks] ✓ 面 E(官方 ACP):ws://${ACP_HOST}:${port} ⚠️ 协议无认证,靠外部 Access 兜底`);
|
|
21851
|
+
return handle;
|
|
21852
|
+
}
|
|
21853
|
+
/**
|
|
21854
|
+
* 停面 E(幂等)。
|
|
21855
|
+
*
|
|
21856
|
+
* ⚠️ **与其余各面同款注意**:`stopFace` **不用** `closeAllConnections()`(那会掐断在飞的请求);
|
|
21857
|
+
* WS 连接由 `wss.close()` 与 socket 自身生命周期收束。
|
|
21858
|
+
*/
|
|
21859
|
+
function stopAcpFace() {
|
|
21860
|
+
const wss = state.wss;
|
|
21861
|
+
if (wss) try {
|
|
21862
|
+
wss.close();
|
|
21863
|
+
} catch {}
|
|
21864
|
+
state.disposers = [];
|
|
21865
|
+
state.wss = null;
|
|
21866
|
+
state.handle = null;
|
|
21867
|
+
const stopped = stopFace(FACE_ACP);
|
|
21868
|
+
if (stopped) console.log("[serenity-hooks] face acp: 已停止");
|
|
21869
|
+
return stopped;
|
|
21870
|
+
}
|
|
21871
|
+
/** ACP 会话用的 provider(取 CCC 现有路由;留空 = 由宿主/请求监听器决定) */
|
|
21872
|
+
function readAcpProvider() {
|
|
21873
|
+
return process.env.SERENITY_ACP_PROVIDER || void 0;
|
|
21874
|
+
}
|
|
21875
|
+
/** ACP 会话用的 model(同上) */
|
|
21876
|
+
function readAcpModel() {
|
|
21877
|
+
return process.env.SERENITY_ACP_MODEL || void 0;
|
|
21878
|
+
}
|
|
21879
|
+
/**
|
|
21880
|
+
* 解析 ACP 会话该用的 `{provider, model}`。
|
|
21881
|
+
*
|
|
21882
|
+
* ## 🔴 为什么必须有这个函数(2026-10-16 容器实测抓出的**真缺陷**)
|
|
21883
|
+
*
|
|
21884
|
+
* 早先我们是**裸传** `provider: readAcpProvider(), model: readAcpModel()` —— 而它们读的是
|
|
21885
|
+
* **环境变量**,默认**未设** ⇒ 传下去就是 `undefined`。后果**不是"用默认模型"**:
|
|
21886
|
+
* 上游 `dsh-acp` 的 `agentOptions()`(`lib/index.js:1356-1361`)**只在字段非 undefined 时才带上**:
|
|
21887
|
+
* ```js
|
|
21888
|
+
* ...config.provider !== void 0 ? { provider: config.provider } : {},
|
|
21889
|
+
* ...config.model !== void 0 ? { model: config.model } : {}
|
|
21890
|
+
* ```
|
|
21891
|
+
* ⇒ **我们传 undefined,agent 就真的没有模型** ⇒ 第一条 `session/prompt` 必失败,逐字报:
|
|
21892
|
+
* `Internal error: turn failed: prompt variable "{{model}}" has no value for this assembly
|
|
21893
|
+
* (section "deployment:persona-prefix")`。
|
|
21894
|
+
*
|
|
21895
|
+
* ⚠️ **网页端不中招**(它经 `agent-default-model` 层拿到默认值)⇒ **只有 ACP 这条入口坏** ——
|
|
21896
|
+
* 也就是说:**手机上第一条消息必然失败,而本机怎么试都正常**。
|
|
21897
|
+
* 🔴 这个缺陷 **typecheck / 2812 条单测 / deploy 预检 全绿**(它是运行时合成形态,
|
|
21898
|
+
* 静态面结构性地看不见)⇒ **只有"容器 + 真模型"那一跑能抓到它**。
|
|
21899
|
+
*
|
|
21900
|
+
* ## 修法 = **读宿主自己的默认模型服务**(不另造真相源)
|
|
21901
|
+
*
|
|
21902
|
+
* 宿主暴露服务 `ctx.agentDefaultModel.currentSelection()`,其契约逐字写着
|
|
21903
|
+
* *"Default model selection for Agents created **without an explicit model**"*
|
|
21904
|
+
* (`dsh-agent-default-model/lib/types/index.d.ts`)—— 这正是"没显式指定时该用什么"的**权威出处**。
|
|
21905
|
+
* ⇒ 我们只是**把它转交给 ACP**,不引入第二套缺省。
|
|
21906
|
+
*
|
|
21907
|
+
* 优先级(**显式 > 宿主默认**):`SERENITY_ACP_PROVIDER/MODEL` 环境变量(调试用)> 宿主默认。
|
|
21908
|
+
* ⚠️ **一处如实标注的弱化**:这里**不打日志**(每次连接都解析一次,打日志会刷屏);
|
|
21909
|
+
* 若宿主服务缺失,**退回"两个都 undefined"=旧行为**(即那条已知会失败的形态)——
|
|
21910
|
+
* 而不是静默编一个模型。⇒ 副作用是"没有默认模型时仍会失败",但那**与宿主自身缺配置时的行为一致**。
|
|
21911
|
+
*
|
|
21912
|
+
* 🔵 **公开导出**(而非模块私有):它是一个**纯函数 + 有独立契约**的单元(本仓同类先例 =
|
|
21913
|
+
* `host/access.ts` 的一组读取器),且**那个真缺陷正是在它这一层**(裸传 undefined)——
|
|
21914
|
+
* 回归钉必须能**直接钉住它**,而不是隔着 WS 握手去间接观察(那样测不出"取到了什么")。
|
|
21915
|
+
*/
|
|
21916
|
+
function resolveAcpSelection(ctx) {
|
|
21917
|
+
const envProvider = readAcpProvider();
|
|
21918
|
+
const envModel = readAcpModel();
|
|
21919
|
+
if (envProvider && envModel) return {
|
|
21920
|
+
provider: envProvider,
|
|
21921
|
+
model: envModel
|
|
21922
|
+
};
|
|
21923
|
+
try {
|
|
21924
|
+
const sel = hostInjected(ctx, "agentDefaultModel")?.currentSelection?.();
|
|
21925
|
+
if (sel?.provider && sel?.model) return {
|
|
21926
|
+
provider: sel.provider,
|
|
21927
|
+
model: sel.model
|
|
21928
|
+
};
|
|
21929
|
+
} catch {}
|
|
21930
|
+
return {
|
|
21931
|
+
provider: envProvider,
|
|
21932
|
+
model: envModel
|
|
21933
|
+
};
|
|
21934
|
+
}
|
|
21935
|
+
/**
|
|
21936
|
+
* 装配(`index.ts` 的 `apply` 调用):按**面板/部署键**启停面 E,配置变化热同步。
|
|
21937
|
+
*
|
|
21938
|
+
* 🔴 **W3 接线**(2026-10-16):本轮之前 `startAcpFace` 虽已实现却**没有任何调用点**
|
|
21939
|
+
* —— 即"面写好了但从不起"。本函数补上它,形式与两个兄弟面一致
|
|
21940
|
+
* (`registerWeixinSendApi` / `registerDiagramTool`):
|
|
21941
|
+
*
|
|
21942
|
+
* ① **反应式**:面板拨开关**只改配置、不重跑 `apply`**(v1.51.1 实测过的那条铁律)
|
|
21943
|
+
* ⇒ 必须监听事件才会"拨了就有反应";
|
|
21944
|
+
* ② 监听**宿主的真事件** `settings/document-updated`(按 ns 过滤),
|
|
21945
|
+
* ⛔ **不监听** `serenity/settings-changed` —— 那个事件**全仓没有任何 `emit` 点**
|
|
21946
|
+
* (=死通道;见 `registerDiagramTool` 的注释),照它接线等于接了个永远不响的铃;
|
|
21947
|
+
* ③ 再挂 `agent/created`/`session/created` 兜底(宿主事件面若换名,至少还有这条能自愈);
|
|
21948
|
+
* ④ 启动时同步一次(`apply` 时按当前配置定初始态)。
|
|
21949
|
+
*
|
|
21950
|
+
* ⚠️ **失败语义**:本面**默认关**且**依赖可选**(`dsh-acp` 是 peer)⇒ 起不来**只记日志**,
|
|
21951
|
+
* 绝不抛出(抛出会让整个插件加载失败,而它只是一个可选面)。
|
|
21952
|
+
*/
|
|
21953
|
+
function registerAcpFace(ctx) {
|
|
21954
|
+
const sync = () => {
|
|
21955
|
+
if (!acpFaceEnabled()) {
|
|
21956
|
+
if (acpFaceActive()) stopAcpFace();
|
|
21957
|
+
return;
|
|
21958
|
+
}
|
|
21959
|
+
if (acpFaceActive()) return;
|
|
21960
|
+
startAcpFace(ctx).catch((err) => {
|
|
21961
|
+
console.error(`[serenity-hooks] ✗ 面 E(官方 ACP)启动失败: ${String(err?.message ?? err)}`);
|
|
21962
|
+
});
|
|
21963
|
+
};
|
|
21964
|
+
try {
|
|
21965
|
+
ctx.on("settings/document-updated", (ns) => {
|
|
21966
|
+
if (String(ns) === "serenity-hooks") sync();
|
|
21967
|
+
});
|
|
21968
|
+
} catch {}
|
|
21969
|
+
for (const eventName of ["agent/created", "session/created"]) try {
|
|
21970
|
+
ctx.on(eventName, () => {
|
|
21971
|
+
sync();
|
|
21972
|
+
});
|
|
21973
|
+
} catch {}
|
|
21974
|
+
sync();
|
|
21975
|
+
}
|
|
21976
|
+
//#endregion
|
|
21491
21977
|
//#region src/seams/lifecycle.ts
|
|
21492
21978
|
/** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
|
|
21493
21979
|
function sessionIdOf(value) {
|
|
@@ -24124,6 +24610,10 @@ const Config = z.object({
|
|
|
24124
24610
|
opencodeProvider: z.object({ autoConfigure: z.boolean().default(true) }),
|
|
24125
24611
|
visionPatch: z.object({ enabled: z.boolean().default(true) }),
|
|
24126
24612
|
diagram: z.object({ enabled: z.boolean().default(false) }),
|
|
24613
|
+
acpFace: z.object({
|
|
24614
|
+
enabled: z.boolean().default(false),
|
|
24615
|
+
port: z.number().min(1024).max(65535).default(ACP_PORT)
|
|
24616
|
+
}),
|
|
24127
24617
|
humanChannel: z.object({
|
|
24128
24618
|
accounts: z.array(humanChannelAccountSchema).default([]),
|
|
24129
24619
|
subscriptions: z.array(humanChannelSubscriptionSchema).default([]),
|
|
@@ -24136,7 +24626,9 @@ const Config = z.object({
|
|
|
24136
24626
|
skiffDebugPort: z.number().min(1024).max(65535).volatile(),
|
|
24137
24627
|
croEnabled: z.boolean().volatile(),
|
|
24138
24628
|
unattendedEnabled: z.boolean().volatile(),
|
|
24139
|
-
diagramEnabled: z.boolean().volatile()
|
|
24629
|
+
diagramEnabled: z.boolean().volatile(),
|
|
24630
|
+
acpFaceEnabled: z.boolean().volatile(),
|
|
24631
|
+
acpFacePort: z.number().min(1024).max(65535).volatile()
|
|
24140
24632
|
});
|
|
24141
24633
|
function apply(ctx, config) {
|
|
24142
24634
|
try {
|
|
@@ -24200,6 +24692,7 @@ function apply(ctx, config) {
|
|
|
24200
24692
|
registerWakeScheduler(ctx);
|
|
24201
24693
|
registerWeixinBridge(ctx);
|
|
24202
24694
|
registerWeixinSendApi(ctx);
|
|
24695
|
+
registerAcpFace(ctx);
|
|
24203
24696
|
registerWeixinOutputGuard(ctx);
|
|
24204
24697
|
registerLifecycle(ctx);
|
|
24205
24698
|
if (config.webFetch?.enabled !== false) registerWebFetchProvider(ctx);
|
package/lib/ports.d.ts
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* 本模块是**零依赖叶模块**(纯常量,无 import)——output-guard(守卫词表)、config-ops
|
|
21
21
|
* (默认值)、settings-section(面板默认)、index(Config 默认)都可安全引用而不引入依赖环。
|
|
22
22
|
*/
|
|
23
|
-
/**
|
|
24
|
-
export type FaceId = 'main' | 'gateway' | 'weixinSend' | 'skiffDebug';
|
|
23
|
+
/** 面标识(五个对外面;A 由宿主监听,B/C/D/E 由本插件监听) */
|
|
24
|
+
export type FaceId = 'main' | 'gateway' | 'weixinSend' | 'skiffDebug' | 'acp';
|
|
25
25
|
/** 一个面的端口语义 */
|
|
26
26
|
export interface PortSpec {
|
|
27
27
|
/** 端口号 */
|
|
@@ -36,7 +36,7 @@ export interface PortSpec {
|
|
|
36
36
|
/**
|
|
37
37
|
* 集中端口表。
|
|
38
38
|
* A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
|
|
39
|
-
* / D=Skiff
|
|
39
|
+
* / D=Skiff 调试(默认关)/ E=ACP(默认关,`0.0.0.0`)。
|
|
40
40
|
*/
|
|
41
41
|
export declare const FACE_PORTS: Readonly<Record<FaceId, PortSpec>>;
|
|
42
42
|
/** 宿主主 WebUI 端口(面 A:插件**不监听**,只读取;`readWebPort` 的回退值) */
|
|
@@ -47,6 +47,8 @@ export declare const GATEWAY_PORT: number;
|
|
|
47
47
|
export declare const WEIXIN_SEND_PORT: number;
|
|
48
48
|
/** Skiff 调试页端口默认值(面 D) */
|
|
49
49
|
export declare const SKIFF_DEBUG_PORT: number;
|
|
50
|
+
/** 官方 ACP 面端口默认值(面 E,2026-10-16) */
|
|
51
|
+
export declare const ACP_PORT: number;
|
|
50
52
|
/**
|
|
51
53
|
* **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
|
|
52
54
|
*
|
|
@@ -41,7 +41,7 @@ function unregisterSkiffSession(sessionId) {
|
|
|
41
41
|
/**
|
|
42
42
|
* 集中端口表。
|
|
43
43
|
* A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
|
|
44
|
-
* / D=Skiff
|
|
44
|
+
* / D=Skiff 调试(默认关)/ E=ACP(默认关,`0.0.0.0`)。
|
|
45
45
|
*/
|
|
46
46
|
const FACE_PORTS = {
|
|
47
47
|
main: {
|
|
@@ -67,6 +67,26 @@ const FACE_PORTS = {
|
|
|
67
67
|
host: "127.0.0.1",
|
|
68
68
|
defaultEnabled: false,
|
|
69
69
|
meaning: "Skiff 调试问答面(人工调试子角色;无认证,仅靠 loopback)"
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* 🔴 面 E:**官方 ACP 面**(2026-10-16,owner 令「跑在 dsh web 进程内」→ 已实测跑通)。
|
|
73
|
+
*
|
|
74
|
+
* 与已退役的**旧面 E(3100)不是一回事**,别混淆:
|
|
75
|
+
* · 旧面 E = 当年**自造**的 HTTP+HTML 问答面,2026-10-15 owner 裁「砍掉外部问答」时**整组退役**;
|
|
76
|
+
* · 本面 = **官方 `@deepseek-ai/dsh-acp`**(官方 SDK + 现成客户端 Agmente),
|
|
77
|
+
* 且**跑在宿主 web 进程内**(不另起进程、不需要 stdio 桥)。
|
|
78
|
+
* ⇒ 端口**另选 3200**:既避开 3080/3081/3082/3099,也让"新旧一眼可分"。
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **安全边界(不许含混)**:**ACP 协议本身不做认证**(`dsh-acp` 的 `authenticate` 恒成功),
|
|
81
|
+
* 而本面按 owner 裁决绑 **`0.0.0.0`**(局域网内任何设备可连)⇒ **必须靠外部接入层兜底**
|
|
82
|
+
* (现为 Cloudflare Access)。⛔ **"隧道不是 ACC 该关心的事"(Q3 裁决)成立,但
|
|
83
|
+
* "所以不需要认证层"不成立** —— 交付文档与诊断输出里都要显式带这个前提。
|
|
84
|
+
*/
|
|
85
|
+
acp: {
|
|
86
|
+
port: 3200,
|
|
87
|
+
host: "0.0.0.0",
|
|
88
|
+
defaultEnabled: false,
|
|
89
|
+
meaning: "官方 ACP 面(Agmente 等标准客户端登入本 CCC;**协议无认证**,靠外部 Access 兜底)"
|
|
70
90
|
}
|
|
71
91
|
};
|
|
72
92
|
/** 宿主主 WebUI 端口(面 A:插件**不监听**,只读取;`readWebPort` 的回退值) */
|
|
@@ -77,6 +97,8 @@ const GATEWAY_PORT = FACE_PORTS.gateway.port;
|
|
|
77
97
|
const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
|
|
78
98
|
/** Skiff 调试页端口默认值(面 D) */
|
|
79
99
|
const SKIFF_DEBUG_PORT = FACE_PORTS.skiffDebug.port;
|
|
100
|
+
/** 官方 ACP 面端口默认值(面 E,2026-10-16) */
|
|
101
|
+
const ACP_PORT = FACE_PORTS.acp.port;
|
|
80
102
|
/**
|
|
81
103
|
* **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
|
|
82
104
|
*
|
|
@@ -103,6 +125,8 @@ function defaultSimpleSettings() {
|
|
|
103
125
|
croEnabled: true,
|
|
104
126
|
unattendedEnabled: false,
|
|
105
127
|
diagramEnabled: false,
|
|
128
|
+
acpFaceEnabled: false,
|
|
129
|
+
acpFacePort: ACP_PORT,
|
|
106
130
|
humanChannel: defaultHumanChannelSettings()
|
|
107
131
|
};
|
|
108
132
|
}
|
|
@@ -199,6 +223,8 @@ function simpleSettingsFromConfig(config) {
|
|
|
199
223
|
croEnabled: unwrapVolatile(config.croEnabled) ?? d.croEnabled,
|
|
200
224
|
unattendedEnabled: unwrapVolatile(config.unattendedEnabled) ?? d.unattendedEnabled,
|
|
201
225
|
diagramEnabled: unwrapVolatile(config.diagramEnabled) ?? config.diagram?.enabled ?? d.diagramEnabled,
|
|
226
|
+
acpFaceEnabled: unwrapVolatile(config.acpFaceEnabled) ?? config.acpFace?.enabled ?? d.acpFaceEnabled,
|
|
227
|
+
acpFacePort: unwrapVolatile(config.acpFacePort) ?? config.acpFace?.port ?? d.acpFacePort,
|
|
202
228
|
humanChannel: humanChannelSettingsFromConfig(config.humanChannel)
|
|
203
229
|
};
|
|
204
230
|
}
|
|
@@ -237,4 +263,4 @@ function registerSettingsSection(ctx, config) {
|
|
|
237
263
|
} catch {}
|
|
238
264
|
}
|
|
239
265
|
//#endregion
|
|
240
|
-
export {
|
|
266
|
+
export { FACE_PORTS as a, MECHANISM_PORTS as c, registerSkiffSession as d, skiffRoleFor as f, ACC_VERSION as h, ACP_PORT as i, SKIFF_DEBUG_PORT as l, unregisterSkiffSession as m, readSimpleSettings as n, GATEWAY_PORT as o, skiffSessionInfo as p, registerSettingsSection as r, MAIN_WEB_PORT as s, SERENITY_SETTINGS_NS as t, WEIXIN_SEND_PORT as u };
|
|
@@ -69,6 +69,13 @@ interface SimpleConfigFragment {
|
|
|
69
69
|
diagram?: {
|
|
70
70
|
enabled?: boolean;
|
|
71
71
|
};
|
|
72
|
+
/** 🔴 面 E(官方 ACP,2026-10-16 owner 令「跑在 dsh web 进程内」):部署层嵌套段。
|
|
73
|
+
* ⚠️ **键名刻意不用 `acp.*`** —— 旧面 E 退役时 `acp.*` 是**被静默忽略**的遗留名,
|
|
74
|
+
* 复用会让"当年那份旧配置"在新面上复活(判据:退役 ≠ 删名,名字仍需保留为"无人读")。 */
|
|
75
|
+
acpFace?: {
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
port?: number;
|
|
78
|
+
};
|
|
72
79
|
/** human-channel(人机信道;**机器级**;2026-09-26 owner 定案):账号 / 订阅表 / 主控 / 出站白名单 */
|
|
73
80
|
humanChannel?: HumanChannelConfigFragment;
|
|
74
81
|
gatewayEnabled?: PanelBoolean;
|
|
@@ -78,6 +85,9 @@ interface SimpleConfigFragment {
|
|
|
78
85
|
croEnabled?: PanelBoolean;
|
|
79
86
|
unattendedEnabled?: PanelBoolean;
|
|
80
87
|
diagramEnabled?: PanelBoolean;
|
|
88
|
+
/** 🔴 面 E(官方 ACP,2026-10-16):开关 + 端口(面板层扁平键) */
|
|
89
|
+
acpFaceEnabled?: PanelBoolean;
|
|
90
|
+
acpFacePort?: PanelNumber;
|
|
81
91
|
}
|
|
82
92
|
/**
|
|
83
93
|
* 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
|
|
@@ -122,6 +132,18 @@ interface SerenitySimpleSettings {
|
|
|
122
132
|
* 零 token、不会被误调),同 `skiff` 的"未开启零资源占用"口径。
|
|
123
133
|
* 依赖面:随包分发 3.8 MB 内置字体(`assets/diagram/`,wasm 对系统字体不可见)。 */
|
|
124
134
|
diagramEnabled: boolean;
|
|
135
|
+
/** 🔴 **面 E:官方 ACP 总开关**(2026-10-16 owner 令「跑在 dsh web 进程内」;**缺省关**)。
|
|
136
|
+
*
|
|
137
|
+
* 开启后:ACC 在自己进程里起一个 **ACP 服务面**(面宿主统一建 listener),
|
|
138
|
+
* 标准 ACP 客户端(Agmente 等)经 WebSocket 登入本 CCC —— **不另起进程、不需 stdio 桥**。
|
|
139
|
+
* **缺省关**:① 它把本 CCC 开给外部客户端;② ⚠️ **ACP 协议本身无认证**(`dsh-acp` 的
|
|
140
|
+
* `authenticate` 恒成功),而本面按 owner 裁决绑 **`0.0.0.0`** ⇒ **开之前必须有外部接入层
|
|
141
|
+
* 兜底**(现为 Cloudflare Access)。缺省关 = "不配就没有暴露面"。
|
|
142
|
+
* ⚠️ **与已退役的旧面 E(3100 自造问答面)无关**:本面是**官方** `@deepseek-ai/dsh-acp`。
|
|
143
|
+
* 实测(P0′,8/8)见 S142 `2026-10-16T0745-…-p0prime-inproc-findings.md`。 */
|
|
144
|
+
acpFaceEnabled: boolean;
|
|
145
|
+
/** 面 E 端口(默认 3200;避开 3080/3081/3082/3099 与已退役的 3100) */
|
|
146
|
+
acpFacePort: number;
|
|
125
147
|
/** **human-channel(人机信道)机器级配置**(2026-09-26 owner 定案;P0-1 立 schema + 投影)。
|
|
126
148
|
* 三张表(账号 / 订阅 / 主控)+ 出站白名单;**跨 CCC** ⇒ 归机器级,CCC 侧 `weixin.*` 随之退役
|
|
127
149
|
* (退役动作在 P0-2~P0-4;本版**只立机器级面**,旧面照旧)。缺省与归一规则见
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.58.1",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1 与 0.2.0-rc.1(双档)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"README.md"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@resvg/resvg-wasm": "2.6.2"
|
|
48
|
+
"@resvg/resvg-wasm": "2.6.2",
|
|
49
|
+
"ws": "^8.22.0"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"typecheck": "tsc --noEmit",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
70
|
+
"@deepseek-ai/dsh-acp": "^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
69
71
|
"@deepseek-ai/dsh-agent": "^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
70
72
|
"@deepseek-ai/dsh-agent-loop": "^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
71
73
|
"@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
@@ -85,6 +87,9 @@
|
|
|
85
87
|
"cordis": "^4.0.0-rc.7"
|
|
86
88
|
},
|
|
87
89
|
"peerDependenciesMeta": {
|
|
90
|
+
"@deepseek-ai/dsh-acp": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
88
93
|
"@deepseek-ai/dsh-host-webserver": {
|
|
89
94
|
"optional": true
|
|
90
95
|
},
|
|
@@ -109,6 +114,7 @@
|
|
|
109
114
|
},
|
|
110
115
|
"devDependencies": {
|
|
111
116
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
117
|
+
"@deepseek-ai/dsh-acp": "0.1.7-rc.1",
|
|
112
118
|
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
|
|
113
119
|
"@deepseek-ai/dsh-agent-loop": "0.1.7-rc.1",
|
|
114
120
|
"@deepseek-ai/dsh-agent-preset-registry": "0.1.7-rc.1",
|
|
@@ -147,6 +153,7 @@
|
|
|
147
153
|
"@types/node": "^20.0.0",
|
|
148
154
|
"@types/qrcode-generator": "^1.0.6",
|
|
149
155
|
"@types/react": "^18.2.0",
|
|
156
|
+
"@types/ws": "^8.18.2",
|
|
150
157
|
"@vitest/coverage-v8": "^1.6.1",
|
|
151
158
|
"marked": "^18.0.11",
|
|
152
159
|
"qrcode-generator": "^2.0.4",
|
|
@@ -154,7 +161,8 @@
|
|
|
154
161
|
"tsdown": "^0.22.14",
|
|
155
162
|
"typescript": "^5.4.0",
|
|
156
163
|
"unrun": "^0.3.1",
|
|
157
|
-
"vitest": "^1.6.0"
|
|
164
|
+
"vitest": "^1.6.0",
|
|
165
|
+
"ws": "8.22.0"
|
|
158
166
|
},
|
|
159
167
|
"pnpm": {
|
|
160
168
|
"onlyBuiltDependencies": [
|