@shgroup/dsh-serenity-hooks 1.30.15 → 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 +1 -1
- package/lib/ccc.d.ts +17 -0
- package/lib/index.js +236 -48
- package/lib/msm-ops.d.ts +1 -1
- package/lib/weixin-bridge.d.ts +31 -7
- package/lib/weixin-hook.d.ts +11 -9
- package/lib/weixin-output-guard.d.ts +57 -0
- package/lib/{weixin-route-BD_yYAZz.js → weixin-route-DIaUYipW.js} +1 -0
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.30.
|
|
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-
|
|
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";
|
|
@@ -1851,17 +1851,33 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1851
1851
|
|
|
1852
1852
|
▸ weixin.autoReplyWithLastMessage(v1.30.10,关闭桥的自动输出):
|
|
1853
1853
|
缺省 true = 现状:桥在 agent 一轮结束后把**最终 assistant 文本**自动回发用户(source="reply")。
|
|
1854
|
-
显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent
|
|
1855
|
-
|
|
1856
|
-
|
|
1854
|
+
显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent——每轮在用户消息**末尾**追加
|
|
1855
|
+
**机制标记** [serenity:weixin-manual-output] + 一行已填好参数的 weixin-send 命令
|
|
1856
|
+
(CCC 根/账号/用户 id),agent 自己决定发什么、发几条(全部记 source="proactive")。
|
|
1857
|
+
**ACC 只给机制与数据,纪律措辞归 CCC**(v1.30.16):怎么写/必须怎么做/后果是什么写在
|
|
1858
|
+
CCC 角色提示词文件里(如 .opencode/skiff/<role>.md),可热改(v1.30.15 起角色提示词热重载)。
|
|
1859
|
+
机械闸门(v1.30.16):本轮 turn 结束仍未成功调用 weixin-send → agent 被打回(≤2 次,
|
|
1860
|
+
提示 = 标记 + 事实);达上限放弃并响亮告警。失败调用不算已送达。
|
|
1857
1861
|
语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
|
|
1858
|
-
|
|
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
|
+
⚠️ 兜底只在**闸门装配成功**时生效(未装配则无法判定是否发送过 → 不冒重复发送风险)。
|
|
1859
1874
|
|
|
1860
1875
|
Config:
|
|
1861
1876
|
{ "weixin": {
|
|
1862
1877
|
"enabled": true,
|
|
1863
1878
|
"hook": "scripts/weixin-message-hook.ts",
|
|
1864
1879
|
"autoReplyWithLastMessage": true,
|
|
1880
|
+
"fallbackOnNoSend": false,
|
|
1865
1881
|
"accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
|
|
1866
1882
|
"routes": [{ "user": "*", "role": "zhaocai" }]
|
|
1867
1883
|
} }
|
|
@@ -1874,7 +1890,8 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1874
1890
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1875
1891
|
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1876
1892
|
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1877
|
-
(CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText
|
|
1893
|
+
(CCC 经 weixin-send MSM → dsp 主动发送入口 → sendProactiveText);
|
|
1894
|
+
source="reply-fallback"(v1.30.17)= 手动模式下 agent 本轮未发送 → 桥兜底转发最终文本
|
|
1878
1895
|
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1879
1896
|
|
|
1880
1897
|
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
@@ -1889,7 +1906,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1889
1906
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1890
1907
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1891
1908
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
1892
|
-
"reply": "已记录(纯文本)", "source": "reply" | "proactive" }
|
|
1909
|
+
"reply": "已记录(纯文本)", "source": "reply" | "proactive" | "reply-fallback" }
|
|
1893
1910
|
|
|
1894
1911
|
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1895
1912
|
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
@@ -3747,7 +3764,7 @@ var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
|
|
|
3747
3764
|
shouldWake: () => shouldWake,
|
|
3748
3765
|
wakeHistoryFor: () => wakeHistoryFor
|
|
3749
3766
|
});
|
|
3750
|
-
const PLUGIN_SOURCE$
|
|
3767
|
+
const PLUGIN_SOURCE$5 = {
|
|
3751
3768
|
kind: "plugin",
|
|
3752
3769
|
plugin: "dsh-serenity-hooks"
|
|
3753
3770
|
};
|
|
@@ -3985,7 +4002,7 @@ async function performAutopilotWake(ctx, root, settings, opts = {}) {
|
|
|
3985
4002
|
type: "text",
|
|
3986
4003
|
text: message
|
|
3987
4004
|
}],
|
|
3988
|
-
source: PLUGIN_SOURCE$
|
|
4005
|
+
source: PLUGIN_SOURCE$5
|
|
3989
4006
|
}));
|
|
3990
4007
|
return {
|
|
3991
4008
|
ok: true,
|
|
@@ -4864,7 +4881,7 @@ function readContextPressure(ctx, session) {
|
|
|
4864
4881
|
return null;
|
|
4865
4882
|
}
|
|
4866
4883
|
}
|
|
4867
|
-
const PLUGIN_SOURCE$
|
|
4884
|
+
const PLUGIN_SOURCE$4 = {
|
|
4868
4885
|
kind: "plugin",
|
|
4869
4886
|
plugin: "dsh-serenity-hooks"
|
|
4870
4887
|
};
|
|
@@ -4928,7 +4945,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
4928
4945
|
if (blocks.length === 0) return downstream;
|
|
4929
4946
|
const reminderAll = createUserMessage({
|
|
4930
4947
|
content: blocks,
|
|
4931
|
-
source: PLUGIN_SOURCE$
|
|
4948
|
+
source: PLUGIN_SOURCE$4
|
|
4932
4949
|
});
|
|
4933
4950
|
if (downstream.kind === "block") return {
|
|
4934
4951
|
kind: "block",
|
|
@@ -5976,7 +5993,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
5976
5993
|
}
|
|
5977
5994
|
return lines.join("\n");
|
|
5978
5995
|
}
|
|
5979
|
-
const PLUGIN_SOURCE$
|
|
5996
|
+
const PLUGIN_SOURCE$3 = {
|
|
5980
5997
|
kind: "plugin",
|
|
5981
5998
|
plugin: "dsh-serenity-hooks"
|
|
5982
5999
|
};
|
|
@@ -5993,7 +6010,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
5993
6010
|
}];
|
|
5994
6011
|
return createUserMessage({
|
|
5995
6012
|
content,
|
|
5996
|
-
source: PLUGIN_SOURCE$
|
|
6013
|
+
source: PLUGIN_SOURCE$3
|
|
5997
6014
|
});
|
|
5998
6015
|
}
|
|
5999
6016
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -6604,11 +6621,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6604
6621
|
sendJson$2(res, 400, { error: chk.error });
|
|
6605
6622
|
return;
|
|
6606
6623
|
}
|
|
6607
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6624
|
+
const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6608
6625
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6609
6626
|
const settings = readWeixinSettings(chk.root);
|
|
6610
6627
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6611
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6628
|
+
const { readWeixinCredential } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6612
6629
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6613
6630
|
accountId: a.accountId,
|
|
6614
6631
|
name: a.name ?? void 0,
|
|
@@ -6640,7 +6657,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6640
6657
|
const { fetchQRCode } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6641
6658
|
const { randomUUID } = await import("node:crypto");
|
|
6642
6659
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6643
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6660
|
+
const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6644
6661
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6645
6662
|
const loginKey = randomUUID();
|
|
6646
6663
|
weixinLogins.set(loginKey, {
|
|
@@ -6661,7 +6678,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6661
6678
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6662
6679
|
return;
|
|
6663
6680
|
}
|
|
6664
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6681
|
+
const { removeWeixinAccount } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6665
6682
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6666
6683
|
removeWeixinAccount(root, body.accountId);
|
|
6667
6684
|
syncCccBridge(ctx, root);
|
|
@@ -6674,7 +6691,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6674
6691
|
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6675
6692
|
return;
|
|
6676
6693
|
}
|
|
6677
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6694
|
+
const { saveWeixinRoutes } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6678
6695
|
const { readSkiffRoles } = await import("./skiff-role-BYvNnwv1.js").then((n) => n.u);
|
|
6679
6696
|
const roles = readSkiffRoles(root);
|
|
6680
6697
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
@@ -6687,7 +6704,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6687
6704
|
}
|
|
6688
6705
|
if (body.action === "set-enabled") {
|
|
6689
6706
|
const enabled = body.enabled === true;
|
|
6690
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6707
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6691
6708
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6692
6709
|
const settings = readWeixinSettings(root);
|
|
6693
6710
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -6734,7 +6751,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6734
6751
|
return;
|
|
6735
6752
|
}
|
|
6736
6753
|
const { pollQRStatus } = await import("./weixin-api-BMpljodH.js").then((n) => n.l);
|
|
6737
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6754
|
+
const { readWeixinSettings } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6738
6755
|
const settings = readWeixinSettings(login.root);
|
|
6739
6756
|
const status = await pollQRStatus({
|
|
6740
6757
|
baseUrl: void 0,
|
|
@@ -6749,7 +6766,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6749
6766
|
});
|
|
6750
6767
|
return;
|
|
6751
6768
|
}
|
|
6752
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
6769
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-DIaUYipW.js").then((n) => n.u);
|
|
6753
6770
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6754
6771
|
const accountId = nextWeixinAccountId(settings);
|
|
6755
6772
|
upsertWeixinAccount(login.root, {
|
|
@@ -7884,7 +7901,7 @@ var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
|
7884
7901
|
sanitizeFocusLine: () => sanitizeFocusLine,
|
|
7885
7902
|
stripAckSuffix: () => stripAckSuffix
|
|
7886
7903
|
});
|
|
7887
|
-
const PLUGIN_SOURCE$
|
|
7904
|
+
const PLUGIN_SOURCE$2 = {
|
|
7888
7905
|
kind: "plugin",
|
|
7889
7906
|
plugin: "dsh-serenity-hooks"
|
|
7890
7907
|
};
|
|
@@ -8174,7 +8191,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
8174
8191
|
type: "text",
|
|
8175
8192
|
text: `${eventToken("rebuild")} The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint.`
|
|
8176
8193
|
}],
|
|
8177
|
-
source: PLUGIN_SOURCE$
|
|
8194
|
+
source: PLUGIN_SOURCE$2
|
|
8178
8195
|
}));
|
|
8179
8196
|
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
8180
8197
|
sessionId: id,
|
|
@@ -8348,7 +8365,7 @@ function buildRebuke(hits) {
|
|
|
8348
8365
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
8349
8366
|
//#endregion
|
|
8350
8367
|
//#region src/output-guard-seam.ts
|
|
8351
|
-
const PLUGIN_SOURCE = {
|
|
8368
|
+
const PLUGIN_SOURCE$1 = {
|
|
8352
8369
|
kind: "plugin",
|
|
8353
8370
|
plugin: "dsh-serenity-hooks"
|
|
8354
8371
|
};
|
|
@@ -8409,7 +8426,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
8409
8426
|
type: "text",
|
|
8410
8427
|
text: buildRebuke(hits)
|
|
8411
8428
|
}],
|
|
8412
|
-
source: PLUGIN_SOURCE
|
|
8429
|
+
source: PLUGIN_SOURCE$1
|
|
8413
8430
|
}));
|
|
8414
8431
|
} catch (error) {
|
|
8415
8432
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -9175,6 +9192,125 @@ qInput.focus()
|
|
|
9175
9192
|
</html>`;
|
|
9176
9193
|
}
|
|
9177
9194
|
//#endregion
|
|
9195
|
+
//#region src/weixin-output-guard.ts
|
|
9196
|
+
const PLUGIN_SOURCE = {
|
|
9197
|
+
kind: "plugin",
|
|
9198
|
+
plugin: "dsh-serenity-hooks"
|
|
9199
|
+
};
|
|
9200
|
+
const manualSessions = /* @__PURE__ */ new Map();
|
|
9201
|
+
/**
|
|
9202
|
+
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
9203
|
+
* v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
|
|
9204
|
+
*/
|
|
9205
|
+
const sentThisTurn = /* @__PURE__ */ new Set();
|
|
9206
|
+
/** 连续打回计数(成功发送即清零) */
|
|
9207
|
+
const rebukeCounts = /* @__PURE__ */ new Map();
|
|
9208
|
+
/** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
|
|
9209
|
+
let guardActive = false;
|
|
9210
|
+
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
9211
|
+
function noteManualOutputSession(sessionId, session) {
|
|
9212
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
9213
|
+
manualSessions.set(sessionId, session);
|
|
9214
|
+
}
|
|
9215
|
+
/** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
|
|
9216
|
+
function forgetManualOutputSession(sessionId) {
|
|
9217
|
+
manualSessions.delete(sessionId);
|
|
9218
|
+
sentThisTurn.delete(sessionId);
|
|
9219
|
+
rebukeCounts.delete(sessionId);
|
|
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
|
+
}
|
|
9236
|
+
/** 从 agent / 会话对象取会话 id(形状宽容) */
|
|
9237
|
+
function sessionIdOf$1(value) {
|
|
9238
|
+
const v = value;
|
|
9239
|
+
const id = v?.session?.id ?? v?.id;
|
|
9240
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
9241
|
+
}
|
|
9242
|
+
/** 工具调用是否为「成功的 weixin-send」 */
|
|
9243
|
+
function isSuccessfulWeixinSend(exec, result) {
|
|
9244
|
+
const e = exec;
|
|
9245
|
+
if (e?.name !== "msm") return false;
|
|
9246
|
+
if (e.arguments?.name !== "weixin-send") return false;
|
|
9247
|
+
return result?.isError !== true;
|
|
9248
|
+
}
|
|
9249
|
+
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
9250
|
+
function buildManualOutputRebuke(session) {
|
|
9251
|
+
return [
|
|
9252
|
+
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
9253
|
+
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
9254
|
+
`msm("weixin-send", ["send", "--ccc", "${session.root}", "--account", "${session.accountId}", "--user", "${session.userId}", "<回复>"])`
|
|
9255
|
+
].join("\n");
|
|
9256
|
+
}
|
|
9257
|
+
/**
|
|
9258
|
+
* 装配闸门(index.ts apply 调用)。
|
|
9259
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff/ACP 临时会话)零干预。
|
|
9260
|
+
*/
|
|
9261
|
+
function registerWeixinOutputGuard(ctx) {
|
|
9262
|
+
try {
|
|
9263
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
9264
|
+
const downstream = await next();
|
|
9265
|
+
try {
|
|
9266
|
+
const sessionId = sessionIdOf$1(exec?.agent);
|
|
9267
|
+
if (sessionId && manualSessions.has(sessionId) && isSuccessfulWeixinSend(exec, result)) {
|
|
9268
|
+
sentThisTurn.add(sessionId);
|
|
9269
|
+
rebukeCounts.delete(sessionId);
|
|
9270
|
+
}
|
|
9271
|
+
} catch {}
|
|
9272
|
+
return downstream;
|
|
9273
|
+
});
|
|
9274
|
+
} catch {
|
|
9275
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:tools/post-execute 不可用");
|
|
9276
|
+
return;
|
|
9277
|
+
}
|
|
9278
|
+
try {
|
|
9279
|
+
ctx.on("agent/turn-stopping", (payload) => {
|
|
9280
|
+
const agent = payload?.agent;
|
|
9281
|
+
if (!agent) return;
|
|
9282
|
+
const sessionId = sessionIdOf$1(agent);
|
|
9283
|
+
if (!sessionId) return;
|
|
9284
|
+
const session = manualSessions.get(sessionId);
|
|
9285
|
+
if (!session) return;
|
|
9286
|
+
if (sentThisTurn.has(sessionId)) return;
|
|
9287
|
+
const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
|
|
9288
|
+
if (count > 2) {
|
|
9289
|
+
rebukeCounts.delete(sessionId);
|
|
9290
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
|
|
9291
|
+
return;
|
|
9292
|
+
}
|
|
9293
|
+
rebukeCounts.set(sessionId, count);
|
|
9294
|
+
try {
|
|
9295
|
+
agent.steer(createUserMessage({
|
|
9296
|
+
content: [{
|
|
9297
|
+
type: "text",
|
|
9298
|
+
text: buildManualOutputRebuke(session)
|
|
9299
|
+
}],
|
|
9300
|
+
source: PLUGIN_SOURCE
|
|
9301
|
+
}));
|
|
9302
|
+
console.log(`[serenity-hooks] weixin 输出闸门:打回 ${count}/2(session=${sessionId}, turn=${payload.turn ?? "?"})`);
|
|
9303
|
+
} catch (error) {
|
|
9304
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门 steer 失败: ${String(error?.message ?? error)}`);
|
|
9305
|
+
}
|
|
9306
|
+
});
|
|
9307
|
+
} catch {
|
|
9308
|
+
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
|
|
9309
|
+
return;
|
|
9310
|
+
}
|
|
9311
|
+
guardActive = true;
|
|
9312
|
+
}
|
|
9313
|
+
//#endregion
|
|
9178
9314
|
//#region src/weixin-hook.ts
|
|
9179
9315
|
/**
|
|
9180
9316
|
* weixin-hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
|
|
@@ -9222,7 +9358,7 @@ function buildOutgoingHookEvent(input) {
|
|
|
9222
9358
|
sessionId: input.sessionId,
|
|
9223
9359
|
role: input.role,
|
|
9224
9360
|
reply: input.reply,
|
|
9225
|
-
...input.source
|
|
9361
|
+
...input.source && input.source !== "reply" ? { source: input.source } : {}
|
|
9226
9362
|
};
|
|
9227
9363
|
}
|
|
9228
9364
|
/**
|
|
@@ -9336,13 +9472,14 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
9336
9472
|
//#region src/weixin-bridge.ts
|
|
9337
9473
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
9338
9474
|
handleIncoming: () => handleIncoming,
|
|
9475
|
+
manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
|
|
9339
9476
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9340
9477
|
sendProactiveText: () => sendProactiveText,
|
|
9341
9478
|
stopAllBridges: () => stopAllBridges,
|
|
9342
9479
|
stopCccBridge: () => stopCccBridge,
|
|
9343
9480
|
syncCccBridge: () => syncCccBridge,
|
|
9344
9481
|
weixinBridgeStatus: () => weixinBridgeStatus,
|
|
9345
|
-
|
|
9482
|
+
weixinManualOutputMarker: () => weixinManualOutputMarker
|
|
9346
9483
|
});
|
|
9347
9484
|
const bridges = /* @__PURE__ */ new Map();
|
|
9348
9485
|
/** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
|
|
@@ -9422,28 +9559,38 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
|
|
|
9422
9559
|
}
|
|
9423
9560
|
}
|
|
9424
9561
|
/**
|
|
9425
|
-
*
|
|
9562
|
+
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
|
|
9563
|
+
* 要让 CCC 定义"重构)。
|
|
9564
|
+
*
|
|
9565
|
+
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
|
|
9566
|
+
* (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
|
|
9567
|
+
* CCC 的角色提示词文件里(如 `.opencode/skiff/zhaocai.md` 的「输出通道」节),CCC 可随时改,
|
|
9568
|
+
* 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
|
|
9426
9569
|
*
|
|
9427
|
-
*
|
|
9428
|
-
*
|
|
9429
|
-
*
|
|
9570
|
+
* 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
|
|
9571
|
+
* "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
|
|
9572
|
+
* 也符合"ACC 管机制、CCC 管内容"的归属二分。
|
|
9430
9573
|
*
|
|
9431
|
-
*
|
|
9432
|
-
*
|
|
9574
|
+
* 标记格式(稳定,供 CCC 提示词引用):
|
|
9575
|
+
* ```
|
|
9576
|
+
* [serenity:weixin-manual-output]
|
|
9577
|
+
* msm("weixin-send", ["send", "--ccc", "<root>", "--account", "<account>", "--user", "<user>", "<回复>"])
|
|
9578
|
+
* ```
|
|
9433
9579
|
*/
|
|
9434
|
-
function
|
|
9435
|
-
return [
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9580
|
+
function weixinManualOutputMarker(root, accountId, userId) {
|
|
9581
|
+
return ["[serenity:weixin-manual-output]", `msm("weixin-send", ["send", "--ccc", "${root}", "--account", "${accountId}", "--user", "${userId}", "<回复>"])`].join("\n");
|
|
9582
|
+
}
|
|
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 !== "";
|
|
9447
9594
|
}
|
|
9448
9595
|
/**
|
|
9449
9596
|
* 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
|
|
@@ -9562,9 +9709,18 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9562
9709
|
if (!ensureWorkspacePromptSection(ref.agent, sessionId)) parts.push(workspaceTrajectoryLine(activeWorkspace.mdPath));
|
|
9563
9710
|
}
|
|
9564
9711
|
const manualOutput = settings.autoReplyWithLastMessage === false;
|
|
9565
|
-
if (manualOutput) parts.push(weixinManualOutputLine(root, accountId, fromUserId));
|
|
9566
9712
|
if (text) parts.push(text);
|
|
9567
9713
|
parts.push(...mediaNotes, ...degradedNotes);
|
|
9714
|
+
if (manualOutput) {
|
|
9715
|
+
clearSentThisTurn(sessionId);
|
|
9716
|
+
noteManualOutputSession(sessionId, {
|
|
9717
|
+
root,
|
|
9718
|
+
accountId,
|
|
9719
|
+
userId: fromUserId,
|
|
9720
|
+
role: roleName
|
|
9721
|
+
});
|
|
9722
|
+
parts.push(weixinManualOutputMarker(root, accountId, fromUserId));
|
|
9723
|
+
} else forgetManualOutputSession(sessionId);
|
|
9568
9724
|
const question = parts.join("\n");
|
|
9569
9725
|
const hookRel = settings.hook;
|
|
9570
9726
|
if (hookRel) invokeWeixinHook(root, hookRel, buildIncomingHookEvent({
|
|
@@ -9579,7 +9735,35 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9579
9735
|
console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9580
9736
|
});
|
|
9581
9737
|
const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
|
|
9582
|
-
if (manualOutput)
|
|
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
|
+
}
|
|
9583
9767
|
if (answer === "") return;
|
|
9584
9768
|
const reply = markdownToPlainText(stripThink(answer));
|
|
9585
9769
|
await sendTextMessage({
|
|
@@ -10138,6 +10322,9 @@ function cleanupSessionState(sessionId) {
|
|
|
10138
10322
|
try {
|
|
10139
10323
|
clearActiveSessionInfo(sessionId);
|
|
10140
10324
|
} catch {}
|
|
10325
|
+
try {
|
|
10326
|
+
forgetManualOutputSession(sessionId);
|
|
10327
|
+
} catch {}
|
|
10141
10328
|
}
|
|
10142
10329
|
/**
|
|
10143
10330
|
* 装配生命周期订阅与资源拆卸。
|
|
@@ -10449,6 +10636,7 @@ function apply(ctx, config) {
|
|
|
10449
10636
|
registerAutopilot(ctx);
|
|
10450
10637
|
registerWeixinBridge(ctx);
|
|
10451
10638
|
registerWeixinSendApi(ctx);
|
|
10639
|
+
registerWeixinOutputGuard(ctx);
|
|
10452
10640
|
registerLifecycle(ctx);
|
|
10453
10641
|
if (config.webFetch?.enabled !== false) registerWebFetchProvider(ctx);
|
|
10454
10642
|
}
|
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 question \u524D\u7F00\u6CE8\u5165\n \u300CReply Output (manual mode)\u300D\u7EAA\u5F8B\u5757\uFF08\u542B\u53EF\u76F4\u63A5\u7167\u6284\u7684 weixin-send \u547D\u4EE4\uFF0CCCC \u6839/\u8D26\u53F7/\u7528\u6237 id\n \u5DF2\u586B\u597D\uFF09\uFF0Cagent \u81EA\u5DF1\u51B3\u5B9A\u53D1\u4EC0\u4E48\u3001\u53D1\u51E0\u6761\uFF08\u5168\u90E8\u8BB0 source=\"proactive\"\uFF09\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;
|
package/lib/weixin-bridge.d.ts
CHANGED
|
@@ -18,16 +18,40 @@ import { type WeixinMessage } from './weixin-api.js';
|
|
|
18
18
|
/** 测试辅助:清空 typing_ticket 缓存(生产零调用) */
|
|
19
19
|
export declare function resetWeixinTypingCache(): void;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
|
|
22
|
+
* 要让 CCC 定义"重构)。
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
|
|
25
|
+
* (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
|
|
26
|
+
* CCC 的角色提示词文件里(如 `.opencode/skiff/zhaocai.md` 的「输出通道」节),CCC 可随时改,
|
|
27
|
+
* 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
|
|
30
|
+
* "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
|
|
31
|
+
* 也符合"ACC 管机制、CCC 管内容"的归属二分。
|
|
32
|
+
*
|
|
33
|
+
* 标记格式(稳定,供 CCC 提示词引用):
|
|
34
|
+
* ```
|
|
35
|
+
* [serenity:weixin-manual-output]
|
|
36
|
+
* msm("weixin-send", ["send", "--ccc", "<root>", "--account", "<account>", "--user", "<user>", "<回复>"])
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
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
|
+
* ④ 有最终文本可转发(空文本无意义)
|
|
29
48
|
*/
|
|
30
|
-
export declare function
|
|
49
|
+
export declare function manualOutputFallbackNeeded(input: {
|
|
50
|
+
fallbackOnNoSend?: boolean;
|
|
51
|
+
guardActive: boolean;
|
|
52
|
+
sentThisTurn: boolean;
|
|
53
|
+
answer: string;
|
|
54
|
+
}): boolean;
|
|
31
55
|
/**
|
|
32
56
|
* 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
|
|
33
57
|
*
|
package/lib/weixin-hook.d.ts
CHANGED
|
@@ -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
|
-
|
|
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?:
|
|
87
|
+
/** 缺省 'reply'(对话回复);'proactive' = 主动发送;'reply-fallback' = 手动模式兜底转发 */
|
|
88
|
+
source?: WeixinOutgoingSource;
|
|
87
89
|
}
|
|
88
90
|
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
89
91
|
export declare function buildIncomingHookEvent(input: IncomingHookInput): WeixinHookIncomingEvent;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weixin-output-guard.ts — 手动输出模式的**机械闸门**(v1.30.16,S142 用户"LLM 不听")
|
|
3
|
+
*
|
|
4
|
+
* 问题(实证):`weixin.autoReplyWithLastMessage: false` 时桥不回发最终文本,输出权归 agent
|
|
5
|
+
* (必须自己调 `msm("weixin-send", …)`)。提示词层面已说明,但用户实测 **LLM 仍经常不发**——
|
|
6
|
+
* 结果用户什么都收不到(静默)。这类"提示词软约束失效"正是 ACC 该用机械守卫兜底的场景
|
|
7
|
+
* (与 output-guard 同一套思路:turn-stopping + agent.steer)。
|
|
8
|
+
*
|
|
9
|
+
* 本模块做三件事(全部是机制,不含任何纪律措辞——措辞归 CCC 角色提示词):
|
|
10
|
+
* ① `noteManualOutputSession`:桥每轮把手动模式会话登记进来(含 root/account/user/role)
|
|
11
|
+
* ② 订阅 `tools/post-execute`:观察本轮是否真的调用了 `msm` 且首个参数为 `weixin-send`
|
|
12
|
+
* (**只在执行成功时**计入——失败调用不算"已送达")
|
|
13
|
+
* ③ 订阅 `agent/turn-stopping`:本轮结束仍没有成功发送 → `agent.steer` 打回一次(≤2 次),
|
|
14
|
+
* 提示内容 = **标记 + 事实**("本轮未发送,用户什么都没收到"),具体怎么做由 CCC 角色
|
|
15
|
+
* 提示词定义(与 per-message 标记同名 `[serenity:weixin-manual-output]`,模型能对上)。
|
|
16
|
+
*
|
|
17
|
+
* 为什么不直接替 agent 发送(R↓):用户拍板的语义是"输出权归 agent"——代发会让 agent 误以为
|
|
18
|
+
* 自己发过、也掩盖了"模型没遵守"这一事实;打回让模型自己完成,且留下可观测的日志。
|
|
19
|
+
*
|
|
20
|
+
* 达上限后放弃并**响亮告警**(不静默失败):宁可让日志里出现一条明确记录,也不要假装正常。
|
|
21
|
+
*/
|
|
22
|
+
import type { Context } from 'cordis';
|
|
23
|
+
/** 打回上限(超过则放弃并告警——避免与模型无限拉扯) */
|
|
24
|
+
export declare const WEIXIN_OUTPUT_REBUKE_MAX = 2;
|
|
25
|
+
/** 手动模式会话上下文(桥登记) */
|
|
26
|
+
export interface ManualOutputSession {
|
|
27
|
+
root: string;
|
|
28
|
+
accountId: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
role: string;
|
|
31
|
+
}
|
|
32
|
+
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
33
|
+
export declare function noteManualOutputSession(sessionId: string, session: ManualOutputSession): void;
|
|
34
|
+
/** 桥:非手动模式 / 会话失效 → 注销(闸门随之失效,零干预) */
|
|
35
|
+
export declare function forgetManualOutputSession(sessionId: string): void;
|
|
36
|
+
/** 是否手动模式会话(测试/诊断用) */
|
|
37
|
+
export declare function isManualOutputSession(sessionId: string): boolean;
|
|
38
|
+
/** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
|
|
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;
|
|
47
|
+
/** 工具调用是否为「成功的 weixin-send」 */
|
|
48
|
+
export declare function isSuccessfulWeixinSend(exec: unknown, result: unknown): boolean;
|
|
49
|
+
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
50
|
+
export declare function buildManualOutputRebuke(session: ManualOutputSession): string;
|
|
51
|
+
/** 测试辅助:清空全部状态 */
|
|
52
|
+
export declare function __resetWeixinOutputGuardForTest(): void;
|
|
53
|
+
/**
|
|
54
|
+
* 装配闸门(index.ts apply 调用)。
|
|
55
|
+
* 只对**已登记的手动模式会话**生效——其他会话(主舱/其他 skiff/ACP 临时会话)零干预。
|
|
56
|
+
*/
|
|
57
|
+
export declare function registerWeixinOutputGuard(ctx: Context): void;
|
|
@@ -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.
|
|
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": {
|