@shgroup/dsh-serenity-hooks 1.30.5 → 1.30.9

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.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
14
14
  * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
15
- * 未完成继续下一轮;followup/waitIdle 抛错(非正常停止)→ dispose 并重新 create agent
15
+ * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
16
16
  * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
17
17
  *
18
18
  * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
@@ -1,4 +1,9 @@
1
1
  /**
2
2
  * kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
3
3
  */
4
- export declare const kitTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
4
+ import type { Context } from 'cordis';
5
+ /**
6
+ * dashboard 工具(v1.30.7:闭包捕获 ctx → health 可附宿主契约探针 F-05)。
7
+ * ctx 仅用于**只读探针**(`ctx.get` / 属性读取),无副作用。
8
+ */
9
+ export declare function createKitTool(ctx: Context): import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -25,6 +25,7 @@ var weixin_api_exports = /* @__PURE__ */ __exportAll({
25
25
  MessageType: () => MessageType,
26
26
  TypingStatus: () => TypingStatus,
27
27
  aes128EcbDecrypt: () => aes128EcbDecrypt,
28
+ assertIlinkOk: () => assertIlinkOk,
28
29
  buildClientVersion: () => buildClientVersion,
29
30
  buildMediaDownloadUrl: () => buildMediaDownloadUrl,
30
31
  downloadMedia: () => downloadMedia,
@@ -120,6 +121,26 @@ async function apiPost(params) {
120
121
  throw err;
121
122
  }
122
123
  }
124
+ /**
125
+ * iLink 业务层返回码校验(v1.30.9 修复):`apiPost` 只判 HTTP 状态,而 iLink 在 **HTTP 200**
126
+ * 的响应体里用 `ret` / `errcode` 表达失败(如 `{ret:1,errmsg:"denied"}`)——此前被当作成功,
127
+ * 于是"发送成功"的判定是假的:回复路径会给一条**从未送达**的消息触发 outgoing hook 记录
128
+ * (与 weixin-bridge 注释"发送失败不记录"矛盾)。发送类调用一律经此校验。
129
+ * 非 JSON 的 200 响应保持既有宽容语义(视为成功)。
130
+ * @param endpoint 端点名(错误信息定位用)
131
+ * @param rawText 响应体原文
132
+ */
133
+ function assertIlinkOk(endpoint, rawText) {
134
+ let json;
135
+ try {
136
+ json = JSON.parse(rawText);
137
+ } catch {
138
+ return;
139
+ }
140
+ const detail = json.errmsg ? `: ${json.errmsg}` : "";
141
+ if (typeof json.ret === "number" && json.ret !== 0) throw new Error(`${endpoint} ret=${json.ret}${detail}`);
142
+ if (typeof json.errcode === "number" && json.errcode !== 0) throw new Error(`${endpoint} errcode=${json.errcode}${detail}`);
143
+ }
123
144
  /** 取登录二维码(无需凭据;实证裸调可用) */
124
145
  async function fetchQRCode(params) {
125
146
  const botType = params.botType ?? "3";
@@ -181,10 +202,11 @@ const MessageItemType = {
181
202
  FILE: 4,
182
203
  VIDEO: 5
183
204
  };
184
- /** 发文本消息(必须回带 context_token 关联对话;md→plain 内置——微信不支持 Markdown) */
205
+ /** 发文本消息(主动发起时可不带 context_token;md→plain 内置——微信不支持 Markdown)。
206
+ * v1.30.9:HTTP 200 但 `ret/errcode != 0` → 抛错(此前静默当成功,导致"记录已送达"的假象)。 */
185
207
  async function sendTextMessage(params) {
186
208
  const clientId = params.clientId ?? `dsp-weixin-${randomBytes(4).toString("hex")}`;
187
- await apiPost({
209
+ assertIlinkOk("ilink/bot/sendmessage", await apiPost({
188
210
  baseUrl: params.baseUrl,
189
211
  endpoint: "ilink/bot/sendmessage",
190
212
  body: JSON.stringify({ msg: {
@@ -201,7 +223,7 @@ async function sendTextMessage(params) {
201
223
  } }),
202
224
  token: params.token,
203
225
  timeoutMs: params.timeoutMs ?? 15e3
204
- });
226
+ }));
205
227
  }
206
228
  /** sendtyping status:1=TYPING(开始)0=CANCEL(结束)。
207
229
  * ⚠️ 对齐**官方 openclaw-weixin 参考实现**(index.ts onReplyStart → status 1 /
@@ -24,6 +24,16 @@ export declare function buildClientVersion(version: string): number;
24
24
  export type FetchLike = typeof fetch;
25
25
  /** 测试注入:替换全局 fetch 实现(生产零调用) */
26
26
  export declare function __setWeixinFetchForTest(fn: FetchLike | null): void;
27
+ /**
28
+ * iLink 业务层返回码校验(v1.30.9 修复):`apiPost` 只判 HTTP 状态,而 iLink 在 **HTTP 200**
29
+ * 的响应体里用 `ret` / `errcode` 表达失败(如 `{ret:1,errmsg:"denied"}`)——此前被当作成功,
30
+ * 于是"发送成功"的判定是假的:回复路径会给一条**从未送达**的消息触发 outgoing hook 记录
31
+ * (与 weixin-bridge 注释"发送失败不记录"矛盾)。发送类调用一律经此校验。
32
+ * 非 JSON 的 200 响应保持既有宽容语义(视为成功)。
33
+ * @param endpoint 端点名(错误信息定位用)
34
+ * @param rawText 响应体原文
35
+ */
36
+ export declare function assertIlinkOk(endpoint: string, rawText: string): void;
27
37
  /** 取登录二维码(无需凭据;实证裸调可用) */
28
38
  export declare function fetchQRCode(params: {
29
39
  baseUrl?: string;
@@ -139,7 +149,8 @@ export declare const MessageItemType: {
139
149
  readonly FILE: 4;
140
150
  readonly VIDEO: 5;
141
151
  };
142
- /** 发文本消息(必须回带 context_token 关联对话;md→plain 内置——微信不支持 Markdown) */
152
+ /** 发文本消息(主动发起时可不带 context_token;md→plain 内置——微信不支持 Markdown)。
153
+ * v1.30.9:HTTP 200 但 `ret/errcode != 0` → 抛错(此前静默当成功,导致"记录已送达"的假象)。 */
143
154
  export declare function sendTextMessage(params: {
144
155
  baseUrl: string;
145
156
  token: string;
@@ -43,8 +43,50 @@ export declare function handleIncoming(ctx: Context, root: string, accountId: st
43
43
  export declare function syncCccBridge(ctx: Context, root: string): void;
44
44
  /** 停止某 CCC 的桥(移除账号/禁用时) */
45
45
  export declare function stopCccBridge(root: string): void;
46
- /** 停止全部桥(插件 dispose) */
46
+ /**
47
+ * 停止全部桥(插件 dispose)
48
+ */
47
49
  export declare function stopAllBridges(): void;
50
+ /** 主动发送失败原因(稳定 code——入口层翻译成 HTTP 状态 + 可行动提示) */
51
+ export type ProactiveSendErrorCode = 'BRIDGE_DISABLED' | 'NO_ACCOUNT' | 'ACCOUNT_NOT_FOUND' | 'ACCOUNT_NOT_BOUND' | 'SEND_FAILED';
52
+ export interface ProactiveSendInput {
53
+ /** CCC 根(绝对路径;由入口层解析名称/路径后传入) */
54
+ root: string;
55
+ /** 目标用户(iLink from_user_id,形如 xxx@im.wechat;别名解析归调用方) */
56
+ toUserId: string;
57
+ /** 文本(md → 微信纯文本由 sendTextMessage 内置转换) */
58
+ text: string;
59
+ /** 发送账号(缺省 = 该 CCC 第一个启用且已绑定的账号) */
60
+ accountId?: string;
61
+ }
62
+ export type ProactiveSendResult = {
63
+ ok: true;
64
+ accountId: string;
65
+ userId: string;
66
+ sessionId: string;
67
+ role: string;
68
+ } | {
69
+ ok: false;
70
+ code: ProactiveSendErrorCode;
71
+ error: string;
72
+ remediation?: string;
73
+ };
74
+ /**
75
+ * 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
76
+ *
77
+ * 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
78
+ * ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
79
+ * 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
80
+ * CCC 侧不必重复实现 sendmessage;③ 多账号可选。
81
+ *
82
+ * 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
83
+ * 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
84
+ *
85
+ * 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
86
+ * @param input CCC 根 + 目标用户 + 文本 +(可选)账号
87
+ * @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
88
+ */
89
+ export declare function sendProactiveText(input: ProactiveSendInput): Promise<ProactiveSendResult>;
48
90
  /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
49
91
  export declare function weixinBridgeStatus(): Array<{
50
92
  ccc: string;
@@ -52,6 +52,13 @@ export interface WeixinHookIncomingEvent extends WeixinHookEventBase {
52
52
  export interface WeixinHookOutgoingEvent extends WeixinHookEventBase {
53
53
  event: 'outgoing';
54
54
  reply: string;
55
+ /**
56
+ * 触发来源(v1.30.9,S142 用户需求"微信桥支持被调用发消息"):
57
+ * - `reply`(缺省,向后兼容)= 收到用户消息后的回复
58
+ * - `proactive` = 桥被调用主动发给指定用户(CCC MSM 经本机入口发起)
59
+ * 记录侧可据此区分"对话回复"与"主动留言"。
60
+ */
61
+ source?: 'reply' | 'proactive';
55
62
  }
56
63
  /** 全部 hook 事件(判别联合) */
57
64
  export type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
@@ -75,6 +82,8 @@ export interface OutgoingHookInput {
75
82
  sessionId: string;
76
83
  role: string;
77
84
  reply: string;
85
+ /** 缺省 'reply'(对话回复);'proactive' = 桥被调用主动发送 */
86
+ source?: 'reply' | 'proactive';
78
87
  }
79
88
  /** 构造 incoming 事件对象(纯函数,可测) */
80
89
  export declare function buildIncomingHookEvent(input: IncomingHookInput): WeixinHookIncomingEvent;
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CX48YNUL.js";
2
+ import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DAsSHsub.js";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { createHash } from "node:crypto";
@@ -77,7 +77,9 @@ function checkLocalstoreGitCompliance(root) {
77
77
  reason: `localstore.json must not be committed (localstore.gitTrack=deny default) but .gitignore does not cover it — add ${LOCALSTORE_FILENAME} to .gitignore (or set localstore.gitTrack=allow to explicitly permit)`
78
78
  };
79
79
  }
80
- /** 读取全文件(顶层分节);文件不存在/坏 JSON 返回空 */
80
+ /** 已告警的坏 localstore 路径(进程级去重;F-07:凭据文件损坏不得静默) */
81
+ const warnedBrokenStores = /* @__PURE__ */ new Set();
82
+ /** 读取全文件(顶层分节);文件不存在 → 空;**坏 JSON → 空 + 响亮告警**(每路径一次) */
81
83
  function readAll(root) {
82
84
  const p = localstorePath(root);
83
85
  if (!existsSync(p)) return {};
@@ -85,7 +87,11 @@ function readAll(root) {
85
87
  const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
86
88
  if (v && typeof v === "object" && !Array.isArray(v)) return v;
87
89
  return {};
88
- } catch {
90
+ } catch (err) {
91
+ if (!warnedBrokenStores.has(p)) {
92
+ warnedBrokenStores.add(p);
93
+ console.warn(`[serenity-hooks] ✗ localstore 解析失败,已按空存储继续(其中凭据/配置对工具表现为"未设置",请修 JSON): ${p} — ${String(err?.message ?? err)}`);
94
+ }
89
95
  return {};
90
96
  }
91
97
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
3
+ *
4
+ * 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
5
+ * 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
6
+ * 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
7
+ * 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
8
+ *
9
+ * 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
10
+ * 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
11
+ *
12
+ * 契约:
13
+ * POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
14
+ * → 200 { ok: true, accountId, userId, sessionId, role }
15
+ * → 4xx/5xx { ok: false, code, error, remediation? }
16
+ * GET /health → 200 { ok: true, port }
17
+ *
18
+ * 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
19
+ * 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
20
+ */
21
+ import { type ServerResponse } from 'node:http';
22
+ import type { Context } from 'cordis';
23
+ import { weixinSendEndpoint } from './weixin-send-endpoint.js';
24
+ export { weixinSendEndpoint };
25
+ /** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
26
+ export declare const WEIXIN_SEND_MAX_TEXT = 4000;
27
+ /** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
28
+ /** 测试辅助:读取当前监听端口(生产零调用) */
29
+ export declare function weixinSendPort(): number | null;
30
+ /** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
31
+ export declare function isLoopbackAddress(address: string | undefined): boolean;
32
+ export interface ParsedSendRequest {
33
+ ok: true;
34
+ ccc: string;
35
+ user: string;
36
+ text: string;
37
+ accountId?: string;
38
+ }
39
+ export type ParseResult = ParsedSendRequest | {
40
+ ok: false;
41
+ error: string;
42
+ remediation?: string;
43
+ };
44
+ /**
45
+ * 校验并归一 /send 请求体(纯函数,可测)。
46
+ * 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
47
+ */
48
+ export declare function parseSendRequest(body: unknown): ParseResult;
49
+ /** CCC 候选(名称/路径解析用) */
50
+ export interface CccCandidate {
51
+ root: string;
52
+ /** basename(工作目录名) */
53
+ dirName: string;
54
+ /** `.serenity` 首行声明的 CCC 名(可能为 null) */
55
+ cccName: string | null;
56
+ }
57
+ /**
58
+ * 按名称/路径解析 CCC 根(纯函数,可测)。
59
+ * 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
60
+ * 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
61
+ */
62
+ export declare function matchCcc(input: string, candidates: CccCandidate[]): {
63
+ ok: true;
64
+ root: string;
65
+ } | {
66
+ ok: false;
67
+ error: string;
68
+ remediation?: string;
69
+ };
70
+ /** 单请求处理(导出供测试直接驱动,不经真实端口) */
71
+ export declare function handleWeixinSendRequest(ctx: Context, req: {
72
+ method?: string;
73
+ url?: string;
74
+ socket?: {
75
+ remoteAddress?: string;
76
+ };
77
+ [Symbol.asyncIterator]?: unknown;
78
+ }, res: ServerResponse): Promise<void>;
79
+ /** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
80
+ export declare function startWeixinSendApi(ctx: Context, port: number): Promise<number>;
81
+ /** 停止监听器(幂等) */
82
+ export declare function stopWeixinSendApi(): void;
83
+ /**
84
+ * 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
85
+ * 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
86
+ */
87
+ export declare function registerWeixinSendApi(ctx: Context): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
3
+ *
4
+ * 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
5
+ * 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
6
+ * 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
7
+ * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
8
+ * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
9
+ *
10
+ * 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
11
+ */
12
+ /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
13
+ export declare function setWeixinSendEndpoint(value: string | null): void;
14
+ /** 当前入口地址(形如 http://127.0.0.1:3082;未启动 → null) */
15
+ export declare function weixinSendEndpoint(): string | null;
16
+ /** 测试辅助:重置(进程内单例,测试间隔离用) */
17
+ export declare function resetWeixinSendEndpointForTest(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.30.5",
3
+ "version": "1.30.9",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {