@shgroup/dsh-serenity-hooks 1.30.16 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dsh.plugin.json +1 -1
- package/lib/ccc.d.ts +20 -3
- package/lib/im-bridge.d.ts +127 -0
- package/lib/im-weixin.d.ts +19 -0
- package/lib/index.js +708 -109
- package/lib/msm-ops.d.ts +1 -1
- package/lib/seams/guards.d.ts +20 -0
- package/lib/tools/im-bridge.d.ts +18 -0
- package/lib/{weixin-api-BMpljodH.js → weixin-api-PjEhmjlZ.js} +98 -2
- package/lib/weixin-api.d.ts +21 -0
- package/lib/weixin-bridge.d.ts +35 -2
- package/lib/weixin-hook.d.ts +23 -9
- package/lib/weixin-output-guard.d.ts +20 -7
- package/lib/{weixin-route-BD_yYAZz.js → weixin-route-wWk4AIwV.js} +2 -1
- package/lib/weixin-send-api.d.ts +3 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3,14 +3,14 @@ import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as re
|
|
|
3
3
|
import { a as readSkiffRoles, d as systemPromptSource, i as isSkiffSessionId, l as roleToolWhitelist, o as resolveRoleSystemPrompt } from "./skiff-role-BYvNnwv1.js";
|
|
4
4
|
import { A as skiffRoleFor, C as listActiveHandymen, D as splitModel, E as requireWhitelistedModel, O as writeFailedStatus, S as handymanProgressPaths, T as readProgress, _ as unregisterSkiffSession, a as startSkiffDebugServer, b as HANDYMAN_GUIDE, c as askSkiff, d as ensureWorkspacePromptSection, f as getSkiffAgent, g as skiffTrajectoryEnabled, h as skiffSessionSnapshot, k as writeProgress, l as createSkiffAgent, m as skiffSessionInfo, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffMsmGate, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as workspaceTrajectoryLine, w as newStopToken, x as buildRoundPrompt, y as waitAgentIdle } from "./skiff-debug-C5ulZKCa.js";
|
|
5
5
|
import { i as hostSessions, n as hostInjected, o as hostWeb, r as hostService, s as hostWebServer, t as hostAgents } from "./access-dU_vG1q8.js";
|
|
6
|
-
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance,
|
|
6
|
+
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-wWk4AIwV.js";
|
|
7
7
|
import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as createSession, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as parseSessionContextFromEvents, n as readLastBound, o as clearActiveSessionInfo, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-BpTayaIS.js";
|
|
8
8
|
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-CP4uMJf_.js";
|
|
9
|
-
import { a as markdownToPlainText, c as
|
|
9
|
+
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-PjEhmjlZ.js";
|
|
10
10
|
import z from "@deepseek-ai/schemastery";
|
|
11
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
12
12
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
13
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
14
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
15
15
|
import { homedir, platform } from "node:os";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -421,10 +421,10 @@ function runCcFs(root, args) {
|
|
|
421
421
|
* 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
|
|
422
422
|
* 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
|
|
423
423
|
*/
|
|
424
|
-
function agentCwd$
|
|
424
|
+
function agentCwd$10(exec) {
|
|
425
425
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
426
426
|
}
|
|
427
|
-
function renderText$
|
|
427
|
+
function renderText$13(value) {
|
|
428
428
|
return [{
|
|
429
429
|
type: "text",
|
|
430
430
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -496,10 +496,10 @@ const ccFsTool = defineTool({
|
|
|
496
496
|
},
|
|
497
497
|
output: {
|
|
498
498
|
schema: { type: "json" },
|
|
499
|
-
render: (args, value) => renderText$
|
|
499
|
+
render: (args, value) => renderText$13(value)
|
|
500
500
|
},
|
|
501
501
|
async execute(args, exec) {
|
|
502
|
-
const root = findSerenityRoot(agentCwd$
|
|
502
|
+
const root = findSerenityRoot(agentCwd$10(exec));
|
|
503
503
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
504
504
|
return runCcFs(root, args);
|
|
505
505
|
}
|
|
@@ -520,6 +520,208 @@ const ACC_VERSION = (() => {
|
|
|
520
520
|
}
|
|
521
521
|
})();
|
|
522
522
|
//#endregion
|
|
523
|
+
//#region src/im-bridge.ts
|
|
524
|
+
/**
|
|
525
|
+
* im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
526
|
+
*
|
|
527
|
+
* 用户原话:「既然微信桥是我们 ACC 提供的,那么 weixin-send 应该是我们 ACC 提供的能力,
|
|
528
|
+
* 当用户配置了微信桥则可用,不配置则不可见」+「设定一个复杂的多层工具,叫 im-bridge,
|
|
529
|
+
* 里面有微信,然后保留后续可能兼容别的 im 的空间」。
|
|
530
|
+
*
|
|
531
|
+
* 分层(E↑):
|
|
532
|
+
* `tools/im-bridge.ts`(工具面:参数 → 结果渲染)
|
|
533
|
+
* → **本模块**(通道注册表 + 可见性判据 + 动作分发 + 错误码翻译)
|
|
534
|
+
* → 通道实现(`im-weixin.ts`:协议/凭据/账号/记录)
|
|
535
|
+
*
|
|
536
|
+
* 边界(R↓):
|
|
537
|
+
* - **本模块零宿主依赖**(不 import cordis/agent)——可见性判定与分发都是纯逻辑,可独立单测。
|
|
538
|
+
* - **通道只提供机制**:本模块不认识"微信";新增 IM 只需注册一个通道实现(R3 家族式扩展)。
|
|
539
|
+
* - **只能操作本会话 CCC**(R4):`root` 由调用方(工具面)从 agent cwd 解析后传入,
|
|
540
|
+
* 本模块不接受"目标 CCC"参数——跨 CCC 发送不在能力面。
|
|
541
|
+
* - **记录归通道**(R5):通道实现负责让每次发送都落既有 hook。
|
|
542
|
+
*/
|
|
543
|
+
/** 文本长度上限(微信侧单条实践上限;超限拒绝而非静默截断——与 HTTP 入口同值) */
|
|
544
|
+
const IM_SEND_MAX_TEXT = 4e3;
|
|
545
|
+
/** 文件大小上限(20MB,与媒体接收上限一致) */
|
|
546
|
+
const IM_FILE_MAX_BYTES = 20971520;
|
|
547
|
+
const channels = /* @__PURE__ */ new Map();
|
|
548
|
+
/** 注册通道(index.ts apply 调用;重复 id 覆盖——测试与热重载友好) */
|
|
549
|
+
function registerImChannel(channel) {
|
|
550
|
+
channels.set(channel.id, channel);
|
|
551
|
+
}
|
|
552
|
+
/** 已注册通道 id(工具 `channel` 参数枚举) */
|
|
553
|
+
function imChannelIds() {
|
|
554
|
+
return [...channels.keys()];
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 可见性判据:本 CCC 是否启用了**任一** IM 通道。
|
|
558
|
+
* 未启用 → 工具面对该 agent **不可见**(`tools.restrict` 移除,而非调用时报错)。
|
|
559
|
+
* @param root 本会话 CCC 根(由调用方解析)
|
|
560
|
+
*/
|
|
561
|
+
function hasEnabledImChannel(root) {
|
|
562
|
+
for (const channel of channels.values()) try {
|
|
563
|
+
if (channel.isEnabled(root)) return true;
|
|
564
|
+
} catch {}
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
/** 错误码 → 可行动提示(模型视角:说清怎么改) */
|
|
568
|
+
function remediationFor(code) {
|
|
569
|
+
switch (code) {
|
|
570
|
+
case "CHANNEL_UNKNOWN": return "Use a registered channel id (see the tool description)";
|
|
571
|
+
case "CHANNEL_NOT_CONFIGURED": return "This CCC has not configured that channel — enable it in .opencode/serenity.json (weixin.enabled) or the settings panel";
|
|
572
|
+
case "ACTION_UNKNOWN": return "Use send / send-file / users / status";
|
|
573
|
+
case "ACTION_UNSUPPORTED": return "This channel does not support that action";
|
|
574
|
+
case "ALIAS_UNKNOWN": return "Use a configured alias (see action=users) or a raw channel user id";
|
|
575
|
+
case "TEXT_REQUIRED": return "Provide text";
|
|
576
|
+
case "TEXT_TOO_LONG": return `Shorten the text to <= ${IM_SEND_MAX_TEXT} characters`;
|
|
577
|
+
case "USER_REQUIRED": return "Provide user";
|
|
578
|
+
case "FILE_REQUIRED": return "Provide file (a path inside this CCC)";
|
|
579
|
+
case "FILE_ESCAPE": return "Use a path inside this CCC";
|
|
580
|
+
case "FILE_NOT_FOUND": return "Check the path exists inside this CCC";
|
|
581
|
+
case "FILE_TOO_LARGE": return `Use a file <= ${Math.round(IM_FILE_MAX_BYTES / 1048576)} MB`;
|
|
582
|
+
default: return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function fail(code, error, remediation) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
code,
|
|
589
|
+
error,
|
|
590
|
+
...remediation ?? remediationFor(code) ? { remediation: remediation ?? remediationFor(code) } : {}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** CCC 内文件读取(路径逃逸/缺失/超限一律拒绝,返回稳定码) */
|
|
594
|
+
function readCccFile(root, relOrAbs) {
|
|
595
|
+
const abs = isAbsolute(relOrAbs) ? resolve(relOrAbs) : resolve(root, relOrAbs);
|
|
596
|
+
const rel = relative(resolve(root), abs);
|
|
597
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return {
|
|
598
|
+
ok: false,
|
|
599
|
+
code: "FILE_ESCAPE",
|
|
600
|
+
error: `file escapes the CCC root: ${relOrAbs}`
|
|
601
|
+
};
|
|
602
|
+
if (!existsSync(abs)) return {
|
|
603
|
+
ok: false,
|
|
604
|
+
code: "FILE_NOT_FOUND",
|
|
605
|
+
error: `file not found: ${relOrAbs}`
|
|
606
|
+
};
|
|
607
|
+
let size = 0;
|
|
608
|
+
try {
|
|
609
|
+
size = statSync(abs).size;
|
|
610
|
+
} catch (err) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
code: "FILE_NOT_FOUND",
|
|
614
|
+
error: `cannot stat file: ${relOrAbs} (${err instanceof Error ? err.message : String(err)})`
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (size > 20971520) return {
|
|
618
|
+
ok: false,
|
|
619
|
+
code: "FILE_TOO_LARGE",
|
|
620
|
+
error: `file is ${size} bytes (> ${IM_FILE_MAX_BYTES})`
|
|
621
|
+
};
|
|
622
|
+
try {
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
data: readFileSync(abs),
|
|
626
|
+
fileName: abs.split("/").pop() ?? "file"
|
|
627
|
+
};
|
|
628
|
+
} catch (err) {
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
code: "FILE_NOT_FOUND",
|
|
632
|
+
error: `cannot read file: ${relOrAbs} (${err instanceof Error ? err.message : String(err)})`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 执行一次 IM 动作(工具面唯一入口)。
|
|
638
|
+
*
|
|
639
|
+
* 顺序:通道解析 → 动作校验 → 用户解析(别名 → id)→ 输入校验 → 调用通道实现。
|
|
640
|
+
* 任何一步失败都返回**稳定错误码 + 可行动提示**(不抛错——工具面统一渲染)。
|
|
641
|
+
*
|
|
642
|
+
* @param root 本会话 CCC 根(调用方从 agent cwd 解析;本函数不接受目标 CCC)
|
|
643
|
+
* @param req 工具参数
|
|
644
|
+
*/
|
|
645
|
+
async function runImBridge(root, req) {
|
|
646
|
+
const channelId = (req.channel ?? "").trim();
|
|
647
|
+
const action = (req.action ?? "").trim();
|
|
648
|
+
if (channelId === "") return fail("CHANNEL_REQUIRED", "channel is required", "Provide channel (e.g. \"weixin\")");
|
|
649
|
+
const channel = channels.get(channelId);
|
|
650
|
+
if (!channel) return fail("CHANNEL_UNKNOWN", `unknown channel "${channelId}" (registered: ${imChannelIds().join(", ") || "(none)"})`);
|
|
651
|
+
if (!channel.isEnabled(root)) return fail("CHANNEL_NOT_CONFIGURED", `channel "${channelId}" is not configured for this CCC (${root})`);
|
|
652
|
+
if (action === "users") return {
|
|
653
|
+
ok: true,
|
|
654
|
+
channel: channelId,
|
|
655
|
+
action,
|
|
656
|
+
detail: { users: channel.listUsers(root) }
|
|
657
|
+
};
|
|
658
|
+
if (action === "status") return {
|
|
659
|
+
ok: true,
|
|
660
|
+
channel: channelId,
|
|
661
|
+
action,
|
|
662
|
+
detail: channel.status(root)
|
|
663
|
+
};
|
|
664
|
+
if (action === "send") {
|
|
665
|
+
const rawUser = (req.user ?? "").trim();
|
|
666
|
+
if (rawUser === "") return fail("USER_REQUIRED", "user is required");
|
|
667
|
+
const text = req.text ?? "";
|
|
668
|
+
if (text === "") return fail("TEXT_REQUIRED", "text is required");
|
|
669
|
+
if (text.length > 4e3) return fail("TEXT_TOO_LONG", `text is ${text.length} characters (> ${IM_SEND_MAX_TEXT})`);
|
|
670
|
+
const resolved = channel.resolveUser(root, rawUser);
|
|
671
|
+
if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
|
|
672
|
+
try {
|
|
673
|
+
return {
|
|
674
|
+
ok: true,
|
|
675
|
+
channel: channelId,
|
|
676
|
+
action,
|
|
677
|
+
detail: {
|
|
678
|
+
...await channel.send({
|
|
679
|
+
root,
|
|
680
|
+
userId: resolved.id,
|
|
681
|
+
text,
|
|
682
|
+
accountId: req.account
|
|
683
|
+
}),
|
|
684
|
+
user: resolved.alias ?? resolved.id
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
} catch (err) {
|
|
688
|
+
return fail("SEND_FAILED", err instanceof Error ? err.message : String(err));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (action === "send-file") {
|
|
692
|
+
if (!channel.sendFile) return fail("ACTION_UNSUPPORTED", `channel "${channelId}" does not support send-file`);
|
|
693
|
+
const rawUser = (req.user ?? "").trim();
|
|
694
|
+
if (rawUser === "") return fail("USER_REQUIRED", "user is required");
|
|
695
|
+
const fileArg = (req.file ?? "").trim();
|
|
696
|
+
if (fileArg === "") return fail("FILE_REQUIRED", "file is required");
|
|
697
|
+
const resolved = channel.resolveUser(root, rawUser);
|
|
698
|
+
if (!resolved) return fail("ALIAS_UNKNOWN", `unknown user "${rawUser}"`);
|
|
699
|
+
const file = readCccFile(root, fileArg);
|
|
700
|
+
if (!file.ok) return fail(file.code, file.error);
|
|
701
|
+
try {
|
|
702
|
+
return {
|
|
703
|
+
ok: true,
|
|
704
|
+
channel: channelId,
|
|
705
|
+
action,
|
|
706
|
+
detail: {
|
|
707
|
+
...await channel.sendFile({
|
|
708
|
+
root,
|
|
709
|
+
userId: resolved.id,
|
|
710
|
+
data: file.data,
|
|
711
|
+
fileName: file.fileName,
|
|
712
|
+
caption: req.caption,
|
|
713
|
+
accountId: req.account
|
|
714
|
+
}),
|
|
715
|
+
user: resolved.alias ?? resolved.id
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
} catch (err) {
|
|
719
|
+
return fail("SEND_FAILED", err instanceof Error ? err.message : String(err));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return fail("ACTION_UNKNOWN", `unknown action "${action}"`);
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
523
725
|
//#region src/seams/guards.ts
|
|
524
726
|
/**
|
|
525
727
|
* guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
|
|
@@ -712,6 +914,55 @@ function syncSafeModeRestriction(agent, root) {
|
|
|
712
914
|
}
|
|
713
915
|
writeRestrictDiag(root);
|
|
714
916
|
}
|
|
917
|
+
/** 条件可见的 IM 工具名(与 tools/im-bridge.ts 的注册名一致) */
|
|
918
|
+
const IM_BRIDGE_TOOL_NAME = "im-bridge";
|
|
919
|
+
/** agent key → restrict disposer(im-bridge 隐藏状态;key 同安全模式 = 会话 id) */
|
|
920
|
+
const imBridgeRestrictions = /* @__PURE__ */ new Map();
|
|
921
|
+
/**
|
|
922
|
+
* 同步 im-bridge 可见性(v1.31.0,S142 用户拍板 Q3「未配置则不可见」)。
|
|
923
|
+
*
|
|
924
|
+
* 判据 = 本会话 CCC 是否启用了**任一** IM 通道(`hasEnabledImChannel`,当前只有 weixin):
|
|
925
|
+
* - 未启用 → `agent.ctx.tools.restrict({ deny: ['im-bridge'] })`:**从 schema 移除**
|
|
926
|
+
* (模型看不到,而不是看得到但调用被拒——用户原话"不配置则不可见")
|
|
927
|
+
* - 启用 → 解除隐藏(CCC 配置热更新即时生效)
|
|
928
|
+
*
|
|
929
|
+
* 与 `syncSafeModeRestriction` 同点调用(pre-step 每步 + 会话就绪),失败不阻断
|
|
930
|
+
* (守卫仍兜底;restrict 不可用时最坏是"看得到但通道未配置 → 返回 CHANNEL_NOT_CONFIGURED")。
|
|
931
|
+
*/
|
|
932
|
+
function syncImBridgeVisibility(agent, root) {
|
|
933
|
+
const key = agent.session.id ?? "global";
|
|
934
|
+
let enabled = false;
|
|
935
|
+
try {
|
|
936
|
+
enabled = hasEnabledImChannel(root);
|
|
937
|
+
} catch (e) {
|
|
938
|
+
console.error(`[serenity-hooks] im-bridge 可见性判据失败 (key=${key}):`, e.message);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const existing = imBridgeRestrictions.get(key);
|
|
942
|
+
if (!enabled && !existing) try {
|
|
943
|
+
const dispose = agent.ctx.tools.restrict({ deny: [IM_BRIDGE_TOOL_NAME] });
|
|
944
|
+
imBridgeRestrictions.set(key, dispose);
|
|
945
|
+
} catch (e) {
|
|
946
|
+
console.error(`[serenity-hooks] im-bridge 隐藏失败 (key=${key}):`, e.message);
|
|
947
|
+
}
|
|
948
|
+
else if (enabled && existing) {
|
|
949
|
+
try {
|
|
950
|
+
existing();
|
|
951
|
+
} catch (e) {
|
|
952
|
+
console.error(`[serenity-hooks] im-bridge 解除隐藏失败 (key=${key}):`, e.message);
|
|
953
|
+
}
|
|
954
|
+
imBridgeRestrictions.delete(key);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长——review F-08 同族) */
|
|
958
|
+
function forgetImBridgeVisibility(sessionId) {
|
|
959
|
+
const existing = imBridgeRestrictions.get(sessionId);
|
|
960
|
+
if (!existing) return;
|
|
961
|
+
try {
|
|
962
|
+
existing();
|
|
963
|
+
} catch {}
|
|
964
|
+
imBridgeRestrictions.delete(sessionId);
|
|
965
|
+
}
|
|
715
966
|
/**
|
|
716
967
|
* 从 exec 参数中提取常见路径字段(write/edit 工具);宽松读取。
|
|
717
968
|
* container_fs 复合工具:path(单路径)/ paths(数组)/ src / dst——取第一个命中(越界检查
|
|
@@ -1316,10 +1567,10 @@ async function runKit(root, args, hostCtx) {
|
|
|
1316
1567
|
/**
|
|
1317
1568
|
* kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
|
|
1318
1569
|
*/
|
|
1319
|
-
function agentCwd$
|
|
1570
|
+
function agentCwd$9(exec) {
|
|
1320
1571
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1321
1572
|
}
|
|
1322
|
-
function renderText$
|
|
1573
|
+
function renderText$12(value) {
|
|
1323
1574
|
return [{
|
|
1324
1575
|
type: "text",
|
|
1325
1576
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -1347,10 +1598,10 @@ function createKitTool(ctx) {
|
|
|
1347
1598
|
},
|
|
1348
1599
|
output: {
|
|
1349
1600
|
schema: { type: "json" },
|
|
1350
|
-
render: (args, value) => renderText$
|
|
1601
|
+
render: (args, value) => renderText$12(value)
|
|
1351
1602
|
},
|
|
1352
1603
|
async execute(args, exec) {
|
|
1353
|
-
return await runKit(findSerenityRoot(agentCwd$
|
|
1604
|
+
return await runKit(findSerenityRoot(agentCwd$9(exec)), args, ctx);
|
|
1354
1605
|
}
|
|
1355
1606
|
});
|
|
1356
1607
|
}
|
|
@@ -1539,10 +1790,10 @@ function runGit(root, args) {
|
|
|
1539
1790
|
/**
|
|
1540
1791
|
* git.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
|
|
1541
1792
|
*/
|
|
1542
|
-
function agentCwd$
|
|
1793
|
+
function agentCwd$8(exec) {
|
|
1543
1794
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1544
1795
|
}
|
|
1545
|
-
function renderText$
|
|
1796
|
+
function renderText$11(value) {
|
|
1546
1797
|
return [{
|
|
1547
1798
|
type: "text",
|
|
1548
1799
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -1581,10 +1832,10 @@ const gitTool = defineTool({
|
|
|
1581
1832
|
},
|
|
1582
1833
|
output: {
|
|
1583
1834
|
schema: { type: "json" },
|
|
1584
|
-
render: (args, value) => renderText$
|
|
1835
|
+
render: (args, value) => renderText$11(value)
|
|
1585
1836
|
},
|
|
1586
1837
|
async execute(args, exec) {
|
|
1587
|
-
const root = findSerenityRoot(agentCwd$
|
|
1838
|
+
const root = findSerenityRoot(agentCwd$8(exec));
|
|
1588
1839
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1589
1840
|
return runGit(root, args);
|
|
1590
1841
|
}
|
|
@@ -1844,28 +2095,47 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1844
2095
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1845
2096
|
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1846
2097
|
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1847
|
-
▸ 主动发送(v1.
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
成功后触发 outgoing hook(source="proactive"
|
|
2098
|
+
▸ 主动发送(v1.31.0:**ACC 工具 im-bridge**):调
|
|
2099
|
+
im-bridge({channel:"weixin", action:"send", user:"<别名|id>", text:"<文本>"}) 给用户发消息——
|
|
2100
|
+
**工具只能操作本会话 CCC**(无 ccc 参数,防误发他容器)→ weixin-bridge.sendProactiveText →
|
|
2101
|
+
成功后触发 outgoing hook(source="proactive")。文件用 action:"send-file" + file:"<CCC 内路径>"
|
|
2102
|
+
(逃逸/缺失/超 20MB 拒绝)+ 可选 caption。**可见性**:本 CCC 配置了 IM 通道
|
|
2103
|
+
(weixin.enabled=true)才出现在模型工具清单里;未配置 → 由 guards 逐 agent
|
|
2104
|
+
tools.restrict({deny:['im-bridge']}) 移除(不是"看得到但被拒")。skiff 角色另需在
|
|
2105
|
+
roles.<role>.tools 白名单列出 im-bridge。
|
|
2106
|
+
历史(v1.30.9~v1.30.x):CCC 侧 MSM weixin-send(走 loopback HTTP 入口 3082)——
|
|
2107
|
+
v1.31.0 已退役(脚本删除 + 注册表注销);3082 入口保留给**非 agent 调用者**。
|
|
1851
2108
|
|
|
1852
2109
|
▸ weixin.autoReplyWithLastMessage(v1.30.10,关闭桥的自动输出):
|
|
1853
2110
|
缺省 true = 现状:桥在 agent 一轮结束后把**最终 assistant 文本**自动回发用户(source="reply")。
|
|
1854
2111
|
显式 false = 关闭:桥**不再转发最终文本**,输出权交给 agent——每轮在用户消息**末尾**追加
|
|
1855
|
-
**机制标记** [serenity:weixin-manual-output] + 一行已填好参数的
|
|
1856
|
-
|
|
2112
|
+
**机制标记** [serenity:weixin-manual-output] + 一行已填好参数的 im-bridge 调用
|
|
2113
|
+
(账号/用户 id),agent 自己决定发什么、发几条(全部记 source="proactive")。
|
|
1857
2114
|
**ACC 只给机制与数据,纪律措辞归 CCC**(v1.30.16):怎么写/必须怎么做/后果是什么写在
|
|
1858
2115
|
CCC 角色提示词文件里(如 .opencode/skiff/<role>.md),可热改(v1.30.15 起角色提示词热重载)。
|
|
1859
|
-
机械闸门(v1.30.16):本轮 turn 结束仍未成功调用
|
|
1860
|
-
|
|
2116
|
+
机械闸门(v1.30.16):本轮 turn 结束仍未成功调用 im-bridge(send / send-file;兼容旧
|
|
2117
|
+
msm("weixin-send") 形态)→ agent 被打回(≤2 次,提示 = 标记 + 事实);达上限放弃并响亮告警。
|
|
2118
|
+
失败调用不算已送达。
|
|
1861
2119
|
语义边界(用户拍板):只关最终文本;系统类消息(新对话通知 / 语音无法解析提示)保留;
|
|
1862
|
-
|
|
2120
|
+
typing 指示与 incoming hook 不受影响。
|
|
2121
|
+
|
|
2122
|
+
▸ weixin.fallbackOnNoSend(v1.30.17,手动模式的**兜底**;缺省 false):
|
|
2123
|
+
仅在 autoReplyWithLastMessage=false 时生效。true → 一轮结束时若 agent **一次都没成功**
|
|
2124
|
+
调用输出工具(闸门打回用尽),桥把该轮最终文本转发给用户(记录 source="reply-fallback",
|
|
2125
|
+
日志留响亮告警)——保证"不丢消息"。agent 自己发过 → 不兜底(输出权仍在 agent)。
|
|
2126
|
+
为什么需要(实证):v1.30.16 闸门会打回 ≤2 次,但实测某模型在**寒暄类消息**上连续 4 轮、
|
|
2127
|
+
跨 3 版 CCC 提示词仍不调工具 → 打回用尽后用户什么都收不到;提示词与闸门均已排除。
|
|
2128
|
+
代价(显式):角色失去"故意静默"能力(它总会产出一段最终文本)→ 需严格静默的角色保持缺省。
|
|
2129
|
+
三态:① autoReplyWithLastMessage=true(缺省)桥总是回发;② =false + fallbackOnNoSend=true
|
|
2130
|
+
= agent 优先、桥兜底(推荐);③ 两者皆 false(缺省)= 严格静默(v1.30.10 语义)。
|
|
2131
|
+
⚠️ 兜底只在**闸门装配成功**时生效(未装配则无法判定是否发送过 → 不冒重复发送风险)。
|
|
1863
2132
|
|
|
1864
2133
|
Config:
|
|
1865
2134
|
{ "weixin": {
|
|
1866
2135
|
"enabled": true,
|
|
1867
2136
|
"hook": "scripts/weixin-message-hook.ts",
|
|
1868
2137
|
"autoReplyWithLastMessage": true,
|
|
2138
|
+
"fallbackOnNoSend": false,
|
|
1869
2139
|
"accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
|
|
1870
2140
|
"routes": [{ "user": "*", "role": "zhaocai" }]
|
|
1871
2141
|
} }
|
|
@@ -1878,7 +2148,8 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1878
2148
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1879
2149
|
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1880
2150
|
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1881
|
-
(CCC 经 weixin-send MSM
|
|
2151
|
+
(CCC 经 im-bridge 工具(v1.31.0;旧 weixin-send MSM 已退役)→ sendProactiveText);
|
|
2152
|
+
source="reply-fallback"(v1.30.17)= 手动模式下 agent 本轮未发送 → 桥兜底转发最终文本
|
|
1882
2153
|
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1883
2154
|
|
|
1884
2155
|
脚本约定(事件 JSON 单行经 stdin 传入;bun 优先 node 兜底):
|
|
@@ -1893,7 +2164,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1893
2164
|
"message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
|
|
1894
2165
|
{ "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
|
|
1895
2166
|
"userId": ..., "sessionId": ..., "role": ...,
|
|
1896
|
-
"reply": "已记录(纯文本)", "source": "reply" | "proactive" }
|
|
2167
|
+
"reply": "已记录(纯文本)", "source": "reply" | "proactive" | "reply-fallback" }
|
|
1897
2168
|
|
|
1898
2169
|
示例脚本(追加到按日文件——持久化归 CCC 自选:文件/DB/远端均可):
|
|
1899
2170
|
const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';
|
|
@@ -2314,13 +2585,13 @@ async function runMsmAsync(root, args) {
|
|
|
2314
2585
|
* 管理面(register/deregister/check/guide/catalog/ccc-config)已移入 container_admin。
|
|
2315
2586
|
* 底层 runMsmAsync/prepareExec 全复用;未命中候选为新增 findSuggestions。
|
|
2316
2587
|
*/
|
|
2317
|
-
function agentCwd$
|
|
2588
|
+
function agentCwd$7(exec) {
|
|
2318
2589
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
2319
2590
|
}
|
|
2320
2591
|
function agentSessionId$1(exec) {
|
|
2321
2592
|
return exec.agent?.session?.id ?? "";
|
|
2322
2593
|
}
|
|
2323
|
-
function renderText$
|
|
2594
|
+
function renderText$10(value) {
|
|
2324
2595
|
return [{
|
|
2325
2596
|
type: "text",
|
|
2326
2597
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2370,10 +2641,10 @@ const msmTool = defineTool({
|
|
|
2370
2641
|
},
|
|
2371
2642
|
output: {
|
|
2372
2643
|
schema: { type: "json" },
|
|
2373
|
-
render: (args, value) => renderText$
|
|
2644
|
+
render: (args, value) => renderText$10(value)
|
|
2374
2645
|
},
|
|
2375
2646
|
async execute(args, exec) {
|
|
2376
|
-
const root = findSerenityRoot(agentCwd$
|
|
2647
|
+
const root = findSerenityRoot(agentCwd$7(exec));
|
|
2377
2648
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2378
2649
|
const sessionId = agentSessionId$1(exec);
|
|
2379
2650
|
const name = args.name;
|
|
@@ -2431,7 +2702,7 @@ CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design doc
|
|
|
2431
2702
|
|
|
2432
2703
|
## Reference
|
|
2433
2704
|
https://github.com/tellmewhattodo/theory-eap`;
|
|
2434
|
-
function renderText$
|
|
2705
|
+
function renderText$9(value) {
|
|
2435
2706
|
return [{
|
|
2436
2707
|
type: "text",
|
|
2437
2708
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2451,7 +2722,7 @@ defineTool({
|
|
|
2451
2722
|
} },
|
|
2452
2723
|
output: {
|
|
2453
2724
|
schema: { type: "string" },
|
|
2454
|
-
render: (_args, value) => renderText$
|
|
2725
|
+
render: (_args, value) => renderText$9(value)
|
|
2455
2726
|
},
|
|
2456
2727
|
async execute(args) {
|
|
2457
2728
|
if (args.section === "variables") return EAP_CONTENT.split("## Pre-Output Self-Check Checklist")[0];
|
|
@@ -2492,7 +2763,7 @@ Requirements → Scope → Solution → Interface → Implementation
|
|
|
2492
2763
|
2. Wait for confirmation or correction (small step)
|
|
2493
2764
|
3. After confirmation, record the decision (into session SESSION.md or a design doc)
|
|
2494
2765
|
4. Move to the next decision point`;
|
|
2495
|
-
function renderText$
|
|
2766
|
+
function renderText$8(value) {
|
|
2496
2767
|
return [{
|
|
2497
2768
|
type: "text",
|
|
2498
2769
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2508,7 +2779,7 @@ defineTool({
|
|
|
2508
2779
|
} },
|
|
2509
2780
|
output: {
|
|
2510
2781
|
schema: { type: "string" },
|
|
2511
|
-
render: (_args, value) => renderText$
|
|
2782
|
+
render: (_args, value) => renderText$8(value)
|
|
2512
2783
|
},
|
|
2513
2784
|
async execute(args) {
|
|
2514
2785
|
if (args.section === "rules") return NEAT_CONTENT.match(/## Four Iron Rules[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
|
|
@@ -2571,7 +2842,7 @@ CCE answers "how structured knowledge should keep evolving across time without l
|
|
|
2571
2842
|
## Relationship with Serenity
|
|
2572
2843
|
Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
|
|
2573
2844
|
the behavioral constraints embedded in CCC system prompts come from CCE.`;
|
|
2574
|
-
function renderText$
|
|
2845
|
+
function renderText$7(value) {
|
|
2575
2846
|
return [{
|
|
2576
2847
|
type: "text",
|
|
2577
2848
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2592,7 +2863,7 @@ defineTool({
|
|
|
2592
2863
|
} },
|
|
2593
2864
|
output: {
|
|
2594
2865
|
schema: { type: "string" },
|
|
2595
|
-
render: (_args, value) => renderText$
|
|
2866
|
+
render: (_args, value) => renderText$7(value)
|
|
2596
2867
|
},
|
|
2597
2868
|
async execute(args) {
|
|
2598
2869
|
const section = args.section;
|
|
@@ -2638,7 +2909,7 @@ const PRAXIS_INDEX = `praxis — 可实践理论注入(认知框架,按需
|
|
|
2638
2909
|
praxis cce(认知连续性工程——容器 5 行为约束 + H_op 操作熵)
|
|
2639
2910
|
无参返回本目录;详情以对应框架原文为准(单一真相源)。
|
|
2640
2911
|
`;
|
|
2641
|
-
function renderText$
|
|
2912
|
+
function renderText$6(value) {
|
|
2642
2913
|
return [{
|
|
2643
2914
|
type: "text",
|
|
2644
2915
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2658,7 +2929,7 @@ const praxisTool = defineTool({
|
|
|
2658
2929
|
} },
|
|
2659
2930
|
output: {
|
|
2660
2931
|
schema: { type: "string" },
|
|
2661
|
-
render: (_args, value) => renderText$
|
|
2932
|
+
render: (_args, value) => renderText$6(value)
|
|
2662
2933
|
},
|
|
2663
2934
|
async execute(args) {
|
|
2664
2935
|
if (args.section === "eap") return EAP_CONTENT;
|
|
@@ -2712,10 +2983,10 @@ function handymanPresetInheritance(parentCtx) {
|
|
|
2712
2983
|
* preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
|
|
2713
2984
|
* subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
|
|
2714
2985
|
*/
|
|
2715
|
-
function agentCwd$
|
|
2986
|
+
function agentCwd$6(exec) {
|
|
2716
2987
|
return (exec.agent?.session)?.header?.cwd ?? process.cwd();
|
|
2717
2988
|
}
|
|
2718
|
-
function renderText$
|
|
2989
|
+
function renderText$5(value) {
|
|
2719
2990
|
return [{
|
|
2720
2991
|
type: "text",
|
|
2721
2992
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2918,11 +3189,11 @@ function createHandymanTool(ctx) {
|
|
|
2918
3189
|
},
|
|
2919
3190
|
output: {
|
|
2920
3191
|
schema: { type: "json" },
|
|
2921
|
-
render: (_args, value) => renderText$
|
|
3192
|
+
render: (_args, value) => renderText$5(value)
|
|
2922
3193
|
},
|
|
2923
3194
|
async execute(args, exec) {
|
|
2924
3195
|
if (args.guide) return { guide: HANDYMAN_GUIDE };
|
|
2925
|
-
const root = findSerenityRoot(agentCwd$
|
|
3196
|
+
const root = findSerenityRoot(agentCwd$6(exec));
|
|
2926
3197
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2927
3198
|
const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
2928
3199
|
if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
|
|
@@ -2983,7 +3254,7 @@ function createHandymanTool(ctx) {
|
|
|
2983
3254
|
* 同步把当前 dsh 会话重命名为该 SESSION 目录名**(sessionTitle.rename,user source
|
|
2984
3255
|
* pin 住标题)。非创建时预命名。
|
|
2985
3256
|
*/
|
|
2986
|
-
function agentCwd$
|
|
3257
|
+
function agentCwd$5(exec) {
|
|
2987
3258
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
2988
3259
|
}
|
|
2989
3260
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
@@ -3099,7 +3370,7 @@ function renameDshSessionForActive(ctx, exec, info, summary) {
|
|
|
3099
3370
|
console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
|
|
3100
3371
|
}
|
|
3101
3372
|
}
|
|
3102
|
-
function renderText$
|
|
3373
|
+
function renderText$4(value) {
|
|
3103
3374
|
return [{
|
|
3104
3375
|
type: "text",
|
|
3105
3376
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3245,10 +3516,10 @@ function createSessionTool(ctx) {
|
|
|
3245
3516
|
},
|
|
3246
3517
|
output: {
|
|
3247
3518
|
schema: { type: "json" },
|
|
3248
|
-
render: (args, value) => renderText$
|
|
3519
|
+
render: (args, value) => renderText$4(value)
|
|
3249
3520
|
},
|
|
3250
3521
|
async execute(args, exec) {
|
|
3251
|
-
const root = findSerenityRoot(agentCwd$
|
|
3522
|
+
const root = findSerenityRoot(agentCwd$5(exec));
|
|
3252
3523
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3253
3524
|
const entries = loadMsmEntries(root);
|
|
3254
3525
|
const hasSessionTool = entries.some((e) => e.name === "session-tool");
|
|
@@ -3354,7 +3625,7 @@ function createSessionTool(ctx) {
|
|
|
3354
3625
|
root,
|
|
3355
3626
|
note: args.note,
|
|
3356
3627
|
summary: args.summary,
|
|
3357
|
-
agentCwd: agentCwd$
|
|
3628
|
+
agentCwd: agentCwd$5(exec),
|
|
3358
3629
|
dshSessionId
|
|
3359
3630
|
});
|
|
3360
3631
|
return {
|
|
@@ -3379,7 +3650,7 @@ function createSessionTool(ctx) {
|
|
|
3379
3650
|
* 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
|
|
3380
3651
|
* doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
|
|
3381
3652
|
*/
|
|
3382
|
-
const ACTIONS = [
|
|
3653
|
+
const ACTIONS$1 = [
|
|
3383
3654
|
"list",
|
|
3384
3655
|
"get",
|
|
3385
3656
|
"set",
|
|
@@ -3387,10 +3658,10 @@ const ACTIONS = [
|
|
|
3387
3658
|
"show",
|
|
3388
3659
|
"doc"
|
|
3389
3660
|
];
|
|
3390
|
-
function agentCwd$
|
|
3661
|
+
function agentCwd$4(exec) {
|
|
3391
3662
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3392
3663
|
}
|
|
3393
|
-
function renderText$
|
|
3664
|
+
function renderText$3(value) {
|
|
3394
3665
|
return [{
|
|
3395
3666
|
type: "text",
|
|
3396
3667
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3402,7 +3673,7 @@ const localstoreTool = defineTool({
|
|
|
3402
3673
|
parameters: {
|
|
3403
3674
|
action: {
|
|
3404
3675
|
type: "string",
|
|
3405
|
-
enum: [...ACTIONS],
|
|
3676
|
+
enum: [...ACTIONS$1],
|
|
3406
3677
|
required: true,
|
|
3407
3678
|
description: "Subcommand: list/get/set/unset/show/doc"
|
|
3408
3679
|
},
|
|
@@ -3422,10 +3693,10 @@ const localstoreTool = defineTool({
|
|
|
3422
3693
|
},
|
|
3423
3694
|
output: {
|
|
3424
3695
|
schema: { type: "json" },
|
|
3425
|
-
render: (args, value) => renderText$
|
|
3696
|
+
render: (args, value) => renderText$3(value)
|
|
3426
3697
|
},
|
|
3427
3698
|
async execute(args, exec) {
|
|
3428
|
-
const root = findSerenityRoot(agentCwd$
|
|
3699
|
+
const root = findSerenityRoot(agentCwd$4(exec));
|
|
3429
3700
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3430
3701
|
return runLocalStore(root, args);
|
|
3431
3702
|
}
|
|
@@ -3581,13 +3852,13 @@ function applySkiffConfig(root) {
|
|
|
3581
3852
|
*
|
|
3582
3853
|
* 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
|
|
3583
3854
|
*/
|
|
3584
|
-
function agentCwd$
|
|
3855
|
+
function agentCwd$3(exec) {
|
|
3585
3856
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3586
3857
|
}
|
|
3587
3858
|
function agentSessionId(exec) {
|
|
3588
3859
|
return exec.agent?.session?.id ?? "";
|
|
3589
3860
|
}
|
|
3590
|
-
function renderText$
|
|
3861
|
+
function renderText$2(value) {
|
|
3591
3862
|
return [{
|
|
3592
3863
|
type: "text",
|
|
3593
3864
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3642,10 +3913,10 @@ const containerAdminTool = defineTool({
|
|
|
3642
3913
|
},
|
|
3643
3914
|
output: {
|
|
3644
3915
|
schema: { type: "json" },
|
|
3645
|
-
render: (args, value) => renderText$
|
|
3916
|
+
render: (args, value) => renderText$2(value)
|
|
3646
3917
|
},
|
|
3647
3918
|
async execute(args, exec) {
|
|
3648
|
-
const root = findSerenityRoot(agentCwd$
|
|
3919
|
+
const root = findSerenityRoot(agentCwd$3(exec));
|
|
3649
3920
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3650
3921
|
const sessionId = agentSessionId(exec);
|
|
3651
3922
|
const domain = args.domain;
|
|
@@ -4350,10 +4621,10 @@ const AUTOPILOT_ACTIONS = [
|
|
|
4350
4621
|
"guide",
|
|
4351
4622
|
"diag-live"
|
|
4352
4623
|
];
|
|
4353
|
-
function agentCwd$
|
|
4624
|
+
function agentCwd$2(exec) {
|
|
4354
4625
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
4355
4626
|
}
|
|
4356
|
-
function renderText(value) {
|
|
4627
|
+
function renderText$1(value) {
|
|
4357
4628
|
return [{
|
|
4358
4629
|
type: "text",
|
|
4359
4630
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -4392,11 +4663,11 @@ function createAutopilotTool(ctx) {
|
|
|
4392
4663
|
} },
|
|
4393
4664
|
output: {
|
|
4394
4665
|
schema: { type: "json" },
|
|
4395
|
-
render: (_args, value) => renderText(value)
|
|
4666
|
+
render: (_args, value) => renderText$1(value)
|
|
4396
4667
|
},
|
|
4397
4668
|
async execute(args, exec) {
|
|
4398
4669
|
if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
|
|
4399
|
-
const root = findSerenityRoot(agentCwd$
|
|
4670
|
+
const root = findSerenityRoot(agentCwd$2(exec));
|
|
4400
4671
|
const result = {};
|
|
4401
4672
|
if (!EXP_SCRIPT) {
|
|
4402
4673
|
result.error = "autopilot-trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
|
|
@@ -5876,7 +6147,7 @@ function codeModeAdaptationLine(ctx, scope) {
|
|
|
5876
6147
|
* 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
|
|
5877
6148
|
* 回退 process.cwd()——对齐 context.ts,否则这些 agent 系统提示词注入为空
|
|
5878
6149
|
* (v1.18.6:workflow subagent 无宁静号上下文 bug) */
|
|
5879
|
-
function agentCwd(context) {
|
|
6150
|
+
function agentCwd$1(context) {
|
|
5880
6151
|
const agent = context.agent;
|
|
5881
6152
|
if (!agent) return void 0;
|
|
5882
6153
|
return agent.session?.header?.cwd ?? process.cwd();
|
|
@@ -5896,7 +6167,7 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
5896
6167
|
name: "serenity-entry",
|
|
5897
6168
|
order: -50,
|
|
5898
6169
|
text: (context) => {
|
|
5899
|
-
const cwd = agentCwd(context);
|
|
6170
|
+
const cwd = agentCwd$1(context);
|
|
5900
6171
|
if (!cwd) return "";
|
|
5901
6172
|
const root = findSerenityRoot(cwd);
|
|
5902
6173
|
if (!root) return "";
|
|
@@ -6103,6 +6374,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
6103
6374
|
injected.add(key);
|
|
6104
6375
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
6105
6376
|
syncSafeModeRestriction(agent, root);
|
|
6377
|
+
try {
|
|
6378
|
+
syncImBridgeVisibility(agent, root);
|
|
6379
|
+
} catch {}
|
|
6106
6380
|
};
|
|
6107
6381
|
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
6108
6382
|
try {
|
|
@@ -6120,6 +6394,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
6120
6394
|
try {
|
|
6121
6395
|
syncSafeModeRestriction(agent, root);
|
|
6122
6396
|
} catch {}
|
|
6397
|
+
try {
|
|
6398
|
+
syncImBridgeVisibility(agent, root);
|
|
6399
|
+
} catch {}
|
|
6123
6400
|
registerEntrySkillSection(agent, root);
|
|
6124
6401
|
}
|
|
6125
6402
|
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
@@ -6608,11 +6885,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6608
6885
|
sendJson$2(res, 400, { error: chk.error });
|
|
6609
6886
|
return;
|
|
6610
6887
|
}
|
|
6611
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6888
|
+
const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6612
6889
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6613
6890
|
const settings = readWeixinSettings(chk.root);
|
|
6614
6891
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6615
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6892
|
+
const { readWeixinCredential } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6616
6893
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6617
6894
|
accountId: a.accountId,
|
|
6618
6895
|
name: a.name ?? void 0,
|
|
@@ -6641,10 +6918,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6641
6918
|
}
|
|
6642
6919
|
const root = chk.root;
|
|
6643
6920
|
if (body.action === "login-start") {
|
|
6644
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6921
|
+
const { fetchQRCode } = await import("./weixin-api-PjEhmjlZ.js").then((n) => n.u);
|
|
6645
6922
|
const { randomUUID } = await import("node:crypto");
|
|
6646
6923
|
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-
|
|
6924
|
+
const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6648
6925
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6649
6926
|
const loginKey = randomUUID();
|
|
6650
6927
|
weixinLogins.set(loginKey, {
|
|
@@ -6665,7 +6942,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6665
6942
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6666
6943
|
return;
|
|
6667
6944
|
}
|
|
6668
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6945
|
+
const { removeWeixinAccount } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6669
6946
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6670
6947
|
removeWeixinAccount(root, body.accountId);
|
|
6671
6948
|
syncCccBridge(ctx, root);
|
|
@@ -6678,7 +6955,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6678
6955
|
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6679
6956
|
return;
|
|
6680
6957
|
}
|
|
6681
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6958
|
+
const { saveWeixinRoutes } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6682
6959
|
const { readSkiffRoles } = await import("./skiff-role-BYvNnwv1.js").then((n) => n.u);
|
|
6683
6960
|
const roles = readSkiffRoles(root);
|
|
6684
6961
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
@@ -6691,7 +6968,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6691
6968
|
}
|
|
6692
6969
|
if (body.action === "set-enabled") {
|
|
6693
6970
|
const enabled = body.enabled === true;
|
|
6694
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6971
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6695
6972
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6696
6973
|
const settings = readWeixinSettings(root);
|
|
6697
6974
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -6737,8 +7014,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6737
7014
|
sendJson$2(res, 200, { status: "expired" });
|
|
6738
7015
|
return;
|
|
6739
7016
|
}
|
|
6740
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6741
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
7017
|
+
const { pollQRStatus } = await import("./weixin-api-PjEhmjlZ.js").then((n) => n.u);
|
|
7018
|
+
const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6742
7019
|
const settings = readWeixinSettings(login.root);
|
|
6743
7020
|
const status = await pollQRStatus({
|
|
6744
7021
|
baseUrl: void 0,
|
|
@@ -6753,7 +7030,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6753
7030
|
});
|
|
6754
7031
|
return;
|
|
6755
7032
|
}
|
|
6756
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
7033
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6757
7034
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6758
7035
|
const accountId = nextWeixinAccountId(settings);
|
|
6759
7036
|
upsertWeixinAccount(login.root, {
|
|
@@ -9185,10 +9462,15 @@ const PLUGIN_SOURCE = {
|
|
|
9185
9462
|
plugin: "dsh-serenity-hooks"
|
|
9186
9463
|
};
|
|
9187
9464
|
const manualSessions = /* @__PURE__ */ new Map();
|
|
9188
|
-
/**
|
|
9465
|
+
/**
|
|
9466
|
+
* 本轮是否已成功发送(**由桥在每轮开始时清空** `clearSentThisTurn`;闸门只置位不清空——
|
|
9467
|
+
* v1.30.17 起桥要在 `askSkiff` 返回后读它决定是否兜底,故 turn-stopping 不再结算清空)。
|
|
9468
|
+
*/
|
|
9189
9469
|
const sentThisTurn = /* @__PURE__ */ new Set();
|
|
9190
9470
|
/** 连续打回计数(成功发送即清零) */
|
|
9191
9471
|
const rebukeCounts = /* @__PURE__ */ new Map();
|
|
9472
|
+
/** 闸门是否装配成功(v1.30.17:桥据此判断能否信任"未发送"这一判定——未装配时不兜底,防重复发送) */
|
|
9473
|
+
let guardActive = false;
|
|
9192
9474
|
/** 桥:登记手动模式会话(每轮 incoming 都调用,幂等覆盖) */
|
|
9193
9475
|
function noteManualOutputSession(sessionId, session) {
|
|
9194
9476
|
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
@@ -9200,25 +9482,52 @@ function forgetManualOutputSession(sessionId) {
|
|
|
9200
9482
|
sentThisTurn.delete(sessionId);
|
|
9201
9483
|
rebukeCounts.delete(sessionId);
|
|
9202
9484
|
}
|
|
9485
|
+
/** 本轮是否已成功发送(测试/诊断用;桥在每轮结束时读它决定是否兜底) */
|
|
9486
|
+
function hasSentThisTurn(sessionId) {
|
|
9487
|
+
return sentThisTurn.has(sessionId);
|
|
9488
|
+
}
|
|
9489
|
+
/**
|
|
9490
|
+
* 桥:**每轮开始时**清空本会话的"已发送"标记(turn 边界由桥掌握——v1.30.17)。
|
|
9491
|
+
* 闸门侧不再于 turn-stopping 结算清空,否则桥读不到本轮结果、无法兜底。
|
|
9492
|
+
*/
|
|
9493
|
+
function clearSentThisTurn(sessionId) {
|
|
9494
|
+
sentThisTurn.delete(sessionId);
|
|
9495
|
+
}
|
|
9496
|
+
/** 闸门是否装配成功(未装配 → 桥不做兜底:宁可静默也不冒重复发送的风险) */
|
|
9497
|
+
function isWeixinOutputGuardActive() {
|
|
9498
|
+
return guardActive;
|
|
9499
|
+
}
|
|
9203
9500
|
/** 从 agent / 会话对象取会话 id(形状宽容) */
|
|
9204
9501
|
function sessionIdOf$1(value) {
|
|
9205
9502
|
const v = value;
|
|
9206
9503
|
const id = v?.session?.id ?? v?.id;
|
|
9207
9504
|
return typeof id === "string" && id !== "" ? id : null;
|
|
9208
9505
|
}
|
|
9209
|
-
/**
|
|
9506
|
+
/**
|
|
9507
|
+
* 工具调用是否为「成功的微信发送」。
|
|
9508
|
+
*
|
|
9509
|
+
* v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
|
|
9510
|
+
* - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
|
|
9511
|
+
* - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
|
|
9512
|
+
*/
|
|
9210
9513
|
function isSuccessfulWeixinSend(exec, result) {
|
|
9211
9514
|
const e = exec;
|
|
9212
|
-
if (
|
|
9213
|
-
|
|
9214
|
-
|
|
9515
|
+
if (result?.isError === true) return false;
|
|
9516
|
+
const args = e?.arguments;
|
|
9517
|
+
if (e?.name === "im-bridge") {
|
|
9518
|
+
if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
|
|
9519
|
+
const action = typeof args?.action === "string" ? args.action : "";
|
|
9520
|
+
return action === "send" || action === "send-file";
|
|
9521
|
+
}
|
|
9522
|
+
if (e?.name === "msm") return args?.name === "weixin-send";
|
|
9523
|
+
return false;
|
|
9215
9524
|
}
|
|
9216
9525
|
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
9217
9526
|
function buildManualOutputRebuke(session) {
|
|
9218
9527
|
return [
|
|
9219
9528
|
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
9220
9529
|
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
9221
|
-
`
|
|
9530
|
+
`im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
|
|
9222
9531
|
].join("\n");
|
|
9223
9532
|
}
|
|
9224
9533
|
/**
|
|
@@ -9250,13 +9559,11 @@ function registerWeixinOutputGuard(ctx) {
|
|
|
9250
9559
|
if (!sessionId) return;
|
|
9251
9560
|
const session = manualSessions.get(sessionId);
|
|
9252
9561
|
if (!session) return;
|
|
9253
|
-
|
|
9254
|
-
sentThisTurn.delete(sessionId);
|
|
9255
|
-
if (sent) return;
|
|
9562
|
+
if (sentThisTurn.has(sessionId)) return;
|
|
9256
9563
|
const count = (rebukeCounts.get(sessionId) ?? 0) + 1;
|
|
9257
9564
|
if (count > 2) {
|
|
9258
9565
|
rebukeCounts.delete(sessionId);
|
|
9259
|
-
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role}
|
|
9566
|
+
console.warn(`[serenity-hooks] ✗ weixin 输出闸门:连续 2 次打回后仍未发送(session=${sessionId}, role=${session.role})——本轮用户将收不到 agent 自己发的消息`);
|
|
9260
9567
|
return;
|
|
9261
9568
|
}
|
|
9262
9569
|
rebukeCounts.set(sessionId, count);
|
|
@@ -9275,7 +9582,9 @@ function registerWeixinOutputGuard(ctx) {
|
|
|
9275
9582
|
});
|
|
9276
9583
|
} catch {
|
|
9277
9584
|
console.warn("[serenity-hooks] ✗ weixin 输出闸门未装配:agent/turn-stopping 不可用");
|
|
9585
|
+
return;
|
|
9278
9586
|
}
|
|
9587
|
+
guardActive = true;
|
|
9279
9588
|
}
|
|
9280
9589
|
//#endregion
|
|
9281
9590
|
//#region src/weixin-hook.ts
|
|
@@ -9325,7 +9634,8 @@ function buildOutgoingHookEvent(input) {
|
|
|
9325
9634
|
sessionId: input.sessionId,
|
|
9326
9635
|
role: input.role,
|
|
9327
9636
|
reply: input.reply,
|
|
9328
|
-
...input.source
|
|
9637
|
+
...input.source && input.source !== "reply" ? { source: input.source } : {},
|
|
9638
|
+
...input.file ? { file: input.file } : {}
|
|
9329
9639
|
};
|
|
9330
9640
|
}
|
|
9331
9641
|
/**
|
|
@@ -9439,7 +9749,9 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
9439
9749
|
//#region src/weixin-bridge.ts
|
|
9440
9750
|
var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
9441
9751
|
handleIncoming: () => handleIncoming,
|
|
9752
|
+
manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
|
|
9442
9753
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9754
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
9443
9755
|
sendProactiveText: () => sendProactiveText,
|
|
9444
9756
|
stopAllBridges: () => stopAllBridges,
|
|
9445
9757
|
stopCccBridge: () => stopCccBridge,
|
|
@@ -9537,14 +9849,26 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
|
|
|
9537
9849
|
* "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
|
|
9538
9850
|
* 也符合"ACC 管机制、CCC 管内容"的归属二分。
|
|
9539
9851
|
*
|
|
9540
|
-
* 标记格式(稳定,供 CCC
|
|
9852
|
+
* 标记格式(稳定,供 CCC 提示词引用;v1.31.0 起为 ACC 工具形态):
|
|
9541
9853
|
* ```
|
|
9542
9854
|
* [serenity:weixin-manual-output]
|
|
9543
|
-
*
|
|
9855
|
+
* im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
|
|
9544
9856
|
* ```
|
|
9545
9857
|
*/
|
|
9546
9858
|
function weixinManualOutputMarker(root, accountId, userId) {
|
|
9547
|
-
return ["[serenity:weixin-manual-output]", `
|
|
9859
|
+
return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
|
|
9860
|
+
}
|
|
9861
|
+
/**
|
|
9862
|
+
* 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
|
|
9863
|
+
*
|
|
9864
|
+
* 四个条件同时成立才兜底:
|
|
9865
|
+
* ① CCC 显式开启 `fallbackOnNoSend`(缺省 false = 严格静默,向后兼容)
|
|
9866
|
+
* ② 机械闸门**已装配**(未装配时无法判定"是否发送过" → 宁可静默也不冒重复发送风险)
|
|
9867
|
+
* ③ 本轮**没有**成功发送(agent 自己发过 → 输出权归 agent,桥不插话)
|
|
9868
|
+
* ④ 有最终文本可转发(空文本无意义)
|
|
9869
|
+
*/
|
|
9870
|
+
function manualOutputFallbackNeeded(input) {
|
|
9871
|
+
return input.fallbackOnNoSend === true && input.guardActive && !input.sentThisTurn && input.answer !== "";
|
|
9548
9872
|
}
|
|
9549
9873
|
/**
|
|
9550
9874
|
* 处理单条微信消息:路由 → skiff 会话(固定 id 创建/延续)→ 提问 → 回复回写。
|
|
@@ -9666,6 +9990,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9666
9990
|
if (text) parts.push(text);
|
|
9667
9991
|
parts.push(...mediaNotes, ...degradedNotes);
|
|
9668
9992
|
if (manualOutput) {
|
|
9993
|
+
clearSentThisTurn(sessionId);
|
|
9669
9994
|
noteManualOutputSession(sessionId, {
|
|
9670
9995
|
root,
|
|
9671
9996
|
accountId,
|
|
@@ -9688,7 +10013,35 @@ async function handleIncoming(ctx, root, accountId, cred, msg) {
|
|
|
9688
10013
|
console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9689
10014
|
});
|
|
9690
10015
|
const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeTrajectory: false })).answer ?? "";
|
|
9691
|
-
if (manualOutput)
|
|
10016
|
+
if (manualOutput) {
|
|
10017
|
+
if (!manualOutputFallbackNeeded({
|
|
10018
|
+
fallbackOnNoSend: settings.fallbackOnNoSend,
|
|
10019
|
+
guardActive: isWeixinOutputGuardActive(),
|
|
10020
|
+
sentThisTurn: hasSentThisTurn(sessionId),
|
|
10021
|
+
answer
|
|
10022
|
+
})) return;
|
|
10023
|
+
const reply = markdownToPlainText(stripThink(answer));
|
|
10024
|
+
await sendTextMessage({
|
|
10025
|
+
baseUrl: cred.baseUrl,
|
|
10026
|
+
token: cred.token,
|
|
10027
|
+
toUserId: fromUserId,
|
|
10028
|
+
text: reply,
|
|
10029
|
+
contextToken: msg.context_token
|
|
10030
|
+
});
|
|
10031
|
+
console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, role=${roleName})`);
|
|
10032
|
+
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
10033
|
+
cccRoot: root,
|
|
10034
|
+
accountId,
|
|
10035
|
+
userId: fromUserId,
|
|
10036
|
+
sessionId,
|
|
10037
|
+
role: roleName,
|
|
10038
|
+
reply,
|
|
10039
|
+
source: "reply-fallback"
|
|
10040
|
+
})).catch((err) => {
|
|
10041
|
+
console.log(`[serenity-hooks] weixin hook outgoing(reply-fallback) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
10042
|
+
});
|
|
10043
|
+
return;
|
|
10044
|
+
}
|
|
9692
10045
|
if (answer === "") return;
|
|
9693
10046
|
const reply = markdownToPlainText(stripThink(answer));
|
|
9694
10047
|
await sendTextMessage({
|
|
@@ -9765,22 +10118,12 @@ function stopAllBridges() {
|
|
|
9765
10118
|
bridges.clear();
|
|
9766
10119
|
}
|
|
9767
10120
|
/**
|
|
9768
|
-
*
|
|
9769
|
-
*
|
|
9770
|
-
*
|
|
9771
|
-
*
|
|
9772
|
-
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
9773
|
-
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
9774
|
-
*
|
|
9775
|
-
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
9776
|
-
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
9777
|
-
*
|
|
9778
|
-
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
9779
|
-
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
9780
|
-
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
10121
|
+
* 解析该 CCC 的发送账号 + 凭据(v1.31.0:文本与文件发送的**单一账号选择真相源**)。
|
|
10122
|
+
* 顺序:桥启用 → 有启用账号 → 指定账号存在 → 凭据已绑定(扫码)。
|
|
10123
|
+
* @param root CCC 根
|
|
10124
|
+
* @param accountId 指定账号(缺省 = 第一个启用账号)
|
|
9781
10125
|
*/
|
|
9782
|
-
|
|
9783
|
-
const { root, toUserId } = input;
|
|
10126
|
+
function resolveWeixinAccount(root, accountId) {
|
|
9784
10127
|
const settings = readWeixinSettings(root);
|
|
9785
10128
|
if (!settings.enabled) return {
|
|
9786
10129
|
ok: false,
|
|
@@ -9795,20 +10138,32 @@ async function sendProactiveText(input) {
|
|
|
9795
10138
|
error: "微信桥未配置任何启用账号",
|
|
9796
10139
|
remediation: "在设置面板「微信桥」扫码绑定账号"
|
|
9797
10140
|
};
|
|
9798
|
-
const
|
|
9799
|
-
if (!accounts.some((a) => a.accountId ===
|
|
10141
|
+
const id = accountId ?? accounts[0].accountId;
|
|
10142
|
+
if (!accounts.some((a) => a.accountId === id)) return {
|
|
9800
10143
|
ok: false,
|
|
9801
10144
|
code: "ACCOUNT_NOT_FOUND",
|
|
9802
|
-
error: `账号不存在或未启用: ${
|
|
10145
|
+
error: `账号不存在或未启用: ${id}`,
|
|
9803
10146
|
remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
|
|
9804
10147
|
};
|
|
9805
|
-
const cred = readWeixinCredential(root,
|
|
10148
|
+
const cred = readWeixinCredential(root, id);
|
|
9806
10149
|
if (!cred) return {
|
|
9807
10150
|
ok: false,
|
|
9808
10151
|
code: "ACCOUNT_NOT_BOUND",
|
|
9809
|
-
error: `账号未绑定(无凭据): ${
|
|
10152
|
+
error: `账号未绑定(无凭据): ${id}`,
|
|
9810
10153
|
remediation: "在设置面板重新扫码绑定"
|
|
9811
10154
|
};
|
|
10155
|
+
return {
|
|
10156
|
+
ok: true,
|
|
10157
|
+
accountId: id,
|
|
10158
|
+
cred
|
|
10159
|
+
};
|
|
10160
|
+
}
|
|
10161
|
+
async function sendProactiveText(input) {
|
|
10162
|
+
const { root, toUserId } = input;
|
|
10163
|
+
const settings = readWeixinSettings(root);
|
|
10164
|
+
const account = resolveWeixinAccount(root, input.accountId);
|
|
10165
|
+
if (!account.ok) return account;
|
|
10166
|
+
const { accountId, cred } = account;
|
|
9812
10167
|
const sessionId = weixinSessionIdFor(toUserId);
|
|
9813
10168
|
const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
9814
10169
|
try {
|
|
@@ -9884,7 +10239,9 @@ function registerWeixinBridge(ctx) {
|
|
|
9884
10239
|
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
9885
10240
|
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
9886
10241
|
*
|
|
9887
|
-
* 调用者 =
|
|
10242
|
+
* 调用者 = **非 agent 的调用者**(外部脚本/集成/运维)。**agent 侧已不走这里**:v1.31.0 起
|
|
10243
|
+
* agent 统一调 `im-bridge` 工具(进程内直调 `sendProactiveText`,无 HTTP 一跳),旧 CCC MSM
|
|
10244
|
+
* `weixin-send` 已退役。本入口保留 = 给容器外的进程一个稳定的本机发送面。
|
|
9888
10245
|
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
9889
10246
|
*
|
|
9890
10247
|
* 契约:
|
|
@@ -10250,6 +10607,9 @@ function cleanupSessionState(sessionId) {
|
|
|
10250
10607
|
try {
|
|
10251
10608
|
forgetManualOutputSession(sessionId);
|
|
10252
10609
|
} catch {}
|
|
10610
|
+
try {
|
|
10611
|
+
forgetImBridgeVisibility(sessionId);
|
|
10612
|
+
} catch {}
|
|
10253
10613
|
}
|
|
10254
10614
|
/**
|
|
10255
10615
|
* 装配生命周期订阅与资源拆卸。
|
|
@@ -10473,6 +10833,243 @@ async function registerWebFetchProvider(ctx) {
|
|
|
10473
10833
|
}
|
|
10474
10834
|
}
|
|
10475
10835
|
//#endregion
|
|
10836
|
+
//#region src/im-weixin.ts
|
|
10837
|
+
/**
|
|
10838
|
+
* im-weixin.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
|
|
10839
|
+
*
|
|
10840
|
+
* 能力归 ACC(S142 用户洞察):本文件吸收原 CCC MSM `.opencode/skills/home-serenity/scripts/
|
|
10841
|
+
* weixin-send.ts` 的 `send` / `send-file` / `users` 语义——协议、凭据、账号选择、别名解析、
|
|
10842
|
+
* 记录全部落在 ACC 侧;CCC 只保留「何时发 / 发什么 / 怎么写」。
|
|
10843
|
+
*
|
|
10844
|
+
* 通道内部契约:
|
|
10845
|
+
* - `isEnabled(root)` = 该 CCC 的 `weixin.enabled`(可见性判据,im-bridge 用它决定工具是否可见)
|
|
10846
|
+
* - `send` 复用 `sendProactiveText`(唯一文本发送实现 + 唯一记录点)
|
|
10847
|
+
* - `sendFile` 走 iLink 上传(`weixin-api.sendFileMessage`)+ 同一 outgoing hook(带 `file` 元数据)
|
|
10848
|
+
*/
|
|
10849
|
+
/**
|
|
10850
|
+
* 别名 → CCC localstore 凭据键(与 v1.30.9 CCC MSM 逐字一致,迁移零认知成本)。
|
|
10851
|
+
* 别名解析属"数据填值",归 ACC(D23)。
|
|
10852
|
+
*/
|
|
10853
|
+
const WEIXIN_ALIAS_MAP = {
|
|
10854
|
+
yh: "WEIXIN_USER_YH",
|
|
10855
|
+
danica: "WEIXIN_USER_DANICA",
|
|
10856
|
+
xiaowang: "WEIXIN_USER_DANICA"
|
|
10857
|
+
};
|
|
10858
|
+
/** 读该 CCC 的一个凭据值(空/缺失 → null) */
|
|
10859
|
+
function credentialValue(root, key) {
|
|
10860
|
+
try {
|
|
10861
|
+
const value = readStore(root, "credential")[key];
|
|
10862
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
10863
|
+
} catch {
|
|
10864
|
+
return null;
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
/** 把通道层的失败码转成一句可读错误(工具面统一映射为 SEND_FAILED) */
|
|
10868
|
+
function channelError(code, error, remediation) {
|
|
10869
|
+
return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
|
|
10870
|
+
}
|
|
10871
|
+
const weixinChannel = {
|
|
10872
|
+
id: "weixin",
|
|
10873
|
+
isEnabled(root) {
|
|
10874
|
+
return readWeixinSettings(root).enabled === true;
|
|
10875
|
+
},
|
|
10876
|
+
resolveUser(root, input) {
|
|
10877
|
+
const raw = input.trim();
|
|
10878
|
+
if (raw === "") return null;
|
|
10879
|
+
const alias = raw.toLowerCase();
|
|
10880
|
+
const key = WEIXIN_ALIAS_MAP[alias];
|
|
10881
|
+
if (key) {
|
|
10882
|
+
const id = credentialValue(root, key);
|
|
10883
|
+
return id ? {
|
|
10884
|
+
alias,
|
|
10885
|
+
id
|
|
10886
|
+
} : null;
|
|
10887
|
+
}
|
|
10888
|
+
if (raw.includes("@")) return {
|
|
10889
|
+
alias: raw,
|
|
10890
|
+
id: raw
|
|
10891
|
+
};
|
|
10892
|
+
return null;
|
|
10893
|
+
},
|
|
10894
|
+
listUsers(root) {
|
|
10895
|
+
const out = [];
|
|
10896
|
+
for (const [alias, key] of Object.entries(WEIXIN_ALIAS_MAP)) {
|
|
10897
|
+
const id = credentialValue(root, key);
|
|
10898
|
+
if (id) out.push({
|
|
10899
|
+
alias,
|
|
10900
|
+
id
|
|
10901
|
+
});
|
|
10902
|
+
}
|
|
10903
|
+
return out;
|
|
10904
|
+
},
|
|
10905
|
+
async send(input) {
|
|
10906
|
+
const result = await sendProactiveText({
|
|
10907
|
+
root: input.root,
|
|
10908
|
+
toUserId: input.userId,
|
|
10909
|
+
text: input.text,
|
|
10910
|
+
...input.accountId ? { accountId: input.accountId } : {}
|
|
10911
|
+
});
|
|
10912
|
+
if (!result.ok) throw channelError(result.code, result.error, result.remediation);
|
|
10913
|
+
return {
|
|
10914
|
+
accountId: result.accountId,
|
|
10915
|
+
userId: result.userId,
|
|
10916
|
+
sessionId: result.sessionId,
|
|
10917
|
+
role: result.role
|
|
10918
|
+
};
|
|
10919
|
+
},
|
|
10920
|
+
async sendFile(input) {
|
|
10921
|
+
const settings = readWeixinSettings(input.root);
|
|
10922
|
+
if (!settings.enabled) throw channelError("BRIDGE_DISABLED", `微信桥未启用(ccc=${input.root})`);
|
|
10923
|
+
const account = resolveWeixinAccount(input.root, input.accountId);
|
|
10924
|
+
if (!account.ok) throw channelError(account.code, account.error, account.remediation);
|
|
10925
|
+
const sent = await sendFileMessage({
|
|
10926
|
+
baseUrl: account.cred.baseUrl,
|
|
10927
|
+
token: account.cred.token,
|
|
10928
|
+
toUserId: input.userId,
|
|
10929
|
+
data: input.data,
|
|
10930
|
+
fileName: input.fileName
|
|
10931
|
+
});
|
|
10932
|
+
if (input.caption) await sendTextMessage({
|
|
10933
|
+
baseUrl: account.cred.baseUrl,
|
|
10934
|
+
token: account.cred.token,
|
|
10935
|
+
toUserId: input.userId,
|
|
10936
|
+
text: input.caption
|
|
10937
|
+
});
|
|
10938
|
+
if (settings.hook) {
|
|
10939
|
+
const role = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
10940
|
+
invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
10941
|
+
cccRoot: input.root,
|
|
10942
|
+
accountId: account.accountId,
|
|
10943
|
+
userId: input.userId,
|
|
10944
|
+
sessionId: weixinSessionIdFor(input.userId),
|
|
10945
|
+
role,
|
|
10946
|
+
reply: input.caption ? `[文件] ${sent.fileName} — ${input.caption}` : `[文件] ${sent.fileName}`,
|
|
10947
|
+
source: "proactive",
|
|
10948
|
+
file: {
|
|
10949
|
+
name: sent.fileName,
|
|
10950
|
+
size: sent.size,
|
|
10951
|
+
...input.caption ? { caption: input.caption } : {}
|
|
10952
|
+
}
|
|
10953
|
+
})).catch((err) => {
|
|
10954
|
+
console.log(`[serenity-hooks] weixin hook outgoing(file) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
10955
|
+
});
|
|
10956
|
+
}
|
|
10957
|
+
return sent;
|
|
10958
|
+
},
|
|
10959
|
+
/**
|
|
10960
|
+
* 通道健康快照(`status` 动作)。
|
|
10961
|
+
*
|
|
10962
|
+
* 显式映射为 JSON 安全形状(而非把内部结构直接返回):内部类型含 `undefined`
|
|
10963
|
+
* 可选字段与 interface(无隐式索引签名)→ 直接返回既过不了 `ImJson` 类型门,
|
|
10964
|
+
* 也会让工具返回值出现不可序列化字段。缺省用条件展开去掉 undefined。
|
|
10965
|
+
*/
|
|
10966
|
+
status(root) {
|
|
10967
|
+
const settings = readWeixinSettings(root);
|
|
10968
|
+
const accounts = weixinBridgeStatus().filter((entry) => entry.ccc === root).flatMap((entry) => entry.accounts.map((account) => ({
|
|
10969
|
+
accountId: account.accountId,
|
|
10970
|
+
lastPollAt: account.lastPollAt,
|
|
10971
|
+
...account.lastError ? { lastError: account.lastError } : {}
|
|
10972
|
+
})));
|
|
10973
|
+
const routes = (settings.routes ?? []).map((route) => ({
|
|
10974
|
+
user: route.user,
|
|
10975
|
+
role: route.role
|
|
10976
|
+
}));
|
|
10977
|
+
return {
|
|
10978
|
+
enabled: settings.enabled === true,
|
|
10979
|
+
autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
|
|
10980
|
+
fallbackOnNoSend: settings.fallbackOnNoSend === true,
|
|
10981
|
+
routes,
|
|
10982
|
+
accounts
|
|
10983
|
+
};
|
|
10984
|
+
}
|
|
10985
|
+
};
|
|
10986
|
+
//#endregion
|
|
10987
|
+
//#region src/tools/im-bridge.ts
|
|
10988
|
+
/**
|
|
10989
|
+
* im-bridge.ts(工具面)— IM 消息发送工具(v1.31.0)
|
|
10990
|
+
*
|
|
10991
|
+
* 用户洞察(S142):微信桥是 ACC 提供的 → **发送能力也应是 ACC 的工具**;
|
|
10992
|
+
* 「当用户配置了微信桥则可用,不配置则不可见」。
|
|
10993
|
+
*
|
|
10994
|
+
* 本文件只做工具面三件事:① 从 agent 会话解析**本会话 CCC**(R4:不接受目标 CCC)
|
|
10995
|
+
* ② 把参数交给 `im-bridge.ts`(能力层)分发 ③ 渲染结果/错误。
|
|
10996
|
+
*
|
|
10997
|
+
* 可见性不在这里判定:`seams/context.ts` 在会话就绪时按 CCC 配置
|
|
10998
|
+
* `agent.ctx.tools.restrict({ deny: ['im-bridge'] })` 把本工具从**未配置通道**的
|
|
10999
|
+
* agent 工具清单中移除(未配置 = 模型看不到,而非"看得到但被拒")。
|
|
11000
|
+
*/
|
|
11001
|
+
const ACTIONS = [
|
|
11002
|
+
"send",
|
|
11003
|
+
"send-file",
|
|
11004
|
+
"users",
|
|
11005
|
+
"status"
|
|
11006
|
+
];
|
|
11007
|
+
function agentCwd(exec) {
|
|
11008
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
11009
|
+
}
|
|
11010
|
+
function renderText(value) {
|
|
11011
|
+
return [{
|
|
11012
|
+
type: "text",
|
|
11013
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
11014
|
+
}];
|
|
11015
|
+
}
|
|
11016
|
+
/**
|
|
11017
|
+
* IM 发送工具(`im-bridge`)。通道维度为将来其他 IM 预留(R3)——
|
|
11018
|
+
* 新增通道只注册实现,不改工具名与参数形状。
|
|
11019
|
+
*/
|
|
11020
|
+
function createImBridgeTool() {
|
|
11021
|
+
return defineTool({
|
|
11022
|
+
name: "im-bridge",
|
|
11023
|
+
description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. yh, danica) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
|
|
11024
|
+
parameters: {
|
|
11025
|
+
channel: {
|
|
11026
|
+
type: "string",
|
|
11027
|
+
description: `IM channel id (e.g. "weixin"); available: ${imChannelIds().join(", ") || "(none)"}`,
|
|
11028
|
+
required: true
|
|
11029
|
+
},
|
|
11030
|
+
action: {
|
|
11031
|
+
type: "string",
|
|
11032
|
+
enum: [...ACTIONS],
|
|
11033
|
+
required: true,
|
|
11034
|
+
description: "send / send-file / users / status"
|
|
11035
|
+
},
|
|
11036
|
+
user: {
|
|
11037
|
+
type: "string",
|
|
11038
|
+
description: "Recipient: configured alias (yh, danica) or raw channel user id (send / send-file)"
|
|
11039
|
+
},
|
|
11040
|
+
text: {
|
|
11041
|
+
type: "string",
|
|
11042
|
+
description: "Message text, plain text only (send)"
|
|
11043
|
+
},
|
|
11044
|
+
file: {
|
|
11045
|
+
type: "string",
|
|
11046
|
+
description: "File path inside this container, relative to the container root (send-file)"
|
|
11047
|
+
},
|
|
11048
|
+
caption: {
|
|
11049
|
+
type: "string",
|
|
11050
|
+
description: "Optional caption sent as a separate message (send-file)"
|
|
11051
|
+
},
|
|
11052
|
+
account: {
|
|
11053
|
+
type: "string",
|
|
11054
|
+
description: "Channel account id (default: the first enabled, bound account)"
|
|
11055
|
+
}
|
|
11056
|
+
},
|
|
11057
|
+
output: {
|
|
11058
|
+
schema: { type: "json" },
|
|
11059
|
+
render: (_args, value) => renderText(value)
|
|
11060
|
+
},
|
|
11061
|
+
async execute(args, exec) {
|
|
11062
|
+
const root = findSerenityRoot(agentCwd(exec));
|
|
11063
|
+
if (!root) return {
|
|
11064
|
+
ok: false,
|
|
11065
|
+
code: "CCC_UNRESOLVED",
|
|
11066
|
+
error: "this session is not inside a Serenity container (no .serenity marker found from the session cwd)"
|
|
11067
|
+
};
|
|
11068
|
+
return runImBridge(root, args);
|
|
11069
|
+
}
|
|
11070
|
+
});
|
|
11071
|
+
}
|
|
11072
|
+
//#endregion
|
|
10476
11073
|
//#region src/index.ts
|
|
10477
11074
|
const name = "dsh-serenity-hooks";
|
|
10478
11075
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
|
|
@@ -10522,6 +11119,7 @@ function apply(ctx, config) {
|
|
|
10522
11119
|
const report = probeHostContract(ctx, readDshVersion());
|
|
10523
11120
|
if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
|
|
10524
11121
|
} catch {}
|
|
11122
|
+
registerImChannel(weixinChannel);
|
|
10525
11123
|
if (config.tools) {
|
|
10526
11124
|
ctx.tools.register(ccFsTool);
|
|
10527
11125
|
ctx.tools.register(createSessionTool(ctx));
|
|
@@ -10533,6 +11131,7 @@ function apply(ctx, config) {
|
|
|
10533
11131
|
ctx.tools.register(localstoreTool);
|
|
10534
11132
|
ctx.tools.register(containerAdminTool);
|
|
10535
11133
|
ctx.tools.register(createAutopilotTool(ctx));
|
|
11134
|
+
ctx.tools.register(createImBridgeTool());
|
|
10536
11135
|
}
|
|
10537
11136
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|
|
10538
11137
|
if (config.keeper) registerKeeper(ctx, {
|