@shgroup/dsh-serenity-hooks 1.30.5 → 1.30.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
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-CX48YNUL.js";
3
- import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-LW4tjF9L.js";
4
- import { C as readProgress, D as writeProgress, E as writeFailedStatus, O as skiffRoleFor, S as newStopToken, T as splitModel, _ as workspaceTrajectoryLine, a as startSkiffDebugServer, b as handymanProgressPaths, c as askSkiff, d as getSkiffAgent, f as skiffMsmGate, g as unregisterSkiffSession, h as skiffTrajectoryEnabled, 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 HANDYMAN_GUIDE, w as requireWhitelistedModel, x as listActiveHandymen, y as buildRoundPrompt } from "./skiff-debug-gYigSgl_.js";
5
- 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-jA0qENOf.js";
6
- import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as extractSessionMdPathFromText, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as findLatestActiveSessionMd, m as parseSessionContextFromEvents, n as readLastBound, o as closeSession, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as createSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-p9oR9dj9.js";
7
- import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BfVxgCZy.js";
8
- 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";
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
+ 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-TONJlpgF.js";
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";
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
+ 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-BMpljodH.js";
9
10
  import z from "@deepseek-ai/schemastery";
10
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
11
12
  import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
@@ -836,6 +837,306 @@ function setSafeMode(root, on) {
836
837
  return { on: isSafeModeOn(root) };
837
838
  }
838
839
  //#endregion
840
+ //#region src/host/contract.ts
841
+ /** dsp 依赖的宿主服务与成员(对照 AI_LAB/dsh-harness-public @ 0.1.2-rc.1 逐一核对) */
842
+ const HOST_SERVICES = [
843
+ {
844
+ id: "tools",
845
+ name: "tools",
846
+ access: "injected",
847
+ members: [{
848
+ name: "register",
849
+ kind: "function"
850
+ }, {
851
+ name: "guard",
852
+ kind: "function"
853
+ }],
854
+ impact: "10 个 ACC 工具无法注册 / 机械守卫失效",
855
+ required: true
856
+ },
857
+ {
858
+ id: "sessions",
859
+ name: "sessions",
860
+ access: "injected",
861
+ members: [
862
+ {
863
+ name: "list",
864
+ kind: "function"
865
+ },
866
+ {
867
+ name: "get",
868
+ kind: "function"
869
+ },
870
+ {
871
+ name: "create",
872
+ kind: "function"
873
+ }
874
+ ],
875
+ impact: "会话定位/rebuild/CCC 解析失效",
876
+ required: true
877
+ },
878
+ {
879
+ id: "agents",
880
+ name: "agents",
881
+ access: "injected",
882
+ members: [{
883
+ name: "create",
884
+ kind: "function"
885
+ }, {
886
+ name: "get",
887
+ kind: "function"
888
+ }],
889
+ impact: "skiff/handyman 无法创建或复用 agent",
890
+ required: true
891
+ },
892
+ {
893
+ id: "webServer",
894
+ name: "webServer",
895
+ access: "injected",
896
+ members: [{
897
+ name: "register",
898
+ kind: "function"
899
+ }, {
900
+ name: "port",
901
+ kind: "value"
902
+ }],
903
+ impact: "状态接口/网关端口发现失效",
904
+ required: true
905
+ },
906
+ {
907
+ id: "settings",
908
+ name: "settings",
909
+ access: "injected",
910
+ members: [{
911
+ name: "installSection",
912
+ kind: "function"
913
+ }],
914
+ impact: "设置面板不安装 → 所有开关静默 no-op",
915
+ required: true
916
+ },
917
+ {
918
+ id: "systemPrompt",
919
+ name: "systemPrompt",
920
+ access: "injected",
921
+ members: [],
922
+ impact: "Induction(入口 skill 段)注入失效",
923
+ required: true
924
+ },
925
+ {
926
+ id: "sessionProjections",
927
+ name: "sessionProjections",
928
+ access: "injected",
929
+ members: [{
930
+ name: "snapshot",
931
+ kind: "function"
932
+ }],
933
+ impact: "上下文压力读数失效 → rebuild 提醒不再触发",
934
+ required: false
935
+ },
936
+ {
937
+ id: "skills",
938
+ name: "skills",
939
+ access: "injected",
940
+ members: [],
941
+ impact: "opencode skill 兼容层(.opencode/skills 注册)失效",
942
+ required: false
943
+ },
944
+ {
945
+ id: "shellEnv",
946
+ name: "shellEnv",
947
+ access: "injected",
948
+ members: [],
949
+ impact: "DSH_SERENITY_* 环境事实注入失效",
950
+ required: false
951
+ },
952
+ {
953
+ id: "agentLoop",
954
+ name: "agentLoop",
955
+ access: "injected",
956
+ members: [],
957
+ impact: "handyman worker agent 无法装配 preset",
958
+ required: false
959
+ },
960
+ {
961
+ id: "sessionTitle",
962
+ name: "sessionTitle",
963
+ access: "lazy",
964
+ members: [{
965
+ name: "rename",
966
+ kind: "function"
967
+ }],
968
+ impact: "会话命名 / rebuild 后重命名跳过(仅告警)",
969
+ required: false
970
+ },
971
+ {
972
+ id: "tokenMeter",
973
+ name: "tokenMeter",
974
+ access: "lazy",
975
+ members: [{
976
+ name: "estimateMessage",
977
+ kind: "function"
978
+ }],
979
+ impact: "rebuild shadow-price 定价失效(计量不准)",
980
+ required: false
981
+ },
982
+ {
983
+ id: "workspaceRegistry",
984
+ name: "workspaceRegistry",
985
+ access: "lazy",
986
+ members: [{
987
+ name: "list",
988
+ kind: "function"
989
+ }],
990
+ impact: "工作区白名单下拉为空(外部访问配置受限)",
991
+ required: false
992
+ },
993
+ {
994
+ id: "sessionPersistence",
995
+ name: "sessionPersistence",
996
+ access: "lazy",
997
+ members: [{
998
+ name: "list",
999
+ kind: "function"
1000
+ }],
1001
+ impact: "CCC 自动发现回落失效(诊断面受限)",
1002
+ required: false
1003
+ },
1004
+ {
1005
+ id: "connection",
1006
+ name: "connection",
1007
+ access: "lazy",
1008
+ members: [{
1009
+ name: "authenticatedUrl",
1010
+ kind: "function"
1011
+ }, {
1012
+ name: "authorizeIndex",
1013
+ kind: "function"
1014
+ }],
1015
+ impact: "双端口网关无法换取 dsh cookie → 外部反代 401",
1016
+ required: false
1017
+ }
1018
+ ];
1019
+ /** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
1020
+ const REQUIRED_HOST_RANGE = "^0.1.2-rc.1";
1021
+ /** 最小 semver 比较(无依赖):返回 -1 / 0 / 1;解析失败返回 null */
1022
+ function compareSemver(a, b) {
1023
+ const parse = (v) => {
1024
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/.exec(v.trim());
1025
+ if (!m) return null;
1026
+ return [
1027
+ Number(m[1]),
1028
+ Number(m[2]),
1029
+ Number(m[3]),
1030
+ m[4] ?? ""
1031
+ ];
1032
+ };
1033
+ const pa = parse(a);
1034
+ const pb = parse(b);
1035
+ if (!pa || !pb) return null;
1036
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
1037
+ const ra = pa[3];
1038
+ const rb = pb[3];
1039
+ if (ra === rb) return 0;
1040
+ if (ra === "") return 1;
1041
+ if (rb === "") return -1;
1042
+ return ra < rb ? -1 : 1;
1043
+ }
1044
+ /**
1045
+ * 宿主版本是否落在 dsp 被验证的范围(`^0.1.2-rc.1`)。
1046
+ * 无版本信息 → 视为未知(ok=false,detail 说明),不猜测。
1047
+ */
1048
+ function checkHostVersion(version) {
1049
+ if (version === null) return {
1050
+ ok: false,
1051
+ detail: `host version unknown (required ${REQUIRED_HOST_RANGE})`
1052
+ };
1053
+ const floor = compareSemver(version, "0.1.2-rc.1");
1054
+ const ceiling = compareSemver(version, "0.2.0");
1055
+ if (floor === null || ceiling === null) return {
1056
+ ok: false,
1057
+ detail: `host version "${version}" not parseable (required ${REQUIRED_HOST_RANGE})`
1058
+ };
1059
+ if (floor < 0) return {
1060
+ ok: false,
1061
+ detail: `host ${version} is below the verified floor (required ${REQUIRED_HOST_RANGE})`
1062
+ };
1063
+ if (ceiling >= 0) return {
1064
+ ok: false,
1065
+ detail: `host ${version} is outside the verified range (required ${REQUIRED_HOST_RANGE})`
1066
+ };
1067
+ return {
1068
+ ok: true,
1069
+ detail: `host ${version} within ${REQUIRED_HOST_RANGE}`
1070
+ };
1071
+ }
1072
+ /** 结构化读取宿主服务(lazy → ctx.get;injected → 直接属性,属性缺失再试 ctx.get) */
1073
+ function readService(ctx, name, access) {
1074
+ return access === "lazy" ? hostService(ctx, name) : hostInjected(ctx, name);
1075
+ }
1076
+ /**
1077
+ * 探针:核对运行中的宿主是否仍满足 dsp 依赖的服务与成员。
1078
+ *
1079
+ * @param ctx 宿主插件上下文(真实 cordis Context;测试可传结构化替身)
1080
+ * @param hostVersion 运行中宿主版本(`readDshVersion()` 结果;null = 未知)
1081
+ * @returns 报告(`ok:false` 时 `issues` 逐条给出缺失项与后果)
1082
+ */
1083
+ function probeHostContract(ctx, hostVersion = null) {
1084
+ const issues = [];
1085
+ let checked = 0;
1086
+ for (const svc of HOST_SERVICES) {
1087
+ checked += 1;
1088
+ let value;
1089
+ try {
1090
+ value = readService(ctx, svc.name, svc.access);
1091
+ } catch {
1092
+ value = void 0;
1093
+ }
1094
+ if (value === void 0 || value === null) {
1095
+ issues.push({
1096
+ id: svc.id,
1097
+ kind: "service",
1098
+ detail: `service "${svc.name}" not available (${svc.access})`,
1099
+ impact: svc.impact,
1100
+ required: svc.required
1101
+ });
1102
+ continue;
1103
+ }
1104
+ for (const member of svc.members) {
1105
+ checked += 1;
1106
+ const actual = value[member.name];
1107
+ if (!(member.kind === "function" ? typeof actual === "function" : actual !== void 0)) issues.push({
1108
+ id: `${svc.id}.${member.name}`,
1109
+ kind: "member",
1110
+ detail: `service "${svc.name}" is missing ${member.kind} "${member.name}"`,
1111
+ impact: svc.impact,
1112
+ required: svc.required
1113
+ });
1114
+ }
1115
+ }
1116
+ const version = checkHostVersion(hostVersion);
1117
+ if (!version.ok) issues.push({
1118
+ id: "host.version",
1119
+ kind: "version",
1120
+ detail: version.detail,
1121
+ impact: "契约未经该宿主版本验证(漂移风险)",
1122
+ required: false
1123
+ });
1124
+ return {
1125
+ ok: issues.every((i) => !i.required),
1126
+ checked,
1127
+ hostVersion,
1128
+ versionOk: version.ok,
1129
+ issues
1130
+ };
1131
+ }
1132
+ /** 单行摘要(启动告警用) */
1133
+ function summarizeHostContract(report) {
1134
+ if (report.ok && report.issues.length === 0) return `host contract ok (${report.checked} checks, host ${report.hostVersion ?? "unknown"})`;
1135
+ const required = report.issues.filter((i) => i.required);
1136
+ const optional = report.issues.filter((i) => !i.required);
1137
+ return `${required.length > 0 ? `host contract BROKEN (${required.length} required)` : `host contract degraded (${optional.length} optional)`}: ${report.issues.map((i) => i.detail).join("; ")}`;
1138
+ }
1139
+ //#endregion
839
1140
  //#region src/kit-ops.ts
840
1141
  /**
841
1142
  * kit-ops.ts — dashboard 纯操作层(acc_kit → dashboard,v1.30;零 DSH 依赖)
@@ -933,7 +1234,7 @@ function checkRegistryHealth(root) {
933
1234
  issues
934
1235
  };
935
1236
  }
936
- async function runKit(root, args) {
1237
+ async function runKit(root, args, hostCtx) {
937
1238
  switch (args.action) {
938
1239
  case "health": {
939
1240
  const cccName = readCccName$1(root);
@@ -973,6 +1274,11 @@ async function runKit(root, args) {
973
1274
  if (root) report.config = loadSerenityConfig(root);
974
1275
  report.accVersion = ACC_VERSION;
975
1276
  report.dshVersion = readDshVersion();
1277
+ if (hostCtx !== void 0) {
1278
+ const contract = probeHostContract(hostCtx, report.dshVersion);
1279
+ report.hostContract = contract;
1280
+ if (!contract.ok) report.status = "degraded";
1281
+ }
976
1282
  return report;
977
1283
  }
978
1284
  case "time": {
@@ -1006,29 +1312,35 @@ function renderText$11(value) {
1006
1312
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
1007
1313
  }];
1008
1314
  }
1009
- const kitTool = defineTool({
1010
- name: "dashboard",
1011
- description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity report, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
1012
- parameters: {
1013
- action: {
1014
- type: "string",
1015
- enum: [...KIT_ACTIONS],
1016
- required: true,
1017
- description: "Subcommand"
1315
+ /**
1316
+ * dashboard 工具(v1.30.7:闭包捕获 ctx → health 可附宿主契约探针 F-05)。
1317
+ * ctx 仅用于**只读探针**(`ctx.get` / 属性读取),无副作用。
1318
+ */
1319
+ function createKitTool(ctx) {
1320
+ return defineTool({
1321
+ name: "dashboard",
1322
+ description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity + host-contract probe, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
1323
+ parameters: {
1324
+ action: {
1325
+ type: "string",
1326
+ enum: [...KIT_ACTIONS],
1327
+ required: true,
1328
+ description: "Subcommand"
1329
+ },
1330
+ seconds: {
1331
+ type: "integer",
1332
+ description: "Seconds to wait (positive integer, default 1)"
1333
+ }
1018
1334
  },
1019
- seconds: {
1020
- type: "integer",
1021
- description: "Seconds to wait (positive integer, default 1)"
1335
+ output: {
1336
+ schema: { type: "json" },
1337
+ render: (args, value) => renderText$11(value)
1338
+ },
1339
+ async execute(args, exec) {
1340
+ return await runKit(findSerenityRoot(agentCwd$8(exec)), args, ctx);
1022
1341
  }
1023
- },
1024
- output: {
1025
- schema: { type: "json" },
1026
- render: (args, value) => renderText$11(value)
1027
- },
1028
- async execute(args, exec) {
1029
- return await runKit(findSerenityRoot(agentCwd$8(exec)), args);
1030
- }
1031
- });
1342
+ });
1343
+ }
1032
1344
  //#endregion
1033
1345
  //#region src/git-ops.ts
1034
1346
  /**
@@ -1265,6 +1577,28 @@ const gitTool = defineTool({
1265
1577
  }
1266
1578
  });
1267
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
1268
1602
  //#region src/msm-ops.ts
1269
1603
  /**
1270
1604
  * msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
@@ -1313,14 +1647,21 @@ function assertPathInsideRoot(root, value, flagName) {
1313
1647
  if (e instanceof Error && e.message.includes("symlink")) throw e;
1314
1648
  }
1315
1649
  }
1316
- /** 业务子进程 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
+ */
1317
1655
  function buildMsmEnv(root) {
1318
- return {
1656
+ const env = {
1319
1657
  ...process.env,
1320
1658
  SERENITY_ROOT: root,
1321
1659
  SERENITY_CCC: readCccName(root) ?? "",
1322
1660
  SERENITY_VERSION: ACC_VERSION
1323
1661
  };
1662
+ const weixinApi = weixinSendEndpoint();
1663
+ if (weixinApi) env.SERENITY_WEIXIN_API = weixinApi;
1664
+ return env;
1324
1665
  }
1325
1666
  /**
1326
1667
  * Windows 兼容(审计观察点 A):`.cmd` 不能直接被 CreateProcess 解析——
@@ -1489,7 +1830,11 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1489
1830
  账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
1490
1831
  (扫码绑定后自动写入,永不进 git 明文面)。
1491
1832
  路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
1492
- 面板(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 上传并自行补记。
1493
1838
 
1494
1839
  Config:
1495
1840
  { "weixin": {
@@ -1505,7 +1850,10 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1505
1850
 
1506
1851
  触发(双向):
1507
1852
  incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
1508
- 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": "可选" }
1509
1857
 
1510
1858
  脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
1511
1859
  #!/usr/bin/env bun 或 node script.js —— 读 process.stdin 整段 JSON.parse
@@ -1519,7 +1867,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1519
1867
  "message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
1520
1868
  { "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
1521
1869
  "userId": ..., "sessionId": ..., "role": ...,
1522
- "reply": "已记录(纯文本)" }
1870
+ "reply": "已记录(纯文本)", "source": "reply" | "proactive" }
1523
1871
 
1524
1872
  示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
1525
1873
  const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
@@ -2332,7 +2680,7 @@ function handymanPresetInheritance(parentCtx) {
2332
2680
  *
2333
2681
  * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
2334
2682
  * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
2335
- * 未完成继续下一轮;followup/waitIdle 抛错(非正常停止)→ dispose 并重新 create agent
2683
+ * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
2336
2684
  * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
2337
2685
  *
2338
2686
  * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
@@ -2347,21 +2695,6 @@ function renderText$4(value) {
2347
2695
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
2348
2696
  }];
2349
2697
  }
2350
- /** 等待 agent 空闲(agent/status → idle);无超时(handyman 可永续,agent 工作多久等多久) */
2351
- function waitIdle(ctx, agent) {
2352
- return new Promise((resolve) => {
2353
- let settled = false;
2354
- const finish = () => {
2355
- if (settled) return;
2356
- settled = true;
2357
- dispose();
2358
- resolve();
2359
- };
2360
- const dispose = ctx.on("agent/status", (payload) => {
2361
- if (payload.agent === agent && payload.status === "idle") finish();
2362
- });
2363
- });
2364
- }
2365
2698
  /** 读取会话最后一个 assistant/message 文本 */
2366
2699
  function lastAssistantText$1(agent) {
2367
2700
  const s = agent.session;
@@ -2461,7 +2794,7 @@ async function runHandymanJob(ctx, opts) {
2461
2794
  plugin: "dsh-serenity-hooks"
2462
2795
  }
2463
2796
  }));
2464
- await waitIdle(ctx, workerAgent);
2797
+ await waitAgentIdle(ctx, workerAgent);
2465
2798
  lastResponse = lastAssistantText$1(workerAgent);
2466
2799
  } catch {
2467
2800
  restarts++;
@@ -2727,7 +3060,7 @@ function renameDshSessionOnUse(deps, session, titles, active, summary) {
2727
3060
  */
2728
3061
  function renameDshSessionForActive(ctx, exec, info, summary) {
2729
3062
  try {
2730
- const titles = ctx.get?.("sessionTitle");
3063
+ const titles = hostService(ctx, "sessionTitle");
2731
3064
  const dshSession = exec.agent?.session;
2732
3065
  if (!dshSession) {
2733
3066
  console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session(info: ${info.sessionId})`);
@@ -3328,6 +3661,40 @@ const containerAdminTool = defineTool({
3328
3661
  }
3329
3662
  });
3330
3663
  //#endregion
3664
+ //#region src/host/effect.ts
3665
+ /** 已告警的标签(进程级去重:重复装配/多测试替身不刷屏,但绝不静默) */
3666
+ const warnedLabels = /* @__PURE__ */ new Set();
3667
+ /**
3668
+ * 登记一个卸载时执行的清理。
3669
+ * @param ctx 宿主插件上下文
3670
+ * @param label 日志标签(定位是哪个资源未装配)
3671
+ * @param cleanup 卸载时执行;抛错被吞掉(不阻断其它清理)
3672
+ * @returns 是否成功装配(false = 宿主无 `ctx.effect`,已告警)
3673
+ */
3674
+ function registerDisposer(ctx, label, cleanup) {
3675
+ const effect = ctx?.effect;
3676
+ if (typeof effect !== "function") {
3677
+ if (!warnedLabels.has(label)) {
3678
+ warnedLabels.add(label);
3679
+ console.warn(`[serenity-hooks] ✗ 资源拆卸未装配(${label}):ctx.effect 不可用(宿主契约缺失)`);
3680
+ }
3681
+ return false;
3682
+ }
3683
+ try {
3684
+ effect.call(ctx, () => () => {
3685
+ try {
3686
+ cleanup();
3687
+ } catch (err) {
3688
+ console.warn(`[serenity-hooks] ✗ 资源清理失败(${label}): ${String(err?.message ?? err)}`);
3689
+ }
3690
+ }, `dsh-serenity-hooks: ${label}`);
3691
+ return true;
3692
+ } catch (err) {
3693
+ console.warn(`[serenity-hooks] ✗ 资源拆卸注册失败(${label}): ${String(err?.message ?? err)}`);
3694
+ return false;
3695
+ }
3696
+ }
3697
+ //#endregion
3331
3698
  //#region src/autopilot-trajectory.ts
3332
3699
  var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
3333
3700
  AUDIT_HISTORY_MAX: () => 50,
@@ -3678,6 +4045,12 @@ function registerAutopilot(ctx) {
3678
4045
  try {
3679
4046
  ctx.on("serenity/settings-changed", () => startTimer());
3680
4047
  } catch {}
4048
+ registerDisposer(ctx, "autopilot 时钟", () => {
4049
+ if (timer) {
4050
+ clearInterval(timer);
4051
+ timer = null;
4052
+ }
4053
+ });
3681
4054
  }
3682
4055
  /**
3683
4056
  * 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
@@ -3697,8 +4070,8 @@ function resolveTargetAgent(ctx, mdPath) {
3697
4070
  const idMatch = dirName.match(/--S(\d{3,})--/);
3698
4071
  const sid = idMatch ? `S${idMatch[1]}` : null;
3699
4072
  const targetRoot = findSerenityRoot(mdPath);
3700
- const sessions = ctx.sessions;
3701
- const agents = ctx.agents;
4073
+ const sessions = hostSessions(ctx);
4074
+ const agents = hostAgents(ctx);
3702
4075
  const candidates = [];
3703
4076
  try {
3704
4077
  for (const s of sessions?.list?.() ?? []) {
@@ -3764,7 +4137,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
3764
4137
  const sameCccTitles = [];
3765
4138
  const agentMissing = { matched: false };
3766
4139
  try {
3767
- const sessions = ctx.sessions;
4140
+ const sessions = hostSessions(ctx);
3768
4141
  for (const s of sessions?.list?.() ?? []) {
3769
4142
  const sess = s;
3770
4143
  const cwd = sess?.header?.cwd ?? "";
@@ -3777,7 +4150,7 @@ function diagnoseTargetUnavailable(ctx, mdPath) {
3777
4150
  const boundMatched = bound !== null && bound.dirName === dirName;
3778
4151
  const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
3779
4152
  if (boundMatched || titleMatched) {
3780
- if (!ctx.agents?.get?.(sess.id ?? "")) agentMissing.matched = true;
4153
+ if (!hostAgents(ctx)?.get?.(sess.id ?? "")) agentMissing.matched = true;
3781
4154
  }
3782
4155
  }
3783
4156
  } catch {}
@@ -3790,7 +4163,7 @@ function resolveAutopilotRoot(ctx) {
3790
4163
  const fromCwd = findSerenityRoot(process.cwd());
3791
4164
  if (fromCwd) return fromCwd;
3792
4165
  try {
3793
- const sessions = ctx.sessions;
4166
+ const sessions = hostSessions(ctx);
3794
4167
  for (const s of sessions?.list?.() ?? []) {
3795
4168
  const r = findSerenityRoot(s?.header?.cwd ?? "");
3796
4169
  if (r) return r;
@@ -3843,7 +4216,7 @@ function getAutopilotStatus(root) {
3843
4216
  function listLiveSessions(ctx) {
3844
4217
  const out = [];
3845
4218
  try {
3846
- const sessions = ctx.sessions;
4219
+ const sessions = hostSessions(ctx);
3847
4220
  for (const s of sessions?.list?.() ?? []) {
3848
4221
  const cwd = s?.header?.cwd ?? null;
3849
4222
  out.push({
@@ -4457,7 +4830,7 @@ function rebuildReminderText(tokensK, thresholdK, escalated = false) {
4457
4830
  /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
4458
4831
  function readContextPressure(ctx, session) {
4459
4832
  try {
4460
- const projections = ctx.get?.("sessionProjections");
4833
+ const projections = hostService(ctx, "sessionProjections");
4461
4834
  if (!projections) return null;
4462
4835
  const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
4463
4836
  if (!pressure || typeof pressure.projectedTokens !== "number") return null;
@@ -4711,6 +5084,10 @@ function defaultAdvancedSettings() {
4711
5084
  publicAsk: {
4712
5085
  key: "",
4713
5086
  allowed: []
5087
+ },
5088
+ weixinApi: {
5089
+ enabled: true,
5090
+ port: 3082
4714
5091
  }
4715
5092
  };
4716
5093
  }
@@ -4761,6 +5138,7 @@ function mergeWithDefaults(raw) {
4761
5138
  const gateway = o.gateway ?? {};
4762
5139
  const persona = o.persona ?? {};
4763
5140
  const publicAsk = o.publicAsk ?? {};
5141
+ const weixinApi = o.weixinApi ?? {};
4764
5142
  return {
4765
5143
  gateway: {
4766
5144
  enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
@@ -4779,6 +5157,10 @@ function mergeWithDefaults(raw) {
4779
5157
  publicAsk: {
4780
5158
  key: typeof publicAsk.key === "string" ? publicAsk.key : def.publicAsk.key,
4781
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
4782
5164
  }
4783
5165
  };
4784
5166
  }
@@ -4816,7 +5198,11 @@ function updateAdvancedSettings(patch) {
4816
5198
  publicAsk: patch.publicAsk !== void 0 ? {
4817
5199
  key: typeof patch.publicAsk.key === "string" ? patch.publicAsk.key : current.publicAsk.key,
4818
5200
  allowed: Array.isArray(patch.publicAsk.allowed) ? patch.publicAsk.allowed.filter((n) => typeof n === "string" && n !== "") : current.publicAsk.allowed
4819
- } : 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
4820
5206
  };
4821
5207
  writeAdvancedSettings(next);
4822
5208
  return next;
@@ -5838,7 +6224,7 @@ function saveFileToTmp(root, fileName, data) {
5838
6224
  writeFileSync(join(dir, filename), bytes);
5839
6225
  return `${FILE_UPLOAD_DIR}/${filename}`;
5840
6226
  }
5841
- function readBody$2(req, maxBytes) {
6227
+ function readBody$3(req, maxBytes) {
5842
6228
  return new Promise((resolve, reject) => {
5843
6229
  let data = "";
5844
6230
  req.on("data", (chunk) => {
@@ -5852,7 +6238,7 @@ function readBody$2(req, maxBytes) {
5852
6238
  req.on("error", reject);
5853
6239
  });
5854
6240
  }
5855
- function sendJson$1(res, code, body) {
6241
+ function sendJson$2(res, code, body) {
5856
6242
  const payload = JSON.stringify(body);
5857
6243
  res.writeHead(code, {
5858
6244
  "content-type": "application/json; charset=utf-8",
@@ -5874,10 +6260,10 @@ function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
5874
6260
  }
5875
6261
  function resolveWorkspace(ctx, params) {
5876
6262
  let sessionCwd;
5877
- if (params.sessionId) sessionCwd = (ctx.sessions?.get?.(params.sessionId))?.header?.cwd;
6263
+ if (params.sessionId) sessionCwd = (hostSessions(ctx)?.get?.(params.sessionId))?.header?.cwd;
5878
6264
  let listCwds = () => [];
5879
6265
  try {
5880
- const sessions = ctx.sessions;
6266
+ const sessions = hostSessions(ctx);
5881
6267
  if (typeof sessions?.list === "function") listCwds = () => sessions.list().map((s) => s.header?.cwd ?? "").filter(Boolean);
5882
6268
  } catch {}
5883
6269
  return resolveWorkspaceCore(sessionCwd, params.workspace, listCwds, process.cwd());
@@ -5890,7 +6276,7 @@ function registerStatusApi(ctx, opts = {}) {
5890
6276
  handler: async (req, res) => {
5891
6277
  try {
5892
6278
  if (req.method !== "GET") {
5893
- sendJson$1(res, 405, { error: "method not allowed" });
6279
+ sendJson$2(res, 405, { error: "method not allowed" });
5894
6280
  return;
5895
6281
  }
5896
6282
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -5900,12 +6286,12 @@ function registerStatusApi(ctx, opts = {}) {
5900
6286
  });
5901
6287
  const root = findSerenityRoot(workspace);
5902
6288
  if (!root) {
5903
- sendJson$1(res, 200, { handymen: [] });
6289
+ sendJson$2(res, 200, { handymen: [] });
5904
6290
  return;
5905
6291
  }
5906
- sendJson$1(res, 200, { handymen: listActiveHandymen(root) });
6292
+ sendJson$2(res, 200, { handymen: listActiveHandymen(root) });
5907
6293
  } catch (err) {
5908
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6294
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
5909
6295
  }
5910
6296
  }
5911
6297
  });
@@ -5915,14 +6301,14 @@ function registerStatusApi(ctx, opts = {}) {
5915
6301
  handler: async (req, res) => {
5916
6302
  try {
5917
6303
  if (req.method !== "POST") {
5918
- sendJson$1(res, 405, { error: "method not allowed" });
6304
+ sendJson$2(res, 405, { error: "method not allowed" });
5919
6305
  return;
5920
6306
  }
5921
6307
  if (req.headers["x-serenity-ui"] !== "1") {
5922
- sendJson$1(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
6308
+ sendJson$2(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
5923
6309
  return;
5924
6310
  }
5925
- const raw = await readBody$2(req, 20971520);
6311
+ const raw = await readBody$3(req, 20971520);
5926
6312
  const body = JSON.parse(raw);
5927
6313
  const workspace = resolveWorkspace(ctx, {
5928
6314
  sessionId: body.sessionId,
@@ -5930,12 +6316,12 @@ function registerStatusApi(ctx, opts = {}) {
5930
6316
  });
5931
6317
  const root = findSerenityRoot(workspace);
5932
6318
  if (!root) {
5933
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6319
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
5934
6320
  return;
5935
6321
  }
5936
- sendJson$1(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
6322
+ sendJson$2(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
5937
6323
  } catch (err) {
5938
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6324
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
5939
6325
  }
5940
6326
  }
5941
6327
  });
@@ -5945,14 +6331,14 @@ function registerStatusApi(ctx, opts = {}) {
5945
6331
  handler: async (req, res) => {
5946
6332
  try {
5947
6333
  if (req.method !== "POST") {
5948
- sendJson$1(res, 405, { error: "method not allowed" });
6334
+ sendJson$2(res, 405, { error: "method not allowed" });
5949
6335
  return;
5950
6336
  }
5951
6337
  if (req.headers["x-serenity-ui"] !== "1") {
5952
- sendJson$1(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
6338
+ sendJson$2(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
5953
6339
  return;
5954
6340
  }
5955
- const raw = await readBody$2(req, 20971520);
6341
+ const raw = await readBody$3(req, 20971520);
5956
6342
  const body = JSON.parse(raw);
5957
6343
  const workspace = resolveWorkspace(ctx, {
5958
6344
  sessionId: body.sessionId,
@@ -5960,12 +6346,12 @@ function registerStatusApi(ctx, opts = {}) {
5960
6346
  });
5961
6347
  const root = findSerenityRoot(workspace);
5962
6348
  if (!root) {
5963
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6349
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
5964
6350
  return;
5965
6351
  }
5966
- sendJson$1(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
6352
+ sendJson$2(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
5967
6353
  } catch (err) {
5968
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6354
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
5969
6355
  }
5970
6356
  }
5971
6357
  });
@@ -5981,7 +6367,7 @@ function registerStatusApi(ctx, opts = {}) {
5981
6367
  workspace: url.searchParams.get("workspace") ?? void 0
5982
6368
  }), configPaths);
5983
6369
  const runtime = ctx.get("codeRuntime");
5984
- sendJson$1(res, 200, {
6370
+ sendJson$2(res, 200, {
5985
6371
  ...status,
5986
6372
  ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
5987
6373
  language: runtime.language,
@@ -5992,10 +6378,10 @@ function registerStatusApi(ctx, opts = {}) {
5992
6378
  }
5993
6379
  if (req.method === "POST") {
5994
6380
  if (req.headers["x-serenity-ui"] !== "1") {
5995
- sendJson$1(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
6381
+ sendJson$2(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
5996
6382
  return;
5997
6383
  }
5998
- const raw = await readBody$2(req, 65536);
6384
+ const raw = await readBody$3(req, 65536);
5999
6385
  const body = JSON.parse(raw);
6000
6386
  const workspace = resolveWorkspace(ctx, {
6001
6387
  sessionId: body.sessionId,
@@ -6003,19 +6389,19 @@ function registerStatusApi(ctx, opts = {}) {
6003
6389
  });
6004
6390
  const root = findSerenityRoot(workspace);
6005
6391
  if (!root) {
6006
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6392
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6007
6393
  return;
6008
6394
  }
6009
6395
  const result = setSafeMode(root, body.on === true);
6010
- sendJson$1(res, 200, {
6396
+ sendJson$2(res, 200, {
6011
6397
  ...getStatus(workspace, configPaths),
6012
6398
  ...result
6013
6399
  });
6014
6400
  return;
6015
6401
  }
6016
- sendJson$1(res, 405, { error: "method not allowed" });
6402
+ sendJson$2(res, 405, { error: "method not allowed" });
6017
6403
  } catch (err) {
6018
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6404
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6019
6405
  }
6020
6406
  }
6021
6407
  });
@@ -6025,34 +6411,33 @@ function registerStatusApi(ctx, opts = {}) {
6025
6411
  handler: async (req, res) => {
6026
6412
  try {
6027
6413
  if (req.headers["x-serenity-ui"] !== "1") {
6028
- sendJson$1(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6414
+ sendJson$2(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
6029
6415
  return;
6030
6416
  }
6031
6417
  if (req.method === "GET") {
6032
6418
  const settings = readAdvancedSettings();
6033
6419
  let known = [];
6034
6420
  try {
6035
- const registry = ctx.get?.("workspaceRegistry");
6036
- known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
6421
+ known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
6037
6422
  } catch {}
6038
- sendJson$1(res, 200, {
6423
+ sendJson$2(res, 200, {
6039
6424
  config: toWire(settings),
6040
6425
  knownWorkspaces: known
6041
6426
  });
6042
6427
  return;
6043
6428
  }
6044
6429
  if (req.method === "PUT") {
6045
- const raw = await readBody$2(req, 131072);
6430
+ const raw = await readBody$3(req, 131072);
6046
6431
  const saved = applyWirePatch(JSON.parse(raw).config ?? {});
6047
6432
  try {
6048
6433
  ctx.emit?.("serenity/config-updated");
6049
6434
  } catch {}
6050
- sendJson$1(res, 200, { config: toWire(saved) });
6435
+ sendJson$2(res, 200, { config: toWire(saved) });
6051
6436
  return;
6052
6437
  }
6053
- sendJson$1(res, 405, { error: "method not allowed" });
6438
+ sendJson$2(res, 405, { error: "method not allowed" });
6054
6439
  } catch (err) {
6055
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6440
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6056
6441
  }
6057
6442
  }
6058
6443
  });
@@ -6062,7 +6447,7 @@ function registerStatusApi(ctx, opts = {}) {
6062
6447
  handler: async (req, res) => {
6063
6448
  try {
6064
6449
  if (req.method !== "GET") {
6065
- sendJson$1(res, 405, { error: "method not allowed" });
6450
+ sendJson$2(res, 405, { error: "method not allowed" });
6066
6451
  return;
6067
6452
  }
6068
6453
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6071,10 +6456,10 @@ function registerStatusApi(ctx, opts = {}) {
6071
6456
  workspace: url.searchParams.get("workspace") ?? void 0
6072
6457
  });
6073
6458
  const root = findSerenityRoot(workspace) ?? "";
6074
- const { discoverCccs } = await import("./skiff-debug-gYigSgl_.js").then((n) => n.i);
6075
- 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) });
6076
6461
  } catch (err) {
6077
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6462
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6078
6463
  }
6079
6464
  }
6080
6465
  });
@@ -6084,24 +6469,24 @@ function registerStatusApi(ctx, opts = {}) {
6084
6469
  handler: async (req, res) => {
6085
6470
  try {
6086
6471
  if (req.headers["x-serenity-ui"] !== "1") {
6087
- sendJson$1(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6472
+ sendJson$2(res, 403, { error: "仅限 WebUI(key 属敏感凭据)" });
6088
6473
  return;
6089
6474
  }
6090
6475
  if (req.method === "PUT") {
6091
- const raw = await readBody$2(req, 16384);
6476
+ const raw = await readBody$3(req, 16384);
6092
6477
  if (JSON.parse(raw).action !== "rotate") {
6093
- sendJson$1(res, 400, { error: "unsupported action (expected \"rotate\")" });
6478
+ sendJson$2(res, 400, { error: "unsupported action (expected \"rotate\")" });
6094
6479
  return;
6095
6480
  }
6096
- sendJson$1(res, 200, { key: rotatePublicAskKey() });
6481
+ sendJson$2(res, 200, { key: rotatePublicAskKey() });
6097
6482
  return;
6098
6483
  }
6099
6484
  if (req.method !== "GET") {
6100
- sendJson$1(res, 405, { error: "method not allowed" });
6485
+ sendJson$2(res, 405, { error: "method not allowed" });
6101
6486
  return;
6102
6487
  }
6103
6488
  const settings = readAdvancedSettings();
6104
- const { readSimpleSettings } = await import("./settings-section-BfVxgCZy.js").then((n) => n.r);
6489
+ const { readSimpleSettings } = await import("./settings-section-BPk0e1du.js").then((n) => n.r);
6105
6490
  const simple = readSimpleSettings();
6106
6491
  const allowed = settings.publicAsk.allowed;
6107
6492
  const port = simple.acpHttpPort ?? 3100;
@@ -6110,7 +6495,7 @@ function registerStatusApi(ctx, opts = {}) {
6110
6495
  name,
6111
6496
  url: `${base}/c/${encodeURIComponent(name)}`
6112
6497
  }));
6113
- sendJson$1(res, 200, {
6498
+ sendJson$2(res, 200, {
6114
6499
  enabled: simple.publicAskEnabled ?? false,
6115
6500
  port,
6116
6501
  key: ensurePublicAskKey(),
@@ -6119,7 +6504,7 @@ function registerStatusApi(ctx, opts = {}) {
6119
6504
  listUrl: `${base}/`
6120
6505
  });
6121
6506
  } catch (err) {
6122
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6507
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6123
6508
  }
6124
6509
  }
6125
6510
  });
@@ -6130,13 +6515,13 @@ function registerStatusApi(ctx, opts = {}) {
6130
6515
  try {
6131
6516
  if (req.method === "POST") {
6132
6517
  if (req.headers["x-serenity-ui"] !== "1") {
6133
- sendJson$1(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6518
+ sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
6134
6519
  return;
6135
6520
  }
6136
- const raw = await readBody$2(req, 16384);
6521
+ const raw = await readBody$3(req, 16384);
6137
6522
  const body = JSON.parse(raw);
6138
6523
  if (body.action !== "wake") {
6139
- sendJson$1(res, 400, { error: "unsupported action (expected \"wake\")" });
6524
+ sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
6140
6525
  return;
6141
6526
  }
6142
6527
  const workspace = resolveWorkspace(ctx, {
@@ -6146,21 +6531,21 @@ function registerStatusApi(ctx, opts = {}) {
6146
6531
  const root = findSerenityRoot(workspace);
6147
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;
6148
6533
  if (!effectiveRoot) {
6149
- sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6534
+ sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
6150
6535
  return;
6151
6536
  }
6152
6537
  const { performAutopilotWake, readAutopilotSettings } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6153
6538
  const settings = readAutopilotSettings(effectiveRoot);
6154
6539
  if (!settings) {
6155
- sendJson$1(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6540
+ sendJson$2(res, 400, { error: "Autopilot Trajectory 未配置(.opencode/serenity.json 缺段)" });
6156
6541
  return;
6157
6542
  }
6158
6543
  const result = await performAutopilotWake(ctx, effectiveRoot, settings, { force: true });
6159
- sendJson$1(res, result.ok ? 200 : 400, result);
6544
+ sendJson$2(res, result.ok ? 200 : 400, result);
6160
6545
  return;
6161
6546
  }
6162
6547
  if (req.method !== "GET") {
6163
- sendJson$1(res, 405, { error: "method not allowed" });
6548
+ sendJson$2(res, 405, { error: "method not allowed" });
6164
6549
  return;
6165
6550
  }
6166
6551
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6172,12 +6557,12 @@ function registerStatusApi(ctx, opts = {}) {
6172
6557
  const { getAutopilotStatus, collectAutopilotCccs } = await Promise.resolve().then(() => autopilot_trajectory_exports);
6173
6558
  const effectiveRoot = url.searchParams.get("ccc")?.trim() ?? (!url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? collectAutopilotCccs(ctx)[0] : null) ?? root;
6174
6559
  if (!effectiveRoot) {
6175
- sendJson$1(res, 200, { status: null });
6560
+ sendJson$2(res, 200, { status: null });
6176
6561
  return;
6177
6562
  }
6178
- sendJson$1(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6563
+ sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
6179
6564
  } catch (err) {
6180
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6565
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6181
6566
  }
6182
6567
  }
6183
6568
  });
@@ -6187,28 +6572,28 @@ function registerStatusApi(ctx, opts = {}) {
6187
6572
  handler: async (req, res) => {
6188
6573
  try {
6189
6574
  if (req.headers["x-serenity-ui"] !== "1") {
6190
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6575
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6191
6576
  return;
6192
6577
  }
6193
6578
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
6194
6579
  if (req.method === "GET") {
6195
6580
  const chk = requireCcc(url.searchParams.get("ccc") ?? void 0);
6196
6581
  if (!chk.ok) {
6197
- sendJson$1(res, 400, { error: chk.error });
6582
+ sendJson$2(res, 400, { error: chk.error });
6198
6583
  return;
6199
6584
  }
6200
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6585
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6201
6586
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
6202
6587
  const settings = readWeixinSettings(chk.root);
6203
6588
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
6204
- const { readWeixinCredential } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6589
+ const { readWeixinCredential } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6205
6590
  const accounts = (settings.accounts ?? []).map((a) => ({
6206
6591
  accountId: a.accountId,
6207
6592
  name: a.name ?? void 0,
6208
6593
  enabled: a.enabled !== false,
6209
6594
  bound: readWeixinCredential(chk.root, a.accountId) !== null
6210
6595
  }));
6211
- sendJson$1(res, 200, {
6596
+ sendJson$2(res, 200, {
6212
6597
  enabled: settings.enabled === true,
6213
6598
  botType: settings.botType ?? void 0,
6214
6599
  accounts,
@@ -6218,22 +6603,22 @@ function registerStatusApi(ctx, opts = {}) {
6218
6603
  return;
6219
6604
  }
6220
6605
  if (req.method !== "POST") {
6221
- sendJson$1(res, 405, { error: "method not allowed" });
6606
+ sendJson$2(res, 405, { error: "method not allowed" });
6222
6607
  return;
6223
6608
  }
6224
- const raw = await readBody$2(req, 131072);
6609
+ const raw = await readBody$3(req, 131072);
6225
6610
  const body = JSON.parse(raw);
6226
6611
  const chk = requireCcc(body.ccc);
6227
6612
  if (!chk.ok) {
6228
- sendJson$1(res, 400, { error: chk.error });
6613
+ sendJson$2(res, 400, { error: chk.error });
6229
6614
  return;
6230
6615
  }
6231
6616
  const root = chk.root;
6232
6617
  if (body.action === "login-start") {
6233
- 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);
6234
6619
  const { randomUUID } = await import("node:crypto");
6235
6620
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6236
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6621
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6237
6622
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
6238
6623
  const loginKey = randomUUID();
6239
6624
  weixinLogins.set(loginKey, {
@@ -6242,7 +6627,7 @@ function registerStatusApi(ctx, opts = {}) {
6242
6627
  startedAt: Date.now(),
6243
6628
  polling: false
6244
6629
  });
6245
- sendJson$1(res, 200, {
6630
+ sendJson$2(res, 200, {
6246
6631
  qrcode: qr.qrcode,
6247
6632
  qrcode_img_content: qr.qrcode_img_content,
6248
6633
  loginKey
@@ -6251,40 +6636,40 @@ function registerStatusApi(ctx, opts = {}) {
6251
6636
  }
6252
6637
  if (body.action === "remove-account") {
6253
6638
  if (!body.accountId) {
6254
- sendJson$1(res, 400, { error: "missing accountId" });
6639
+ sendJson$2(res, 400, { error: "missing accountId" });
6255
6640
  return;
6256
6641
  }
6257
- const { removeWeixinAccount } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6642
+ const { removeWeixinAccount } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6258
6643
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6259
6644
  removeWeixinAccount(root, body.accountId);
6260
6645
  syncCccBridge(ctx, root);
6261
- sendJson$1(res, 200, { removed: body.accountId });
6646
+ sendJson$2(res, 200, { removed: body.accountId });
6262
6647
  return;
6263
6648
  }
6264
6649
  if (body.action === "save-routes") {
6265
6650
  const routes = body.routes ?? [];
6266
6651
  if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
6267
- sendJson$1(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6652
+ sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6268
6653
  return;
6269
6654
  }
6270
- const { saveWeixinRoutes } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6271
- const { readSkiffRoles } = await import("./skiff-role-LW4tjF9L.js").then((n) => n.c);
6655
+ const { saveWeixinRoutes } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6656
+ const { readSkiffRoles } = await import("./skiff-role-DlrbHPLD.js").then((n) => n.c);
6272
6657
  const roles = readSkiffRoles(root);
6273
6658
  for (const r of routes) if (!roles.has(r.role)) {
6274
- 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)` });
6275
6660
  return;
6276
6661
  }
6277
6662
  saveWeixinRoutes(root, routes);
6278
- sendJson$1(res, 200, { saved: routes.length });
6663
+ sendJson$2(res, 200, { saved: routes.length });
6279
6664
  return;
6280
6665
  }
6281
6666
  if (body.action === "set-enabled") {
6282
6667
  const enabled = body.enabled === true;
6283
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6668
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6284
6669
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6285
6670
  const settings = readWeixinSettings(root);
6286
6671
  if (enabled && (settings.accounts ?? []).length === 0) {
6287
- sendJson$1(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6672
+ sendJson$2(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
6288
6673
  return;
6289
6674
  }
6290
6675
  setWeixinEnabled(root, enabled);
@@ -6293,12 +6678,12 @@ function registerStatusApi(ctx, opts = {}) {
6293
6678
  const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6294
6679
  stopCccBridge(root);
6295
6680
  }
6296
- sendJson$1(res, 200, { enabled });
6681
+ sendJson$2(res, 200, { enabled });
6297
6682
  return;
6298
6683
  }
6299
- sendJson$1(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6684
+ sendJson$2(res, 400, { error: `unsupported action: ${body.action ?? ""}` });
6300
6685
  } catch (err) {
6301
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6686
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6302
6687
  }
6303
6688
  }
6304
6689
  });
@@ -6308,26 +6693,26 @@ function registerStatusApi(ctx, opts = {}) {
6308
6693
  handler: async (req, res) => {
6309
6694
  try {
6310
6695
  if (req.headers["x-serenity-ui"] !== "1") {
6311
- sendJson$1(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6696
+ sendJson$2(res, 403, { error: "微信桥配置仅限 WebUI(client 专用)" });
6312
6697
  return;
6313
6698
  }
6314
6699
  if (req.method !== "GET") {
6315
- sendJson$1(res, 405, { error: "method not allowed" });
6700
+ sendJson$2(res, 405, { error: "method not allowed" });
6316
6701
  return;
6317
6702
  }
6318
6703
  const key = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("key") ?? "";
6319
6704
  const login = weixinLogins.get(key);
6320
6705
  if (!login) {
6321
- sendJson$1(res, 404, { error: "login not found or expired" });
6706
+ sendJson$2(res, 404, { error: "login not found or expired" });
6322
6707
  return;
6323
6708
  }
6324
6709
  if (Date.now() - login.startedAt > WEIXIN_LOGIN_TTL_MS) {
6325
6710
  weixinLogins.delete(key);
6326
- sendJson$1(res, 200, { status: "expired" });
6711
+ sendJson$2(res, 200, { status: "expired" });
6327
6712
  return;
6328
6713
  }
6329
- const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
6330
- const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6714
+ const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6715
+ const { readWeixinSettings } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6331
6716
  const settings = readWeixinSettings(login.root);
6332
6717
  const status = await pollQRStatus({
6333
6718
  baseUrl: void 0,
@@ -6336,13 +6721,13 @@ function registerStatusApi(ctx, opts = {}) {
6336
6721
  switch (status.status) {
6337
6722
  case "confirmed": {
6338
6723
  if (!status.bot_token) {
6339
- sendJson$1(res, 200, {
6724
+ sendJson$2(res, 200, {
6340
6725
  status: "error",
6341
6726
  error: "confirmed but no bot_token"
6342
6727
  });
6343
6728
  return;
6344
6729
  }
6345
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
6730
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BJ4g4ZNq.js").then((n) => n.u);
6346
6731
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6347
6732
  const accountId = nextWeixinAccountId(settings);
6348
6733
  upsertWeixinAccount(login.root, {
@@ -6357,7 +6742,7 @@ function registerStatusApi(ctx, opts = {}) {
6357
6742
  });
6358
6743
  syncCccBridge(ctx, login.root);
6359
6744
  weixinLogins.delete(key);
6360
- sendJson$1(res, 200, {
6745
+ sendJson$2(res, 200, {
6361
6746
  status: "confirmed",
6362
6747
  accountId,
6363
6748
  tokenSaved: true
@@ -6366,14 +6751,14 @@ function registerStatusApi(ctx, opts = {}) {
6366
6751
  }
6367
6752
  case "expired":
6368
6753
  weixinLogins.delete(key);
6369
- sendJson$1(res, 200, { status: "expired" });
6754
+ sendJson$2(res, 200, { status: "expired" });
6370
6755
  return;
6371
6756
  default:
6372
- sendJson$1(res, 200, { status: status.status ?? "wait" });
6757
+ sendJson$2(res, 200, { status: status.status ?? "wait" });
6373
6758
  return;
6374
6759
  }
6375
6760
  } catch (err) {
6376
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6761
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6377
6762
  }
6378
6763
  }
6379
6764
  });
@@ -6395,7 +6780,7 @@ function registerStatusApi(ctx, opts = {}) {
6395
6780
  const root = sessionsRootDir();
6396
6781
  if (req.method === "GET") {
6397
6782
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
6398
- sendJson$1(res, 200, {
6783
+ sendJson$2(res, 200, {
6399
6784
  root,
6400
6785
  olderThanDays: days,
6401
6786
  dryRun: true,
@@ -6409,15 +6794,15 @@ function registerStatusApi(ctx, opts = {}) {
6409
6794
  return;
6410
6795
  }
6411
6796
  if (req.method !== "POST") {
6412
- sendJson$1(res, 405, { error: "method not allowed" });
6797
+ sendJson$2(res, 405, { error: "method not allowed" });
6413
6798
  return;
6414
6799
  }
6415
6800
  if (req.headers["x-serenity-ui"] !== "1") {
6416
- sendJson$1(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6801
+ sendJson$2(res, 403, { error: "会话清理仅限 WebUI(client 专用)" });
6417
6802
  return;
6418
6803
  }
6419
6804
  const { result } = performCleanup(root, cutoffDaysAgo(days), liveIds);
6420
- sendJson$1(res, 200, {
6805
+ sendJson$2(res, 200, {
6421
6806
  root,
6422
6807
  olderThanDays: days,
6423
6808
  dryRun: false,
@@ -6425,7 +6810,7 @@ function registerStatusApi(ctx, opts = {}) {
6425
6810
  errors: result.errors
6426
6811
  });
6427
6812
  } catch (err) {
6428
- sendJson$1(res, 400, { error: err.message ?? String(err) });
6813
+ sendJson$2(res, 400, { error: err.message ?? String(err) });
6429
6814
  }
6430
6815
  }
6431
6816
  });
@@ -6859,6 +7244,38 @@ function workspaceDenyResponse(rpcId) {
6859
7244
  });
6860
7245
  }
6861
7246
  /**
7247
+ * 判定 workspace 创建请求的端点(v1.30.6,review F-03)。
7248
+ *
7249
+ * 宿主 rc.1 的 Remote 端点由 `typertEndpoint({namespace,method})` 派生为
7250
+ * `workspace/create`,因此浏览器实际请求 `POST /api/workspace/create`
7251
+ * (packages/typert/registry/src/service.ts:63-69;client/connection/src/client/rpc.ts:44)。
7252
+ * 旧实现只匹配 `/api/workspace.create` → 该分支从未命中,外部白名单与禁建校验失效。
7253
+ * 保留旧端点以兼容旧宿主。
7254
+ */
7255
+ function isWorkspaceCreatePath(pathname) {
7256
+ return pathname === "/api/workspace/create" || pathname === "/api/workspace.create";
7257
+ }
7258
+ /**
7259
+ * 从 workspace 创建请求体解析 `{ rpcId, path }`(v1.30.6,review F-03)。
7260
+ *
7261
+ * rc.1 wire 信封:`{ type:'client-request', rpcId, method:'workspace/create',
7262
+ * payload:{ args:{ path } } }`(packages/api/gateway/src/index.ts:955 `args: payload.args`);
7263
+ * 旧宿主为 `payload.path`。解析失败返回 rpcId='unknown' + path=undefined。
7264
+ */
7265
+ function parseWorkspaceCreateBody(body) {
7266
+ try {
7267
+ const parsed = JSON.parse(body);
7268
+ const rpcId = typeof parsed.rpcId === "string" ? parsed.rpcId : "unknown";
7269
+ const raw = parsed.payload?.args?.path ?? parsed.payload?.path;
7270
+ return typeof raw === "string" ? {
7271
+ rpcId,
7272
+ path: raw
7273
+ } : { rpcId };
7274
+ } catch {
7275
+ return { rpcId: "unknown" };
7276
+ }
7277
+ }
7278
+ /**
6862
7279
  * 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过)
6863
7280
  */
6864
7281
  function injectPolyfillHtml(html) {
@@ -6978,7 +7395,7 @@ function createDshCookieProvider(connection, authority) {
6978
7395
  //#endregion
6979
7396
  //#region src/gateway.ts
6980
7397
  /** 读取请求体(≤ maxBytes;超限 reject) */
6981
- function readBody$1(req, maxBytes) {
7398
+ function readBody$2(req, maxBytes) {
6982
7399
  return new Promise((resolve, reject) => {
6983
7400
  let data = "";
6984
7401
  req.on("data", (chunk) => {
@@ -7102,14 +7519,10 @@ function startGateway(config, getAccounts, getDshCookie) {
7102
7519
  }
7103
7520
  const session = authed(req);
7104
7521
  if (session !== void 0) {
7105
- if (req.method === "POST" && url.pathname === "/api/workspace.create") {
7106
- if (!allowWorkspaceCreate) {
7107
- readBody$1(req, 131072).then((body) => {
7108
- let rpcId = "unknown";
7109
- try {
7110
- const parsed = JSON.parse(body);
7111
- if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
7112
- } catch {}
7522
+ if (req.method === "POST" && isWorkspaceCreatePath(url.pathname)) {
7523
+ readBody$2(req, 131072).then((body) => {
7524
+ const { rpcId, path } = parseWorkspaceCreateBody(body);
7525
+ if (!allowWorkspaceCreate) {
7113
7526
  res.writeHead(403, { "content-type": "application/json" });
7114
7527
  res.end(JSON.stringify({
7115
7528
  type: "server-response",
@@ -7123,31 +7536,8 @@ function startGateway(config, getAccounts, getDshCookie) {
7123
7536
  }
7124
7537
  }
7125
7538
  }));
7126
- }).catch(() => {
7127
- res.writeHead(400, { "content-type": "application/json" });
7128
- res.end(JSON.stringify({
7129
- type: "server-response",
7130
- rpcId: "unknown",
7131
- result: {
7132
- ok: false,
7133
- error: {
7134
- code: "bad-request",
7135
- message: "body too large",
7136
- details: {}
7137
- }
7138
- }
7139
- }));
7140
- });
7141
- return;
7142
- }
7143
- readBody$1(req, 131072).then((body) => {
7144
- let rpcId = "unknown";
7145
- let path;
7146
- try {
7147
- const parsed = JSON.parse(body);
7148
- if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
7149
- if (typeof parsed.payload?.path === "string") path = parsed.payload.path;
7150
- } catch {}
7539
+ return;
7540
+ }
7151
7541
  if (!workspaceAllowed(allowWorkspaces, path)) {
7152
7542
  res.writeHead(403, { "content-type": "application/json" });
7153
7543
  res.end(workspaceDenyResponse(rpcId));
@@ -7372,7 +7762,7 @@ function buildDshCookieProvider(ctx, mainPort) {
7372
7762
  let diagnosed = false;
7373
7763
  return () => {
7374
7764
  try {
7375
- const connection = ctx.get?.("connection");
7765
+ const connection = hostService(ctx, "connection");
7376
7766
  if (!connection || typeof connection.authenticatedUrl !== "function" || typeof connection.authorizeIndex !== "function") {
7377
7767
  if (!diagnosed) {
7378
7768
  diagnosed = true;
@@ -7404,7 +7794,7 @@ function registerGateway(ctx) {
7404
7794
  let lastSig = null;
7405
7795
  const sync = () => {
7406
7796
  try {
7407
- const webServer = ctx.get?.("webServer");
7797
+ const webServer = hostWebServer(ctx);
7408
7798
  if (!webServer?.port) return;
7409
7799
  const enabled = readSimpleSettings().gatewayEnabled;
7410
7800
  const settings = readAdvancedSettings();
@@ -7455,6 +7845,11 @@ function registerGateway(ctx) {
7455
7845
  ctx.on("serenity/settings-changed", () => {
7456
7846
  sync();
7457
7847
  });
7848
+ registerDisposer(ctx, "gateway 第二监听器", () => {
7849
+ current?.dispose();
7850
+ current = null;
7851
+ lastSig = null;
7852
+ });
7458
7853
  }
7459
7854
  //#endregion
7460
7855
  //#region src/rebuild.ts
@@ -7702,7 +8097,7 @@ function renameAfterRebuild(ctx, agent, pending) {
7702
8097
  dirName,
7703
8098
  mdPath
7704
8099
  }, pending.summary);
7705
- const titles = ctx.get?.("sessionTitle");
8100
+ const titles = hostService(ctx, "sessionTitle");
7706
8101
  if (!titles || typeof titles.rename !== "function") {
7707
8102
  console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
7708
8103
  return;
@@ -7738,7 +8133,7 @@ function registerRebuildTurnHook(ctx) {
7738
8133
  }
7739
8134
  pendingRebuilds.delete(id);
7740
8135
  try {
7741
- const tokenMeter = ctx.get?.("tokenMeter");
8136
+ const tokenMeter = hostService(ctx, "tokenMeter");
7742
8137
  const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
7743
8138
  if (performRebuild(agent.session, pending, meter)) {
7744
8139
  renameAfterRebuild(ctx, agent, pending);
@@ -8096,14 +8491,14 @@ var AcpServer = class {
8096
8491
  sessionId: result.sessionId
8097
8492
  };
8098
8493
  }
8099
- /** session/cancel:{ sessionId } → 中断当前 prompt(DSH interrupt) */
8494
+ /** session/cancel:{ sessionId } → 取消该会话的当前活动(Agent.cancel,宿主 rc.1 契约) */
8100
8495
  cancel(params) {
8101
8496
  const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
8102
8497
  if (!sessionId) return { cancelled: false };
8103
8498
  const agent = getSkiffAgent(sessionId);
8104
8499
  if (!agent) return { cancelled: false };
8105
8500
  try {
8106
- agent.interrupt?.();
8501
+ agent.cancel({ kind: "user" });
8107
8502
  } catch {}
8108
8503
  return { cancelled: true };
8109
8504
  }
@@ -8192,8 +8587,8 @@ function rpcError(id, code, message) {
8192
8587
  * 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
8193
8588
  */
8194
8589
  /** 运行中的 ACP HTTP 服务(单实例;进程级) */
8195
- let active = null;
8196
- function readBody(req) {
8590
+ let active$1 = null;
8591
+ function readBody$1(req) {
8197
8592
  return new Promise((resolve, reject) => {
8198
8593
  const chunks = [];
8199
8594
  req.on("data", (c) => chunks.push(c));
@@ -8201,7 +8596,7 @@ function readBody(req) {
8201
8596
  req.on("error", reject);
8202
8597
  });
8203
8598
  }
8204
- function sendJson(res, status, payload) {
8599
+ function sendJson$1(res, status, payload) {
8205
8600
  const body = jscSafeJsonText(JSON.stringify(payload));
8206
8601
  res.writeHead(status, {
8207
8602
  "Content-Type": "application/json; charset=utf-8",
@@ -8223,7 +8618,7 @@ function sendHtml(res, html) {
8223
8618
  * @param defaultRoot 默认 CCC 根(问答页候选发现兜底;resolveSkiffRoot 传入)
8224
8619
  */
8225
8620
  async function startAcpHttpServer(ctx, port, defaultRoot) {
8226
- if (active) return;
8621
+ if (active$1) return;
8227
8622
  const server = createServer((req, res) => {
8228
8623
  handle(ctx, defaultRoot ?? "", req, res);
8229
8624
  });
@@ -8231,7 +8626,7 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8231
8626
  server.once("error", reject);
8232
8627
  server.listen(port, "127.0.0.1", () => resolve());
8233
8628
  });
8234
- active = {
8629
+ active$1 = {
8235
8630
  server,
8236
8631
  port
8237
8632
  };
@@ -8242,11 +8637,11 @@ async function startAcpHttpServer(ctx, port, defaultRoot) {
8242
8637
  console.log(`[serenity-hooks] ✓ ACP HTTP: http://127.0.0.1:${port}(${faces.join(" + ") || "未开启任何面"};session/new 支持 {ccc, role, sessionId?})`);
8243
8638
  }
8244
8639
  function stopAcpHttpServer() {
8245
- if (!active) return;
8640
+ if (!active$1) return;
8246
8641
  try {
8247
- active.server.close();
8642
+ active$1.server.close();
8248
8643
  } catch {}
8249
- active = null;
8644
+ active$1 = null;
8250
8645
  }
8251
8646
  async function handle(ctx, defaultRoot, req, res) {
8252
8647
  try {
@@ -8255,14 +8650,14 @@ async function handle(ctx, defaultRoot, req, res) {
8255
8650
  const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown";
8256
8651
  if (req.method === "POST" && url === "/") {
8257
8652
  if (!s.acpEnabled) {
8258
- 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)" });
8259
8654
  return;
8260
8655
  }
8261
8656
  let body;
8262
8657
  try {
8263
- body = JSON.parse(await readBody(req));
8658
+ body = JSON.parse(await readBody$1(req));
8264
8659
  } catch {
8265
- sendJson(res, 400, [{
8660
+ sendJson$1(res, 400, [{
8266
8661
  jsonrpc: "2.0",
8267
8662
  id: null,
8268
8663
  error: {
@@ -8272,7 +8667,7 @@ async function handle(ctx, defaultRoot, req, res) {
8272
8667
  }]);
8273
8668
  return;
8274
8669
  }
8275
- sendJson(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8670
+ sendJson$1(res, 200, await dispatchRpc(new AcpServer(ctx), body));
8276
8671
  return;
8277
8672
  }
8278
8673
  if (!s.publicAskEnabled) {
@@ -8281,10 +8676,10 @@ async function handle(ctx, defaultRoot, req, res) {
8281
8676
  return;
8282
8677
  }
8283
8678
  if (req.method === "POST" && (url === "/ask" || url.startsWith("/c/"))) {
8284
- sendJson(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8679
+ sendJson$1(res, 403, { error: "public ask disabled (enable in Serenity settings)" });
8285
8680
  return;
8286
8681
  }
8287
- sendJson(res, 404, { error: "not found" });
8682
+ sendJson$1(res, 404, { error: "not found" });
8288
8683
  return;
8289
8684
  }
8290
8685
  const containerAllowed = (name) => {
@@ -8315,16 +8710,16 @@ async function handle(ctx, defaultRoot, req, res) {
8315
8710
  const name = decodeURIComponent(aMatch[1] ?? "");
8316
8711
  const ccc = (await discoverCccs(ctx, defaultRoot || (findSerenityRoot(process.cwd()) ?? ""))).find((c) => c.name === name);
8317
8712
  if (!ccc || !containerAllowed(name)) {
8318
- 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` });
8319
8714
  return;
8320
8715
  }
8321
8716
  let body;
8322
8717
  try {
8323
- const parsed = JSON.parse(await readBody(req));
8718
+ const parsed = JSON.parse(await readBody$1(req));
8324
8719
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8325
8720
  body = parsed;
8326
8721
  } catch {
8327
- sendJson(res, 400, { error: "invalid JSON body" });
8722
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8328
8723
  return;
8329
8724
  }
8330
8725
  await handleAskParsed(ctx, ccc, body, res, ip);
@@ -8333,11 +8728,11 @@ async function handle(ctx, defaultRoot, req, res) {
8333
8728
  if (req.method === "POST" && url === "/ask") {
8334
8729
  let body;
8335
8730
  try {
8336
- const parsed = JSON.parse(await readBody(req));
8731
+ const parsed = JSON.parse(await readBody$1(req));
8337
8732
  if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
8338
8733
  body = parsed;
8339
8734
  } catch {
8340
- sendJson(res, 400, { error: "invalid JSON body" });
8735
+ sendJson$1(res, 400, { error: "invalid JSON body" });
8341
8736
  return;
8342
8737
  }
8343
8738
  const cccValue = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : void 0;
@@ -8353,15 +8748,15 @@ async function handle(ctx, defaultRoot, req, res) {
8353
8748
  };
8354
8749
  }
8355
8750
  if (!ccc || !containerAllowed(ccc.name)) {
8356
- sendJson(res, 403, { error: "container is not open for public ask" });
8751
+ sendJson$1(res, 403, { error: "container is not open for public ask" });
8357
8752
  return;
8358
8753
  }
8359
8754
  await handleAskParsed(ctx, ccc, body, res, ip);
8360
8755
  return;
8361
8756
  }
8362
- sendJson(res, 404, { error: "not found" });
8757
+ sendJson$1(res, 404, { error: "not found" });
8363
8758
  } catch (err) {
8364
- sendJson(res, 500, { error: err?.message ?? String(err) });
8759
+ sendJson$1(res, 500, { error: err?.message ?? String(err) });
8365
8760
  }
8366
8761
  }
8367
8762
  /**
@@ -8373,12 +8768,12 @@ async function handle(ctx, defaultRoot, req, res) {
8373
8768
  async function handleAskParsed(ctx, ccc, body, res, ip) {
8374
8769
  const key = typeof body.key === "string" ? body.key : void 0;
8375
8770
  if (isPublicAskIpLocked(ip)) {
8376
- 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)" });
8377
8772
  return;
8378
8773
  }
8379
8774
  if (!verifyPublicAskKey(key)) {
8380
8775
  const remaining = recordPublicAskFail(ip);
8381
- 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" });
8382
8777
  return;
8383
8778
  }
8384
8779
  resetPublicAskIpFail(ip);
@@ -8387,7 +8782,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8387
8782
  const question = typeof body.question === "string" ? body.question : "";
8388
8783
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
8389
8784
  if (!question.trim()) {
8390
- sendJson(res, 400, { error: "empty question" });
8785
+ sendJson$1(res, 400, { error: "empty question" });
8391
8786
  return;
8392
8787
  }
8393
8788
  const roles = readSkiffRoles(root);
@@ -8395,7 +8790,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8395
8790
  const effectiveRole = roleName && explicit ? roleName : roles.keys().next().value;
8396
8791
  const effectiveCfg = (roleName && explicit ? explicit : effectiveRole ? roles.get(effectiveRole) : void 0) ?? void 0;
8397
8792
  if (!effectiveRole || !effectiveCfg) {
8398
- sendJson(res, 400, { error: `no skiff role available in ccc: ${root}` });
8793
+ sendJson$1(res, 400, { error: `no skiff role available in ccc: ${root}` });
8399
8794
  return;
8400
8795
  }
8401
8796
  let agent;
@@ -8413,7 +8808,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
8413
8808
  agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, hc?.defaultModel)).agent;
8414
8809
  }
8415
8810
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
8416
- sendJson(res, 200, {
8811
+ sendJson$1(res, 200, {
8417
8812
  answer: result.answer,
8418
8813
  answer_html: renderSkiffMarkdown(result.answer, true),
8419
8814
  sessionId: result.sessionId,
@@ -8794,7 +9189,8 @@ function buildOutgoingHookEvent(input) {
8794
9189
  userId: input.userId,
8795
9190
  sessionId: input.sessionId,
8796
9191
  role: input.role,
8797
- reply: input.reply
9192
+ reply: input.reply,
9193
+ ...input.source === "proactive" ? { source: "proactive" } : {}
8798
9194
  };
8799
9195
  }
8800
9196
  /**
@@ -8909,6 +9305,8 @@ function invokeWeixinHook(root, hookRel, event) {
8909
9305
  var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
8910
9306
  handleIncoming: () => handleIncoming,
8911
9307
  registerWeixinBridge: () => registerWeixinBridge,
9308
+ sendProactiveText: () => sendProactiveText,
9309
+ stopAllBridges: () => stopAllBridges,
8912
9310
  stopCccBridge: () => stopCccBridge,
8913
9311
  syncCccBridge: () => syncCccBridge,
8914
9312
  weixinBridgeStatus: () => weixinBridgeStatus
@@ -9037,7 +9435,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9037
9435
  const existing = getSkiffAgent(sessionId);
9038
9436
  const hc = readHandymanConfig(root);
9039
9437
  if (existing) try {
9040
- ensureSkiffSession(root, existing, roleName, role);
9438
+ ensureSkiffSession(root, existing, roleName, role, true);
9041
9439
  } catch (err) {
9042
9440
  console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
9043
9441
  }
@@ -9188,6 +9586,93 @@ function stopCccBridge(root) {
9188
9586
  for (const loop of bridge.loops.values()) loop.stopped = true;
9189
9587
  bridges.delete(root);
9190
9588
  }
9589
+ /**
9590
+ * 停止全部桥(插件 dispose)
9591
+ */
9592
+ function stopAllBridges() {
9593
+ for (const bridge of bridges.values()) for (const loop of bridge.loops.values()) loop.stopped = true;
9594
+ bridges.clear();
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
+ }
9191
9676
  /** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
9192
9677
  function weixinBridgeStatus() {
9193
9678
  const out = [];
@@ -9208,7 +9693,7 @@ function weixinBridgeStatus() {
9208
9693
  function registerWeixinBridge(ctx) {
9209
9694
  const syncFromLive = () => {
9210
9695
  try {
9211
- const cwds = (ctx.sessions?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
9696
+ const cwds = (hostSessions(ctx)?.list?.() ?? []).map((s) => s.header?.cwd ?? "").filter(Boolean);
9212
9697
  const roots = [...new Set(cwds.map((c) => findSerenityRoot(c))).values()].filter((r) => r !== null);
9213
9698
  for (const root of roots) syncCccBridge(ctx, root);
9214
9699
  } catch {}
@@ -9219,6 +9704,410 @@ function registerWeixinBridge(ctx) {
9219
9704
  } catch {}
9220
9705
  }
9221
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
10064
+ //#region src/seams/lifecycle.ts
10065
+ /** 从 Agent / Session / 事件负载中取会话 id(形状宽容:未知结构返回 null) */
10066
+ function sessionIdOf(value) {
10067
+ const v = value;
10068
+ const id = v?.session?.id ?? v?.id ?? v?.header?.id;
10069
+ return typeof id === "string" && id !== "" ? id : null;
10070
+ }
10071
+ /** 单会话内存态清理(幂等;无对应状态即 no-op) */
10072
+ function cleanupSessionState(sessionId) {
10073
+ try {
10074
+ unregisterSkiffSession(sessionId);
10075
+ } catch {}
10076
+ try {
10077
+ clearActiveSessionInfo(sessionId);
10078
+ } catch {}
10079
+ }
10080
+ /**
10081
+ * 装配生命周期订阅与资源拆卸。
10082
+ * @param ctx 宿主插件上下文
10083
+ */
10084
+ function registerLifecycle(ctx) {
10085
+ try {
10086
+ ctx.on("agent/disposed", (payload) => {
10087
+ const id = sessionIdOf(payload?.agent);
10088
+ if (id) cleanupSessionState(id);
10089
+ });
10090
+ } catch {}
10091
+ try {
10092
+ ctx.on("session/disposed", (session) => {
10093
+ const id = sessionIdOf(session);
10094
+ if (id) cleanupSessionState(id);
10095
+ });
10096
+ } catch {}
10097
+ const disposeAll = () => {
10098
+ try {
10099
+ stopSkiffDebugServer();
10100
+ } catch {}
10101
+ try {
10102
+ stopAcpHttpServer();
10103
+ } catch {}
10104
+ try {
10105
+ stopAllBridges();
10106
+ } catch {}
10107
+ };
10108
+ registerDisposer(ctx, "self-started resources (skiff-debug/acp/weixin)", disposeAll);
10109
+ }
10110
+ //#endregion
9222
10111
  //#region src/index.ts
9223
10112
  const name = "dsh-serenity-hooks";
9224
10113
  /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
@@ -9262,10 +10151,14 @@ const Config = z.object({
9262
10151
  })
9263
10152
  });
9264
10153
  function apply(ctx, config) {
10154
+ try {
10155
+ const report = probeHostContract(ctx, readDshVersion());
10156
+ if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
10157
+ } catch {}
9265
10158
  if (config.tools) {
9266
10159
  ctx.tools.register(ccFsTool);
9267
10160
  ctx.tools.register(createSessionTool(ctx));
9268
- ctx.tools.register(kitTool);
10161
+ ctx.tools.register(createKitTool(ctx));
9269
10162
  ctx.tools.register(gitTool);
9270
10163
  ctx.tools.register(msmTool);
9271
10164
  ctx.tools.register(praxisTool);
@@ -9300,6 +10193,8 @@ function apply(ctx, config) {
9300
10193
  registerAcp(ctx);
9301
10194
  registerAutopilot(ctx);
9302
10195
  registerWeixinBridge(ctx);
10196
+ registerWeixinSendApi(ctx);
10197
+ registerLifecycle(ctx);
9303
10198
  }
9304
10199
  /**
9305
10200
  * F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
@@ -9341,7 +10236,7 @@ function registerSkiff(ctx) {
9341
10236
  function resolveSkiffRoot(ctx) {
9342
10237
  const liveRoots = [];
9343
10238
  try {
9344
- const sessions = ctx.sessions;
10239
+ const sessions = hostSessions(ctx);
9345
10240
  for (const s of sessions?.list?.() ?? []) {
9346
10241
  const cwd = s?.header?.cwd;
9347
10242
  if (typeof cwd === "string") {
@@ -9358,7 +10253,7 @@ function resolveSkiffRoot(ctx) {
9358
10253
  /** 主 WebUI 端口(WebUI 链接;webServer 未装配回退 3080) */
9359
10254
  function readWebPort(ctx) {
9360
10255
  try {
9361
- const ws = ctx.webServer;
10256
+ const ws = hostWebServer(ctx);
9362
10257
  return typeof ws?.port === "number" ? ws.port : 3080;
9363
10258
  } catch {
9364
10259
  return 3080;