@shgroup/dsh-serenity-hooks 1.30.17 → 1.31.0

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
@@ -3,14 +3,14 @@ import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as re
3
3
  import { a as readSkiffRoles, d as systemPromptSource, i as isSkiffSessionId, l as roleToolWhitelist, o as resolveRoleSystemPrompt } from "./skiff-role-BYvNnwv1.js";
4
4
  import { A as skiffRoleFor, C as listActiveHandymen, D as splitModel, E as requireWhitelistedModel, O as writeFailedStatus, S as handymanProgressPaths, T as readProgress, _ as unregisterSkiffSession, a as startSkiffDebugServer, b as HANDYMAN_GUIDE, c as askSkiff, d as ensureWorkspacePromptSection, f as getSkiffAgent, g as skiffTrajectoryEnabled, h as skiffSessionSnapshot, k as writeProgress, l as createSkiffAgent, m as skiffSessionInfo, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffMsmGate, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as workspaceTrajectoryLine, w as newStopToken, x as buildRoundPrompt, y as waitAgentIdle } from "./skiff-debug-C5ulZKCa.js";
5
5
  import { i as hostSessions, n as hostInjected, o as hostWeb, r as hostService, s as hostWebServer, t as hostAgents } from "./access-dU_vG1q8.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-DIaUYipW.js";
6
+ import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-wWk4AIwV.js";
7
7
  import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as createSession, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as parseSessionContextFromEvents, n as readLastBound, o as clearActiveSessionInfo, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-BpTayaIS.js";
8
8
  import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-CP4uMJf_.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
+ import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-PjEhmjlZ.js";
10
10
  import z from "@deepseek-ai/schemastery";
11
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
12
12
  import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
13
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
13
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
14
14
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
15
15
  import { homedir, platform } from "node:os";
16
16
  import { fileURLToPath } from "node:url";
@@ -421,10 +421,10 @@ function runCcFs(root, args) {
421
421
  * 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
422
422
  * 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
423
423
  */
424
- function agentCwd$9(exec) {
424
+ function agentCwd$10(exec) {
425
425
  return exec.agent?.session?.header?.cwd ?? process.cwd();
426
426
  }
427
- function renderText$12(value) {
427
+ function renderText$13(value) {
428
428
  return [{
429
429
  type: "text",
430
430
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -496,10 +496,10 @@ const ccFsTool = defineTool({
496
496
  },
497
497
  output: {
498
498
  schema: { type: "json" },
499
- render: (args, value) => renderText$12(value)
499
+ render: (args, value) => renderText$13(value)
500
500
  },
501
501
  async execute(args, exec) {
502
- const root = findSerenityRoot(agentCwd$9(exec));
502
+ const root = findSerenityRoot(agentCwd$10(exec));
503
503
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
504
504
  return runCcFs(root, args);
505
505
  }
@@ -520,6 +520,208 @@ const ACC_VERSION = (() => {
520
520
  }
521
521
  })();
522
522
  //#endregion
523
+ //#region src/im-bridge.ts
524
+ /**
525
+ * im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
526
+ *
527
+ * 用户原话:「既然微信桥是我们 ACC 提供的,那么 weixin-send 应该是我们 ACC 提供的能力,
528
+ * 当用户配置了微信桥则可用,不配置则不可见」+「设定一个复杂的多层工具,叫 im-bridge,
529
+ * 里面有微信,然后保留后续可能兼容别的 im 的空间」。
530
+ *
531
+ * 分层(E↑):
532
+ * `tools/im-bridge.ts`(工具面:参数 → 结果渲染)
533
+ * → **本模块**(通道注册表 + 可见性判据 + 动作分发 + 错误码翻译)
534
+ * → 通道实现(`im-weixin.ts`:协议/凭据/账号/记录)
535
+ *
536
+ * 边界(R↓):
537
+ * - **本模块零宿主依赖**(不 import cordis/agent)——可见性判定与分发都是纯逻辑,可独立单测。
538
+ * - **通道只提供机制**:本模块不认识"微信";新增 IM 只需注册一个通道实现(R3 家族式扩展)。
539
+ * - **只能操作本会话 CCC**(R4):`root` 由调用方(工具面)从 agent cwd 解析后传入,
540
+ * 本模块不接受"目标 CCC"参数——跨 CCC 发送不在能力面。
541
+ * - **记录归通道**(R5):通道实现负责让每次发送都落既有 hook。
542
+ */
543
+ /** 文本长度上限(微信侧单条实践上限;超限拒绝而非静默截断——与 HTTP 入口同值) */
544
+ const IM_SEND_MAX_TEXT = 4e3;
545
+ /** 文件大小上限(20MB,与媒体接收上限一致) */
546
+ const IM_FILE_MAX_BYTES = 20971520;
547
+ const channels = /* @__PURE__ */ new Map();
548
+ /** 注册通道(index.ts apply 调用;重复 id 覆盖——测试与热重载友好) */
549
+ function registerImChannel(channel) {
550
+ channels.set(channel.id, channel);
551
+ }
552
+ /** 已注册通道 id(工具 `channel` 参数枚举) */
553
+ function imChannelIds() {
554
+ return [...channels.keys()];
555
+ }
556
+ /**
557
+ * 可见性判据:本 CCC 是否启用了**任一** IM 通道。
558
+ * 未启用 → 工具面对该 agent **不可见**(`tools.restrict` 移除,而非调用时报错)。
559
+ * @param root 本会话 CCC 根(由调用方解析)
560
+ */
561
+ function hasEnabledImChannel(root) {
562
+ for (const channel of channels.values()) try {
563
+ if (channel.isEnabled(root)) return true;
564
+ } catch {}
565
+ return false;
566
+ }
567
+ /** 错误码 → 可行动提示(模型视角:说清怎么改) */
568
+ function remediationFor(code) {
569
+ switch (code) {
570
+ case "CHANNEL_UNKNOWN": return "Use a registered channel id (see the tool description)";
571
+ case "CHANNEL_NOT_CONFIGURED": return "This CCC has not configured that channel — enable it in .opencode/serenity.json (weixin.enabled) or the settings panel";
572
+ case "ACTION_UNKNOWN": return "Use send / send-file / users / status";
573
+ case "ACTION_UNSUPPORTED": return "This channel does not support that action";
574
+ case "ALIAS_UNKNOWN": return "Use a configured alias (see action=users) or a raw channel user id";
575
+ case "TEXT_REQUIRED": return "Provide text";
576
+ case "TEXT_TOO_LONG": return `Shorten the text to <= ${IM_SEND_MAX_TEXT} characters`;
577
+ case "USER_REQUIRED": return "Provide user";
578
+ case "FILE_REQUIRED": return "Provide file (a path inside this CCC)";
579
+ case "FILE_ESCAPE": return "Use a path inside this CCC";
580
+ case "FILE_NOT_FOUND": return "Check the path exists inside this CCC";
581
+ case "FILE_TOO_LARGE": return `Use a file <= ${Math.round(IM_FILE_MAX_BYTES / 1048576)} MB`;
582
+ default: return;
583
+ }
584
+ }
585
+ function fail(code, error, remediation) {
586
+ return {
587
+ ok: false,
588
+ code,
589
+ error,
590
+ ...remediation ?? remediationFor(code) ? { remediation: remediation ?? remediationFor(code) } : {}
591
+ };
592
+ }
593
+ /** CCC 内文件读取(路径逃逸/缺失/超限一律拒绝,返回稳定码) */
594
+ function readCccFile(root, relOrAbs) {
595
+ const abs = isAbsolute(relOrAbs) ? resolve(relOrAbs) : resolve(root, relOrAbs);
596
+ const rel = relative(resolve(root), abs);
597
+ if (rel.startsWith("..") || isAbsolute(rel)) return {
598
+ ok: false,
599
+ code: "FILE_ESCAPE",
600
+ error: `file escapes the CCC root: ${relOrAbs}`
601
+ };
602
+ if (!existsSync(abs)) return {
603
+ ok: false,
604
+ code: "FILE_NOT_FOUND",
605
+ error: `file not found: ${relOrAbs}`
606
+ };
607
+ let size = 0;
608
+ try {
609
+ size = statSync(abs).size;
610
+ } catch (err) {
611
+ return {
612
+ ok: false,
613
+ code: "FILE_NOT_FOUND",
614
+ error: `cannot stat file: ${relOrAbs} (${err instanceof Error ? err.message : String(err)})`
615
+ };
616
+ }
617
+ if (size > 20971520) return {
618
+ ok: false,
619
+ code: "FILE_TOO_LARGE",
620
+ error: `file is ${size} bytes (> ${IM_FILE_MAX_BYTES})`
621
+ };
622
+ try {
623
+ return {
624
+ ok: true,
625
+ data: readFileSync(abs),
626
+ fileName: abs.split("/").pop() ?? "file"
627
+ };
628
+ } catch (err) {
629
+ return {
630
+ ok: false,
631
+ code: "FILE_NOT_FOUND",
632
+ error: `cannot read file: ${relOrAbs} (${err instanceof Error ? err.message : String(err)})`
633
+ };
634
+ }
635
+ }
636
+ /**
637
+ * 执行一次 IM 动作(工具面唯一入口)。
638
+ *
639
+ * 顺序:通道解析 → 动作校验 → 用户解析(别名 → id)→ 输入校验 → 调用通道实现。
640
+ * 任何一步失败都返回**稳定错误码 + 可行动提示**(不抛错——工具面统一渲染)。
641
+ *
642
+ * @param root 本会话 CCC 根(调用方从 agent cwd 解析;本函数不接受目标 CCC)
643
+ * @param req 工具参数
644
+ */
645
+ async function runImBridge(root, req) {
646
+ const channelId = (req.channel ?? "").trim();
647
+ const action = (req.action ?? "").trim();
648
+ if (channelId === "") return fail("CHANNEL_REQUIRED", "channel is required", "Provide channel (e.g. \"weixin\")");
649
+ const channel = channels.get(channelId);
650
+ if (!channel) return fail("CHANNEL_UNKNOWN", `unknown channel "${channelId}" (registered: ${imChannelIds().join(", ") || "(none)"})`);
651
+ if (!channel.isEnabled(root)) return fail("CHANNEL_NOT_CONFIGURED", `channel "${channelId}" is not configured for this CCC (${root})`);
652
+ if (action === "users") return {
653
+ ok: true,
654
+ channel: channelId,
655
+ action,
656
+ detail: { users: channel.listUsers(root) }
657
+ };
658
+ if (action === "status") return {
659
+ ok: true,
660
+ channel: channelId,
661
+ action,
662
+ detail: channel.status(root)
663
+ };
664
+ if (action === "send") {
665
+ const rawUser = (req.user ?? "").trim();
666
+ if (rawUser === "") return fail("USER_REQUIRED", "user is required");
667
+ const text = req.text ?? "";
668
+ if (text === "") return fail("TEXT_REQUIRED", "text is required");
669
+ if (text.length > 4e3) return fail("TEXT_TOO_LONG", `text is ${text.length} characters (> ${IM_SEND_MAX_TEXT})`);
670
+ const resolved = channel.resolveUser(root, rawUser);
671
+ if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
672
+ try {
673
+ return {
674
+ ok: true,
675
+ channel: channelId,
676
+ action,
677
+ detail: {
678
+ ...await channel.send({
679
+ root,
680
+ userId: resolved.id,
681
+ text,
682
+ accountId: req.account
683
+ }),
684
+ user: resolved.alias ?? resolved.id
685
+ }
686
+ };
687
+ } catch (err) {
688
+ return fail("SEND_FAILED", err instanceof Error ? err.message : String(err));
689
+ }
690
+ }
691
+ if (action === "send-file") {
692
+ if (!channel.sendFile) return fail("ACTION_UNSUPPORTED", `channel "${channelId}" does not support send-file`);
693
+ const rawUser = (req.user ?? "").trim();
694
+ if (rawUser === "") return fail("USER_REQUIRED", "user is required");
695
+ const fileArg = (req.file ?? "").trim();
696
+ if (fileArg === "") return fail("FILE_REQUIRED", "file is required");
697
+ const resolved = channel.resolveUser(root, rawUser);
698
+ if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
699
+ const file = readCccFile(root, fileArg);
700
+ if (!file.ok) return fail(file.code, file.error);
701
+ try {
702
+ return {
703
+ ok: true,
704
+ channel: channelId,
705
+ action,
706
+ detail: {
707
+ ...await channel.sendFile({
708
+ root,
709
+ userId: resolved.id,
710
+ data: file.data,
711
+ fileName: file.fileName,
712
+ caption: req.caption,
713
+ accountId: req.account
714
+ }),
715
+ user: resolved.alias ?? resolved.id
716
+ }
717
+ };
718
+ } catch (err) {
719
+ return fail("SEND_FAILED", err instanceof Error ? err.message : String(err));
720
+ }
721
+ }
722
+ return fail("ACTION_UNKNOWN", `unknown action "${action}"`);
723
+ }
724
+ //#endregion
523
725
  //#region src/seams/guards.ts
524
726
  /**
525
727
  * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
@@ -712,6 +914,55 @@ function syncSafeModeRestriction(agent, root) {
712
914
  }
713
915
  writeRestrictDiag(root);
714
916
  }
917
+ /** 条件可见的 IM 工具名(与 tools/im-bridge.ts 的注册名一致) */
918
+ const IM_BRIDGE_TOOL_NAME = "im-bridge";
919
+ /** agent key → restrict disposer(im-bridge 隐藏状态;key 同安全模式 = 会话 id) */
920
+ const imBridgeRestrictions = /* @__PURE__ */ new Map();
921
+ /**
922
+ * 同步 im-bridge 可见性(v1.31.0,S142 用户拍板 Q3「未配置则不可见」)。
923
+ *
924
+ * 判据 = 本会话 CCC 是否启用了**任一** IM 通道(`hasEnabledImChannel`,当前只有 weixin):
925
+ * - 未启用 → `agent.ctx.tools.restrict({ deny: ['im-bridge'] })`:**从 schema 移除**
926
+ * (模型看不到,而不是看得到但调用被拒——用户原话"不配置则不可见")
927
+ * - 启用 → 解除隐藏(CCC 配置热更新即时生效)
928
+ *
929
+ * 与 `syncSafeModeRestriction` 同点调用(pre-step 每步 + 会话就绪),失败不阻断
930
+ * (守卫仍兜底;restrict 不可用时最坏是"看得到但通道未配置 → 返回 CHANNEL_NOT_CONFIGURED")。
931
+ */
932
+ function syncImBridgeVisibility(agent, root) {
933
+ const key = agent.session.id ?? "global";
934
+ let enabled = false;
935
+ try {
936
+ enabled = hasEnabledImChannel(root);
937
+ } catch (e) {
938
+ console.error(`[serenity-hooks] im-bridge 可见性判据失败 (key=${key}):`, e.message);
939
+ return;
940
+ }
941
+ const existing = imBridgeRestrictions.get(key);
942
+ if (!enabled && !existing) try {
943
+ const dispose = agent.ctx.tools.restrict({ deny: [IM_BRIDGE_TOOL_NAME] });
944
+ imBridgeRestrictions.set(key, dispose);
945
+ } catch (e) {
946
+ console.error(`[serenity-hooks] im-bridge 隐藏失败 (key=${key}):`, e.message);
947
+ }
948
+ else if (enabled && existing) {
949
+ try {
950
+ existing();
951
+ } catch (e) {
952
+ console.error(`[serenity-hooks] im-bridge 解除隐藏失败 (key=${key}):`, e.message);
953
+ }
954
+ imBridgeRestrictions.delete(key);
955
+ }
956
+ }
957
+ /** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长——review F-08 同族) */
958
+ function forgetImBridgeVisibility(sessionId) {
959
+ const existing = imBridgeRestrictions.get(sessionId);
960
+ if (!existing) return;
961
+ try {
962
+ existing();
963
+ } catch {}
964
+ imBridgeRestrictions.delete(sessionId);
965
+ }
715
966
  /**
716
967
  * 从 exec 参数中提取常见路径字段(write/edit 工具);宽松读取。
717
968
  * container_fs 复合工具:path(单路径)/ paths(数组)/ src / dst——取第一个命中(越界检查
@@ -1316,10 +1567,10 @@ async function runKit(root, args, hostCtx) {
1316
1567
  /**
1317
1568
  * kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
1318
1569
  */
1319
- function agentCwd$8(exec) {
1570
+ function agentCwd$9(exec) {
1320
1571
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1321
1572
  }
1322
- function renderText$11(value) {
1573
+ function renderText$12(value) {
1323
1574
  return [{
1324
1575
  type: "text",
1325
1576
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1347,10 +1598,10 @@ function createKitTool(ctx) {
1347
1598
  },
1348
1599
  output: {
1349
1600
  schema: { type: "json" },
1350
- render: (args, value) => renderText$11(value)
1601
+ render: (args, value) => renderText$12(value)
1351
1602
  },
1352
1603
  async execute(args, exec) {
1353
- return await runKit(findSerenityRoot(agentCwd$8(exec)), args, ctx);
1604
+ return await runKit(findSerenityRoot(agentCwd$9(exec)), args, ctx);
1354
1605
  }
1355
1606
  });
1356
1607
  }
@@ -1539,10 +1790,10 @@ function runGit(root, args) {
1539
1790
  /**
1540
1791
  * git.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
1541
1792
  */
1542
- function agentCwd$7(exec) {
1793
+ function agentCwd$8(exec) {
1543
1794
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1544
1795
  }
1545
- function renderText$10(value) {
1796
+ function renderText$11(value) {
1546
1797
  return [{
1547
1798
  type: "text",
1548
1799
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1581,10 +1832,10 @@ const gitTool = defineTool({
1581
1832
  },
1582
1833
  output: {
1583
1834
  schema: { type: "json" },
1584
- render: (args, value) => renderText$10(value)
1835
+ render: (args, value) => renderText$11(value)
1585
1836
  },
1586
1837
  async execute(args, exec) {
1587
- const root = findSerenityRoot(agentCwd$7(exec));
1838
+ const root = findSerenityRoot(agentCwd$8(exec));
1588
1839
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
1589
1840
  return runGit(root, args);
1590
1841
  }
@@ -1844,26 +2095,33 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1844
2095
  (扫码绑定后自动写入,永不进 git 明文面)。
1845
2096
  路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
1846
2097
  面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
1847
- ▸ 主动发送(v1.30.9):CCC 用 msm("weixin-send", ["send", "--ccc", "<CCC>", "--user", "<别名|id>", "<文本>"])
1848
- 给用户发消息——经 dsp 主动发送入口(**只绑 127.0.0.1**,plugin 全局配置 ~/.dsh/serenity-hooks.json
1849
- 的 weixinApi.port,默认 3082;公网不可达故无密钥)→ weixin-bridge.sendProactiveText →
1850
- 成功后触发 outgoing hook(source="proactive")。send-file 仍由 MSM 直连 iLink 上传并自行补记。
2098
+ ▸ 主动发送(v1.31.0:**ACC 工具 im-bridge**):调
2099
+ im-bridge({channel:"weixin", action:"send", user:"<别名|id>", text:"<文本>"}) 给用户发消息——
2100
+ **工具只能操作本会话 CCC**(无 ccc 参数,防误发他容器)→ weixin-bridge.sendProactiveText →
2101
+ 成功后触发 outgoing hook(source="proactive")。文件用 action:"send-file" + file:"<CCC 内路径>"
2102
+ (逃逸/缺失/超 20MB 拒绝)+ 可选 caption。**可见性**:本 CCC 配置了 IM 通道
2103
+ (weixin.enabled=true)才出现在模型工具清单里;未配置 → 由 guards 逐 agent
2104
+ tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒")。skiff 角色另需在
2105
+ roles.<role>.tools 白名单列出 im-bridge。
2106
+ 历史(v1.30.9~v1.30.x):CCC 侧 MSM weixin-send(走 loopback HTTP 入口 3082)——
2107
+ v1.31.0 已退役(脚本删除 + 注册表注销);3082 入口保留给**非 agent 调用者**。
1851
2108
 
1852
2109
  ▸ weixin.autoReplyWithLastMessage(v1.30.10,关闭桥的自动输出):
1853
2110
  缺省 true = 现状:桥在 agent 一轮结束后把**最终 assistant 文本**自动回发用户(source="reply")。
1854
2111
  显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent——每轮在用户消息**末尾**追加
1855
- **机制标记** [serenity:weixin-manual-output] + 一行已填好参数的 weixin-send 命令
1856
- (CCC 根/账号/用户 id),agent 自己决定发什么、发几条(全部记 source="proactive")。
2112
+ **机制标记** [serenity:weixin-manual-output] + 一行已填好参数的 im-bridge 调用
2113
+ (账号/用户 id),agent 自己决定发什么、发几条(全部记 source="proactive")。
1857
2114
  **ACC 只给机制与数据,纪律措辞归 CCC**(v1.30.16):怎么写/必须怎么做/后果是什么写在
1858
2115
  CCC 角色提示词文件里(如 .opencode/skiff/<role>.md),可热改(v1.30.15 起角色提示词热重载)。
1859
- 机械闸门(v1.30.16):本轮 turn 结束仍未成功调用 weixin-send → agent 被打回(≤2 次,
1860
- 提示 = 标记 + 事实);达上限放弃并响亮告警。失败调用不算已送达。
2116
+ 机械闸门(v1.30.16):本轮 turn 结束仍未成功调用 im-bridge(send / send-file;兼容旧
2117
+ msm("weixin-send") 形态)→ agent 被打回(≤2 次,提示 = 标记 + 事实);达上限放弃并响亮告警。
2118
+ 失败调用不算已送达。
1861
2119
  语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
1862
2120
  typing 指示与 incoming hook 不受影响。
1863
2121
 
1864
2122
  ▸ weixin.fallbackOnNoSend(v1.30.17,手动模式的**兜底**;缺省 false):
1865
2123
  仅在 autoReplyWithLastMessage=false 时生效。true → 一轮结束时若 agent **一次都没成功**
1866
- 调用 weixin-send(闸门打回用尽),桥把该轮最终文本转发给用户(记录 source="reply-fallback",
2124
+ 调用输出工具(闸门打回用尽),桥把该轮最终文本转发给用户(记录 source="reply-fallback",
1867
2125
  日志留响亮告警)——保证"不丢消息"。agent 自己发过 → 不兜底(输出权仍在 agent)。
1868
2126
  为什么需要(实证):v1.30.16 闸门会打回 ≤2 次,但实测某模型在**寒暄类消息**上连续 4 轮、
1869
2127
  跨 3 版 CCC 提示词仍不调工具 → 打回用尽后用户什么都收不到;提示词与闸门均已排除。
@@ -1890,7 +2148,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1890
2148
  incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
1891
2149
  outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1892
2150
  · source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
1893
- (CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText);
2151
+ (CCC 经 im-bridge 工具(v1.31.0;旧 weixin-send MSM 已退役)→ sendProactiveText);
1894
2152
  source="reply-fallback"(v1.30.17)= 手动模式下 agent 本轮未发送 → 桥兜底转发最终文本
1895
2153
  · file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
1896
2154
 
@@ -2327,13 +2585,13 @@ async function runMsmAsync(root, args) {
2327
2585
  * 管理面(register/deregister/check/guide/catalog/ccc-config)已移入 container_admin。
2328
2586
  * 底层 runMsmAsync/prepareExec 全复用;未命中候选为新增 findSuggestions。
2329
2587
  */
2330
- function agentCwd$6(exec) {
2588
+ function agentCwd$7(exec) {
2331
2589
  return exec.agent?.session?.header?.cwd ?? process.cwd();
2332
2590
  }
2333
2591
  function agentSessionId$1(exec) {
2334
2592
  return exec.agent?.session?.id ?? "";
2335
2593
  }
2336
- function renderText$9(value) {
2594
+ function renderText$10(value) {
2337
2595
  return [{
2338
2596
  type: "text",
2339
2597
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2383,10 +2641,10 @@ const msmTool = defineTool({
2383
2641
  },
2384
2642
  output: {
2385
2643
  schema: { type: "json" },
2386
- render: (args, value) => renderText$9(value)
2644
+ render: (args, value) => renderText$10(value)
2387
2645
  },
2388
2646
  async execute(args, exec) {
2389
- const root = findSerenityRoot(agentCwd$6(exec));
2647
+ const root = findSerenityRoot(agentCwd$7(exec));
2390
2648
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
2391
2649
  const sessionId = agentSessionId$1(exec);
2392
2650
  const name = args.name;
@@ -2444,7 +2702,7 @@ CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design doc
2444
2702
 
2445
2703
  ## Reference
2446
2704
  https://github.com/tellmewhattodo/theory-eap`;
2447
- function renderText$8(value) {
2705
+ function renderText$9(value) {
2448
2706
  return [{
2449
2707
  type: "text",
2450
2708
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2464,7 +2722,7 @@ defineTool({
2464
2722
  } },
2465
2723
  output: {
2466
2724
  schema: { type: "string" },
2467
- render: (_args, value) => renderText$8(value)
2725
+ render: (_args, value) => renderText$9(value)
2468
2726
  },
2469
2727
  async execute(args) {
2470
2728
  if (args.section === "variables") return EAP_CONTENT.split("## Pre-Output Self-Check Checklist")[0];
@@ -2505,7 +2763,7 @@ Requirements → Scope → Solution → Interface → Implementation
2505
2763
  2. Wait for confirmation or correction (small step)
2506
2764
  3. After confirmation, record the decision (into session SESSION.md or a design doc)
2507
2765
  4. Move to the next decision point`;
2508
- function renderText$7(value) {
2766
+ function renderText$8(value) {
2509
2767
  return [{
2510
2768
  type: "text",
2511
2769
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2521,7 +2779,7 @@ defineTool({
2521
2779
  } },
2522
2780
  output: {
2523
2781
  schema: { type: "string" },
2524
- render: (_args, value) => renderText$7(value)
2782
+ render: (_args, value) => renderText$8(value)
2525
2783
  },
2526
2784
  async execute(args) {
2527
2785
  if (args.section === "rules") return NEAT_CONTENT.match(/## Four Iron Rules[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
@@ -2584,7 +2842,7 @@ CCE answers "how structured knowledge should keep evolving across time without l
2584
2842
  ## Relationship with Serenity
2585
2843
  Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
2586
2844
  the behavioral constraints embedded in CCC system prompts come from CCE.`;
2587
- function renderText$6(value) {
2845
+ function renderText$7(value) {
2588
2846
  return [{
2589
2847
  type: "text",
2590
2848
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2605,7 +2863,7 @@ defineTool({
2605
2863
  } },
2606
2864
  output: {
2607
2865
  schema: { type: "string" },
2608
- render: (_args, value) => renderText$6(value)
2866
+ render: (_args, value) => renderText$7(value)
2609
2867
  },
2610
2868
  async execute(args) {
2611
2869
  const section = args.section;
@@ -2651,7 +2909,7 @@ const PRAXIS_INDEX = `praxis — 可实践理论注入(认知框架,按需
2651
2909
  praxis cce(认知连续性工程——容器 5 行为约束 + H_op 操作熵)
2652
2910
  无参返回本目录;详情以对应框架原文为准(单一真相源)。
2653
2911
  `;
2654
- function renderText$5(value) {
2912
+ function renderText$6(value) {
2655
2913
  return [{
2656
2914
  type: "text",
2657
2915
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2671,7 +2929,7 @@ const praxisTool = defineTool({
2671
2929
  } },
2672
2930
  output: {
2673
2931
  schema: { type: "string" },
2674
- render: (_args, value) => renderText$5(value)
2932
+ render: (_args, value) => renderText$6(value)
2675
2933
  },
2676
2934
  async execute(args) {
2677
2935
  if (args.section === "eap") return EAP_CONTENT;
@@ -2725,10 +2983,10 @@ function handymanPresetInheritance(parentCtx) {
2725
2983
  * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
2726
2984
  * subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
2727
2985
  */
2728
- function agentCwd$5(exec) {
2986
+ function agentCwd$6(exec) {
2729
2987
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
2730
2988
  }
2731
- function renderText$4(value) {
2989
+ function renderText$5(value) {
2732
2990
  return [{
2733
2991
  type: "text",
2734
2992
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -2931,11 +3189,11 @@ function createHandymanTool(ctx) {
2931
3189
  },
2932
3190
  output: {
2933
3191
  schema: { type: "json" },
2934
- render: (_args, value) => renderText$4(value)
3192
+ render: (_args, value) => renderText$5(value)
2935
3193
  },
2936
3194
  async execute(args, exec) {
2937
3195
  if (args.guide) return { guide: HANDYMAN_GUIDE };
2938
- const root = findSerenityRoot(agentCwd$5(exec));
3196
+ const root = findSerenityRoot(agentCwd$6(exec));
2939
3197
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
2940
3198
  const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
2941
3199
  if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
@@ -2996,7 +3254,7 @@ function createHandymanTool(ctx) {
2996
3254
  * 同步把当前 dsh 会话重命名为该 SESSION 目录名**(sessionTitle.rename,user source
2997
3255
  * pin 住标题)。非创建时预命名。
2998
3256
  */
2999
- function agentCwd$4(exec) {
3257
+ function agentCwd$5(exec) {
3000
3258
  return exec.agent?.session?.header?.cwd ?? process.cwd();
3001
3259
  }
3002
3260
  /** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
@@ -3112,7 +3370,7 @@ function renameDshSessionForActive(ctx, exec, info, summary) {
3112
3370
  console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
3113
3371
  }
3114
3372
  }
3115
- function renderText$3(value) {
3373
+ function renderText$4(value) {
3116
3374
  return [{
3117
3375
  type: "text",
3118
3376
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -3258,10 +3516,10 @@ function createSessionTool(ctx) {
3258
3516
  },
3259
3517
  output: {
3260
3518
  schema: { type: "json" },
3261
- render: (args, value) => renderText$3(value)
3519
+ render: (args, value) => renderText$4(value)
3262
3520
  },
3263
3521
  async execute(args, exec) {
3264
- const root = findSerenityRoot(agentCwd$4(exec));
3522
+ const root = findSerenityRoot(agentCwd$5(exec));
3265
3523
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
3266
3524
  const entries = loadMsmEntries(root);
3267
3525
  const hasSessionTool = entries.some((e) => e.name === "session-tool");
@@ -3367,7 +3625,7 @@ function createSessionTool(ctx) {
3367
3625
  root,
3368
3626
  note: args.note,
3369
3627
  summary: args.summary,
3370
- agentCwd: agentCwd$4(exec),
3628
+ agentCwd: agentCwd$5(exec),
3371
3629
  dshSessionId
3372
3630
  });
3373
3631
  return {
@@ -3392,7 +3650,7 @@ function createSessionTool(ctx) {
3392
3650
  * 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
3393
3651
  * doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
3394
3652
  */
3395
- const ACTIONS = [
3653
+ const ACTIONS$1 = [
3396
3654
  "list",
3397
3655
  "get",
3398
3656
  "set",
@@ -3400,10 +3658,10 @@ const ACTIONS = [
3400
3658
  "show",
3401
3659
  "doc"
3402
3660
  ];
3403
- function agentCwd$3(exec) {
3661
+ function agentCwd$4(exec) {
3404
3662
  return exec.agent?.session?.header?.cwd ?? process.cwd();
3405
3663
  }
3406
- function renderText$2(value) {
3664
+ function renderText$3(value) {
3407
3665
  return [{
3408
3666
  type: "text",
3409
3667
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -3415,7 +3673,7 @@ const localstoreTool = defineTool({
3415
3673
  parameters: {
3416
3674
  action: {
3417
3675
  type: "string",
3418
- enum: [...ACTIONS],
3676
+ enum: [...ACTIONS$1],
3419
3677
  required: true,
3420
3678
  description: "Subcommand: list/get/set/unset/show/doc"
3421
3679
  },
@@ -3435,10 +3693,10 @@ const localstoreTool = defineTool({
3435
3693
  },
3436
3694
  output: {
3437
3695
  schema: { type: "json" },
3438
- render: (args, value) => renderText$2(value)
3696
+ render: (args, value) => renderText$3(value)
3439
3697
  },
3440
3698
  async execute(args, exec) {
3441
- const root = findSerenityRoot(agentCwd$3(exec));
3699
+ const root = findSerenityRoot(agentCwd$4(exec));
3442
3700
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
3443
3701
  return runLocalStore(root, args);
3444
3702
  }
@@ -3594,13 +3852,13 @@ function applySkiffConfig(root) {
3594
3852
  *
3595
3853
  * 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
3596
3854
  */
3597
- function agentCwd$2(exec) {
3855
+ function agentCwd$3(exec) {
3598
3856
  return exec.agent?.session?.header?.cwd ?? process.cwd();
3599
3857
  }
3600
3858
  function agentSessionId(exec) {
3601
3859
  return exec.agent?.session?.id ?? "";
3602
3860
  }
3603
- function renderText$1(value) {
3861
+ function renderText$2(value) {
3604
3862
  return [{
3605
3863
  type: "text",
3606
3864
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -3655,10 +3913,10 @@ const containerAdminTool = defineTool({
3655
3913
  },
3656
3914
  output: {
3657
3915
  schema: { type: "json" },
3658
- render: (args, value) => renderText$1(value)
3916
+ render: (args, value) => renderText$2(value)
3659
3917
  },
3660
3918
  async execute(args, exec) {
3661
- const root = findSerenityRoot(agentCwd$2(exec));
3919
+ const root = findSerenityRoot(agentCwd$3(exec));
3662
3920
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
3663
3921
  const sessionId = agentSessionId(exec);
3664
3922
  const domain = args.domain;
@@ -4363,10 +4621,10 @@ const AUTOPILOT_ACTIONS = [
4363
4621
  "guide",
4364
4622
  "diag-live"
4365
4623
  ];
4366
- function agentCwd$1(exec) {
4624
+ function agentCwd$2(exec) {
4367
4625
  return exec.agent?.session?.header?.cwd ?? process.cwd();
4368
4626
  }
4369
- function renderText(value) {
4627
+ function renderText$1(value) {
4370
4628
  return [{
4371
4629
  type: "text",
4372
4630
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -4405,11 +4663,11 @@ function createAutopilotTool(ctx) {
4405
4663
  } },
4406
4664
  output: {
4407
4665
  schema: { type: "json" },
4408
- render: (_args, value) => renderText(value)
4666
+ render: (_args, value) => renderText$1(value)
4409
4667
  },
4410
4668
  async execute(args, exec) {
4411
4669
  if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
4412
- const root = findSerenityRoot(agentCwd$1(exec));
4670
+ const root = findSerenityRoot(agentCwd$2(exec));
4413
4671
  const result = {};
4414
4672
  if (!EXP_SCRIPT) {
4415
4673
  result.error = "autopilot-trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
@@ -5889,7 +6147,7 @@ function codeModeAdaptationLine(ctx, scope) {
5889
6147
  * 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
5890
6148
  * 回退 process.cwd()——对齐 context.ts,否则这些 agent 系统提示词注入为空
5891
6149
  * (v1.18.6:workflow subagent 无宁静号上下文 bug) */
5892
- function agentCwd(context) {
6150
+ function agentCwd$1(context) {
5893
6151
  const agent = context.agent;
5894
6152
  if (!agent) return void 0;
5895
6153
  return agent.session?.header?.cwd ?? process.cwd();
@@ -5909,7 +6167,7 @@ function registerEntrySkillSectionGlobal(ctx) {
5909
6167
  name: "serenity-entry",
5910
6168
  order: -50,
5911
6169
  text: (context) => {
5912
- const cwd = agentCwd(context);
6170
+ const cwd = agentCwd$1(context);
5913
6171
  if (!cwd) return "";
5914
6172
  const root = findSerenityRoot(cwd);
5915
6173
  if (!root) return "";
@@ -6116,6 +6374,9 @@ function registerContext(ctx, opts = {}) {
6116
6374
  injected.add(key);
6117
6375
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
6118
6376
  syncSafeModeRestriction(agent, root);
6377
+ try {
6378
+ syncImBridgeVisibility(agent, root);
6379
+ } catch {}
6119
6380
  };
6120
6381
  if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
6121
6382
  try {
@@ -6133,6 +6394,9 @@ function registerContext(ctx, opts = {}) {
6133
6394
  try {
6134
6395
  syncSafeModeRestriction(agent, root);
6135
6396
  } catch {}
6397
+ try {
6398
+ syncImBridgeVisibility(agent, root);
6399
+ } catch {}
6136
6400
  registerEntrySkillSection(agent, root);
6137
6401
  }
6138
6402
  if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
@@ -6621,11 +6885,11 @@ function registerStatusApi(ctx, opts = {}) {
6621
6885
  sendJson$2(res, 400, { error: chk.error });
6622
6886
  return;
6623
6887
  }
6624
- const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6888
+ const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6625
6889
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
6626
6890
  const settings = readWeixinSettings(chk.root);
6627
6891
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
6628
- const { readWeixinCredential } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6892
+ const { readWeixinCredential } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6629
6893
  const accounts = (settings.accounts ?? []).map((a) => ({
6630
6894
  accountId: a.accountId,
6631
6895
  name: a.name ?? void 0,
@@ -6654,10 +6918,10 @@ function registerStatusApi(ctx, opts = {}) {
6654
6918
  }
6655
6919
  const root = chk.root;
6656
6920
  if (body.action === "login-start") {
6657
- const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6921
+ const { fetchQRCode } = await import("./weixin-api-PjEhmjlZ.js").then((n) => n.u);
6658
6922
  const { randomUUID } = await import("node:crypto");
6659
6923
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6660
- const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6924
+ const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6661
6925
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
6662
6926
  const loginKey = randomUUID();
6663
6927
  weixinLogins.set(loginKey, {
@@ -6678,7 +6942,7 @@ function registerStatusApi(ctx, opts = {}) {
6678
6942
  sendJson$2(res, 400, { error: "missing accountId" });
6679
6943
  return;
6680
6944
  }
6681
- const { removeWeixinAccount } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6945
+ const { removeWeixinAccount } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6682
6946
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6683
6947
  removeWeixinAccount(root, body.accountId);
6684
6948
  syncCccBridge(ctx, root);
@@ -6691,7 +6955,7 @@ function registerStatusApi(ctx, opts = {}) {
6691
6955
  sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6692
6956
  return;
6693
6957
  }
6694
- const { saveWeixinRoutes } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6958
+ const { saveWeixinRoutes } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6695
6959
  const { readSkiffRoles } = await import("./skiff-role-BYvNnwv1.js").then((n) => n.u);
6696
6960
  const roles = readSkiffRoles(root);
6697
6961
  for (const r of routes) if (!roles.has(r.role)) {
@@ -6704,7 +6968,7 @@ function registerStatusApi(ctx, opts = {}) {
6704
6968
  }
6705
6969
  if (body.action === "set-enabled") {
6706
6970
  const enabled = body.enabled === true;
6707
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6971
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6708
6972
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6709
6973
  const settings = readWeixinSettings(root);
6710
6974
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -6750,8 +7014,8 @@ function registerStatusApi(ctx, opts = {}) {
6750
7014
  sendJson$2(res, 200, { status: "expired" });
6751
7015
  return;
6752
7016
  }
6753
- const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6754
- const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
7017
+ const { pollQRStatus } = await import("./weixin-api-PjEhmjlZ.js").then((n) => n.u);
7018
+ const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6755
7019
  const settings = readWeixinSettings(login.root);
6756
7020
  const status = await pollQRStatus({
6757
7021
  baseUrl: void 0,
@@ -6766,7 +7030,7 @@ function registerStatusApi(ctx, opts = {}) {
6766
7030
  });
6767
7031
  return;
6768
7032
  }
6769
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
7033
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
6770
7034
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6771
7035
  const accountId = nextWeixinAccountId(settings);
6772
7036
  upsertWeixinAccount(login.root, {
@@ -9239,19 +9503,31 @@ function sessionIdOf$1(value) {
9239
9503
  const id = v?.session?.id ?? v?.id;
9240
9504
  return typeof id === "string" && id !== "" ? id : null;
9241
9505
  }
9242
- /** 工具调用是否为「成功的 weixin-send」 */
9506
+ /**
9507
+ * 工具调用是否为「成功的微信发送」。
9508
+ *
9509
+ * v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
9510
+ * - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
9511
+ * - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
9512
+ */
9243
9513
  function isSuccessfulWeixinSend(exec, result) {
9244
9514
  const e = exec;
9245
- if (e?.name !== "msm") return false;
9246
- if (e.arguments?.name !== "weixin-send") return false;
9247
- return result?.isError !== true;
9515
+ if (result?.isError === true) return false;
9516
+ const args = e?.arguments;
9517
+ if (e?.name === "im-bridge") {
9518
+ if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
9519
+ const action = typeof args?.action === "string" ? args.action : "";
9520
+ return action === "send" || action === "send-file";
9521
+ }
9522
+ if (e?.name === "msm") return args?.name === "weixin-send";
9523
+ return false;
9248
9524
  }
9249
9525
  /** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
9250
9526
  function buildManualOutputRebuke(session) {
9251
9527
  return [
9252
9528
  "[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
9253
9529
  `ccc=${session.root} account=${session.accountId} user=${session.userId}`,
9254
- `msm("weixin-send", ["send", "--ccc", "${session.root}", "--account", "${session.accountId}", "--user", "${session.userId}", "<回复>"])`
9530
+ `im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
9255
9531
  ].join("\n");
9256
9532
  }
9257
9533
  /**
@@ -9358,7 +9634,8 @@ function buildOutgoingHookEvent(input) {
9358
9634
  sessionId: input.sessionId,
9359
9635
  role: input.role,
9360
9636
  reply: input.reply,
9361
- ...input.source && input.source !== "reply" ? { source: input.source } : {}
9637
+ ...input.source && input.source !== "reply" ? { source: input.source } : {},
9638
+ ...input.file ? { file: input.file } : {}
9362
9639
  };
9363
9640
  }
9364
9641
  /**
@@ -9474,6 +9751,7 @@ var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
9474
9751
  handleIncoming: () => handleIncoming,
9475
9752
  manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
9476
9753
  registerWeixinBridge: () => registerWeixinBridge,
9754
+ resolveWeixinAccount: () => resolveWeixinAccount,
9477
9755
  sendProactiveText: () => sendProactiveText,
9478
9756
  stopAllBridges: () => stopAllBridges,
9479
9757
  stopCccBridge: () => stopCccBridge,
@@ -9571,14 +9849,14 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
9571
9849
  * "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
9572
9850
  * 也符合"ACC 管机制、CCC 管内容"的归属二分。
9573
9851
  *
9574
- * 标记格式(稳定,供 CCC 提示词引用):
9852
+ * 标记格式(稳定,供 CCC 提示词引用;v1.31.0 起为 ACC 工具形态):
9575
9853
  * ```
9576
9854
  * [serenity:weixin-manual-output]
9577
- * msm("weixin-send", ["send", "--ccc", "<root>", "--account", "<account>", "--user", "<user>", "<回复>"])
9855
+ * im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
9578
9856
  * ```
9579
9857
  */
9580
9858
  function weixinManualOutputMarker(root, accountId, userId) {
9581
- return ["[serenity:weixin-manual-output]", `msm("weixin-send", ["send", "--ccc", "${root}", "--account", "${accountId}", "--user", "${userId}", "<回复>"])`].join("\n");
9859
+ return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
9582
9860
  }
9583
9861
  /**
9584
9862
  * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
@@ -9840,22 +10118,12 @@ function stopAllBridges() {
9840
10118
  bridges.clear();
9841
10119
  }
9842
10120
  /**
9843
- * 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
9844
- *
9845
- * 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
9846
- * ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
9847
- * 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
9848
- * CCC 侧不必重复实现 sendmessage;③ 多账号可选。
9849
- *
9850
- * 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
9851
- * 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
9852
- *
9853
- * 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
9854
- * @param input CCC 根 + 目标用户 + 文本 +(可选)账号
9855
- * @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
10121
+ * 解析该 CCC 的发送账号 + 凭据(v1.31.0:文本与文件发送的**单一账号选择真相源**)。
10122
+ * 顺序:桥启用 → 有启用账号 → 指定账号存在 → 凭据已绑定(扫码)。
10123
+ * @param root CCC 根
10124
+ * @param accountId 指定账号(缺省 = 第一个启用账号)
9856
10125
  */
9857
- async function sendProactiveText(input) {
9858
- const { root, toUserId } = input;
10126
+ function resolveWeixinAccount(root, accountId) {
9859
10127
  const settings = readWeixinSettings(root);
9860
10128
  if (!settings.enabled) return {
9861
10129
  ok: false,
@@ -9870,20 +10138,32 @@ async function sendProactiveText(input) {
9870
10138
  error: "微信桥未配置任何启用账号",
9871
10139
  remediation: "在设置面板「微信桥」扫码绑定账号"
9872
10140
  };
9873
- const accountId = input.accountId ?? accounts[0].accountId;
9874
- if (!accounts.some((a) => a.accountId === accountId)) return {
10141
+ const id = accountId ?? accounts[0].accountId;
10142
+ if (!accounts.some((a) => a.accountId === id)) return {
9875
10143
  ok: false,
9876
10144
  code: "ACCOUNT_NOT_FOUND",
9877
- error: `账号不存在或未启用: ${accountId}`,
10145
+ error: `账号不存在或未启用: ${id}`,
9878
10146
  remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
9879
10147
  };
9880
- const cred = readWeixinCredential(root, accountId);
10148
+ const cred = readWeixinCredential(root, id);
9881
10149
  if (!cred) return {
9882
10150
  ok: false,
9883
10151
  code: "ACCOUNT_NOT_BOUND",
9884
- error: `账号未绑定(无凭据): ${accountId}`,
10152
+ error: `账号未绑定(无凭据): ${id}`,
9885
10153
  remediation: "在设置面板重新扫码绑定"
9886
10154
  };
10155
+ return {
10156
+ ok: true,
10157
+ accountId: id,
10158
+ cred
10159
+ };
10160
+ }
10161
+ async function sendProactiveText(input) {
10162
+ const { root, toUserId } = input;
10163
+ const settings = readWeixinSettings(root);
10164
+ const account = resolveWeixinAccount(root, input.accountId);
10165
+ if (!account.ok) return account;
10166
+ const { accountId, cred } = account;
9887
10167
  const sessionId = weixinSessionIdFor(toUserId);
9888
10168
  const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
9889
10169
  try {
@@ -9959,7 +10239,9 @@ function registerWeixinBridge(ctx) {
9959
10239
  * 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
9960
10240
  * 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
9961
10241
  *
9962
- * 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
10242
+ * 调用者 = **非 agent 的调用者**(外部脚本/集成/运维)。**agent 侧已不走这里**:v1.31.0 起
10243
+ * agent 统一调 `im-bridge` 工具(进程内直调 `sendProactiveText`,无 HTTP 一跳),旧 CCC MSM
10244
+ * `weixin-send` 已退役。本入口保留 = 给容器外的进程一个稳定的本机发送面。
9963
10245
  * 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
9964
10246
  *
9965
10247
  * 契约:
@@ -10325,6 +10607,9 @@ function cleanupSessionState(sessionId) {
10325
10607
  try {
10326
10608
  forgetManualOutputSession(sessionId);
10327
10609
  } catch {}
10610
+ try {
10611
+ forgetImBridgeVisibility(sessionId);
10612
+ } catch {}
10328
10613
  }
10329
10614
  /**
10330
10615
  * 装配生命周期订阅与资源拆卸。
@@ -10548,6 +10833,243 @@ async function registerWebFetchProvider(ctx) {
10548
10833
  }
10549
10834
  }
10550
10835
  //#endregion
10836
+ //#region src/im-weixin.ts
10837
+ /**
10838
+ * im-weixin.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
10839
+ *
10840
+ * 能力归 ACC(S142 用户洞察):本文件吸收原 CCC MSM `.opencode/skills/home-serenity/scripts/
10841
+ * weixin-send.ts` 的 `send` / `send-file` / `users` 语义——协议、凭据、账号选择、别名解析、
10842
+ * 记录全部落在 ACC 侧;CCC 只保留「何时发 / 发什么 / 怎么写」。
10843
+ *
10844
+ * 通道内部契约:
10845
+ * - `isEnabled(root)` = 该 CCC 的 `weixin.enabled`(可见性判据,im-bridge 用它决定工具是否可见)
10846
+ * - `send` 复用 `sendProactiveText`(唯一文本发送实现 + 唯一记录点)
10847
+ * - `sendFile` 走 iLink 上传(`weixin-api.sendFileMessage`)+ 同一 outgoing hook(带 `file` 元数据)
10848
+ */
10849
+ /**
10850
+ * 别名 → CCC localstore 凭据键(与 v1.30.9 CCC MSM 逐字一致,迁移零认知成本)。
10851
+ * 别名解析属"数据填值",归 ACC(D23)。
10852
+ */
10853
+ const WEIXIN_ALIAS_MAP = {
10854
+ yh: "WEIXIN_USER_YH",
10855
+ danica: "WEIXIN_USER_DANICA",
10856
+ xiaowang: "WEIXIN_USER_DANICA"
10857
+ };
10858
+ /** 读该 CCC 的一个凭据值(空/缺失 → null) */
10859
+ function credentialValue(root, key) {
10860
+ try {
10861
+ const value = readStore(root, "credential")[key];
10862
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
10863
+ } catch {
10864
+ return null;
10865
+ }
10866
+ }
10867
+ /** 把通道层的失败码转成一句可读错误(工具面统一映射为 SEND_FAILED) */
10868
+ function channelError(code, error, remediation) {
10869
+ return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
10870
+ }
10871
+ const weixinChannel = {
10872
+ id: "weixin",
10873
+ isEnabled(root) {
10874
+ return readWeixinSettings(root).enabled === true;
10875
+ },
10876
+ resolveUser(root, input) {
10877
+ const raw = input.trim();
10878
+ if (raw === "") return null;
10879
+ const alias = raw.toLowerCase();
10880
+ const key = WEIXIN_ALIAS_MAP[alias];
10881
+ if (key) {
10882
+ const id = credentialValue(root, key);
10883
+ return id ? {
10884
+ alias,
10885
+ id
10886
+ } : null;
10887
+ }
10888
+ if (raw.includes("@")) return {
10889
+ alias: raw,
10890
+ id: raw
10891
+ };
10892
+ return null;
10893
+ },
10894
+ listUsers(root) {
10895
+ const out = [];
10896
+ for (const [alias, key] of Object.entries(WEIXIN_ALIAS_MAP)) {
10897
+ const id = credentialValue(root, key);
10898
+ if (id) out.push({
10899
+ alias,
10900
+ id
10901
+ });
10902
+ }
10903
+ return out;
10904
+ },
10905
+ async send(input) {
10906
+ const result = await sendProactiveText({
10907
+ root: input.root,
10908
+ toUserId: input.userId,
10909
+ text: input.text,
10910
+ ...input.accountId ? { accountId: input.accountId } : {}
10911
+ });
10912
+ if (!result.ok) throw channelError(result.code, result.error, result.remediation);
10913
+ return {
10914
+ accountId: result.accountId,
10915
+ userId: result.userId,
10916
+ sessionId: result.sessionId,
10917
+ role: result.role
10918
+ };
10919
+ },
10920
+ async sendFile(input) {
10921
+ const settings = readWeixinSettings(input.root);
10922
+ if (!settings.enabled) throw channelError("BRIDGE_DISABLED", `微信桥未启用(ccc=${input.root})`);
10923
+ const account = resolveWeixinAccount(input.root, input.accountId);
10924
+ if (!account.ok) throw channelError(account.code, account.error, account.remediation);
10925
+ const sent = await sendFileMessage({
10926
+ baseUrl: account.cred.baseUrl,
10927
+ token: account.cred.token,
10928
+ toUserId: input.userId,
10929
+ data: input.data,
10930
+ fileName: input.fileName
10931
+ });
10932
+ if (input.caption) await sendTextMessage({
10933
+ baseUrl: account.cred.baseUrl,
10934
+ token: account.cred.token,
10935
+ toUserId: input.userId,
10936
+ text: input.caption
10937
+ });
10938
+ if (settings.hook) {
10939
+ const role = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
10940
+ invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
10941
+ cccRoot: input.root,
10942
+ accountId: account.accountId,
10943
+ userId: input.userId,
10944
+ sessionId: weixinSessionIdFor(input.userId),
10945
+ role,
10946
+ reply: input.caption ? `[文件] ${sent.fileName} — ${input.caption}` : `[文件] ${sent.fileName}`,
10947
+ source: "proactive",
10948
+ file: {
10949
+ name: sent.fileName,
10950
+ size: sent.size,
10951
+ ...input.caption ? { caption: input.caption } : {}
10952
+ }
10953
+ })).catch((err) => {
10954
+ console.log(`[serenity-hooks] weixin hook outgoing(file) error: ${err instanceof Error ? err.message : String(err)}`);
10955
+ });
10956
+ }
10957
+ return sent;
10958
+ },
10959
+ /**
10960
+ * 通道健康快照(`status` 动作)。
10961
+ *
10962
+ * 显式映射为 JSON 安全形状(而非把内部结构直接返回):内部类型含 `undefined`
10963
+ * 可选字段与 interface(无隐式索引签名)→ 直接返回既过不了 `ImJson` 类型门,
10964
+ * 也会让工具返回值出现不可序列化字段。缺省用条件展开去掉 undefined。
10965
+ */
10966
+ status(root) {
10967
+ const settings = readWeixinSettings(root);
10968
+ const accounts = weixinBridgeStatus().filter((entry) => entry.ccc === root).flatMap((entry) => entry.accounts.map((account) => ({
10969
+ accountId: account.accountId,
10970
+ lastPollAt: account.lastPollAt,
10971
+ ...account.lastError ? { lastError: account.lastError } : {}
10972
+ })));
10973
+ const routes = (settings.routes ?? []).map((route) => ({
10974
+ user: route.user,
10975
+ role: route.role
10976
+ }));
10977
+ return {
10978
+ enabled: settings.enabled === true,
10979
+ autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
10980
+ fallbackOnNoSend: settings.fallbackOnNoSend === true,
10981
+ routes,
10982
+ accounts
10983
+ };
10984
+ }
10985
+ };
10986
+ //#endregion
10987
+ //#region src/tools/im-bridge.ts
10988
+ /**
10989
+ * im-bridge.ts(工具面)— IM 消息发送工具(v1.31.0)
10990
+ *
10991
+ * 用户洞察(S142):微信桥是 ACC 提供的 → **发送能力也应是 ACC 的工具**;
10992
+ * 「当用户配置了微信桥则可用,不配置则不可见」。
10993
+ *
10994
+ * 本文件只做工具面三件事:① 从 agent 会话解析**本会话 CCC**(R4:不接受目标 CCC)
10995
+ * ② 把参数交给 `im-bridge.ts`(能力层)分发 ③ 渲染结果/错误。
10996
+ *
10997
+ * 可见性不在这里判定:`seams/context.ts` 在会话就绪时按 CCC 配置
10998
+ * `agent.ctx.tools.restrict({ deny: ['im-bridge'] })` 把本工具从**未配置通道**的
10999
+ * agent 工具清单中移除(未配置 = 模型看不到,而非"看得到但被拒")。
11000
+ */
11001
+ const ACTIONS = [
11002
+ "send",
11003
+ "send-file",
11004
+ "users",
11005
+ "status"
11006
+ ];
11007
+ function agentCwd(exec) {
11008
+ return exec.agent?.session?.header?.cwd ?? process.cwd();
11009
+ }
11010
+ function renderText(value) {
11011
+ return [{
11012
+ type: "text",
11013
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
11014
+ }];
11015
+ }
11016
+ /**
11017
+ * IM 发送工具(`im-bridge`)。通道维度为将来其他 IM 预留(R3)——
11018
+ * 新增通道只注册实现,不改工具名与参数形状。
11019
+ */
11020
+ function createImBridgeTool() {
11021
+ return defineTool({
11022
+ name: "im-bridge",
11023
+ description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. yh, danica) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
11024
+ parameters: {
11025
+ channel: {
11026
+ type: "string",
11027
+ description: `IM channel id (e.g. "weixin"); available: ${imChannelIds().join(", ") || "(none)"}`,
11028
+ required: true
11029
+ },
11030
+ action: {
11031
+ type: "string",
11032
+ enum: [...ACTIONS],
11033
+ required: true,
11034
+ description: "send / send-file / users / status"
11035
+ },
11036
+ user: {
11037
+ type: "string",
11038
+ description: "Recipient: configured alias (yh, danica) or raw channel user id (send / send-file)"
11039
+ },
11040
+ text: {
11041
+ type: "string",
11042
+ description: "Message text, plain text only (send)"
11043
+ },
11044
+ file: {
11045
+ type: "string",
11046
+ description: "File path inside this container, relative to the container root (send-file)"
11047
+ },
11048
+ caption: {
11049
+ type: "string",
11050
+ description: "Optional caption sent as a separate message (send-file)"
11051
+ },
11052
+ account: {
11053
+ type: "string",
11054
+ description: "Channel account id (default: the first enabled, bound account)"
11055
+ }
11056
+ },
11057
+ output: {
11058
+ schema: { type: "json" },
11059
+ render: (_args, value) => renderText(value)
11060
+ },
11061
+ async execute(args, exec) {
11062
+ const root = findSerenityRoot(agentCwd(exec));
11063
+ if (!root) return {
11064
+ ok: false,
11065
+ code: "CCC_UNRESOLVED",
11066
+ error: "this session is not inside a Serenity container (no .serenity marker found from the session cwd)"
11067
+ };
11068
+ return runImBridge(root, args);
11069
+ }
11070
+ });
11071
+ }
11072
+ //#endregion
10551
11073
  //#region src/index.ts
10552
11074
  const name = "dsh-serenity-hooks";
10553
11075
  /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
@@ -10597,6 +11119,7 @@ function apply(ctx, config) {
10597
11119
  const report = probeHostContract(ctx, readDshVersion());
10598
11120
  if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
10599
11121
  } catch {}
11122
+ registerImChannel(weixinChannel);
10600
11123
  if (config.tools) {
10601
11124
  ctx.tools.register(ccFsTool);
10602
11125
  ctx.tools.register(createSessionTool(ctx));
@@ -10608,6 +11131,7 @@ function apply(ctx, config) {
10608
11131
  ctx.tools.register(localstoreTool);
10609
11132
  ctx.tools.register(containerAdminTool);
10610
11133
  ctx.tools.register(createAutopilotTool(ctx));
11134
+ ctx.tools.register(createImBridgeTool());
10611
11135
  }
10612
11136
  if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
10613
11137
  if (config.keeper) registerKeeper(ctx, {