@shgroup/dsh-serenity-hooks 1.30.16 → 1.30.17

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.30.16",
3
+ "version": "1.30.17",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——10 个工具(container_fs/logbook/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/autopilot-trajectory)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/自主巡航。适配 DSH 0.1.2-rc.1(deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/ccc.d.ts CHANGED
@@ -152,6 +152,23 @@ export interface WeixinSettings {
152
152
  * agent 一轮未输出 → 静默不兜底(否则开关语义被破坏)。
153
153
  */
154
154
  autoReplyWithLastMessage?: boolean;
155
+ /**
156
+ * 手动输出模式下的**兜底**(v1.30.17,S142 用户拍板"鲁棒修法";**缺省 false = 严格静默**)。
157
+ *
158
+ * 仅在 `autoReplyWithLastMessage: false` 时生效:
159
+ * - `true` → 一轮结束时若 agent **一次都没成功调用 `weixin-send`**,桥把该轮最终文本转发给用户
160
+ * (记录 `source: "reply-fallback"`,并在日志留响亮告警)。
161
+ * - `false`(缺省)→ 保持 v1.30.10 的"静默不兜底"语义。
162
+ *
163
+ * 为什么需要(实证 R↓):v1.30.16 的机械闸门会打回 ≤2 次;但用户实测某模型在**寒暄类消息**上
164
+ * 连续 4 轮(turn 186/191/192/193)、跨 3 版 CCC 提示词(v0/v1/v2)仍不调工具 → 打回用尽后
165
+ * **用户什么都收不到**。提示词与闸门均已排除(同一模型在任务类消息下正常发送)→ 桥兜底是
166
+ * 唯一能保证"不丢消息"的机械手段。
167
+ *
168
+ * 代价(显式):角色失去"故意静默"能力(它总会产出一段最终文本)→ 需要严格静默的角色保持缺省。
169
+ * agent 只要自己发过(哪怕只发一条),桥就不兜底——输出权仍在 agent。
170
+ */
171
+ fallbackOnNoSend?: boolean;
155
172
  }
156
173
  /** 微信桥账号配置(serenity.json 内;凭据部分在 localstore) */
157
174
  export interface WeixinAccountConfig {
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ 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-BD_yYAZz.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";
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
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";
@@ -1859,13 +1859,25 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1859
1859
  机械闸门(v1.30.16):本轮 turn 结束仍未成功调用 weixin-send → agent 被打回(≤2 次,
1860
1860
  提示 = 标记 + 事实);达上限放弃并响亮告警。失败调用不算已送达。
1861
1861
  语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
1862
- **agent 一轮未输出 → 静默不兜底**(否则开关失去意义);typing 指示与 incoming hook 不受影响。
1862
+ typing 指示与 incoming hook 不受影响。
1863
+
1864
+ ▸ weixin.fallbackOnNoSend(v1.30.17,手动模式的**兜底**;缺省 false):
1865
+ 仅在 autoReplyWithLastMessage=false 时生效。true → 一轮结束时若 agent **一次都没成功**
1866
+ 调用 weixin-send(闸门打回用尽),桥把该轮最终文本转发给用户(记录 source="reply-fallback",
1867
+ 日志留响亮告警)——保证"不丢消息"。agent 自己发过 → 不兜底(输出权仍在 agent)。
1868
+ 为什么需要(实证):v1.30.16 闸门会打回 ≤2 次,但实测某模型在**寒暄类消息**上连续 4 轮、
1869
+ 跨 3 版 CCC 提示词仍不调工具 → 打回用尽后用户什么都收不到;提示词与闸门均已排除。
1870
+ 代价(显式):角色失去"故意静默"能力(它总会产出一段最终文本)→ 需严格静默的角色保持缺省。
1871
+ 三态:① autoReplyWithLastMessage=true(缺省)桥总是回发;② =false + fallbackOnNoSend=true
1872
+ = agent 优先、桥兜底(推荐);③ 两者皆 false(缺省)= 严格静默(v1.30.10 语义)。
1873
+ ⚠️ 兜底只在**闸门装配成功**时生效(未装配则无法判定是否发送过 → 不冒重复发送风险)。
1863
1874
 
1864
1875
  Config:
1865
1876
  { "weixin": {
1866
1877
  "enabled": true,
1867
1878
  "hook": "scripts/weixin-message-hook.ts",
1868
1879
  "autoReplyWithLastMessage": true,
1880
+ "fallbackOnNoSend": false,
1869
1881
  "accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
1870
1882
  "routes": [{ "user": "*", "role": "zhaocai" }]
1871
1883
  } }
@@ -1878,7 +1890,8 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1878
1890
  incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
1879
1891
  outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
1880
1892
  · source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
1881
- (CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText
1893
+ (CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText);
1894
+ source="reply-fallback"(v1.30.17)= 手动模式下 agent 本轮未发送 → 桥兜底转发最终文本
1882
1895
  · file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
1883
1896
 
1884
1897
  脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
@@ -1893,7 +1906,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1893
1906
  "message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
1894
1907
  { "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
1895
1908
  "userId": ..., "sessionId": ..., "role": ...,
1896
- "reply": "已记录(纯文本)", "source": "reply" | "proactive" }
1909
+ "reply": "已记录(纯文本)", "source": "reply" | "proactive" | "reply-fallback" }
1897
1910
 
1898
1911
  示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
1899
1912
  const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
@@ -6608,11 +6621,11 @@ function registerStatusApi(ctx, opts = {}) {
6608
6621
  sendJson$2(res, 400, { error: chk.error });
6609
6622
  return;
6610
6623
  }
6611
- const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6624
+ const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6612
6625
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
6613
6626
  const settings = readWeixinSettings(chk.root);
6614
6627
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
6615
- const { readWeixinCredential } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6628
+ const { readWeixinCredential } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6616
6629
  const accounts = (settings.accounts ?? []).map((a) => ({
6617
6630
  accountId: a.accountId,
6618
6631
  name: a.name ?? void 0,
@@ -6644,7 +6657,7 @@ function registerStatusApi(ctx, opts = {}) {
6644
6657
  const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6645
6658
  const { randomUUID } = await import("node:crypto");
6646
6659
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
6647
- const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6660
+ const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6648
6661
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
6649
6662
  const loginKey = randomUUID();
6650
6663
  weixinLogins.set(loginKey, {
@@ -6665,7 +6678,7 @@ function registerStatusApi(ctx, opts = {}) {
6665
6678
  sendJson$2(res, 400, { error: "missing accountId" });
6666
6679
  return;
6667
6680
  }
6668
- const { removeWeixinAccount } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6681
+ const { removeWeixinAccount } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6669
6682
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6670
6683
  removeWeixinAccount(root, body.accountId);
6671
6684
  syncCccBridge(ctx, root);
@@ -6678,7 +6691,7 @@ function registerStatusApi(ctx, opts = {}) {
6678
6691
  sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
6679
6692
  return;
6680
6693
  }
6681
- const { saveWeixinRoutes } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6694
+ const { saveWeixinRoutes } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6682
6695
  const { readSkiffRoles } = await import("./skiff-role-BYvNnwv1.js").then((n) => n.u);
6683
6696
  const roles = readSkiffRoles(root);
6684
6697
  for (const r of routes) if (!roles.has(r.role)) {
@@ -6691,7 +6704,7 @@ function registerStatusApi(ctx, opts = {}) {
6691
6704
  }
6692
6705
  if (body.action === "set-enabled") {
6693
6706
  const enabled = body.enabled === true;
6694
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6707
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6695
6708
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6696
6709
  const settings = readWeixinSettings(root);
6697
6710
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -6738,7 +6751,7 @@ function registerStatusApi(ctx, opts = {}) {
6738
6751
  return;
6739
6752
  }
6740
6753
  const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
6741
- const { readWeixinSettings } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6754
+ const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6742
6755
  const settings = readWeixinSettings(login.root);
6743
6756
  const status = await pollQRStatus({
6744
6757
  baseUrl: void 0,
@@ -6753,7 +6766,7 @@ function registerStatusApi(ctx, opts = {}) {
6753
6766
  });
6754
6767
  return;
6755
6768
  }
6756
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BD_yYAZz.js").then((n) => n.u);
6769
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
6757
6770
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
6758
6771
  const accountId = nextWeixinAccountId(settings);
6759
6772
  upsertWeixinAccount(login.root, {
@@ -9185,10 +9198,15 @@ const PLUGIN_SOURCE = {
9185
9198
  plugin: "dsh-serenity-hooks"
9186
9199
  };
9187
9200
  const manualSessions = /* @__PURE__ */ new Map();
9188
- /** 本轮已成功发送的会话(turn-stopping 时结算并清空) */
9201
+ /**
9202
+ * 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
9203
+ * v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
9204
+ */
9189
9205
  const sentThisTurn = /* @__PURE__ */ new Set();
9190
9206
  /** 连续打回计数(成功发送即清零) */
9191
9207
  const rebukeCounts = /* @__PURE__ */ new Map();
9208
+ /** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
9209
+ let guardActive = false;
9192
9210
  /** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
9193
9211
  function noteManualOutputSession(sessionId, session) {
9194
9212
  if (typeof sessionId !== "string" || sessionId === "") return;
@@ -9200,6 +9218,21 @@ function forgetManualOutputSession(sessionId) {
9200
9218
  sentThisTurn.delete(sessionId);
9201
9219
  rebukeCounts.delete(sessionId);
9202
9220
  }
9221
+ /** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
9222
+ function hasSentThisTurn(sessionId) {
9223
+ return sentThisTurn.has(sessionId);
9224
+ }
9225
+ /**
9226
+ * 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
9227
+ * 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
9228
+ */
9229
+ function clearSentThisTurn(sessionId) {
9230
+ sentThisTurn.delete(sessionId);
9231
+ }
9232
+ /** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
9233
+ function isWeixinOutputGuardActive() {
9234
+ return guardActive;
9235
+ }
9203
9236
  /** 从 agent / 会话对象取会话 id(形状宽容) */
9204
9237
  function sessionIdOf$1(value) {
9205
9238
  const v = value;
@@ -9250,13 +9283,11 @@ function registerWeixinOutputGuard(ctx) {
9250
9283
  if (!sessionId) return;
9251
9284
  const session = manualSessions.get(sessionId);
9252
9285
  if (!session) return;
9253
- const sent = sentThisTurn.has(sessionId);
9254
- sentThisTurn.delete(sessionId);
9255
- if (sent) return;
9286
+ if (sentThisTurn.has(sessionId)) return;
9256
9287
  const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
9257
9288
  if (count > 2) {
9258
9289
  rebukeCounts.delete(sessionId);
9259
- console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到任何回复`);
9290
+ console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
9260
9291
  return;
9261
9292
  }
9262
9293
  rebukeCounts.set(sessionId, count);
@@ -9275,7 +9306,9 @@ function registerWeixinOutputGuard(ctx) {
9275
9306
  });
9276
9307
  } catch {
9277
9308
  console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
9309
+ return;
9278
9310
  }
9311
+ guardActive = true;
9279
9312
  }
9280
9313
  //#endregion
9281
9314
  //#region src/weixin-hook.ts
@@ -9325,7 +9358,7 @@ function buildOutgoingHookEvent(input) {
9325
9358
  sessionId: input.sessionId,
9326
9359
  role: input.role,
9327
9360
  reply: input.reply,
9328
- ...input.source === "proactive" ? { source: "proactive" } : {}
9361
+ ...input.source && input.source !== "reply" ? { source: input.source } : {}
9329
9362
  };
9330
9363
  }
9331
9364
  /**
@@ -9439,6 +9472,7 @@ function invokeWeixinHook(root, hookRel, event) {
9439
9472
  //#region src/weixin-bridge.ts
9440
9473
  var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
9441
9474
  handleIncoming: () => handleIncoming,
9475
+ manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
9442
9476
  registerWeixinBridge: () => registerWeixinBridge,
9443
9477
  sendProactiveText: () => sendProactiveText,
9444
9478
  stopAllBridges: () => stopAllBridges,
@@ -9547,6 +9581,18 @@ function weixinManualOutputMarker(root, accountId, userId) {
9547
9581
  return ["[serenity:weixin-manual-output]", `msm("weixin-send", ["send", "--ccc", "${root}", "--account", "${accountId}", "--user", "${userId}", "<回复>"])`].join("\n");
9548
9582
  }
9549
9583
  /**
9584
+ * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
9585
+ *
9586
+ * 四个条件同时成立才兜底:
9587
+ * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
9588
+ * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
9589
+ * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
9590
+ * ④ 有最终文本可转发(空文本无意义)
9591
+ */
9592
+ function manualOutputFallbackNeeded(input) {
9593
+ return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
9594
+ }
9595
+ /**
9550
9596
  * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
9551
9597
  *
9552
9598
  * 会话语义:`weixinSessionIdFor(fromUserId)` 固定可重建——同用户长期同一会话;
@@ -9666,6 +9712,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9666
9712
  if (text) parts.push(text);
9667
9713
  parts.push(...mediaNotes, ...degradedNotes);
9668
9714
  if (manualOutput) {
9715
+ clearSentThisTurn(sessionId);
9669
9716
  noteManualOutputSession(sessionId, {
9670
9717
  root,
9671
9718
  accountId,
@@ -9688,7 +9735,35 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
9688
9735
  console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
9689
9736
  });
9690
9737
  const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
9691
- if (manualOutput) return;
9738
+ if (manualOutput) {
9739
+ if (!manualOutputFallbackNeeded({
9740
+ fallbackOnNoSend: settings.fallbackOnNoSend,
9741
+ guardActive: isWeixinOutputGuardActive(),
9742
+ sentThisTurn: hasSentThisTurn(sessionId),
9743
+ answer
9744
+ })) return;
9745
+ const reply = markdownToPlainText(stripThink(answer));
9746
+ await sendTextMessage({
9747
+ baseUrl: cred.baseUrl,
9748
+ token: cred.token,
9749
+ toUserId: fromUserId,
9750
+ text: reply,
9751
+ contextToken: msg.context_token
9752
+ });
9753
+ console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, role=${roleName})`);
9754
+ if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
9755
+ cccRoot: root,
9756
+ accountId,
9757
+ userId: fromUserId,
9758
+ sessionId,
9759
+ role: roleName,
9760
+ reply,
9761
+ source: "reply-fallback"
9762
+ })).catch((err) => {
9763
+ console.log(`[serenity-hooks] weixin hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
9764
+ });
9765
+ return;
9766
+ }
9692
9767
  if (answer === "") return;
9693
9768
  const reply = markdownToPlainText(stripThink(answer));
9694
9769
  await sendTextMessage({
package/lib/msm-ops.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare const MSM_ACTIONS: readonly MsmAction[];
19
19
  export declare const ACC_CATALOG = "\u2550\u2550\u2550 ACC Usage Catalog \u2550\u2550\u2550\nDirectory of ACC capabilities \u2014 each area lists where to go for details (guides live with their feature; this catalog only points).\n\n \u2460 \u4F1A\u8BDD\u4E0E\u8F68\u8FF9 \u2192 logbook\uFF08list/show/create/use/close/health/qa/archive/summary/hook-develop-guide + rebuild \u8D85\u9650\u91CD\u5EFA\uFF09\n session_rebuild \u5DF2\u5E76\u5165 logbook rebuild\uFF08\u8D85\u9650\u91CD\u5EFA\uFF1B\u9608\u503C K \u89C1 container_admin ccc-config / \u8BBE\u7F6E\u9762\u677F\u91CD\u5EFA\u9608\u503CK\uFF09\n \u2461 \u8BA4\u77E5\u8D28\u91CF\u6846\u67B6 \u2192 praxis\uFF08eap/neat/cce \u4E09\u5408\u4E00\u2014\u2014\u53EF\u5B9E\u8DF5\u7406\u8BBA\u6CE8\u5165\uFF0Csection \u6E10\u8FDB\u62AB\u9732\uFF0C\u65E0\u53C2\u5373\u76EE\u5F55\uFF09\n \u2462 \u5DE5\u5177\u4E0E\u6267\u884C \u2192 container_fs\uFF08\u5BB9\u5668\u6587\u4EF6\u7CFB\u7EDF 15 \u5B50\u547D\u4EE4\uFF09/ container_git\uFF08git\uFF09/ dashboard\uFF08health \u542B\u6CE8\u518C\u8868\u68C0\u67E5/time/wait\uFF09\n acc_msm \u5DF2\u5E76\u5165 msm\uFF08\u6267\u884C+\u53D1\u73B0\uFF09\u4E0E container_admin\uFF08\u7BA1\u7406\uFF09\u2014\u2014msm(\"<name>\", [\"<args>\"])\uFF1Binspect=true \u67E5\u7528\u6CD5\n handyman\uFF08\u6742\u5DE5 agent \u7F16\u6392\u2014\u2014guide \u5B50\u547D\u4EE4\uFF1Ahandyman guide\uFF09\n \u2463 \u89D2\u8272\u4E0E\u5BF9\u5916\u9762 \u2192 container_admin role\uFF08Skiff \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF1Aguide/validate/apply/list\u2014\u2014skiff_admin \u5DF2\u5E76\u5165\uFF09\n ACP\uFF08\u7A0B\u5E8F\u5316 JSON-RPC 3100\uFF09/ Skiff \u95EE\u7B54\u9875\uFF08\u516C\u7F51 ask\uFF09\u2014\u2014\u9762\u677F\u300C\u5916\u90E8\u80FD\u529B\u300D\u7EC4\n \u2464 \u81EA\u4E3B\u4E0E\u63A5\u5165 \u2192 autopilot-trajectory\uFF08Autopilot \u4E00\u7AD9\u5F0F\uFF1Aall/init/random/diag/doc/check/status/guide\uFF09\n weixin \u5FAE\u4FE1\u6865\uFF08\u914D\u7F6E/\u626B\u7801/\u8DEF\u7531/\u6D88\u606F hook\u2014\u2014CCC \u4FA7 weixin-doctor MSM\uFF1Amsm(\"weixin-doctor\", [\"guide\"])\uFF09\n \u2465 CCC \u914D\u7F6E\u603B\u89C8 \u2192 container_admin msm ccc-config\uFF088 \u6BB5\uFF1Ahandyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin\uFF09\n \u2466 \u6CE8\u518C\u8868\u4E0E\u5B89\u5168 \u2192 mech-registry.json \u7531 container_admin msm register/deregister \u7BA1\u7406\uFF08\u5199\u4FDD\u62A4\u2014\u2014\u4E0D\u53EF\u76F4\u63A5\u7F16\u8F91\uFF09\n dashboard health \u8F93\u51FA registry \u5B8C\u6574\u6027\u68C0\u67E5\uFF1B\u635F\u574F\u6062\u590D\u6307\u5F15\u89C1 health registry.issues\n\n\u6BCF\u533A\uFF1A\u4E00\u53E5\u8BDD\u5B9A\u4F4D + \u8BE6\u7EC6\u5165\u53E3\uFF08\u5DE5\u5177\u540D / guide \u5B50\u547D\u4EE4\uFF09\u3002\u8BE6\u60C5\u6C38\u8FDC\u4EE5\u5BF9\u5E94\u5DE5\u5177\u7684 guide/ccc-config \u4E3A\u5355\u4E00\u771F\u76F8\u6E90\u3002\n";
20
20
  export declare const MSM_GUIDE = "MSM Development Manual (Mech & Semi-Mech framework)\n\n## What it is\nMSM = the executable-unit layer. Mech is pure TS with zero LLM reasoning; Semi-Mech is a TS framework + LLM decision points.\nMSM is ACC's deterministic executable-unit layer \u2014 all shell/exec operations go through MSM and cannot be bypassed.\n\n## Registering a new MSM (container_admin msm register)\n1. Write the script under <skill>/scripts/ (runnable via tsx; must include a main() CLI guard with an import.meta.url check)\n2. container_admin msm register --domain msm --action register <name> --skill <s> --path <script path relative to root> --category <mech|semi-mech> --description <desc>\n3. Auto-writes mech-registry.json (preserving the original format) + git commit (commits only the registry file)\n4. Validation: path must be inside root, script must exist, name must be globally unique\n\n## flag schema (v1)\nflags is a new-style object array for parameter validation and path-escape guarding:\n [{\"name\":\"output\",\"type\":\"string\",\"description\":\"output path\"},\n {\"name\":\"target\",\"type\":\"path\",\"description\":\"operation target (type:path enables in-root validation + symlink defense)\"},\n {\"name\":\"force\",\"type\":\"boolean\",\"description\":\"force mode\",\"default\":false}]\n- {name, type} format \u2014 new style, type:\"path\" enables the path-escape guard\n- {flag, description} format \u2014 old style, CLI flag description string\n- On registration, flags are passed via container_admin msm register --flags '<json>'\n\n## Script conventions\n- Top-of-file documentation: purpose / usage / exit codes\n- Exit codes: 0 success / 1 user / 2 system / 3 operator (per ACC protocol classification)\n- main() CLI guard (DC-M2): the script must start with\n if (import.meta.url === `file://${process.argv[1]}`) { main() }\n or an equivalent isMain / require.main === check \u2014 so vitest imports never trigger top-level code\n- A paired .test.ts or .spec.ts (DC-M1, vitest)\n- Business subprocess environment: SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION injected\n\n## Interaction & confirmation conventions (no blocking confirmation)\n- MSMs run in **user-less subprocesses** (spawn/execFile, 600s timeout) \u2014 **never** use\n readline / prompt / process.stdin etc. to block waiting for user input (it would hang until killed by the timeout)\n- When a second confirmation is needed: **directly return the confirmation request** (list the operation to perform and its impact + how to retry with the confirmation flag),\n returning a non-zero exit code (or an explicit hint)\n- After the agent confirms, **re-invoke the MSM with the confirmation flag** (e.g. --confirm / --yes / --force) to retry\n- Standard mode (two-phase):\n 1. First call: destructive/confirmation-requiring operation detected and no confirmation flag present \u2192 output the confirmation request (list operation/impact/rollback),\n exit non-zero (e.g. 1 user), **perform no changes**\n 2. After the agent evaluates, re-invoke: with the confirmation flag \u2192 execute and output the result\n- Applies to: delete/overwrite/push/batch operations and other irreversible or wide-impact operations\n- Anti-example (forbidden): readline waiting for user input, process.stdin.on('data') blocking waits\n (MSMs have no stdin interaction channel \u2014 they hang until the 600s timeout)\n\n## Quality checks (container_admin msm check, DC-M1~M4)\nDC-M1 has .test.ts/.spec.ts; DC-M2 has a main() guard (function main( / isMain / require.main === / import.meta.url);\nDC-M3 bidirectional: scripts unregistered + registry references missing scripts; DC-M4 path-type flags marked type:\"path\"\n\n## Self-description (protocol flags, first argument only)\nmsm(\"<name>\", [\"--list\"]) \u2014 list all MSMs\nmsm(\"<name>\", [\"--schema\", \"<n>\"]) \u2014 view a specific MSM's parameter schema\nmsm(\"<name>\", [\"--format=json\"]) \u2014 JSON output mode (remaining args passed through losslessly)\n";
21
21
  /** CCC 配置参考(对齐 osp ccc-config action) */
22
- export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"|\"guide\"]) \u6392\u67E5 + \u5927\u800C\u5168\u6307\u5357\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n \u25B8 \u4E3B\u52A8\u53D1\u9001\uFF08v1.30.9\uFF09\uFF1ACCC \u7528 msm(\"weixin-send\", [\"send\", \"--ccc\", \"<CCC>\", \"--user\", \"<\u522B\u540D|id>\", \"<\u6587\u672C>\"])\n \u7ED9\u7528\u6237\u53D1\u6D88\u606F\u2014\u2014\u7ECF dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3\uFF08**\u53EA\u7ED1 127.0.0.1**\uFF0Cplugin \u5168\u5C40\u914D\u7F6E ~/.dsh/serenity-hooks.json\n \u7684 weixinApi.port\uFF0C\u9ED8\u8BA4 3082\uFF1B\u516C\u7F51\u4E0D\u53EF\u8FBE\u6545\u65E0\u5BC6\u94A5\uFF09\u2192 weixin-bridge.sendProactiveText \u2192\n \u6210\u529F\u540E\u89E6\u53D1 outgoing hook\uFF08source=\"proactive\"\uFF09\u3002send-file \u4ECD\u7531 MSM \u76F4\u8FDE iLink \u4E0A\u4F20\u5E76\u81EA\u884C\u8865\u8BB0\u3002\n\n \u25B8 weixin.autoReplyWithLastMessage\uFF08v1.30.10\uFF0C\u5173\u95ED\u6865\u7684\u81EA\u52A8\u8F93\u51FA\uFF09\uFF1A\n \u7F3A\u7701 true = \u73B0\u72B6\uFF1A\u6865\u5728 agent \u4E00\u8F6E\u7ED3\u675F\u540E\u628A**\u6700\u7EC8 assistant \u6587\u672C**\u81EA\u52A8\u56DE\u53D1\u7528\u6237\uFF08source=\"reply\"\uFF09\u3002\n \u663E\u5F0F false = \u5173\u95ED\uFF1A\u6865**\u4E0D\u518D\u8F6C\u53D1\u6700\u7EC8\u6587\u672C**\uFF0C\u8F93\u51FA\u6743\u4EA4\u7ED9 agent\u2014\u2014\u6BCF\u8F6E\u5728\u7528\u6237\u6D88\u606F**\u672B\u5C3E**\u8FFD\u52A0\n **\u673A\u5236\u6807\u8BB0** [serenity:weixin-manual-output] + \u4E00\u884C\u5DF2\u586B\u597D\u53C2\u6570\u7684 weixin-send \u547D\u4EE4\n \uFF08CCC \u6839/\u8D26\u53F7/\u7528\u6237 id\uFF09\uFF0Cagent \u81EA\u5DF1\u51B3\u5B9A\u53D1\u4EC0\u4E48\u3001\u53D1\u51E0\u6761\uFF08\u5168\u90E8\u8BB0 source=\"proactive\"\uFF09\u3002\n **ACC \u53EA\u7ED9\u673A\u5236\u4E0E\u6570\u636E\uFF0C\u7EAA\u5F8B\u63AA\u8F9E\u5F52 CCC**\uFF08v1.30.16\uFF09\uFF1A\u600E\u4E48\u5199/\u5FC5\u987B\u600E\u4E48\u505A/\u540E\u679C\u662F\u4EC0\u4E48\u5199\u5728\n CCC \u89D2\u8272\u63D0\u793A\u8BCD\u6587\u4EF6\u91CC\uFF08\u5982 .opencode/skiff/<role>.md\uFF09\uFF0C\u53EF\u70ED\u6539\uFF08v1.30.15 \u8D77\u89D2\u8272\u63D0\u793A\u8BCD\u70ED\u91CD\u8F7D\uFF09\u3002\n \u673A\u68B0\u95F8\u95E8\uFF08v1.30.16\uFF09\uFF1A\u672C\u8F6E turn \u7ED3\u675F\u4ECD\u672A\u6210\u529F\u8C03\u7528 weixin-send \u2192 agent \u88AB\u6253\u56DE\uFF08\u22642 \u6B21\uFF0C\n \u63D0\u793A = \u6807\u8BB0 + \u4E8B\u5B9E\uFF09\uFF1B\u8FBE\u4E0A\u9650\u653E\u5F03\u5E76\u54CD\u4EAE\u544A\u8B66\u3002\u5931\u8D25\u8C03\u7528\u4E0D\u7B97\u5DF2\u9001\u8FBE\u3002\n \u8BED\u4E49\u8FB9\u754C\uFF08\u7528\u6237\u62CD\u677F\uFF09\uFF1A\u53EA\u5173\u6700\u7EC8\u6587\u672C\uFF1B\u7CFB\u7EDF\u7C7B\u6D88\u606F\uFF08\u65B0\u5BF9\u8BDD\u901A\u77E5 / \u8BED\u97F3\u65E0\u6CD5\u89E3\u6790\u63D0\u793A\uFF09\u4FDD\u7559\uFF1B\n **agent \u4E00\u8F6E\u672A\u8F93\u51FA \u2192 \u9759\u9ED8\u4E0D\u515C\u5E95**\uFF08\u5426\u5219\u5F00\u5173\u5931\u53BB\u610F\u4E49\uFF09\uFF1Btyping \u6307\u793A\u4E0E incoming hook \u4E0D\u53D7\u5F71\u54CD\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"autoReplyWithLastMessage\": true,\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n \u00B7 source=\"reply\"\uFF08\u7F3A\u7701\uFF09= \u56DE\u590D\u7528\u6237\u6D88\u606F\uFF1Bsource=\"proactive\"\uFF08v1.30.9\uFF09= bot \u4E3B\u52A8\u53D1\u8D77\n \uFF08CCC \u7ECF weixin-send MSM \u2192 dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3 \u2192 sendProactiveText\uFF09\n \u00B7 file\uFF08\u4EC5 send-file \u8865\u8BB0\uFF09\uFF1A{ \"name\": \"\u62A5\u544A.pdf\", \"size\": 12345, \"caption\": \"\u53EF\u9009\" }\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\", \"source\": \"reply\" | \"proactive\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
22
+ export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"|\"guide\"]) \u6392\u67E5 + \u5927\u800C\u5168\u6307\u5357\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n \u25B8 \u4E3B\u52A8\u53D1\u9001\uFF08v1.30.9\uFF09\uFF1ACCC \u7528 msm(\"weixin-send\", [\"send\", \"--ccc\", \"<CCC>\", \"--user\", \"<\u522B\u540D|id>\", \"<\u6587\u672C>\"])\n \u7ED9\u7528\u6237\u53D1\u6D88\u606F\u2014\u2014\u7ECF dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3\uFF08**\u53EA\u7ED1 127.0.0.1**\uFF0Cplugin \u5168\u5C40\u914D\u7F6E ~/.dsh/serenity-hooks.json\n \u7684 weixinApi.port\uFF0C\u9ED8\u8BA4 3082\uFF1B\u516C\u7F51\u4E0D\u53EF\u8FBE\u6545\u65E0\u5BC6\u94A5\uFF09\u2192 weixin-bridge.sendProactiveText \u2192\n \u6210\u529F\u540E\u89E6\u53D1 outgoing hook\uFF08source=\"proactive\"\uFF09\u3002send-file \u4ECD\u7531 MSM \u76F4\u8FDE iLink \u4E0A\u4F20\u5E76\u81EA\u884C\u8865\u8BB0\u3002\n\n \u25B8 weixin.autoReplyWithLastMessage\uFF08v1.30.10\uFF0C\u5173\u95ED\u6865\u7684\u81EA\u52A8\u8F93\u51FA\uFF09\uFF1A\n \u7F3A\u7701 true = \u73B0\u72B6\uFF1A\u6865\u5728 agent \u4E00\u8F6E\u7ED3\u675F\u540E\u628A**\u6700\u7EC8 assistant \u6587\u672C**\u81EA\u52A8\u56DE\u53D1\u7528\u6237\uFF08source=\"reply\"\uFF09\u3002\n \u663E\u5F0F false = \u5173\u95ED\uFF1A\u6865**\u4E0D\u518D\u8F6C\u53D1\u6700\u7EC8\u6587\u672C**\uFF0C\u8F93\u51FA\u6743\u4EA4\u7ED9 agent\u2014\u2014\u6BCF\u8F6E\u5728\u7528\u6237\u6D88\u606F**\u672B\u5C3E**\u8FFD\u52A0\n **\u673A\u5236\u6807\u8BB0** [serenity:weixin-manual-output] + \u4E00\u884C\u5DF2\u586B\u597D\u53C2\u6570\u7684 weixin-send \u547D\u4EE4\n \uFF08CCC \u6839/\u8D26\u53F7/\u7528\u6237 id\uFF09\uFF0Cagent \u81EA\u5DF1\u51B3\u5B9A\u53D1\u4EC0\u4E48\u3001\u53D1\u51E0\u6761\uFF08\u5168\u90E8\u8BB0 source=\"proactive\"\uFF09\u3002\n **ACC \u53EA\u7ED9\u673A\u5236\u4E0E\u6570\u636E\uFF0C\u7EAA\u5F8B\u63AA\u8F9E\u5F52 CCC**\uFF08v1.30.16\uFF09\uFF1A\u600E\u4E48\u5199/\u5FC5\u987B\u600E\u4E48\u505A/\u540E\u679C\u662F\u4EC0\u4E48\u5199\u5728\n CCC \u89D2\u8272\u63D0\u793A\u8BCD\u6587\u4EF6\u91CC\uFF08\u5982 .opencode/skiff/<role>.md\uFF09\uFF0C\u53EF\u70ED\u6539\uFF08v1.30.15 \u8D77\u89D2\u8272\u63D0\u793A\u8BCD\u70ED\u91CD\u8F7D\uFF09\u3002\n \u673A\u68B0\u95F8\u95E8\uFF08v1.30.16\uFF09\uFF1A\u672C\u8F6E turn \u7ED3\u675F\u4ECD\u672A\u6210\u529F\u8C03\u7528 weixin-send \u2192 agent \u88AB\u6253\u56DE\uFF08\u22642 \u6B21\uFF0C\n \u63D0\u793A = \u6807\u8BB0 + \u4E8B\u5B9E\uFF09\uFF1B\u8FBE\u4E0A\u9650\u653E\u5F03\u5E76\u54CD\u4EAE\u544A\u8B66\u3002\u5931\u8D25\u8C03\u7528\u4E0D\u7B97\u5DF2\u9001\u8FBE\u3002\n \u8BED\u4E49\u8FB9\u754C\uFF08\u7528\u6237\u62CD\u677F\uFF09\uFF1A\u53EA\u5173\u6700\u7EC8\u6587\u672C\uFF1B\u7CFB\u7EDF\u7C7B\u6D88\u606F\uFF08\u65B0\u5BF9\u8BDD\u901A\u77E5 / \u8BED\u97F3\u65E0\u6CD5\u89E3\u6790\u63D0\u793A\uFF09\u4FDD\u7559\uFF1B\n typing \u6307\u793A\u4E0E incoming hook \u4E0D\u53D7\u5F71\u54CD\u3002\n\n \u25B8 weixin.fallbackOnNoSend\uFF08v1.30.17\uFF0C\u624B\u52A8\u6A21\u5F0F\u7684**\u515C\u5E95**\uFF1B\u7F3A\u7701 false\uFF09\uFF1A\n \u4EC5\u5728 autoReplyWithLastMessage=false \u65F6\u751F\u6548\u3002true \u2192 \u4E00\u8F6E\u7ED3\u675F\u65F6\u82E5 agent **\u4E00\u6B21\u90FD\u6CA1\u6210\u529F**\n \u8C03\u7528 weixin-send\uFF08\u95F8\u95E8\u6253\u56DE\u7528\u5C3D\uFF09\uFF0C\u6865\u628A\u8BE5\u8F6E\u6700\u7EC8\u6587\u672C\u8F6C\u53D1\u7ED9\u7528\u6237\uFF08\u8BB0\u5F55 source=\"reply-fallback\"\uFF0C\n \u65E5\u5FD7\u7559\u54CD\u4EAE\u544A\u8B66\uFF09\u2014\u2014\u4FDD\u8BC1\"\u4E0D\u4E22\u6D88\u606F\"\u3002agent \u81EA\u5DF1\u53D1\u8FC7 \u2192 \u4E0D\u515C\u5E95\uFF08\u8F93\u51FA\u6743\u4ECD\u5728 agent\uFF09\u3002\n \u4E3A\u4EC0\u4E48\u9700\u8981\uFF08\u5B9E\u8BC1\uFF09\uFF1Av1.30.16 \u95F8\u95E8\u4F1A\u6253\u56DE \u22642 \u6B21\uFF0C\u4F46\u5B9E\u6D4B\u67D0\u6A21\u578B\u5728**\u5BD2\u6684\u7C7B\u6D88\u606F**\u4E0A\u8FDE\u7EED 4 \u8F6E\u3001\n \u8DE8 3 \u7248 CCC \u63D0\u793A\u8BCD\u4ECD\u4E0D\u8C03\u5DE5\u5177 \u2192 \u6253\u56DE\u7528\u5C3D\u540E\u7528\u6237\u4EC0\u4E48\u90FD\u6536\u4E0D\u5230\uFF1B\u63D0\u793A\u8BCD\u4E0E\u95F8\u95E8\u5747\u5DF2\u6392\u9664\u3002\n \u4EE3\u4EF7\uFF08\u663E\u5F0F\uFF09\uFF1A\u89D2\u8272\u5931\u53BB\"\u6545\u610F\u9759\u9ED8\"\u80FD\u529B\uFF08\u5B83\u603B\u4F1A\u4EA7\u51FA\u4E00\u6BB5\u6700\u7EC8\u6587\u672C\uFF09\u2192 \u9700\u4E25\u683C\u9759\u9ED8\u7684\u89D2\u8272\u4FDD\u6301\u7F3A\u7701\u3002\n \u4E09\u6001\uFF1A\u2460 autoReplyWithLastMessage=true\uFF08\u7F3A\u7701\uFF09\u6865\u603B\u662F\u56DE\u53D1\uFF1B\u2461 =false + fallbackOnNoSend=true\n = agent \u4F18\u5148\u3001\u6865\u515C\u5E95\uFF08\u63A8\u8350\uFF09\uFF1B\u2462 \u4E24\u8005\u7686 false\uFF08\u7F3A\u7701\uFF09= \u4E25\u683C\u9759\u9ED8\uFF08v1.30.10 \u8BED\u4E49\uFF09\u3002\n \u26A0\uFE0F \u515C\u5E95\u53EA\u5728**\u95F8\u95E8\u88C5\u914D\u6210\u529F**\u65F6\u751F\u6548\uFF08\u672A\u88C5\u914D\u5219\u65E0\u6CD5\u5224\u5B9A\u662F\u5426\u53D1\u9001\u8FC7 \u2192 \u4E0D\u5192\u91CD\u590D\u53D1\u9001\u98CE\u9669\uFF09\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"autoReplyWithLastMessage\": true,\n \"fallbackOnNoSend\": false,\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n \u00B7 source=\"reply\"\uFF08\u7F3A\u7701\uFF09= \u56DE\u590D\u7528\u6237\u6D88\u606F\uFF1Bsource=\"proactive\"\uFF08v1.30.9\uFF09= bot \u4E3B\u52A8\u53D1\u8D77\n \uFF08CCC \u7ECF weixin-send MSM \u2192 dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3 \u2192 sendProactiveText\uFF09\uFF1B\n source=\"reply-fallback\"\uFF08v1.30.17\uFF09= \u624B\u52A8\u6A21\u5F0F\u4E0B agent \u672C\u8F6E\u672A\u53D1\u9001 \u2192 \u6865\u515C\u5E95\u8F6C\u53D1\u6700\u7EC8\u6587\u672C\n \u00B7 file\uFF08\u4EC5 send-file \u8865\u8BB0\uFF09\uFF1A{ \"name\": \"\u62A5\u544A.pdf\", \"size\": 12345, \"caption\": \"\u53EF\u9009\" }\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\", \"source\": \"reply\" | \"proactive\" | \"reply-fallback\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
23
23
  export interface MsmFlag {
24
24
  name: string;
25
25
  type?: string;
@@ -37,6 +37,21 @@ export declare function resetWeixinTypingCache(): void;
37
37
  * ```
38
38
  */
39
39
  export declare function weixinManualOutputMarker(root: string, accountId: string, userId: string): string;
40
+ /**
41
+ * 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
42
+ *
43
+ * 四个条件同时成立才兜底:
44
+ * ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
45
+ * ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
46
+ * ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
47
+ * ④ 有最终文本可转发(空文本无意义)
48
+ */
49
+ export declare function manualOutputFallbackNeeded(input: {
50
+ fallbackOnNoSend?: boolean;
51
+ guardActive: boolean;
52
+ sentThisTurn: boolean;
53
+ answer: string;
54
+ }): boolean;
40
55
  /**
41
56
  * 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
42
57
  *
@@ -48,17 +48,19 @@ export interface WeixinHookIncomingEvent extends WeixinHookEventBase {
48
48
  media: WeixinHookMediaRef[];
49
49
  };
50
50
  }
51
+ /**
52
+ * outgoing 事件来源:
53
+ * - `reply`(缺省,向后兼容)= 收到用户消息后的回复
54
+ * - `proactive` = 桥被调用主动发给指定用户(CCC MSM 经本机入口发起)
55
+ * - `reply-fallback`(v1.30.17)= 手动输出模式下 agent 本轮一次都没发送成功 → 桥兜底转发其最终文本
56
+ */
57
+ export type WeixinOutgoingSource = 'reply' | 'proactive' | 'reply-fallback';
51
58
  /** outgoing 事件(bot → 用户):回复文本(已剥离 think) */
52
59
  export interface WeixinHookOutgoingEvent extends WeixinHookEventBase {
53
60
  event: 'outgoing';
54
61
  reply: string;
55
- /**
56
- * 触发来源(v1.30.9,S142 用户需求"微信桥支持被调用发消息"):
57
- * - `reply`(缺省,向后兼容)= 收到用户消息后的回复
58
- * - `proactive` = 桥被调用主动发给指定用户(CCC MSM 经本机入口发起)
59
- * 记录侧可据此区分"对话回复"与"主动留言"。
60
- */
61
- source?: 'reply' | 'proactive';
62
+ /** 触发来源(v1.30.9 引入 `proactive`;v1.30.17 增 `reply-fallback`) */
63
+ source?: WeixinOutgoingSource;
62
64
  }
63
65
  /** 全部 hook 事件(判别联合) */
64
66
  export type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
@@ -82,8 +84,8 @@ export interface OutgoingHookInput {
82
84
  sessionId: string;
83
85
  role: string;
84
86
  reply: string;
85
- /** 缺省 'reply'(对话回复);'proactive' = 桥被调用主动发送 */
86
- source?: 'reply' | 'proactive';
87
+ /** 缺省 'reply'(对话回复);'proactive' = 主动发送;'reply-fallback' = 手动模式兜底转发 */
88
+ source?: WeixinOutgoingSource;
87
89
  }
88
90
  /** 构造 incoming 事件对象(纯函数,可测) */
89
91
  export declare function buildIncomingHookEvent(input: IncomingHookInput): WeixinHookIncomingEvent;
@@ -35,8 +35,15 @@ export declare function noteManualOutputSession(sessionId: string, session: Manu
35
35
  export declare function forgetManualOutputSession(sessionId: string): void;
36
36
  /** 是否手动模式会话(测试/诊断用) */
37
37
  export declare function isManualOutputSession(sessionId: string): boolean;
38
- /** 本轮是否已成功发送(测试/诊断用) */
38
+ /** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
39
39
  export declare function hasSentThisTurn(sessionId: string): boolean;
40
+ /**
41
+ * 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
42
+ * 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
43
+ */
44
+ export declare function clearSentThisTurn(sessionId: string): void;
45
+ /** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
46
+ export declare function isWeixinOutputGuardActive(): boolean;
40
47
  /** 工具调用是否为「成功的 weixin-send」 */
41
48
  export declare function isSuccessfulWeixinSend(exec: unknown, result: unknown): boolean;
42
49
  /** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
@@ -348,6 +348,7 @@ function readWeixinSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
348
348
  botType: typeof w.botType === "string" && w.botType !== "" ? w.botType : void 0,
349
349
  hook: typeof w.hook === "string" && w.hook.trim() !== "" ? w.hook.trim() : void 0,
350
350
  autoReplyWithLastMessage: w.autoReplyWithLastMessage !== false,
351
+ fallbackOnNoSend: w.fallbackOnNoSend === true,
351
352
  accounts: Array.isArray(w.accounts) ? w.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.accountId === "string" && a.accountId !== "").map((a) => ({
352
353
  accountId: a.accountId,
353
354
  name: typeof a.name === "string" ? a.name : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.30.16",
3
+ "version": "1.30.17",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:10 个工具(container_fs/logbook/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/autopilot-trajectory)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/自主巡航。适配 DSH 0.1.2-rc.1。",
5
5
  "license": "MIT",
6
6
  "repository": {