@shgroup/dsh-serenity-hooks 1.30.8 → 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.
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
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";
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,7 +1830,11 @@ 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 上传并自行补记。
1805
1838
 
1806
1839
  Config:
1807
1840
  { "weixin": {
@@ -1817,7 +1850,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1817
1850
 
1818
1851
  触发(双向):
1819
1852
  incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
1820
- outgoing = bot → 用户:回复发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1853
+ outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1854
+ · source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
1855
+ (CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText)
1856
+ · file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
1821
1857
 
1822
1858
  脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
1823
1859
  #!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
@@ -1831,7 +1867,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1831
1867
  "message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
1832
1868
  { "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
1833
1869
  "userId": ..., "sessionId": ..., "role": ...,
1834
- "reply": "已记录(纯文本)" }
1870
+ "reply": "已记录(纯文本)", "source": "reply" | "proactive" }
1835
1871
 
1836
1872
  示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
1837
1873
  const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
@@ -5048,6 +5084,10 @@ function defaultAdvancedSettings() {
5048
5084
  publicAsk: {
5049
5085
  key: "",
5050
5086
  allowed: []
5087
+ },
5088
+ weixinApi: {
5089
+ enabled: true,
5090
+ port: 3082
5051
5091
  }
5052
5092
  };
5053
5093
  }
@@ -5098,6 +5138,7 @@ function mergeWithDefaults(raw) {
5098
5138
  const gateway = o.gateway ?? {};
5099
5139
  const persona = o.persona ?? {};
5100
5140
  const publicAsk = o.publicAsk ?? {};
5141
+ const weixinApi = o.weixinApi ?? {};
5101
5142
  return {
5102
5143
  gateway: {
5103
5144
  enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
@@ -5116,6 +5157,10 @@ function mergeWithDefaults(raw) {
5116
5157
  publicAsk: {
5117
5158
  key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
5118
5159
  allowed: Array.isArray(publicAsk.allowed) ? publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : def.publicAsk.allowed
5160
+ },
5161
+ weixinApi: {
5162
+ enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
5163
+ port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
5119
5164
  }
5120
5165
  };
5121
5166
  }
@@ -5153,7 +5198,11 @@ function updateAdvancedSettings(patch) {
5153
5198
  publicAsk: patch.publicAsk !== void 0 ? {
5154
5199
  key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
5155
5200
  allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
5156
- } : current.publicAsk
5201
+ } : current.publicAsk,
5202
+ weixinApi: patch.weixinApi !== void 0 ? {
5203
+ enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
5204
+ port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
5205
+ } : current.weixinApi
5157
5206
  };
5158
5207
  writeAdvancedSettings(next);
5159
5208
  return next;
@@ -6175,7 +6224,7 @@ function saveFileToTmp(root, fileName, data) {
6175
6224
  writeFileSync(join(dir, filename), bytes);
6176
6225
  return `${FILE_UPLOAD_DIR}/${filename}`;
6177
6226
  }
6178
- function readBody$2(req, maxBytes) {
6227
+ function readBody$3(req, maxBytes) {
6179
6228
  return new Promise((resolve, reject) => {
6180
6229
  let data = "";
6181
6230
  req.on("data", (chunk) => {
@@ -6189,7 +6238,7 @@ function readBody$2(req, maxBytes) {
6189
6238
  req.on("error", reject);
6190
6239
  });
6191
6240
  }
6192
- function sendJson$1(res, code, body) {
6241
+ function sendJson$2(res, code, body) {
6193
6242
  const payload = JSON.stringify(body);
6194
6243
  res.writeHead(code, {
6195
6244
  "content-type": "application/json; charset=utf-8",
@@ -6227,7 +6276,7 @@ function registerStatusApi(ctx, opts = {}) {
6227
6276
  handler: async (req, res) => {
6228
6277
  try {
6229
6278
  if (req.method !== "GET") {
6230
- sendJson$1(res, 405, { error: "method not allowed" });
6279
+ sendJson$2(res, 405, { error: "method not allowed" });
6231
6280
  return;
6232
6281
  }
6233
6282
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6237,12 +6286,12 @@ function registerStatusApi(ctx, opts = {}) {
6237
6286
  });
6238
6287
  const root = findSerenityRoot(workspace);
6239
6288
  if (!root) {
6240
- sendJson$1(res, 200, { handymen: [] });
6289
+ sendJson$2(res, 200, { handymen: [] });
6241
6290
  return;
6242
6291
  }
6243
- sendJson$1(res, 200, { handymen: listActiveHandymen(root) });
6292
+ sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
6244
6293
  } catch (err) {
6245
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6294
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6246
6295
  }
6247
6296
  }
6248
6297
  });
@@ -6252,14 +6301,14 @@ function registerStatusApi(ctx, opts = {}) {
6252
6301
  handler: async (req, res) => {
6253
6302
  try {
6254
6303
  if (req.method !== "POST") {
6255
- sendJson$1(res, 405, { error: "method not allowed" });
6304
+ sendJson$2(res, 405, { error: "method not allowed" });
6256
6305
  return;
6257
6306
  }
6258
6307
  if (req.headers["x-serenity-ui"] !== "1") {
6259
- sendJson$1(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
6308
+ sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
6260
6309
  return;
6261
6310
  }
6262
- const raw = await readBody$2(req, 20971520);
6311
+ const raw = await readBody$3(req, 20971520);
6263
6312
  const body = JSON.parse(raw);
6264
6313
  const workspace = resolveWorkspace(ctx, {
6265
6314
  sessionId: body.sessionId,
@@ -6267,12 +6316,12 @@ function registerStatusApi(ctx, opts = {}) {
6267
6316
  });
6268
6317
  const root = findSerenityRoot(workspace);
6269
6318
  if (!root) {
6270
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6319
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6271
6320
  return;
6272
6321
  }
6273
- sendJson$1(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6322
+ sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6274
6323
  } catch (err) {
6275
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6324
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6276
6325
  }
6277
6326
  }
6278
6327
  });
@@ -6282,14 +6331,14 @@ function registerStatusApi(ctx, opts = {}) {
6282
6331
  handler: async (req, res) => {
6283
6332
  try {
6284
6333
  if (req.method !== "POST") {
6285
- sendJson$1(res, 405, { error: "method not allowed" });
6334
+ sendJson$2(res, 405, { error: "method not allowed" });
6286
6335
  return;
6287
6336
  }
6288
6337
  if (req.headers["x-serenity-ui"] !== "1") {
6289
- sendJson$1(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
6338
+ sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
6290
6339
  return;
6291
6340
  }
6292
- const raw = await readBody$2(req, 20971520);
6341
+ const raw = await readBody$3(req, 20971520);
6293
6342
  const body = JSON.parse(raw);
6294
6343
  const workspace = resolveWorkspace(ctx, {
6295
6344
  sessionId: body.sessionId,
@@ -6297,12 +6346,12 @@ function registerStatusApi(ctx, opts = {}) {
6297
6346
  });
6298
6347
  const root = findSerenityRoot(workspace);
6299
6348
  if (!root) {
6300
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6349
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6301
6350
  return;
6302
6351
  }
6303
- sendJson$1(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6352
+ sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6304
6353
  } catch (err) {
6305
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6354
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6306
6355
  }
6307
6356
  }
6308
6357
  });
@@ -6318,7 +6367,7 @@ function registerStatusApi(ctx, opts = {}) {
6318
6367
  workspace: url.searchParams.get("workspace") ?? void 0
6319
6368
  }), configPaths);
6320
6369
  const runtime = ctx.get("codeRuntime");
6321
- sendJson$1(res, 200, {
6370
+ sendJson$2(res, 200, {
6322
6371
  ...status,
6323
6372
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
6324
6373
  language: runtime.language,
@@ -6329,10 +6378,10 @@ function registerStatusApi(ctx, opts = {}) {
6329
6378
  }
6330
6379
  if (req.method === "POST") {
6331
6380
  if (req.headers["x-serenity-ui"] !== "1") {
6332
- sendJson$1(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
6381
+ sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
6333
6382
  return;
6334
6383
  }
6335
- const raw = await readBody$2(req, 65536);
6384
+ const raw = await readBody$3(req, 65536);
6336
6385
  const body = JSON.parse(raw);
6337
6386
  const workspace = resolveWorkspace(ctx, {
6338
6387
  sessionId: body.sessionId,
@@ -6340,19 +6389,19 @@ function registerStatusApi(ctx, opts = {}) {
6340
6389
  });
6341
6390
  const root = findSerenityRoot(workspace);
6342
6391
  if (!root) {
6343
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6392
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6344
6393
  return;
6345
6394
  }
6346
6395
  const result = setSafeMode(root, body.on === true);
6347
- sendJson$1(res, 200, {
6396
+ sendJson$2(res, 200, {
6348
6397
  ...getStatus(workspace, configPaths),
6349
6398
  ...result
6350
6399
  });
6351
6400
  return;
6352
6401
  }
6353
- sendJson$1(res, 405, { error: "method not allowed" });
6402
+ sendJson$2(res, 405, { error: "method not allowed" });
6354
6403
  } catch (err) {
6355
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6404
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6356
6405
  }
6357
6406
  }
6358
6407
  });
@@ -6362,7 +6411,7 @@ function registerStatusApi(ctx, opts = {}) {
6362
6411
  handler: async (req, res) => {
6363
6412
  try {
6364
6413
  if (req.headers["x-serenity-ui"] !== "1") {
6365
- sendJson$1(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6414
+ sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6366
6415
  return;
6367
6416
  }
6368
6417
  if (req.method === "GET") {
@@ -6371,24 +6420,24 @@ function registerStatusApi(ctx, opts = {}) {
6371
6420
  try {
6372
6421
  known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
6373
6422
  } catch {}
6374
- sendJson$1(res, 200, {
6423
+ sendJson$2(res, 200, {
6375
6424
  config: toWire(settings),
6376
6425
  knownWorkspaces: known
6377
6426
  });
6378
6427
  return;
6379
6428
  }
6380
6429
  if (req.method === "PUT") {
6381
- const raw = await readBody$2(req, 131072);
6430
+ const raw = await readBody$3(req, 131072);
6382
6431
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
6383
6432
  try {
6384
6433
  ctx.emit?.("serenity/config-updated");
6385
6434
  } catch {}
6386
- sendJson$1(res, 200, { config: toWire(saved) });
6435
+ sendJson$2(res, 200, { config: toWire(saved) });
6387
6436
  return;
6388
6437
  }
6389
- sendJson$1(res, 405, { error: "method not allowed" });
6438
+ sendJson$2(res, 405, { error: "method not allowed" });
6390
6439
  } catch (err) {
6391
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6440
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6392
6441
  }
6393
6442
  }
6394
6443
  });
@@ -6398,7 +6447,7 @@ function registerStatusApi(ctx, opts = {}) {
6398
6447
  handler: async (req, res) => {
6399
6448
  try {
6400
6449
  if (req.method !== "GET") {
6401
- sendJson$1(res, 405, { error: "method not allowed" });
6450
+ sendJson$2(res, 405, { error: "method not allowed" });
6402
6451
  return;
6403
6452
  }
6404
6453
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6407,10 +6456,10 @@ function registerStatusApi(ctx, opts = {}) {
6407
6456
  workspace: url.searchParams.get("workspace") ?? void 0
6408
6457
  });
6409
6458
  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) });
6459
+ const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
6460
+ sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
6412
6461
  } catch (err) {
6413
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6462
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6414
6463
  }
6415
6464
  }
6416
6465
  });
@@ -6420,20 +6469,20 @@ function registerStatusApi(ctx, opts = {}) {
6420
6469
  handler: async (req, res) => {
6421
6470
  try {
6422
6471
  if (req.headers["x-serenity-ui"] !== "1") {
6423
- sendJson$1(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6472
+ sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6424
6473
  return;
6425
6474
  }
6426
6475
  if (req.method === "PUT") {
6427
- const raw = await readBody$2(req, 16384);
6476
+ const raw = await readBody$3(req, 16384);
6428
6477
  if (JSON.parse(raw).action !== "rotate") {
6429
- sendJson$1(res, 400, { error: "unsupported action (expected \"rotate\")" });
6478
+ sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
6430
6479
  return;
6431
6480
  }
6432
- sendJson$1(res, 200, { key: rotatePublicAskKey() });
6481
+ sendJson$2(res, 200, { key: rotatePublicAskKey() });
6433
6482
  return;
6434
6483
  }
6435
6484
  if (req.method !== "GET") {
6436
- sendJson$1(res, 405, { error: "method not allowed" });
6485
+ sendJson$2(res, 405, { error: "method not allowed" });
6437
6486
  return;
6438
6487
  }
6439
6488
  const settings = readAdvancedSettings();
@@ -6446,7 +6495,7 @@ function registerStatusApi(ctx, opts = {}) {
6446
6495
  name,
6447
6496
  url: `${base}/c/${encodeURIComponent(name)}`
6448
6497
  }));
6449
- sendJson$1(res, 200, {
6498
+ sendJson$2(res, 200, {
6450
6499
  enabled: simple.publicAskEnabled ?? false,
6451
6500
  port,
6452
6501
  key: ensurePublicAskKey(),
@@ -6455,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
6455
6504
  listUrl: `${base}/`
6456
6505
  });
6457
6506
  } catch (err) {
6458
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6507
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6459
6508
  }
6460
6509
  }
6461
6510
  });
@@ -6466,13 +6515,13 @@ function registerStatusApi(ctx, opts = {}) {
6466
6515
  try {
6467
6516
  if (req.method === "POST") {
6468
6517
  if (req.headers["x-serenity-ui"] !== "1") {
6469
- sendJson$1(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6518
+ sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6470
6519
  return;
6471
6520
  }
6472
- const raw = await readBody$2(req, 16384);
6521
+ const raw = await readBody$3(req, 16384);
6473
6522
  const body = JSON.parse(raw);
6474
6523
  if (body.action !== "wake") {
6475
- sendJson$1(res, 400, { error: "unsupported action (expected \"wake\")" });
6524
+ sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
6476
6525
  return;
6477
6526
  }
6478
6527
  const workspace = resolveWorkspace(ctx, {
@@ -6482,21 +6531,21 @@ function registerStatusApi(ctx, opts = {}) {
6482
6531
  const root = findSerenityRoot(workspace);
6483
6532
  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
6533
  if (!effectiveRoot) {
6485
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6534
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6486
6535
  return;
6487
6536
  }
6488
6537
  const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6489
6538
  const settings = readAutopilotSettings(effectiveRoot);
6490
6539
  if (!settings) {
6491
- sendJson$1(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6540
+ sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6492
6541
  return;
6493
6542
  }
6494
6543
  const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
6495
- sendJson$1(res, result.ok ? 200 : 400, result);
6544
+ sendJson$2(res, result.ok ? 200 : 400, result);
6496
6545
  return;
6497
6546
  }
6498
6547
  if (req.method !== "GET") {
6499
- sendJson$1(res, 405, { error: "method not allowed" });
6548
+ sendJson$2(res, 405, { error: "method not allowed" });
6500
6549
  return;
6501
6550
  }
6502
6551
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6508,12 +6557,12 @@ function registerStatusApi(ctx, opts = {}) {
6508
6557
  const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6509
6558
  const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
6510
6559
  if (!effectiveRoot) {
6511
- sendJson$1(res, 200, { status: null });
6560
+ sendJson$2(res, 200, { status: null });
6512
6561
  return;
6513
6562
  }
6514
- sendJson$1(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6563
+ sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6515
6564
  } catch (err) {
6516
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6565
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6517
6566
  }
6518
6567
  }
6519
6568
  });
@@ -6523,14 +6572,14 @@ function registerStatusApi(ctx, opts = {}) {
6523
6572
  handler: async (req, res) => {
6524
6573
  try {
6525
6574
  if (req.headers["x-serenity-ui"] !== "1") {
6526
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6575
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6527
6576
  return;
6528
6577
  }
6529
6578
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
6530
6579
  if (req.method === "GET") {
6531
6580
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
6532
6581
  if (!chk.ok) {
6533
- sendJson$1(res, 400, { error: chk.error });
6582
+ sendJson$2(res, 400, { error: chk.error });
6534
6583
  return;
6535
6584
  }
6536
6585
  const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
@@ -6544,7 +6593,7 @@ function registerStatusApi(ctx, opts = {}) {
6544
6593
  enabled: a.enabled !== false,
6545
6594
  bound: readWeixinCredential(chk.root, a.accountId) !== null
6546
6595
  }));
6547
- sendJson$1(res, 200, {
6596
+ sendJson$2(res, 200, {
6548
6597
  enabled: settings.enabled === true,
6549
6598
  botType: settings.botType ?? void 0,
6550
6599
  accounts,
@@ -6554,19 +6603,19 @@ function registerStatusApi(ctx, opts = {}) {
6554
6603
  return;
6555
6604
  }
6556
6605
  if (req.method !== "POST") {
6557
- sendJson$1(res, 405, { error: "method not allowed" });
6606
+ sendJson$2(res, 405, { error: "method not allowed" });
6558
6607
  return;
6559
6608
  }
6560
- const raw = await readBody$2(req, 131072);
6609
+ const raw = await readBody$3(req, 131072);
6561
6610
  const body = JSON.parse(raw);
6562
6611
  const chk = requireCcc(body.ccc);
6563
6612
  if (!chk.ok) {
6564
- sendJson$1(res, 400, { error: chk.error });
6613
+ sendJson$2(res, 400, { error: chk.error });
6565
6614
  return;
6566
6615
  }
6567
6616
  const root = chk.root;
6568
6617
  if (body.action === "login-start") {
6569
- const { fetchQRCode } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6618
+ const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6570
6619
  const { randomUUID } = await import("node:crypto");
6571
6620
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6572
6621
  const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
@@ -6578,7 +6627,7 @@ function registerStatusApi(ctx, opts = {}) {
6578
6627
  startedAt: Date.now(),
6579
6628
  polling: false
6580
6629
  });
6581
- sendJson$1(res, 200, {
6630
+ sendJson$2(res, 200, {
6582
6631
  qrcode: qr.qrcode,
6583
6632
  qrcode_img_content: qr.qrcode_img_content,
6584
6633
  loginKey
@@ -6587,31 +6636,31 @@ function registerStatusApi(ctx, opts = {}) {
6587
6636
  }
6588
6637
  if (body.action === "remove-account") {
6589
6638
  if (!body.accountId) {
6590
- sendJson$1(res, 400, { error: "missing accountId" });
6639
+ sendJson$2(res, 400, { error: "missing accountId" });
6591
6640
  return;
6592
6641
  }
6593
6642
  const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6594
6643
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6595
6644
  removeWeixinAccount(root, body.accountId);
6596
6645
  syncCccBridge(ctx, root);
6597
- sendJson$1(res, 200, { removed: body.accountId });
6646
+ sendJson$2(res, 200, { removed: body.accountId });
6598
6647
  return;
6599
6648
  }
6600
6649
  if (body.action === "save-routes") {
6601
6650
  const routes = body.routes ?? [];
6602
6651
  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}, ...])" });
6652
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6604
6653
  return;
6605
6654
  }
6606
6655
  const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6607
6656
  const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
6608
6657
  const roles = readSkiffRoles(root);
6609
6658
  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)` });
6659
+ sendJson$2(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
6611
6660
  return;
6612
6661
  }
6613
6662
  saveWeixinRoutes(root, routes);
6614
- sendJson$1(res, 200, { saved: routes.length });
6663
+ sendJson$2(res, 200, { saved: routes.length });
6615
6664
  return;
6616
6665
  }
6617
6666
  if (body.action === "set-enabled") {
@@ -6620,7 +6669,7 @@ function registerStatusApi(ctx, opts = {}) {
6620
6669
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6621
6670
  const settings = readWeixinSettings(root);
6622
6671
  if (enabled && (settings.accounts ?? []).length === 0) {
6623
- sendJson$1(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6672
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6624
6673
  return;
6625
6674
  }
6626
6675
  setWeixinEnabled(root, enabled);
@@ -6629,12 +6678,12 @@ function registerStatusApi(ctx, opts = {}) {
6629
6678
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6630
6679
  stopCccBridge(root);
6631
6680
  }
6632
- sendJson$1(res, 200, { enabled });
6681
+ sendJson$2(res, 200, { enabled });
6633
6682
  return;
6634
6683
  }
6635
- sendJson$1(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6684
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6636
6685
  } catch (err) {
6637
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6686
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6638
6687
  }
6639
6688
  }
6640
6689
  });
@@ -6644,25 +6693,25 @@ function registerStatusApi(ctx, opts = {}) {
6644
6693
  handler: async (req, res) => {
6645
6694
  try {
6646
6695
  if (req.headers["x-serenity-ui"] !== "1") {
6647
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6696
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6648
6697
  return;
6649
6698
  }
6650
6699
  if (req.method !== "GET") {
6651
- sendJson$1(res, 405, { error: "method not allowed" });
6700
+ sendJson$2(res, 405, { error: "method not allowed" });
6652
6701
  return;
6653
6702
  }
6654
6703
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
6655
6704
  const login = weixinLogins.get(key);
6656
6705
  if (!login) {
6657
- sendJson$1(res, 404, { error: "login not found or expired" });
6706
+ sendJson$2(res, 404, { error: "login not found or expired" });
6658
6707
  return;
6659
6708
  }
6660
6709
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
6661
6710
  weixinLogins.delete(key);
6662
- sendJson$1(res, 200, { status: "expired" });
6711
+ sendJson$2(res, 200, { status: "expired" });
6663
6712
  return;
6664
6713
  }
6665
- const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6714
+ const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6666
6715
  const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6667
6716
  const settings = readWeixinSettings(login.root);
6668
6717
  const status = await pollQRStatus({
@@ -6672,7 +6721,7 @@ function registerStatusApi(ctx, opts = {}) {
6672
6721
  switch (status.status) {
6673
6722
  case "confirmed": {
6674
6723
  if (!status.bot_token) {
6675
- sendJson$1(res, 200, {
6724
+ sendJson$2(res, 200, {
6676
6725
  status: "error",
6677
6726
  error: "confirmed but no bot_token"
6678
6727
  });
@@ -6693,7 +6742,7 @@ function registerStatusApi(ctx, opts = {}) {
6693
6742
  });
6694
6743
  syncCccBridge(ctx, login.root);
6695
6744
  weixinLogins.delete(key);
6696
- sendJson$1(res, 200, {
6745
+ sendJson$2(res, 200, {
6697
6746
  status: "confirmed",
6698
6747
  accountId,
6699
6748
  tokenSaved: true
@@ -6702,14 +6751,14 @@ function registerStatusApi(ctx, opts = {}) {
6702
6751
  }
6703
6752
  case "expired":
6704
6753
  weixinLogins.delete(key);
6705
- sendJson$1(res, 200, { status: "expired" });
6754
+ sendJson$2(res, 200, { status: "expired" });
6706
6755
  return;
6707
6756
  default:
6708
- sendJson$1(res, 200, { status: status.status ?? "wait" });
6757
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
6709
6758
  return;
6710
6759
  }
6711
6760
  } catch (err) {
6712
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6761
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6713
6762
  }
6714
6763
  }
6715
6764
  });
@@ -6731,7 +6780,7 @@ function registerStatusApi(ctx, opts = {}) {
6731
6780
  const root = sessionsRootDir();
6732
6781
  if (req.method === "GET") {
6733
6782
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
6734
- sendJson$1(res, 200, {
6783
+ sendJson$2(res, 200, {
6735
6784
  root,
6736
6785
  olderThanDays: days,
6737
6786
  dryRun: true,
@@ -6745,15 +6794,15 @@ function registerStatusApi(ctx, opts = {}) {
6745
6794
  return;
6746
6795
  }
6747
6796
  if (req.method !== "POST") {
6748
- sendJson$1(res, 405, { error: "method not allowed" });
6797
+ sendJson$2(res, 405, { error: "method not allowed" });
6749
6798
  return;
6750
6799
  }
6751
6800
  if (req.headers["x-serenity-ui"] !== "1") {
6752
- sendJson$1(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6801
+ sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6753
6802
  return;
6754
6803
  }
6755
6804
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
6756
- sendJson$1(res, 200, {
6805
+ sendJson$2(res, 200, {
6757
6806
  root,
6758
6807
  olderThanDays: days,
6759
6808
  dryRun: false,
@@ -6761,7 +6810,7 @@ function registerStatusApi(ctx, opts = {}) {
6761
6810
  errors: result.errors
6762
6811
  });
6763
6812
  } catch (err) {
6764
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6813
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6765
6814
  }
6766
6815
  }
6767
6816
  });
@@ -7346,7 +7395,7 @@ function createDshCookieProvider(connection, authority) {
7346
7395
  //#endregion
7347
7396
  //#region src/gateway.ts
7348
7397
  /** 读取请求体(≤ maxBytes;超限 reject) */
7349
- function readBody$1(req, maxBytes) {
7398
+ function readBody$2(req, maxBytes) {
7350
7399
  return new Promise((resolve, reject) => {
7351
7400
  let data = "";
7352
7401
  req.on("data", (chunk) => {
@@ -7471,7 +7520,7 @@ function startGateway(config, getAccounts, getDshCookie) {
7471
7520
  const session = authed(req);
7472
7521
  if (session !== void 0) {
7473
7522
  if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
7474
- readBody$1(req, 131072).then((body) => {
7523
+ readBody$2(req, 131072).then((body) => {
7475
7524
  const { rpcId, path } = parseWorkspaceCreateBody(body);
7476
7525
  if (!allowWorkspaceCreate) {
7477
7526
  res.writeHead(403, { "content-type": "application/json" });
@@ -8538,8 +8587,8 @@ function rpcError(id, code, message) {
8538
8587
  * 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
8539
8588
  */
8540
8589
  /** 运行中的 ACP HTTP 服务(单实例;进程级) */
8541
- let active = null;
8542
- function readBody(req) {
8590
+ let active$1 = null;
8591
+ function readBody$1(req) {
8543
8592
  return new Promise((resolve, reject) => {
8544
8593
  const chunks = [];
8545
8594
  req.on("data", (c) => chunks.push(c));
@@ -8547,7 +8596,7 @@ function readBody(req) {
8547
8596
  req.on("error", reject);
8548
8597
  });
8549
8598
  }
8550
- function sendJson(res, status, payload) {
8599
+ function sendJson$1(res, status, payload) {
8551
8600
  const body = jscSafeJsonText(JSON.stringify(payload));
8552
8601
  res.writeHead(status, {
8553
8602
  "Content-Type": "application/json; charset=utf-8",
@@ -8569,7 +8618,7 @@ function sendHtml(res, html) {
8569
8618
  * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
8570
8619
  */
8571
8620
  async function startAcpHttpServer(ctx, port, defaultRoot) {
8572
- if (active) return;
8621
+ if (active$1) return;
8573
8622
  const server = createServer((req, res) => {
8574
8623
  handle(ctx, defaultRoot ?? "", req, res);
8575
8624
  });
@@ -8577,7 +8626,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8577
8626
  server.once("error", reject);
8578
8627
  server.listen(port, "127.0.0.1", () => resolve());
8579
8628
  });
8580
- active = {
8629
+ active$1 = {
8581
8630
  server,
8582
8631
  port
8583
8632
  };
@@ -8588,11 +8637,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8588
8637
  console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
8589
8638
  }
8590
8639
  function stopAcpHttpServer() {
8591
- if (!active) return;
8640
+ if (!active$1) return;
8592
8641
  try {
8593
- active.server.close();
8642
+ active$1.server.close();
8594
8643
  } catch {}
8595
- active = null;
8644
+ active$1 = null;
8596
8645
  }
8597
8646
  async function handle(ctx, defaultRoot, req, res) {
8598
8647
  try {
@@ -8601,14 +8650,14 @@ async function handle(ctx, defaultRoot, req, res) {
8601
8650
  const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
8602
8651
  if (req.method === "POST" && url === "/") {
8603
8652
  if (!s.acpEnabled) {
8604
- sendJson(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
8653
+ sendJson$1(res, 403, { error: "ACP JSON-RPC disabled (enable in Serenity settings)" });
8605
8654
  return;
8606
8655
  }
8607
8656
  let body;
8608
8657
  try {
8609
- body = JSON.parse(await readBody(req));
8658
+ body = JSON.parse(await readBody$1(req));
8610
8659
  } catch {
8611
- sendJson(res, 400, [{
8660
+ sendJson$1(res, 400, [{
8612
8661
  jsonrpc: "2.0",
8613
8662
  id: null,
8614
8663
  error: {
@@ -8618,7 +8667,7 @@ async function handle(ctx, defaultRoot, req, res) {
8618
8667
  }]);
8619
8668
  return;
8620
8669
  }
8621
- sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8670
+ sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8622
8671
  return;
8623
8672
  }
8624
8673
  if (!s.publicAskEnabled) {
@@ -8627,10 +8676,10 @@ async function handle(ctx, defaultRoot, req, res) {
8627
8676
  return;
8628
8677
  }
8629
8678
  if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
8630
- sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8679
+ sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8631
8680
  return;
8632
8681
  }
8633
- sendJson(res, 404, { error: "not found" });
8682
+ sendJson$1(res, 404, { error: "not found" });
8634
8683
  return;
8635
8684
  }
8636
8685
  const containerAllowed = (name) => {
@@ -8661,16 +8710,16 @@ async function handle(ctx, defaultRoot, req, res) {
8661
8710
  const name = decodeURIComponent(aMatch[1] ?? "");
8662
8711
  const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
8663
8712
  if (!ccc || !containerAllowed(name)) {
8664
- sendJson(res, 403, { error: `container "${name}" is not open for public ask` });
8713
+ sendJson$1(res, 403, { error: `container "${name}" is not open for public ask` });
8665
8714
  return;
8666
8715
  }
8667
8716
  let body;
8668
8717
  try {
8669
- const parsed = JSON.parse(await readBody(req));
8718
+ const parsed = JSON.parse(await readBody$1(req));
8670
8719
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8671
8720
  body = parsed;
8672
8721
  } catch {
8673
- sendJson(res, 400, { error: "invalid JSON body" });
8722
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8674
8723
  return;
8675
8724
  }
8676
8725
  await handleAskParsed(ctx, ccc, body, res, ip);
@@ -8679,11 +8728,11 @@ async function handle(ctx, defaultRoot, req, res) {
8679
8728
  if (req.method === "POST" && url === "/ask") {
8680
8729
  let body;
8681
8730
  try {
8682
- const parsed = JSON.parse(await readBody(req));
8731
+ const parsed = JSON.parse(await readBody$1(req));
8683
8732
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8684
8733
  body = parsed;
8685
8734
  } catch {
8686
- sendJson(res, 400, { error: "invalid JSON body" });
8735
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8687
8736
  return;
8688
8737
  }
8689
8738
  const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
@@ -8699,15 +8748,15 @@ async function handle(ctx, defaultRoot, req, res) {
8699
8748
  };
8700
8749
  }
8701
8750
  if (!ccc || !containerAllowed(ccc.name)) {
8702
- sendJson(res, 403, { error: "container is not open for public ask" });
8751
+ sendJson$1(res, 403, { error: "container is not open for public ask" });
8703
8752
  return;
8704
8753
  }
8705
8754
  await handleAskParsed(ctx, ccc, body, res, ip);
8706
8755
  return;
8707
8756
  }
8708
- sendJson(res, 404, { error: "not found" });
8757
+ sendJson$1(res, 404, { error: "not found" });
8709
8758
  } catch (err) {
8710
- sendJson(res, 500, { error: err?.message ?? String(err) });
8759
+ sendJson$1(res, 500, { error: err?.message ?? String(err) });
8711
8760
  }
8712
8761
  }
8713
8762
  /**
@@ -8719,12 +8768,12 @@ async function handle(ctx, defaultRoot, req, res) {
8719
8768
  async function handleAskParsed(ctx, ccc, body, res, ip) {
8720
8769
  const key = typeof body.key === "string" ? body.key : void 0;
8721
8770
  if (isPublicAskIpLocked(ip)) {
8722
- sendJson(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
8771
+ sendJson$1(res, 429, { error: "too many failed attempts — temporarily locked (retry later)" });
8723
8772
  return;
8724
8773
  }
8725
8774
  if (!verifyPublicAskKey(key)) {
8726
8775
  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" });
8776
+ 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
8777
  return;
8729
8778
  }
8730
8779
  resetPublicAskIpFail(ip);
@@ -8733,7 +8782,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8733
8782
  const question = typeof body.question === "string" ? body.question : "";
8734
8783
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
8735
8784
  if (!question.trim()) {
8736
- sendJson(res, 400, { error: "empty question" });
8785
+ sendJson$1(res, 400, { error: "empty question" });
8737
8786
  return;
8738
8787
  }
8739
8788
  const roles = readSkiffRoles(root);
@@ -8741,7 +8790,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8741
8790
  const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
8742
8791
  const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
8743
8792
  if (!effectiveRole || !effectiveCfg) {
8744
- sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
8793
+ sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
8745
8794
  return;
8746
8795
  }
8747
8796
  let agent;
@@ -8759,7 +8808,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8759
8808
  agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
8760
8809
  }
8761
8810
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
8762
- sendJson(res, 200, {
8811
+ sendJson$1(res, 200, {
8763
8812
  answer: result.answer,
8764
8813
  answer_html: renderSkiffMarkdown(result.answer, true),
8765
8814
  sessionId: result.sessionId,
@@ -9140,7 +9189,8 @@ function buildOutgoingHookEvent(input) {
9140
9189
  userId: input.userId,
9141
9190
  sessionId: input.sessionId,
9142
9191
  role: input.role,
9143
- reply: input.reply
9192
+ reply: input.reply,
9193
+ ...input.source === "proactive" ? { source: "proactive" } : {}
9144
9194
  };
9145
9195
  }
9146
9196
  /**
@@ -9255,6 +9305,7 @@ function invokeWeixinHook(root, hookRel, event) {
9255
9305
  var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
9256
9306
  handleIncoming: () => handleIncoming,
9257
9307
  registerWeixinBridge: () => registerWeixinBridge,
9308
+ sendProactiveText: () => sendProactiveText,
9258
9309
  stopAllBridges: () => stopAllBridges,
9259
9310
  stopCccBridge: () => stopCccBridge,
9260
9311
  syncCccBridge: () => syncCccBridge,
@@ -9384,7 +9435,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9384
9435
  const existing = getSkiffAgent(sessionId);
9385
9436
  const hc = readHandymanConfig(root);
9386
9437
  if (existing) try {
9387
- ensureSkiffSession(root, existing, roleName, role);
9438
+ ensureSkiffSession(root, existing, roleName, role, true);
9388
9439
  } catch (err) {
9389
9440
  console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
9390
9441
  }
@@ -9535,11 +9586,93 @@ function stopCccBridge(root) {
9535
9586
  for (const loop of bridge.loops.values()) loop.stopped = true;
9536
9587
  bridges.delete(root);
9537
9588
  }
9538
- /** 停止全部桥(插件 dispose) */
9589
+ /**
9590
+ * 停止全部桥(插件 dispose)
9591
+ */
9539
9592
  function stopAllBridges() {
9540
9593
  for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
9541
9594
  bridges.clear();
9542
9595
  }
9596
+ /**
9597
+ * 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
9598
+ *
9599
+ * 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
9600
+ * ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
9601
+ * 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
9602
+ * CCC 侧不必重复实现 sendmessage;③ 多账号可选。
9603
+ *
9604
+ * 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
9605
+ * 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
9606
+ *
9607
+ * 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
9608
+ * @param input CCC 根 + 目标用户 + 文本 +(可选)账号
9609
+ * @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
9610
+ */
9611
+ async function sendProactiveText(input) {
9612
+ const { root, toUserId } = input;
9613
+ const settings = readWeixinSettings(root);
9614
+ if (!settings.enabled) return {
9615
+ ok: false,
9616
+ code: "BRIDGE_DISABLED",
9617
+ error: `微信桥未启用(ccc=${root})`,
9618
+ remediation: "在 CCC 的 .opencode/serenity.json weixin.enabled 打开"
9619
+ };
9620
+ const accounts = (settings.accounts ?? []).filter((a) => a.enabled !== false);
9621
+ if (accounts.length === 0) return {
9622
+ ok: false,
9623
+ code: "NO_ACCOUNT",
9624
+ error: "微信桥未配置任何启用账号",
9625
+ remediation: "在设置面板「微信桥」扫码绑定账号"
9626
+ };
9627
+ const accountId = input.accountId ?? accounts[0].accountId;
9628
+ if (!accounts.some((a) => a.accountId === accountId)) return {
9629
+ ok: false,
9630
+ code: "ACCOUNT_NOT_FOUND",
9631
+ error: `账号不存在或未启用: ${accountId}`,
9632
+ remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
9633
+ };
9634
+ const cred = readWeixinCredential(root, accountId);
9635
+ if (!cred) return {
9636
+ ok: false,
9637
+ code: "ACCOUNT_NOT_BOUND",
9638
+ error: `账号未绑定(无凭据): ${accountId}`,
9639
+ remediation: "在设置面板重新扫码绑定"
9640
+ };
9641
+ const sessionId = weixinSessionIdFor(toUserId);
9642
+ const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
9643
+ try {
9644
+ await sendTextMessage({
9645
+ baseUrl: cred.baseUrl,
9646
+ token: cred.token,
9647
+ toUserId,
9648
+ text: input.text
9649
+ });
9650
+ } catch (err) {
9651
+ return {
9652
+ ok: false,
9653
+ code: "SEND_FAILED",
9654
+ error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
9655
+ };
9656
+ }
9657
+ if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
9658
+ cccRoot: root,
9659
+ accountId,
9660
+ userId: toUserId,
9661
+ sessionId,
9662
+ role,
9663
+ reply: markdownToPlainText(input.text),
9664
+ source: "proactive"
9665
+ })).catch((err) => {
9666
+ console.log(`[serenity-hooks] weixin hook outgoing(proactive) error: ${err instanceof Error ? err.message : String(err)}`);
9667
+ });
9668
+ return {
9669
+ ok: true,
9670
+ accountId,
9671
+ userId: toUserId,
9672
+ sessionId,
9673
+ role
9674
+ };
9675
+ }
9543
9676
  /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
9544
9677
  function weixinBridgeStatus() {
9545
9678
  const out = [];
@@ -9571,6 +9704,363 @@ function registerWeixinBridge(ctx) {
9571
9704
  } catch {}
9572
9705
  }
9573
9706
  //#endregion
9707
+ //#region src/weixin-send-api.ts
9708
+ /**
9709
+ * weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
9710
+ *
9711
+ * 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
9712
+ * 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
9713
+ * 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
9714
+ * 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
9715
+ *
9716
+ * 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
9717
+ * 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
9718
+ *
9719
+ * 契约:
9720
+ * POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
9721
+ * → 200 { ok: true, accountId, userId, sessionId, role }
9722
+ * → 4xx/5xx { ok: false, code, error, remediation? }
9723
+ * GET /health → 200 { ok: true, port }
9724
+ *
9725
+ * 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
9726
+ * 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
9727
+ */
9728
+ /** 请求体上限(文本消息远小于此;防滥用) */
9729
+ const MAX_BODY_BYTES = 65536;
9730
+ /** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
9731
+ const WEIXIN_SEND_MAX_TEXT = 4e3;
9732
+ /** 运行中的入口(null = 未启动) */
9733
+ let active = null;
9734
+ /** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
9735
+ /** 测试辅助:读取当前监听端口(生产零调用) */
9736
+ function weixinSendPort() {
9737
+ return active?.port ?? null;
9738
+ }
9739
+ /** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
9740
+ function isLoopbackAddress(address) {
9741
+ if (typeof address !== "string" || address === "") return false;
9742
+ const a = address.startsWith("::ffff:") ? address.slice(7) : address;
9743
+ return a === "127.0.0.1" || a === "::1";
9744
+ }
9745
+ /**
9746
+ * 校验并归一 /send 请求体(纯函数,可测)。
9747
+ * 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
9748
+ */
9749
+ function parseSendRequest(body) {
9750
+ if (body === null || typeof body !== "object") return {
9751
+ ok: false,
9752
+ error: "body must be a JSON object"
9753
+ };
9754
+ const o = body;
9755
+ const ccc = typeof o.ccc === "string" ? o.ccc.trim() : "";
9756
+ if (ccc === "") return {
9757
+ ok: false,
9758
+ error: "missing \"ccc\"",
9759
+ remediation: "ccc = CCC 名称或绝对路径(必填,无默认值)"
9760
+ };
9761
+ const user = typeof o.user === "string" ? o.user.trim() : "";
9762
+ if (user === "") return {
9763
+ ok: false,
9764
+ error: "missing \"user\"",
9765
+ remediation: "user = iLink from_user_id(形如 xxx@im.wechat);别名解析归调用方"
9766
+ };
9767
+ if (!user.includes("@")) return {
9768
+ ok: false,
9769
+ error: `invalid "user": ${user}`,
9770
+ remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
9771
+ };
9772
+ const text = typeof o.text === "string" ? o.text.trim() : "";
9773
+ if (text === "") return {
9774
+ ok: false,
9775
+ error: "missing \"text\"",
9776
+ remediation: "text = 要发送的文本(非空)"
9777
+ };
9778
+ if (text.length > 4e3) return {
9779
+ ok: false,
9780
+ error: `text too long: ${text.length} > ${WEIXIN_SEND_MAX_TEXT}`
9781
+ };
9782
+ const accountId = typeof o.accountId === "string" && o.accountId.trim() !== "" ? o.accountId.trim() : void 0;
9783
+ return {
9784
+ ok: true,
9785
+ ccc,
9786
+ user,
9787
+ text,
9788
+ ...accountId ? { accountId } : {}
9789
+ };
9790
+ }
9791
+ /**
9792
+ * 按名称/路径解析 CCC 根(纯函数,可测)。
9793
+ * 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
9794
+ * 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
9795
+ */
9796
+ function matchCcc(input, candidates) {
9797
+ const trimmed = input.trim();
9798
+ if (trimmed === "") return {
9799
+ ok: false,
9800
+ error: "empty ccc"
9801
+ };
9802
+ if (trimmed.startsWith("/")) {
9803
+ const root = findSerenityRoot(trimmed);
9804
+ if (!root) return {
9805
+ ok: false,
9806
+ error: `no CCC found from path: ${trimmed}`,
9807
+ remediation: "确认该路径在某个 CCC 内(含 .serenity 标记)"
9808
+ };
9809
+ return {
9810
+ ok: true,
9811
+ root
9812
+ };
9813
+ }
9814
+ const lower = trimmed.toLowerCase();
9815
+ const exactPath = candidates.find((c) => c.root.toLowerCase() === lower);
9816
+ if (exactPath) return {
9817
+ ok: true,
9818
+ root: exactPath.root
9819
+ };
9820
+ const byName = candidates.filter((c) => c.dirName.toLowerCase() === lower || c.cccName !== null && c.cccName.toLowerCase() === lower);
9821
+ if (byName.length === 1) return {
9822
+ ok: true,
9823
+ root: byName[0].root
9824
+ };
9825
+ if (byName.length > 1) return {
9826
+ ok: false,
9827
+ error: `ambiguous ccc name: ${trimmed}`,
9828
+ remediation: `同名多个,请用绝对路径:${byName.map((c) => c.root).join(" | ")}`
9829
+ };
9830
+ return {
9831
+ ok: false,
9832
+ error: `unknown ccc: ${trimmed}`,
9833
+ remediation: candidates.length === 0 ? "当前进程未发现任何 CCC(打开一个 CCC 会话后重试)" : `已知 CCC: ${candidates.map((c) => c.dirName).join(", ")}`
9834
+ };
9835
+ }
9836
+ /** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
9837
+ async function collectCandidates(ctx) {
9838
+ const { discoverCccs } = await import("./skiff-debug-TONJlpgF.js").then((n) => n.i);
9839
+ const entries = await discoverCccs(ctx, process.cwd());
9840
+ const seen = /* @__PURE__ */ new Set();
9841
+ const out = [];
9842
+ for (const e of entries) {
9843
+ if (!e?.root || seen.has(e.root)) continue;
9844
+ seen.add(e.root);
9845
+ out.push({
9846
+ root: e.root,
9847
+ dirName: e.name || basename(e.root),
9848
+ cccName: readCccName$2(e.root)
9849
+ });
9850
+ }
9851
+ return out;
9852
+ }
9853
+ function sendJson(res, status, payload) {
9854
+ const body = JSON.stringify(payload);
9855
+ res.writeHead(status, {
9856
+ "content-type": "application/json; charset=utf-8",
9857
+ "content-length": Buffer.byteLength(body)
9858
+ });
9859
+ res.end(body);
9860
+ }
9861
+ async function readBody(req) {
9862
+ const chunks = [];
9863
+ let total = 0;
9864
+ for await (const chunk of req) {
9865
+ const buf = chunk;
9866
+ total += buf.length;
9867
+ if (total > MAX_BODY_BYTES) throw new Error(`body too large (> ${MAX_BODY_BYTES} bytes)`);
9868
+ chunks.push(buf);
9869
+ }
9870
+ return Buffer.concat(chunks).toString("utf-8");
9871
+ }
9872
+ /** HTTP 状态映射(失败 code → 语义化状态;调用方按 code 判定,不看文案) */
9873
+ function statusForCode(code) {
9874
+ switch (code) {
9875
+ case "ACCOUNT_NOT_BOUND":
9876
+ case "BRIDGE_DISABLED":
9877
+ case "NO_ACCOUNT": return 409;
9878
+ case "ACCOUNT_NOT_FOUND": return 400;
9879
+ case "SEND_FAILED": return 502;
9880
+ default: return 400;
9881
+ }
9882
+ }
9883
+ /** 单请求处理(导出供测试直接驱动,不经真实端口) */
9884
+ async function handleWeixinSendRequest(ctx, req, res) {
9885
+ if (!isLoopbackAddress(req.socket?.remoteAddress)) {
9886
+ sendJson(res, 403, {
9887
+ ok: false,
9888
+ code: "FORBIDDEN",
9889
+ error: "only loopback clients allowed"
9890
+ });
9891
+ return;
9892
+ }
9893
+ const path = (req.url ?? "/").split("?")[0];
9894
+ if (req.method === "GET" && path === "/health") {
9895
+ sendJson(res, 200, {
9896
+ ok: true,
9897
+ port: weixinSendPort()
9898
+ });
9899
+ return;
9900
+ }
9901
+ if (path !== "/send") {
9902
+ sendJson(res, 404, {
9903
+ ok: false,
9904
+ code: "NOT_FOUND",
9905
+ error: `unknown path: ${path}`
9906
+ });
9907
+ return;
9908
+ }
9909
+ if (req.method !== "POST") {
9910
+ sendJson(res, 405, {
9911
+ ok: false,
9912
+ code: "METHOD_NOT_ALLOWED",
9913
+ error: "use POST /send"
9914
+ });
9915
+ return;
9916
+ }
9917
+ let raw;
9918
+ try {
9919
+ raw = await readBody(req);
9920
+ } catch (err) {
9921
+ sendJson(res, 413, {
9922
+ ok: false,
9923
+ code: "BODY_TOO_LARGE",
9924
+ error: String(err?.message ?? err)
9925
+ });
9926
+ return;
9927
+ }
9928
+ let body;
9929
+ try {
9930
+ body = JSON.parse(raw === "" ? "{}" : raw);
9931
+ } catch (err) {
9932
+ sendJson(res, 400, {
9933
+ ok: false,
9934
+ code: "BAD_JSON",
9935
+ error: `invalid JSON: ${String(err?.message ?? err)}`
9936
+ });
9937
+ return;
9938
+ }
9939
+ const parsed = parseSendRequest(body);
9940
+ if (!parsed.ok) {
9941
+ sendJson(res, 400, {
9942
+ ok: false,
9943
+ code: "BAD_REQUEST",
9944
+ error: parsed.error,
9945
+ remediation: parsed.remediation
9946
+ });
9947
+ return;
9948
+ }
9949
+ let root;
9950
+ try {
9951
+ const matched = matchCcc(parsed.ccc, await collectCandidates(ctx));
9952
+ if (!matched.ok) {
9953
+ sendJson(res, 404, {
9954
+ ok: false,
9955
+ code: "UNKNOWN_CCC",
9956
+ error: matched.error,
9957
+ remediation: matched.remediation
9958
+ });
9959
+ return;
9960
+ }
9961
+ root = matched.root;
9962
+ } catch (err) {
9963
+ sendJson(res, 500, {
9964
+ ok: false,
9965
+ code: "CCC_DISCOVERY_FAILED",
9966
+ error: String(err?.message ?? err)
9967
+ });
9968
+ return;
9969
+ }
9970
+ const result = await sendProactiveText({
9971
+ root,
9972
+ toUserId: parsed.user,
9973
+ text: parsed.text,
9974
+ ...parsed.accountId ? { accountId: parsed.accountId } : {}
9975
+ });
9976
+ if (result.ok) {
9977
+ sendJson(res, 200, {
9978
+ ok: true,
9979
+ accountId: result.accountId,
9980
+ userId: result.userId,
9981
+ sessionId: result.sessionId,
9982
+ role: result.role
9983
+ });
9984
+ return;
9985
+ }
9986
+ sendJson(res, statusForCode(result.code), {
9987
+ ok: false,
9988
+ code: result.code,
9989
+ error: result.error,
9990
+ remediation: result.remediation
9991
+ });
9992
+ }
9993
+ /** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
9994
+ async function startWeixinSendApi(ctx, port) {
9995
+ if (active) return active.port;
9996
+ const server = createServer((req, res) => {
9997
+ handleWeixinSendRequest(ctx, req, res).catch((err) => {
9998
+ console.warn(`[serenity-hooks] ✗ weixin send api 处理失败: ${String(err?.message ?? err)}`);
9999
+ try {
10000
+ sendJson(res, 500, {
10001
+ ok: false,
10002
+ code: "INTERNAL",
10003
+ error: "internal error"
10004
+ });
10005
+ } catch {}
10006
+ });
10007
+ });
10008
+ await new Promise((resolve, reject) => {
10009
+ server.once("error", reject);
10010
+ server.listen(port, "127.0.0.1", () => resolve());
10011
+ });
10012
+ server.unref();
10013
+ const addr = server.address();
10014
+ const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
10015
+ active = {
10016
+ server,
10017
+ port: actualPort
10018
+ };
10019
+ setWeixinSendEndpoint(`http://127.0.0.1:${actualPort}`);
10020
+ console.log(`[serenity-hooks] ✓ 微信主动发送入口: http://127.0.0.1:${actualPort}/send(仅 loopback;CCC MSM 调用)`);
10021
+ return actualPort;
10022
+ }
10023
+ /** 停止监听器(幂等) */
10024
+ function stopWeixinSendApi() {
10025
+ if (!active) return;
10026
+ try {
10027
+ active.server.close();
10028
+ } catch {}
10029
+ active = null;
10030
+ setWeixinSendEndpoint(null);
10031
+ }
10032
+ /**
10033
+ * 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
10034
+ * 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
10035
+ */
10036
+ function registerWeixinSendApi(ctx) {
10037
+ const sync = () => {
10038
+ let cfg;
10039
+ try {
10040
+ cfg = readAdvancedSettings().weixinApi;
10041
+ } catch (err) {
10042
+ console.warn(`[serenity-hooks] ✗ weixin send api 配置读取失败,跳过同步: ${String(err?.message ?? err)}`);
10043
+ return;
10044
+ }
10045
+ const want = cfg.enabled && cfg.port > 0;
10046
+ if (want && !active) {
10047
+ startWeixinSendApi(ctx, cfg.port).catch((err) => {
10048
+ console.warn(`[serenity-hooks] ✗ 微信主动发送入口启动失败(port=${cfg.port}): ${String(err?.message ?? err)}`);
10049
+ });
10050
+ return;
10051
+ }
10052
+ if (!want && active) stopWeixinSendApi();
10053
+ };
10054
+ try {
10055
+ ctx.on("serenity/config-updated", sync);
10056
+ } catch {}
10057
+ try {
10058
+ ctx.on("serenity/settings-changed", sync);
10059
+ } catch {}
10060
+ sync();
10061
+ registerDisposer(ctx, "weixin send api", () => stopWeixinSendApi());
10062
+ }
10063
+ //#endregion
9574
10064
  //#region src/seams/lifecycle.ts
9575
10065
  /** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
9576
10066
  function sessionIdOf(value) {
@@ -9703,6 +10193,7 @@ function apply(ctx, config) {
9703
10193
  registerAcp(ctx);
9704
10194
  registerAutopilot(ctx);
9705
10195
  registerWeixinBridge(ctx);
10196
+ registerWeixinSendApi(ctx);
9706
10197
  registerLifecycle(ctx);
9707
10198
  }
9708
10199
  /**