@shgroup/dsh-serenity-hooks 1.30.8 → 1.30.10

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/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
3
3
  import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-DlrbHPLD.js";
4
- import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-8pF60JBO.js";
4
+ import { C as newStopToken, D as writeFailedStatus, E as splitModel, O as writeProgress, S as listActiveHandymen, T as requireWhitelistedModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as buildRoundPrompt, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, k as skiffRoleFor, l as createSkiffAgent, m as skiffSessionSnapshot, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionInfo, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as waitAgentIdle, w as readProgress, x as handymanProgressPaths, y as HANDYMAN_GUIDE } from "./skiff-debug-TONJlpgF.js";
5
5
  import { i as hostSessions, n as hostInjected, o as hostWebServer, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
6
- import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, 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-BJ4g4ZNq.js";
6
+ import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, 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-BD_yYAZz.js";
7
7
  import { C as summarize, S as showSession, _ as readActiveSessionMd, a as archiveSessions, b as sessionsRoot, c as createSession, d as findSession, f as getActiveSessionInfo, g as qaCheck, h as parseSessionContextFromEvents, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as listSessions, n as readLastBound, o as clearActiveSessionInfo, p as healthCheck, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findLatestActiveSessionMd, v as resolveSessionByTitle, w as useSession, x as setActiveSessionInfo, y as sessionEvents } from "./session-bound-D2ANqVn-.js";
8
8
  import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BPk0e1du.js";
9
- import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-OUCuw0Ws.js";
9
+ import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-BMpljodH.js";
10
10
  import z from "@deepseek-ai/schemastery";
11
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
12
12
  import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
@@ -1577,6 +1577,28 @@ const gitTool = defineTool({
1577
1577
  }
1578
1578
  });
1579
1579
  //#endregion
1580
+ //#region src/weixin-send-endpoint.ts
1581
+ /**
1582
+ * weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
1583
+ *
1584
+ * 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
1585
+ * 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
1586
+ * 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
1587
+ * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
1588
+ * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
1589
+ *
1590
+ * 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
1591
+ */
1592
+ let endpoint = null;
1593
+ /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
1594
+ function setWeixinSendEndpoint(value) {
1595
+ endpoint = value;
1596
+ }
1597
+ /** 当前入口地址(形如 http://127.0.0.1:3082;未启动 → null) */
1598
+ function weixinSendEndpoint() {
1599
+ return endpoint;
1600
+ }
1601
+ //#endregion
1580
1602
  //#region src/msm-ops.ts
1581
1603
  /**
1582
1604
  * msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
@@ -1625,14 +1647,21 @@ function assertPathInsideRoot(root, value, flagName) {
1625
1647
  if (e instanceof Error && e.message.includes("symlink")) throw e;
1626
1648
  }
1627
1649
  }
1628
- /** 业务子进程 env:注入 SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION(对齐 osp) */
1650
+ /**
1651
+ * 业务子进程 env:注入 SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION(对齐 osp)
1652
+ * + SERENITY_WEIXIN_API(v1.30.9:微信主动发送入口地址——MSM 脚本零硬编码端口;
1653
+ * 未启动则不注入,脚本回退默认值)。
1654
+ */
1629
1655
  function buildMsmEnv(root) {
1630
- return {
1656
+ const env = {
1631
1657
  ...process.env,
1632
1658
  SERENITY_ROOT: root,
1633
1659
  SERENITY_CCC: readCccName(root) ?? "",
1634
1660
  SERENITY_VERSION: ACC_VERSION
1635
1661
  };
1662
+ const weixinApi = weixinSendEndpoint();
1663
+ if (weixinApi) env.SERENITY_WEIXIN_API = weixinApi;
1664
+ return env;
1636
1665
  }
1637
1666
  /**
1638
1667
  * Windows 兼容(审计观察点 A):`.cmd` 不能直接被 CreateProcess 解析——
@@ -1801,12 +1830,25 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1801
1830
  账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
1802
1831
  (扫码绑定后自动写入,永不进 git 明文面)。
1803
1832
  路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
1804
- 面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"]) 排查。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
1833
+ 面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
1834
+ ▸ 主动发送(v1.30.9):CCC 用 msm("weixin-send", ["send", "--ccc", "<CCC>", "--user", "<别名|id>", "<文本>"])
1835
+ 给用户发消息——经 dsp 主动发送入口(**只绑 127.0.0.1**,plugin 全局配置 ~/.dsh/serenity-hooks.json
1836
+ 的 weixinApi.port,默认 3082;公网不可达故无密钥)→ weixin-bridge.sendProactiveText →
1837
+ 成功后触发 outgoing hook(source="proactive")。send-file 仍由 MSM 直连 iLink 上传并自行补记。
1838
+
1839
+ ▸ weixin.autoReplyWithLastMessage(v1.30.10,关闭桥的自动输出):
1840
+ 缺省 true = 现状:桥在 agent 一轮结束后把**最终 assistant 文本**自动回发用户(source="reply")。
1841
+ 显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent——每轮 question 前缀注入
1842
+ 「Reply Output (manual mode)」纪律块(含可直接照抄的 weixin-send 命令,CCC 根/账号/用户 id
1843
+ 已填好),agent 自己决定发什么、发几条(全部记 source="proactive")。
1844
+ 语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
1845
+ **agent 一轮未输出 → 静默不兜底**(否则开关失去意义);typing 指示与 incoming hook 不受影响。
1805
1846
 
1806
1847
  Config:
1807
1848
  { "weixin": {
1808
1849
  "enabled": true,
1809
1850
  "hook": "scripts/weixin-message-hook.ts",
1851
+ "autoReplyWithLastMessage": true,
1810
1852
  "accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
1811
1853
  "routes": [{ "user": "*", "role": "zhaocai" }]
1812
1854
  } }
@@ -1817,7 +1859,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1817
1859
 
1818
1860
  触发(双向):
1819
1861
  incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
1820
- outgoing = bot → 用户:回复发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1862
+ outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1863
+ · source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
1864
+ (CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText)
1865
+ · file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
1821
1866
 
1822
1867
  脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
1823
1868
  #!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
@@ -1831,7 +1876,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1831
1876
  "message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
1832
1877
  { "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
1833
1878
  "userId": ..., "sessionId": ..., "role": ...,
1834
- "reply": "已记录(纯文本)" }
1879
+ "reply": "已记录(纯文本)", "source": "reply" | "proactive" }
1835
1880
 
1836
1881
  示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
1837
1882
  const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
@@ -5048,6 +5093,10 @@ function defaultAdvancedSettings() {
5048
5093
  publicAsk: {
5049
5094
  key: "",
5050
5095
  allowed: []
5096
+ },
5097
+ weixinApi: {
5098
+ enabled: true,
5099
+ port: 3082
5051
5100
  }
5052
5101
  };
5053
5102
  }
@@ -5098,6 +5147,7 @@ function mergeWithDefaults(raw) {
5098
5147
  const gateway = o.gateway ?? {};
5099
5148
  const persona = o.persona ?? {};
5100
5149
  const publicAsk = o.publicAsk ?? {};
5150
+ const weixinApi = o.weixinApi ?? {};
5101
5151
  return {
5102
5152
  gateway: {
5103
5153
  enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
@@ -5116,6 +5166,10 @@ function mergeWithDefaults(raw) {
5116
5166
  publicAsk: {
5117
5167
  key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
5118
5168
  allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
5169
+ },
5170
+ weixinApi: {
5171
+ enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
5172
+ port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
5119
5173
  }
5120
5174
  };
5121
5175
  }
@@ -5153,7 +5207,11 @@ function updateAdvancedSettings(patch) {
5153
5207
  publicAsk: patch.publicAsk !== void 0 ? {
5154
5208
  key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
5155
5209
  allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
5156
- } : current.publicAsk
5210
+ } : current.publicAsk,
5211
+ weixinApi: patch.weixinApi !== void 0 ? {
5212
+ enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
5213
+ port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
5214
+ } : current.weixinApi
5157
5215
  };
5158
5216
  writeAdvancedSettings(next);
5159
5217
  return next;
@@ -6175,7 +6233,7 @@ function saveFileToTmp(root, fileName, data) {
6175
6233
  writeFileSync(join(dir, filename), bytes);
6176
6234
  return `${FILE_UPLOAD_DIR}/${filename}`;
6177
6235
  }
6178
- function readBody$2(req, maxBytes) {
6236
+ function readBody$3(req, maxBytes) {
6179
6237
  return new Promise((resolve, reject) => {
6180
6238
  let data = "";
6181
6239
  req.on("data", (chunk) => {
@@ -6189,7 +6247,7 @@ function readBody$2(req, maxBytes) {
6189
6247
  req.on("error", reject);
6190
6248
  });
6191
6249
  }
6192
- function sendJson$1(res, code, body) {
6250
+ function sendJson$2(res, code, body) {
6193
6251
  const payload = JSON.stringify(body);
6194
6252
  res.writeHead(code, {
6195
6253
  "content-type": "application/json; charset=utf-8",
@@ -6227,7 +6285,7 @@ function registerStatusApi(ctx, opts = {}) {
6227
6285
  handler: async (req, res) => {
6228
6286
  try {
6229
6287
  if (req.method !== "GET") {
6230
- sendJson$1(res, 405, { error: "method not allowed" });
6288
+ sendJson$2(res, 405, { error: "method not allowed" });
6231
6289
  return;
6232
6290
  }
6233
6291
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6237,12 +6295,12 @@ function registerStatusApi(ctx, opts = {}) {
6237
6295
  });
6238
6296
  const root = findSerenityRoot(workspace);
6239
6297
  if (!root) {
6240
- sendJson$1(res, 200, { handymen: [] });
6298
+ sendJson$2(res, 200, { handymen: [] });
6241
6299
  return;
6242
6300
  }
6243
- sendJson$1(res, 200, { handymen: listActiveHandymen(root) });
6301
+ sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
6244
6302
  } catch (err) {
6245
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6303
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6246
6304
  }
6247
6305
  }
6248
6306
  });
@@ -6252,14 +6310,14 @@ function registerStatusApi(ctx, opts = {}) {
6252
6310
  handler: async (req, res) => {
6253
6311
  try {
6254
6312
  if (req.method !== "POST") {
6255
- sendJson$1(res, 405, { error: "method not allowed" });
6313
+ sendJson$2(res, 405, { error: "method not allowed" });
6256
6314
  return;
6257
6315
  }
6258
6316
  if (req.headers["x-serenity-ui"] !== "1") {
6259
- sendJson$1(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
6317
+ sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
6260
6318
  return;
6261
6319
  }
6262
- const raw = await readBody$2(req, 20971520);
6320
+ const raw = await readBody$3(req, 20971520);
6263
6321
  const body = JSON.parse(raw);
6264
6322
  const workspace = resolveWorkspace(ctx, {
6265
6323
  sessionId: body.sessionId,
@@ -6267,12 +6325,12 @@ function registerStatusApi(ctx, opts = {}) {
6267
6325
  });
6268
6326
  const root = findSerenityRoot(workspace);
6269
6327
  if (!root) {
6270
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6328
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6271
6329
  return;
6272
6330
  }
6273
- sendJson$1(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6331
+ sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6274
6332
  } catch (err) {
6275
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6333
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6276
6334
  }
6277
6335
  }
6278
6336
  });
@@ -6282,14 +6340,14 @@ function registerStatusApi(ctx, opts = {}) {
6282
6340
  handler: async (req, res) => {
6283
6341
  try {
6284
6342
  if (req.method !== "POST") {
6285
- sendJson$1(res, 405, { error: "method not allowed" });
6343
+ sendJson$2(res, 405, { error: "method not allowed" });
6286
6344
  return;
6287
6345
  }
6288
6346
  if (req.headers["x-serenity-ui"] !== "1") {
6289
- sendJson$1(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
6347
+ sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
6290
6348
  return;
6291
6349
  }
6292
- const raw = await readBody$2(req, 20971520);
6350
+ const raw = await readBody$3(req, 20971520);
6293
6351
  const body = JSON.parse(raw);
6294
6352
  const workspace = resolveWorkspace(ctx, {
6295
6353
  sessionId: body.sessionId,
@@ -6297,12 +6355,12 @@ function registerStatusApi(ctx, opts = {}) {
6297
6355
  });
6298
6356
  const root = findSerenityRoot(workspace);
6299
6357
  if (!root) {
6300
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6358
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6301
6359
  return;
6302
6360
  }
6303
- sendJson$1(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6361
+ sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6304
6362
  } catch (err) {
6305
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6363
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6306
6364
  }
6307
6365
  }
6308
6366
  });
@@ -6318,7 +6376,7 @@ function registerStatusApi(ctx, opts = {}) {
6318
6376
  workspace: url.searchParams.get("workspace") ?? void 0
6319
6377
  }), configPaths);
6320
6378
  const runtime = ctx.get("codeRuntime");
6321
- sendJson$1(res, 200, {
6379
+ sendJson$2(res, 200, {
6322
6380
  ...status,
6323
6381
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
6324
6382
  language: runtime.language,
@@ -6329,10 +6387,10 @@ function registerStatusApi(ctx, opts = {}) {
6329
6387
  }
6330
6388
  if (req.method === "POST") {
6331
6389
  if (req.headers["x-serenity-ui"] !== "1") {
6332
- sendJson$1(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
6390
+ sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
6333
6391
  return;
6334
6392
  }
6335
- const raw = await readBody$2(req, 65536);
6393
+ const raw = await readBody$3(req, 65536);
6336
6394
  const body = JSON.parse(raw);
6337
6395
  const workspace = resolveWorkspace(ctx, {
6338
6396
  sessionId: body.sessionId,
@@ -6340,19 +6398,19 @@ function registerStatusApi(ctx, opts = {}) {
6340
6398
  });
6341
6399
  const root = findSerenityRoot(workspace);
6342
6400
  if (!root) {
6343
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6401
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6344
6402
  return;
6345
6403
  }
6346
6404
  const result = setSafeMode(root, body.on === true);
6347
- sendJson$1(res, 200, {
6405
+ sendJson$2(res, 200, {
6348
6406
  ...getStatus(workspace, configPaths),
6349
6407
  ...result
6350
6408
  });
6351
6409
  return;
6352
6410
  }
6353
- sendJson$1(res, 405, { error: "method not allowed" });
6411
+ sendJson$2(res, 405, { error: "method not allowed" });
6354
6412
  } catch (err) {
6355
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6413
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6356
6414
  }
6357
6415
  }
6358
6416
  });
@@ -6362,7 +6420,7 @@ function registerStatusApi(ctx, opts = {}) {
6362
6420
  handler: async (req, res) => {
6363
6421
  try {
6364
6422
  if (req.headers["x-serenity-ui"] !== "1") {
6365
- sendJson$1(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6423
+ sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6366
6424
  return;
6367
6425
  }
6368
6426
  if (req.method === "GET") {
@@ -6371,24 +6429,24 @@ function registerStatusApi(ctx, opts = {}) {
6371
6429
  try {
6372
6430
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
6373
6431
  } catch {}
6374
- sendJson$1(res, 200, {
6432
+ sendJson$2(res, 200, {
6375
6433
  config: toWire(settings),
6376
6434
  knownWorkspaces: known
6377
6435
  });
6378
6436
  return;
6379
6437
  }
6380
6438
  if (req.method === "PUT") {
6381
- const raw = await readBody$2(req, 131072);
6439
+ const raw = await readBody$3(req, 131072);
6382
6440
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
6383
6441
  try {
6384
6442
  ctx.emit?.("serenity/config-updated");
6385
6443
  } catch {}
6386
- sendJson$1(res, 200, { config: toWire(saved) });
6444
+ sendJson$2(res, 200, { config: toWire(saved) });
6387
6445
  return;
6388
6446
  }
6389
- sendJson$1(res, 405, { error: "method not allowed" });
6447
+ sendJson$2(res, 405, { error: "method not allowed" });
6390
6448
  } catch (err) {
6391
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6449
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6392
6450
  }
6393
6451
  }
6394
6452
  });
@@ -6398,7 +6456,7 @@ function registerStatusApi(ctx, opts = {}) {
6398
6456
  handler: async (req, res) => {
6399
6457
  try {
6400
6458
  if (req.method !== "GET") {
6401
- sendJson$1(res, 405, { error: "method not allowed" });
6459
+ sendJson$2(res, 405, { error: "method not allowed" });
6402
6460
  return;
6403
6461
  }
6404
6462
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6407,10 +6465,10 @@ function registerStatusApi(ctx, opts = {}) {
6407
6465
  workspace: url.searchParams.get("workspace") ?? void 0
6408
6466
  });
6409
6467
  const root = findSerenityRoot(workspace) ?? "";
6410
- const { discoverCccs } = await import("./skiff-debug-8pF60JBO.js").then((n) => n.i);
6411
- sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
6468
+ const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
6469
+ sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
6412
6470
  } catch (err) {
6413
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6471
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6414
6472
  }
6415
6473
  }
6416
6474
  });
@@ -6420,20 +6478,20 @@ function registerStatusApi(ctx, opts = {}) {
6420
6478
  handler: async (req, res) => {
6421
6479
  try {
6422
6480
  if (req.headers["x-serenity-ui"] !== "1") {
6423
- sendJson$1(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6481
+ sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6424
6482
  return;
6425
6483
  }
6426
6484
  if (req.method === "PUT") {
6427
- const raw = await readBody$2(req, 16384);
6485
+ const raw = await readBody$3(req, 16384);
6428
6486
  if (JSON.parse(raw).action !== "rotate") {
6429
- sendJson$1(res, 400, { error: "unsupported action (expected \"rotate\")" });
6487
+ sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
6430
6488
  return;
6431
6489
  }
6432
- sendJson$1(res, 200, { key: rotatePublicAskKey() });
6490
+ sendJson$2(res, 200, { key: rotatePublicAskKey() });
6433
6491
  return;
6434
6492
  }
6435
6493
  if (req.method !== "GET") {
6436
- sendJson$1(res, 405, { error: "method not allowed" });
6494
+ sendJson$2(res, 405, { error: "method not allowed" });
6437
6495
  return;
6438
6496
  }
6439
6497
  const settings = readAdvancedSettings();
@@ -6446,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
6446
6504
  name,
6447
6505
  url: `${base}/c/${encodeURIComponent(name)}`
6448
6506
  }));
6449
- sendJson$1(res, 200, {
6507
+ sendJson$2(res, 200, {
6450
6508
  enabled: simple.publicAskEnabled ?? false,
6451
6509
  port,
6452
6510
  key: ensurePublicAskKey(),
@@ -6455,7 +6513,7 @@ function registerStatusApi(ctx, opts = {}) {
6455
6513
  listUrl: `${base}/`
6456
6514
  });
6457
6515
  } catch (err) {
6458
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6516
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6459
6517
  }
6460
6518
  }
6461
6519
  });
@@ -6466,13 +6524,13 @@ function registerStatusApi(ctx, opts = {}) {
6466
6524
  try {
6467
6525
  if (req.method === "POST") {
6468
6526
  if (req.headers["x-serenity-ui"] !== "1") {
6469
- sendJson$1(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6527
+ sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6470
6528
  return;
6471
6529
  }
6472
- const raw = await readBody$2(req, 16384);
6530
+ const raw = await readBody$3(req, 16384);
6473
6531
  const body = JSON.parse(raw);
6474
6532
  if (body.action !== "wake") {
6475
- sendJson$1(res, 400, { error: "unsupported action (expected \"wake\")" });
6533
+ sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
6476
6534
  return;
6477
6535
  }
6478
6536
  const workspace = resolveWorkspace(ctx, {
@@ -6482,21 +6540,21 @@ function registerStatusApi(ctx, opts = {}) {
6482
6540
  const root = findSerenityRoot(workspace);
6483
6541
  const effectiveRoot = body.ccc?.trim() || !body.workspace && !body.sessionId ? body.ccc?.trim() ?? (await Promise.resolve().then(() => autopilot_trajectory_exports)).collectAutopilotCccs(ctx)[0] ?? root : root;
6484
6542
  if (!effectiveRoot) {
6485
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6543
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6486
6544
  return;
6487
6545
  }
6488
6546
  const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6489
6547
  const settings = readAutopilotSettings(effectiveRoot);
6490
6548
  if (!settings) {
6491
- sendJson$1(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6549
+ sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6492
6550
  return;
6493
6551
  }
6494
6552
  const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
6495
- sendJson$1(res, result.ok ? 200 : 400, result);
6553
+ sendJson$2(res, result.ok ? 200 : 400, result);
6496
6554
  return;
6497
6555
  }
6498
6556
  if (req.method !== "GET") {
6499
- sendJson$1(res, 405, { error: "method not allowed" });
6557
+ sendJson$2(res, 405, { error: "method not allowed" });
6500
6558
  return;
6501
6559
  }
6502
6560
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6508,12 +6566,12 @@ function registerStatusApi(ctx, opts = {}) {
6508
6566
  const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6509
6567
  const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
6510
6568
  if (!effectiveRoot) {
6511
- sendJson$1(res, 200, { status: null });
6569
+ sendJson$2(res, 200, { status: null });
6512
6570
  return;
6513
6571
  }
6514
- sendJson$1(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6572
+ sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6515
6573
  } catch (err) {
6516
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6574
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6517
6575
  }
6518
6576
  }
6519
6577
  });
@@ -6523,28 +6581,28 @@ function registerStatusApi(ctx, opts = {}) {
6523
6581
  handler: async (req, res) => {
6524
6582
  try {
6525
6583
  if (req.headers["x-serenity-ui"] !== "1") {
6526
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6584
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6527
6585
  return;
6528
6586
  }
6529
6587
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
6530
6588
  if (req.method === "GET") {
6531
6589
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
6532
6590
  if (!chk.ok) {
6533
- sendJson$1(res, 400, { error: chk.error });
6591
+ sendJson$2(res, 400, { error: chk.error });
6534
6592
  return;
6535
6593
  }
6536
- const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6594
+ const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6537
6595
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
6538
6596
  const settings = readWeixinSettings(chk.root);
6539
6597
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
6540
- const { readWeixinCredential } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6598
+ const { readWeixinCredential } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6541
6599
  const accounts = (settings.accounts ?? []).map((a) => ({
6542
6600
  accountId: a.accountId,
6543
6601
  name: a.name ?? void 0,
6544
6602
  enabled: a.enabled !== false,
6545
6603
  bound: readWeixinCredential(chk.root, a.accountId) !== null
6546
6604
  }));
6547
- sendJson$1(res, 200, {
6605
+ sendJson$2(res, 200, {
6548
6606
  enabled: settings.enabled === true,
6549
6607
  botType: settings.botType ?? void 0,
6550
6608
  accounts,
@@ -6554,22 +6612,22 @@ function registerStatusApi(ctx, opts = {}) {
6554
6612
  return;
6555
6613
  }
6556
6614
  if (req.method !== "POST") {
6557
- sendJson$1(res, 405, { error: "method not allowed" });
6615
+ sendJson$2(res, 405, { error: "method not allowed" });
6558
6616
  return;
6559
6617
  }
6560
- const raw = await readBody$2(req, 131072);
6618
+ const raw = await readBody$3(req, 131072);
6561
6619
  const body = JSON.parse(raw);
6562
6620
  const chk = requireCcc(body.ccc);
6563
6621
  if (!chk.ok) {
6564
- sendJson$1(res, 400, { error: chk.error });
6622
+ sendJson$2(res, 400, { error: chk.error });
6565
6623
  return;
6566
6624
  }
6567
6625
  const root = chk.root;
6568
6626
  if (body.action === "login-start") {
6569
- const { fetchQRCode } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6627
+ const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6570
6628
  const { randomUUID } = await import("node:crypto");
6571
6629
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6572
- const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6630
+ const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6573
6631
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
6574
6632
  const loginKey = randomUUID();
6575
6633
  weixinLogins.set(loginKey, {
@@ -6578,7 +6636,7 @@ function registerStatusApi(ctx, opts = {}) {
6578
6636
  startedAt: Date.now(),
6579
6637
  polling: false
6580
6638
  });
6581
- sendJson$1(res, 200, {
6639
+ sendJson$2(res, 200, {
6582
6640
  qrcode: qr.qrcode,
6583
6641
  qrcode_img_content: qr.qrcode_img_content,
6584
6642
  loginKey
@@ -6587,40 +6645,40 @@ function registerStatusApi(ctx, opts = {}) {
6587
6645
  }
6588
6646
  if (body.action === "remove-account") {
6589
6647
  if (!body.accountId) {
6590
- sendJson$1(res, 400, { error: "missing accountId" });
6648
+ sendJson$2(res, 400, { error: "missing accountId" });
6591
6649
  return;
6592
6650
  }
6593
- const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6651
+ const { removeWeixinAccount } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6594
6652
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6595
6653
  removeWeixinAccount(root, body.accountId);
6596
6654
  syncCccBridge(ctx, root);
6597
- sendJson$1(res, 200, { removed: body.accountId });
6655
+ sendJson$2(res, 200, { removed: body.accountId });
6598
6656
  return;
6599
6657
  }
6600
6658
  if (body.action === "save-routes") {
6601
6659
  const routes = body.routes ?? [];
6602
6660
  if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
6603
- sendJson$1(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6661
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6604
6662
  return;
6605
6663
  }
6606
- const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6664
+ const { saveWeixinRoutes } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6607
6665
  const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
6608
6666
  const roles = readSkiffRoles(root);
6609
6667
  for (const r of routes) if (!roles.has(r.role)) {
6610
- sendJson$1(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
6668
+ sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
6611
6669
  return;
6612
6670
  }
6613
6671
  saveWeixinRoutes(root, routes);
6614
- sendJson$1(res, 200, { saved: routes.length });
6672
+ sendJson$2(res, 200, { saved: routes.length });
6615
6673
  return;
6616
6674
  }
6617
6675
  if (body.action === "set-enabled") {
6618
6676
  const enabled = body.enabled === true;
6619
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6677
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6620
6678
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6621
6679
  const settings = readWeixinSettings(root);
6622
6680
  if (enabled && (settings.accounts ?? []).length === 0) {
6623
- sendJson$1(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6681
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6624
6682
  return;
6625
6683
  }
6626
6684
  setWeixinEnabled(root, enabled);
@@ -6629,12 +6687,12 @@ function registerStatusApi(ctx, opts = {}) {
6629
6687
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6630
6688
  stopCccBridge(root);
6631
6689
  }
6632
- sendJson$1(res, 200, { enabled });
6690
+ sendJson$2(res, 200, { enabled });
6633
6691
  return;
6634
6692
  }
6635
- sendJson$1(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6693
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6636
6694
  } catch (err) {
6637
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6695
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6638
6696
  }
6639
6697
  }
6640
6698
  });
@@ -6644,26 +6702,26 @@ function registerStatusApi(ctx, opts = {}) {
6644
6702
  handler: async (req, res) => {
6645
6703
  try {
6646
6704
  if (req.headers["x-serenity-ui"] !== "1") {
6647
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6705
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6648
6706
  return;
6649
6707
  }
6650
6708
  if (req.method !== "GET") {
6651
- sendJson$1(res, 405, { error: "method not allowed" });
6709
+ sendJson$2(res, 405, { error: "method not allowed" });
6652
6710
  return;
6653
6711
  }
6654
6712
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
6655
6713
  const login = weixinLogins.get(key);
6656
6714
  if (!login) {
6657
- sendJson$1(res, 404, { error: "login not found or expired" });
6715
+ sendJson$2(res, 404, { error: "login not found or expired" });
6658
6716
  return;
6659
6717
  }
6660
6718
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
6661
6719
  weixinLogins.delete(key);
6662
- sendJson$1(res, 200, { status: "expired" });
6720
+ sendJson$2(res, 200, { status: "expired" });
6663
6721
  return;
6664
6722
  }
6665
- const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6666
- const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6723
+ const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6724
+ const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6667
6725
  const settings = readWeixinSettings(login.root);
6668
6726
  const status = await pollQRStatus({
6669
6727
  baseUrl: void 0,
@@ -6672,13 +6730,13 @@ function registerStatusApi(ctx, opts = {}) {
6672
6730
  switch (status.status) {
6673
6731
  case "confirmed": {
6674
6732
  if (!status.bot_token) {
6675
- sendJson$1(res, 200, {
6733
+ sendJson$2(res, 200, {
6676
6734
  status: "error",
6677
6735
  error: "confirmed but no bot_token"
6678
6736
  });
6679
6737
  return;
6680
6738
  }
6681
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6739
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6682
6740
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6683
6741
  const accountId = nextWeixinAccountId(settings);
6684
6742
  upsertWeixinAccount(login.root, {
@@ -6693,7 +6751,7 @@ function registerStatusApi(ctx, opts = {}) {
6693
6751
  });
6694
6752
  syncCccBridge(ctx, login.root);
6695
6753
  weixinLogins.delete(key);
6696
- sendJson$1(res, 200, {
6754
+ sendJson$2(res, 200, {
6697
6755
  status: "confirmed",
6698
6756
  accountId,
6699
6757
  tokenSaved: true
@@ -6702,14 +6760,14 @@ function registerStatusApi(ctx, opts = {}) {
6702
6760
  }
6703
6761
  case "expired":
6704
6762
  weixinLogins.delete(key);
6705
- sendJson$1(res, 200, { status: "expired" });
6763
+ sendJson$2(res, 200, { status: "expired" });
6706
6764
  return;
6707
6765
  default:
6708
- sendJson$1(res, 200, { status: status.status ?? "wait" });
6766
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
6709
6767
  return;
6710
6768
  }
6711
6769
  } catch (err) {
6712
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6770
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6713
6771
  }
6714
6772
  }
6715
6773
  });
@@ -6731,7 +6789,7 @@ function registerStatusApi(ctx, opts = {}) {
6731
6789
  const root = sessionsRootDir();
6732
6790
  if (req.method === "GET") {
6733
6791
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
6734
- sendJson$1(res, 200, {
6792
+ sendJson$2(res, 200, {
6735
6793
  root,
6736
6794
  olderThanDays: days,
6737
6795
  dryRun: true,
@@ -6745,15 +6803,15 @@ function registerStatusApi(ctx, opts = {}) {
6745
6803
  return;
6746
6804
  }
6747
6805
  if (req.method !== "POST") {
6748
- sendJson$1(res, 405, { error: "method not allowed" });
6806
+ sendJson$2(res, 405, { error: "method not allowed" });
6749
6807
  return;
6750
6808
  }
6751
6809
  if (req.headers["x-serenity-ui"] !== "1") {
6752
- sendJson$1(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6810
+ sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6753
6811
  return;
6754
6812
  }
6755
6813
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
6756
- sendJson$1(res, 200, {
6814
+ sendJson$2(res, 200, {
6757
6815
  root,
6758
6816
  olderThanDays: days,
6759
6817
  dryRun: false,
@@ -6761,7 +6819,7 @@ function registerStatusApi(ctx, opts = {}) {
6761
6819
  errors: result.errors
6762
6820
  });
6763
6821
  } catch (err) {
6764
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6822
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6765
6823
  }
6766
6824
  }
6767
6825
  });
@@ -7346,7 +7404,7 @@ function createDshCookieProvider(connection, authority) {
7346
7404
  //#endregion
7347
7405
  //#region src/gateway.ts
7348
7406
  /** 读取请求体(≤ maxBytes;超限 reject) */
7349
- function readBody$1(req, maxBytes) {
7407
+ function readBody$2(req, maxBytes) {
7350
7408
  return new Promise((resolve, reject) => {
7351
7409
  let data = "";
7352
7410
  req.on("data", (chunk) => {
@@ -7471,7 +7529,7 @@ function startGateway(config, getAccounts, getDshCookie) {
7471
7529
  const session = authed(req);
7472
7530
  if (session !== void 0) {
7473
7531
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
7474
- readBody$1(req, 131072).then((body) => {
7532
+ readBody$2(req, 131072).then((body) => {
7475
7533
  const { rpcId, path } = parseWorkspaceCreateBody(body);
7476
7534
  if (!allowWorkspaceCreate) {
7477
7535
  res.writeHead(403, { "content-type": "application/json" });
@@ -8538,8 +8596,8 @@ function rpcError(id, code, message) {
8538
8596
  * 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
8539
8597
  */
8540
8598
  /** 运行中的 ACP HTTP 服务(单实例;进程级) */
8541
- let active = null;
8542
- function readBody(req) {
8599
+ let active$1 = null;
8600
+ function readBody$1(req) {
8543
8601
  return new Promise((resolve, reject) => {
8544
8602
  const chunks = [];
8545
8603
  req.on("data", (c) => chunks.push(c));
@@ -8547,7 +8605,7 @@ function readBody(req) {
8547
8605
  req.on("error", reject);
8548
8606
  });
8549
8607
  }
8550
- function sendJson(res, status, payload) {
8608
+ function sendJson$1(res, status, payload) {
8551
8609
  const body = jscSafeJsonText(JSON.stringify(payload));
8552
8610
  res.writeHead(status, {
8553
8611
  "Content-Type": "application/json; charset=utf-8",
@@ -8569,7 +8627,7 @@ function sendHtml(res, html) {
8569
8627
  * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
8570
8628
  */
8571
8629
  async function startAcpHttpServer(ctx, port, defaultRoot) {
8572
- if (active) return;
8630
+ if (active$1) return;
8573
8631
  const server = createServer((req, res) => {
8574
8632
  handle(ctx, defaultRoot ?? "", req, res);
8575
8633
  });
@@ -8577,7 +8635,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8577
8635
  server.once("error", reject);
8578
8636
  server.listen(port, "127.0.0.1", () => resolve());
8579
8637
  });
8580
- active = {
8638
+ active$1 = {
8581
8639
  server,
8582
8640
  port
8583
8641
  };
@@ -8588,11 +8646,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8588
8646
  console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
8589
8647
  }
8590
8648
  function stopAcpHttpServer() {
8591
- if (!active) return;
8649
+ if (!active$1) return;
8592
8650
  try {
8593
- active.server.close();
8651
+ active$1.server.close();
8594
8652
  } catch {}
8595
- active = null;
8653
+ active$1 = null;
8596
8654
  }
8597
8655
  async function handle(ctx, defaultRoot, req, res) {
8598
8656
  try {
@@ -8601,14 +8659,14 @@ async function handle(ctx, defaultRoot, req, res) {
8601
8659
  const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
8602
8660
  if (req.method === "POST" && url === "/") {
8603
8661
  if (!s.acpEnabled) {
8604
- sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
8662
+ sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
8605
8663
  return;
8606
8664
  }
8607
8665
  let body;
8608
8666
  try {
8609
- body = JSON.parse(await readBody(req));
8667
+ body = JSON.parse(await readBody$1(req));
8610
8668
  } catch {
8611
- sendJson(res, 400, [{
8669
+ sendJson$1(res, 400, [{
8612
8670
  jsonrpc: "2.0",
8613
8671
  id: null,
8614
8672
  error: {
@@ -8618,7 +8676,7 @@ async function handle(ctx, defaultRoot, req, res) {
8618
8676
  }]);
8619
8677
  return;
8620
8678
  }
8621
- sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8679
+ sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8622
8680
  return;
8623
8681
  }
8624
8682
  if (!s.publicAskEnabled) {
@@ -8627,10 +8685,10 @@ async function handle(ctx, defaultRoot, req, res) {
8627
8685
  return;
8628
8686
  }
8629
8687
  if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
8630
- sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8688
+ sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8631
8689
  return;
8632
8690
  }
8633
- sendJson(res, 404, { error: "not found" });
8691
+ sendJson$1(res, 404, { error: "not found" });
8634
8692
  return;
8635
8693
  }
8636
8694
  const containerAllowed = (name) => {
@@ -8661,16 +8719,16 @@ async function handle(ctx, defaultRoot, req, res) {
8661
8719
  const name = decodeURIComponent(aMatch[1] ?? "");
8662
8720
  const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
8663
8721
  if (!ccc || !containerAllowed(name)) {
8664
- sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
8722
+ sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
8665
8723
  return;
8666
8724
  }
8667
8725
  let body;
8668
8726
  try {
8669
- const parsed = JSON.parse(await readBody(req));
8727
+ const parsed = JSON.parse(await readBody$1(req));
8670
8728
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8671
8729
  body = parsed;
8672
8730
  } catch {
8673
- sendJson(res, 400, { error: "invalid JSON body" });
8731
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8674
8732
  return;
8675
8733
  }
8676
8734
  await handleAskParsed(ctx, ccc, body, res, ip);
@@ -8679,11 +8737,11 @@ async function handle(ctx, defaultRoot, req, res) {
8679
8737
  if (req.method === "POST" && url === "/ask") {
8680
8738
  let body;
8681
8739
  try {
8682
- const parsed = JSON.parse(await readBody(req));
8740
+ const parsed = JSON.parse(await readBody$1(req));
8683
8741
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8684
8742
  body = parsed;
8685
8743
  } catch {
8686
- sendJson(res, 400, { error: "invalid JSON body" });
8744
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8687
8745
  return;
8688
8746
  }
8689
8747
  const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
@@ -8699,15 +8757,15 @@ async function handle(ctx, defaultRoot, req, res) {
8699
8757
  };
8700
8758
  }
8701
8759
  if (!ccc || !containerAllowed(ccc.name)) {
8702
- sendJson(res, 403, { error: "container is not open for public ask" });
8760
+ sendJson$1(res, 403, { error: "container is not open for public ask" });
8703
8761
  return;
8704
8762
  }
8705
8763
  await handleAskParsed(ctx, ccc, body, res, ip);
8706
8764
  return;
8707
8765
  }
8708
- sendJson(res, 404, { error: "not found" });
8766
+ sendJson$1(res, 404, { error: "not found" });
8709
8767
  } catch (err) {
8710
- sendJson(res, 500, { error: err?.message ?? String(err) });
8768
+ sendJson$1(res, 500, { error: err?.message ?? String(err) });
8711
8769
  }
8712
8770
  }
8713
8771
  /**
@@ -8719,12 +8777,12 @@ async function handle(ctx, defaultRoot, req, res) {
8719
8777
  async function handleAskParsed(ctx, ccc, body, res, ip) {
8720
8778
  const key = typeof body.key === "string" ? body.key : void 0;
8721
8779
  if (isPublicAskIpLocked(ip)) {
8722
- sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
8780
+ sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
8723
8781
  return;
8724
8782
  }
8725
8783
  if (!verifyPublicAskKey(key)) {
8726
8784
  const remaining = recordPublicAskFail(ip);
8727
- sendJson(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
8785
+ sendJson$1(res, remaining > 0 ? 429 : 401, { error: remaining > 0 ? `too many failed attempts — locked for ${Math.ceil(remaining / 6e4)} min` : "invalid or missing key" });
8728
8786
  return;
8729
8787
  }
8730
8788
  resetPublicAskIpFail(ip);
@@ -8733,7 +8791,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8733
8791
  const question = typeof body.question === "string" ? body.question : "";
8734
8792
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
8735
8793
  if (!question.trim()) {
8736
- sendJson(res, 400, { error: "empty question" });
8794
+ sendJson$1(res, 400, { error: "empty question" });
8737
8795
  return;
8738
8796
  }
8739
8797
  const roles = readSkiffRoles(root);
@@ -8741,7 +8799,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8741
8799
  const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
8742
8800
  const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
8743
8801
  if (!effectiveRole || !effectiveCfg) {
8744
- sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
8802
+ sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
8745
8803
  return;
8746
8804
  }
8747
8805
  let agent;
@@ -8759,7 +8817,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8759
8817
  agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
8760
8818
  }
8761
8819
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
8762
- sendJson(res, 200, {
8820
+ sendJson$1(res, 200, {
8763
8821
  answer: result.answer,
8764
8822
  answer_html: renderSkiffMarkdown(result.answer, true),
8765
8823
  sessionId: result.sessionId,
@@ -9140,7 +9198,8 @@ function buildOutgoingHookEvent(input) {
9140
9198
  userId: input.userId,
9141
9199
  sessionId: input.sessionId,
9142
9200
  role: input.role,
9143
- reply: input.reply
9201
+ reply: input.reply,
9202
+ ...input.source === "proactive" ? { source: "proactive" } : {}
9144
9203
  };
9145
9204
  }
9146
9205
  /**
@@ -9255,10 +9314,12 @@ function invokeWeixinHook(root, hookRel, event) {
9255
9314
  var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
9256
9315
  handleIncoming: () => handleIncoming,
9257
9316
  registerWeixinBridge: () => registerWeixinBridge,
9317
+ sendProactiveText: () => sendProactiveText,
9258
9318
  stopAllBridges: () => stopAllBridges,
9259
9319
  stopCccBridge: () => stopCccBridge,
9260
9320
  syncCccBridge: () => syncCccBridge,
9261
- weixinBridgeStatus: () => weixinBridgeStatus
9321
+ weixinBridgeStatus: () => weixinBridgeStatus,
9322
+ weixinManualOutputLine: () => weixinManualOutputLine
9262
9323
  });
9263
9324
  const bridges = /* @__PURE__ */ new Map();
9264
9325
  /** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
@@ -9338,6 +9399,30 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
9338
9399
  }
9339
9400
  }
9340
9401
  /**
9402
+ * 手动输出模式的**输出纪律块**(v1.30.10,`weixin.autoReplyWithLastMessage: false` 时注入)。
9403
+ *
9404
+ * 为什么必须注入(R↓):关掉自动回发后,agent 若不知道"桥不会替你转发",用户就收不到任何回复
9405
+ * ——机制变更必须伴随可执行的行动指引。因此这里给的是**可直接照抄的完整命令**(CCC 根 + 账号 +
9406
+ * 用户 id 全部填好),而不是抽象提示(E↑)。
9407
+ *
9408
+ * 参数全部显式:`--ccc`(必填,无隐式当前 CCC)、`--account`(沿用收到消息的账号,避免多账号下
9409
+ * 用错 bot 身份回话)、`--user`(该用户 iLink id,免去别名解析)。
9410
+ */
9411
+ function weixinManualOutputLine(root, accountId, userId) {
9412
+ return [
9413
+ "── Reply Output (manual mode) ──",
9414
+ "The bridge will NOT forward your final message to the user for this CCC.",
9415
+ "Send everything the user should see yourself, e.g.:",
9416
+ ` msm("weixin-send", ["send", "--ccc", "${root}", "--account", "${accountId}", "--user", "${userId}", "<your reply>"])`,
9417
+ "Rules:",
9418
+ " 1. Send your reply before the turn ends — if you send nothing, the user sees nothing (no fallback).",
9419
+ " 2. You may send several messages (progress / final answer); split only when it helps the reader.",
9420
+ " 3. Plain text only — WeChat renders no markdown.",
9421
+ " 4. Each send is recorded automatically (source=proactive); do not repeat the same content.",
9422
+ "── ──"
9423
+ ].join("\n");
9424
+ }
9425
+ /**
9341
9426
  * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
9342
9427
  *
9343
9428
  * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
@@ -9384,7 +9469,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9384
9469
  const existing = getSkiffAgent(sessionId);
9385
9470
  const hc = readHandymanConfig(root);
9386
9471
  if (existing) try {
9387
- ensureSkiffSession(root, existing, roleName, role);
9472
+ ensureSkiffSession(root, existing, roleName, role, true);
9388
9473
  } catch (err) {
9389
9474
  console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
9390
9475
  }
@@ -9451,6 +9536,8 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9451
9536
  const parts = [];
9452
9537
  const activeWorkspace = getActiveSessionInfo(sessionId);
9453
9538
  if (activeWorkspace?.mdPath) parts.push(workspaceTrajectoryLine(activeWorkspace.mdPath));
9539
+ const manualOutput = settings.autoReplyWithLastMessage === false;
9540
+ if (manualOutput) parts.push(weixinManualOutputLine(root, accountId, fromUserId));
9454
9541
  if (text) parts.push(text);
9455
9542
  parts.push(...mediaNotes, ...degradedNotes);
9456
9543
  const question = parts.join("\n");
@@ -9467,6 +9554,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9467
9554
  console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
9468
9555
  });
9469
9556
  const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
9557
+ if (manualOutput) return;
9470
9558
  if (answer === "") return;
9471
9559
  const reply = markdownToPlainText(stripThink(answer));
9472
9560
  await sendTextMessage({
@@ -9535,11 +9623,93 @@ function stopCccBridge(root) {
9535
9623
  for (const loop of bridge.loops.values()) loop.stopped = true;
9536
9624
  bridges.delete(root);
9537
9625
  }
9538
- /** 停止全部桥(插件 dispose) */
9626
+ /**
9627
+ * 停止全部桥(插件 dispose)
9628
+ */
9539
9629
  function stopAllBridges() {
9540
9630
  for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
9541
9631
  bridges.clear();
9542
9632
  }
9633
+ /**
9634
+ * 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
9635
+ *
9636
+ * 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
9637
+ * ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
9638
+ * 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
9639
+ * CCC 侧不必重复实现 sendmessage;③ 多账号可选。
9640
+ *
9641
+ * 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
9642
+ * 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
9643
+ *
9644
+ * 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
9645
+ * @param input CCC 根 + 目标用户 + 文本 +(可选)账号
9646
+ * @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
9647
+ */
9648
+ async function sendProactiveText(input) {
9649
+ const { root, toUserId } = input;
9650
+ const settings = readWeixinSettings(root);
9651
+ if (!settings.enabled) return {
9652
+ ok: false,
9653
+ code: "BRIDGE_DISABLED",
9654
+ error: `微信桥未启用(ccc=${root})`,
9655
+ remediation: "在 CCC 的 .opencode/serenity.json weixin.enabled 打开"
9656
+ };
9657
+ const accounts = (settings.accounts ?? []).filter((a) => a.enabled !== false);
9658
+ if (accounts.length === 0) return {
9659
+ ok: false,
9660
+ code: "NO_ACCOUNT",
9661
+ error: "微信桥未配置任何启用账号",
9662
+ remediation: "在设置面板「微信桥」扫码绑定账号"
9663
+ };
9664
+ const accountId = input.accountId ?? accounts[0].accountId;
9665
+ if (!accounts.some((a) => a.accountId === accountId)) return {
9666
+ ok: false,
9667
+ code: "ACCOUNT_NOT_FOUND",
9668
+ error: `账号不存在或未启用: ${accountId}`,
9669
+ remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
9670
+ };
9671
+ const cred = readWeixinCredential(root, accountId);
9672
+ if (!cred) return {
9673
+ ok: false,
9674
+ code: "ACCOUNT_NOT_BOUND",
9675
+ error: `账号未绑定(无凭据): ${accountId}`,
9676
+ remediation: "在设置面板重新扫码绑定"
9677
+ };
9678
+ const sessionId = weixinSessionIdFor(toUserId);
9679
+ const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
9680
+ try {
9681
+ await sendTextMessage({
9682
+ baseUrl: cred.baseUrl,
9683
+ token: cred.token,
9684
+ toUserId,
9685
+ text: input.text
9686
+ });
9687
+ } catch (err) {
9688
+ return {
9689
+ ok: false,
9690
+ code: "SEND_FAILED",
9691
+ error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
9692
+ };
9693
+ }
9694
+ if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
9695
+ cccRoot: root,
9696
+ accountId,
9697
+ userId: toUserId,
9698
+ sessionId,
9699
+ role,
9700
+ reply: markdownToPlainText(input.text),
9701
+ source: "proactive"
9702
+ })).catch((err) => {
9703
+ console.log(`[serenity-hooks] weixin hook outgoing(proactive) error: ${err instanceof Error ? err.message : String(err)}`);
9704
+ });
9705
+ return {
9706
+ ok: true,
9707
+ accountId,
9708
+ userId: toUserId,
9709
+ sessionId,
9710
+ role
9711
+ };
9712
+ }
9543
9713
  /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
9544
9714
  function weixinBridgeStatus() {
9545
9715
  const out = [];
@@ -9571,6 +9741,363 @@ function registerWeixinBridge(ctx) {
9571
9741
  } catch {}
9572
9742
  }
9573
9743
  //#endregion
9744
+ //#region src/weixin-send-api.ts
9745
+ /**
9746
+ * weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
9747
+ *
9748
+ * 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
9749
+ * 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
9750
+ * 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
9751
+ * 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
9752
+ *
9753
+ * 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
9754
+ * 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
9755
+ *
9756
+ * 契约:
9757
+ * POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
9758
+ * → 200 { ok: true, accountId, userId, sessionId, role }
9759
+ * → 4xx/5xx { ok: false, code, error, remediation? }
9760
+ * GET /health → 200 { ok: true, port }
9761
+ *
9762
+ * 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
9763
+ * 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
9764
+ */
9765
+ /** 请求体上限(文本消息远小于此;防滥用) */
9766
+ const MAX_BODY_BYTES = 65536;
9767
+ /** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
9768
+ const WEIXIN_SEND_MAX_TEXT = 4e3;
9769
+ /** 运行中的入口(null = 未启动) */
9770
+ let active = null;
9771
+ /** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
9772
+ /** 测试辅助:读取当前监听端口(生产零调用) */
9773
+ function weixinSendPort() {
9774
+ return active?.port ?? null;
9775
+ }
9776
+ /** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
9777
+ function isLoopbackAddress(address) {
9778
+ if (typeof address !== "string" || address === "") return false;
9779
+ const a = address.startsWith("::ffff:") ? address.slice(7) : address;
9780
+ return a === "127.0.0.1" || a === "::1";
9781
+ }
9782
+ /**
9783
+ * 校验并归一 /send 请求体(纯函数,可测)。
9784
+ * 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
9785
+ */
9786
+ function parseSendRequest(body) {
9787
+ if (body === null || typeof body !== "object") return {
9788
+ ok: false,
9789
+ error: "body must be a JSON object"
9790
+ };
9791
+ const o = body;
9792
+ const ccc = typeof o.ccc === "string" ? o.ccc.trim() : "";
9793
+ if (ccc === "") return {
9794
+ ok: false,
9795
+ error: "missing \"ccc\"",
9796
+ remediation: "ccc = CCC 名称或绝对路径(必填,无默认值)"
9797
+ };
9798
+ const user = typeof o.user === "string" ? o.user.trim() : "";
9799
+ if (user === "") return {
9800
+ ok: false,
9801
+ error: "missing \"user\"",
9802
+ remediation: "user = iLink from_user_id(形如 xxx@im.wechat);别名解析归调用方"
9803
+ };
9804
+ if (!user.includes("@")) return {
9805
+ ok: false,
9806
+ error: `invalid "user": ${user}`,
9807
+ remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
9808
+ };
9809
+ const text = typeof o.text === "string" ? o.text.trim() : "";
9810
+ if (text === "") return {
9811
+ ok: false,
9812
+ error: "missing \"text\"",
9813
+ remediation: "text = 要发送的文本(非空)"
9814
+ };
9815
+ if (text.length > 4e3) return {
9816
+ ok: false,
9817
+ error: `text too long: ${text.length} > ${WEIXIN_SEND_MAX_TEXT}`
9818
+ };
9819
+ const accountId = typeof o.accountId === "string" && o.accountId.trim() !== "" ? o.accountId.trim() : void 0;
9820
+ return {
9821
+ ok: true,
9822
+ ccc,
9823
+ user,
9824
+ text,
9825
+ ...accountId ? { accountId } : {}
9826
+ };
9827
+ }
9828
+ /**
9829
+ * 按名称/路径解析 CCC 根(纯函数,可测)。
9830
+ * 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
9831
+ * 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
9832
+ */
9833
+ function matchCcc(input, candidates) {
9834
+ const trimmed = input.trim();
9835
+ if (trimmed === "") return {
9836
+ ok: false,
9837
+ error: "empty ccc"
9838
+ };
9839
+ if (trimmed.startsWith("/")) {
9840
+ const root = findSerenityRoot(trimmed);
9841
+ if (!root) return {
9842
+ ok: false,
9843
+ error: `no CCC found from path: ${trimmed}`,
9844
+ remediation: "确认该路径在某个 CCC 内(含 .serenity 标记)"
9845
+ };
9846
+ return {
9847
+ ok: true,
9848
+ root
9849
+ };
9850
+ }
9851
+ const lower = trimmed.toLowerCase();
9852
+ const exactPath = candidates.find((c) => c.root.toLowerCase() === lower);
9853
+ if (exactPath) return {
9854
+ ok: true,
9855
+ root: exactPath.root
9856
+ };
9857
+ const byName = candidates.filter((c) => c.dirName.toLowerCase() === lower || c.cccName !== null && c.cccName.toLowerCase() === lower);
9858
+ if (byName.length === 1) return {
9859
+ ok: true,
9860
+ root: byName[0].root
9861
+ };
9862
+ if (byName.length > 1) return {
9863
+ ok: false,
9864
+ error: `ambiguous ccc name: ${trimmed}`,
9865
+ remediation: `同名多个,请用绝对路径:${byName.map((c) => c.root).join(" | ")}`
9866
+ };
9867
+ return {
9868
+ ok: false,
9869
+ error: `unknown ccc: ${trimmed}`,
9870
+ remediation: candidates.length === 0 ? "当前进程未发现任何 CCC(打开一个 CCC 会话后重试)" : `已知 CCC: ${candidates.map((c) => c.dirName).join(", ")}`
9871
+ };
9872
+ }
9873
+ /** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
9874
+ async function collectCandidates(ctx) {
9875
+ const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
9876
+ const entries = await discoverCccs(ctx, process.cwd());
9877
+ const seen = /* @__PURE__ */ new Set();
9878
+ const out = [];
9879
+ for (const e of entries) {
9880
+ if (!e?.root || seen.has(e.root)) continue;
9881
+ seen.add(e.root);
9882
+ out.push({
9883
+ root: e.root,
9884
+ dirName: e.name || basename(e.root),
9885
+ cccName: readCccName$2(e.root)
9886
+ });
9887
+ }
9888
+ return out;
9889
+ }
9890
+ function sendJson(res, status, payload) {
9891
+ const body = JSON.stringify(payload);
9892
+ res.writeHead(status, {
9893
+ "content-type": "application/json; charset=utf-8",
9894
+ "content-length": Buffer.byteLength(body)
9895
+ });
9896
+ res.end(body);
9897
+ }
9898
+ async function readBody(req) {
9899
+ const chunks = [];
9900
+ let total = 0;
9901
+ for await (const chunk of req) {
9902
+ const buf = chunk;
9903
+ total += buf.length;
9904
+ if (total > MAX_BODY_BYTES) throw new Error(`body too large (> ${MAX_BODY_BYTES} bytes)`);
9905
+ chunks.push(buf);
9906
+ }
9907
+ return Buffer.concat(chunks).toString("utf-8");
9908
+ }
9909
+ /** HTTP 状态映射(失败 code → 语义化状态;调用方按 code 判定,不看文案) */
9910
+ function statusForCode(code) {
9911
+ switch (code) {
9912
+ case "ACCOUNT_NOT_BOUND":
9913
+ case "BRIDGE_DISABLED":
9914
+ case "NO_ACCOUNT": return 409;
9915
+ case "ACCOUNT_NOT_FOUND": return 400;
9916
+ case "SEND_FAILED": return 502;
9917
+ default: return 400;
9918
+ }
9919
+ }
9920
+ /** 单请求处理(导出供测试直接驱动,不经真实端口) */
9921
+ async function handleWeixinSendRequest(ctx, req, res) {
9922
+ if (!isLoopbackAddress(req.socket?.remoteAddress)) {
9923
+ sendJson(res, 403, {
9924
+ ok: false,
9925
+ code: "FORBIDDEN",
9926
+ error: "only loopback clients allowed"
9927
+ });
9928
+ return;
9929
+ }
9930
+ const path = (req.url ?? "/").split("?")[0];
9931
+ if (req.method === "GET" && path === "/health") {
9932
+ sendJson(res, 200, {
9933
+ ok: true,
9934
+ port: weixinSendPort()
9935
+ });
9936
+ return;
9937
+ }
9938
+ if (path !== "/send") {
9939
+ sendJson(res, 404, {
9940
+ ok: false,
9941
+ code: "NOT_FOUND",
9942
+ error: `unknown path: ${path}`
9943
+ });
9944
+ return;
9945
+ }
9946
+ if (req.method !== "POST") {
9947
+ sendJson(res, 405, {
9948
+ ok: false,
9949
+ code: "METHOD_NOT_ALLOWED",
9950
+ error: "use POST /send"
9951
+ });
9952
+ return;
9953
+ }
9954
+ let raw;
9955
+ try {
9956
+ raw = await readBody(req);
9957
+ } catch (err) {
9958
+ sendJson(res, 413, {
9959
+ ok: false,
9960
+ code: "BODY_TOO_LARGE",
9961
+ error: String(err?.message ?? err)
9962
+ });
9963
+ return;
9964
+ }
9965
+ let body;
9966
+ try {
9967
+ body = JSON.parse(raw === "" ? "{}" : raw);
9968
+ } catch (err) {
9969
+ sendJson(res, 400, {
9970
+ ok: false,
9971
+ code: "BAD_JSON",
9972
+ error: `invalid JSON: ${String(err?.message ?? err)}`
9973
+ });
9974
+ return;
9975
+ }
9976
+ const parsed = parseSendRequest(body);
9977
+ if (!parsed.ok) {
9978
+ sendJson(res, 400, {
9979
+ ok: false,
9980
+ code: "BAD_REQUEST",
9981
+ error: parsed.error,
9982
+ remediation: parsed.remediation
9983
+ });
9984
+ return;
9985
+ }
9986
+ let root;
9987
+ try {
9988
+ const matched = matchCcc(parsed.ccc, await collectCandidates(ctx));
9989
+ if (!matched.ok) {
9990
+ sendJson(res, 404, {
9991
+ ok: false,
9992
+ code: "UNKNOWN_CCC",
9993
+ error: matched.error,
9994
+ remediation: matched.remediation
9995
+ });
9996
+ return;
9997
+ }
9998
+ root = matched.root;
9999
+ } catch (err) {
10000
+ sendJson(res, 500, {
10001
+ ok: false,
10002
+ code: "CCC_DISCOVERY_FAILED",
10003
+ error: String(err?.message ?? err)
10004
+ });
10005
+ return;
10006
+ }
10007
+ const result = await sendProactiveText({
10008
+ root,
10009
+ toUserId: parsed.user,
10010
+ text: parsed.text,
10011
+ ...parsed.accountId ? { accountId: parsed.accountId } : {}
10012
+ });
10013
+ if (result.ok) {
10014
+ sendJson(res, 200, {
10015
+ ok: true,
10016
+ accountId: result.accountId,
10017
+ userId: result.userId,
10018
+ sessionId: result.sessionId,
10019
+ role: result.role
10020
+ });
10021
+ return;
10022
+ }
10023
+ sendJson(res, statusForCode(result.code), {
10024
+ ok: false,
10025
+ code: result.code,
10026
+ error: result.error,
10027
+ remediation: result.remediation
10028
+ });
10029
+ }
10030
+ /** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
10031
+ async function startWeixinSendApi(ctx, port) {
10032
+ if (active) return active.port;
10033
+ const server = createServer((req, res) => {
10034
+ handleWeixinSendRequest(ctx, req, res).catch((err) => {
10035
+ console.warn(`[serenity-hooks] ✗ weixin send api 处理失败: ${String(err?.message ?? err)}`);
10036
+ try {
10037
+ sendJson(res, 500, {
10038
+ ok: false,
10039
+ code: "INTERNAL",
10040
+ error: "internal error"
10041
+ });
10042
+ } catch {}
10043
+ });
10044
+ });
10045
+ await new Promise((resolve, reject) => {
10046
+ server.once("error", reject);
10047
+ server.listen(port, "127.0.0.1", () => resolve());
10048
+ });
10049
+ server.unref();
10050
+ const addr = server.address();
10051
+ const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
10052
+ active = {
10053
+ server,
10054
+ port: actualPort
10055
+ };
10056
+ setWeixinSendEndpoint(`http://127.0.0.1:${actualPort}`);
10057
+ console.log(`[serenity-hooks] ✓ 微信主动发送入口: http://127.0.0.1:${actualPort}/send(仅 loopback;CCC MSM 调用)`);
10058
+ return actualPort;
10059
+ }
10060
+ /** 停止监听器(幂等) */
10061
+ function stopWeixinSendApi() {
10062
+ if (!active) return;
10063
+ try {
10064
+ active.server.close();
10065
+ } catch {}
10066
+ active = null;
10067
+ setWeixinSendEndpoint(null);
10068
+ }
10069
+ /**
10070
+ * 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
10071
+ * 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
10072
+ */
10073
+ function registerWeixinSendApi(ctx) {
10074
+ const sync = () => {
10075
+ let cfg;
10076
+ try {
10077
+ cfg = readAdvancedSettings().weixinApi;
10078
+ } catch (err) {
10079
+ console.warn(`[serenity-hooks] ✗ weixin send api 配置读取失败,跳过同步: ${String(err?.message ?? err)}`);
10080
+ return;
10081
+ }
10082
+ const want = cfg.enabled && cfg.port > 0;
10083
+ if (want && !active) {
10084
+ startWeixinSendApi(ctx, cfg.port).catch((err) => {
10085
+ console.warn(`[serenity-hooks] ✗ 微信主动发送入口启动失败(port=${cfg.port}): ${String(err?.message ?? err)}`);
10086
+ });
10087
+ return;
10088
+ }
10089
+ if (!want && active) stopWeixinSendApi();
10090
+ };
10091
+ try {
10092
+ ctx.on("serenity/config-updated", sync);
10093
+ } catch {}
10094
+ try {
10095
+ ctx.on("serenity/settings-changed", sync);
10096
+ } catch {}
10097
+ sync();
10098
+ registerDisposer(ctx, "weixin send api", () => stopWeixinSendApi());
10099
+ }
10100
+ //#endregion
9574
10101
  //#region src/seams/lifecycle.ts
9575
10102
  /** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
9576
10103
  function sessionIdOf(value) {
@@ -9703,6 +10230,7 @@ function apply(ctx, config) {
9703
10230
  registerAcp(ctx);
9704
10231
  registerAutopilot(ctx);
9705
10232
  registerWeixinBridge(ctx);
10233
+ registerWeixinSendApi(ctx);
9706
10234
  registerLifecycle(ctx);
9707
10235
  }
9708
10236
  /**