@shgroup/dsh-serenity-hooks 1.30.17 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dsh.plugin.json +1 -1
- package/lib/ccc.d.ts +5 -5
- package/lib/im-bridge.d.ts +127 -0
- package/lib/im-weixin.d.ts +19 -0
- package/lib/index.js +626 -102
- 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 +20 -2
- package/lib/weixin-hook.d.ts +12 -0
- package/lib/weixin-output-guard.d.ts +12 -6
- package/lib/{weixin-route-DIaUYipW.js → weixin-route-wWk4AIwV.js} +1 -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,26 +2095,33 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1844
2095
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1845
2096
|
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1846
2097
|
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"|"guide"]) 排查 + 大而全指南。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1847
|
-
▸ 主动发送(v1.
|
|
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 不受影响。
|
|
1863
2121
|
|
|
1864
2122
|
▸ weixin.fallbackOnNoSend(v1.30.17,手动模式的**兜底**;缺省 false):
|
|
1865
2123
|
仅在 autoReplyWithLastMessage=false 时生效。true → 一轮结束时若 agent **一次都没成功**
|
|
1866
|
-
|
|
2124
|
+
调用输出工具(闸门打回用尽),桥把该轮最终文本转发给用户(记录 source="reply-fallback",
|
|
1867
2125
|
日志留响亮告警)——保证"不丢消息"。agent 自己发过 → 不兜底(输出权仍在 agent)。
|
|
1868
2126
|
为什么需要(实证):v1.30.16 闸门会打回 ≤2 次,但实测某模型在**寒暄类消息**上连续 4 轮、
|
|
1869
2127
|
跨 3 版 CCC 提示词仍不调工具 → 打回用尽后用户什么都收不到;提示词与闸门均已排除。
|
|
@@ -1890,7 +2148,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1890
2148
|
incoming = 用户 → bot:路由命中后、媒体落盘后触发(文本含语音转写;媒体带落盘 relPath)
|
|
1891
2149
|
outgoing = bot → 用户:发送成功后触发(reply = 用户实际收到的纯文本,已剥离 think)
|
|
1892
2150
|
· source="reply"(缺省)= 回复用户消息;source="proactive"(v1.30.9)= bot 主动发起
|
|
1893
|
-
(CCC 经 weixin-send MSM
|
|
2151
|
+
(CCC 经 im-bridge 工具(v1.31.0;旧 weixin-send MSM 已退役)→ sendProactiveText);
|
|
1894
2152
|
source="reply-fallback"(v1.30.17)= 手动模式下 agent 本轮未发送 → 桥兜底转发最终文本
|
|
1895
2153
|
· file(仅 send-file 补记):{ "name": "报告.pdf", "size": 12345, "caption": "可选" }
|
|
1896
2154
|
|
|
@@ -2327,13 +2585,13 @@ async function runMsmAsync(root, args) {
|
|
|
2327
2585
|
* 管理面(register/deregister/check/guide/catalog/ccc-config)已移入 container_admin。
|
|
2328
2586
|
* 底层 runMsmAsync/prepareExec 全复用;未命中候选为新增 findSuggestions。
|
|
2329
2587
|
*/
|
|
2330
|
-
function agentCwd$
|
|
2588
|
+
function agentCwd$7(exec) {
|
|
2331
2589
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
2332
2590
|
}
|
|
2333
2591
|
function agentSessionId$1(exec) {
|
|
2334
2592
|
return exec.agent?.session?.id ?? "";
|
|
2335
2593
|
}
|
|
2336
|
-
function renderText$
|
|
2594
|
+
function renderText$10(value) {
|
|
2337
2595
|
return [{
|
|
2338
2596
|
type: "text",
|
|
2339
2597
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2383,10 +2641,10 @@ const msmTool = defineTool({
|
|
|
2383
2641
|
},
|
|
2384
2642
|
output: {
|
|
2385
2643
|
schema: { type: "json" },
|
|
2386
|
-
render: (args, value) => renderText$
|
|
2644
|
+
render: (args, value) => renderText$10(value)
|
|
2387
2645
|
},
|
|
2388
2646
|
async execute(args, exec) {
|
|
2389
|
-
const root = findSerenityRoot(agentCwd$
|
|
2647
|
+
const root = findSerenityRoot(agentCwd$7(exec));
|
|
2390
2648
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2391
2649
|
const sessionId = agentSessionId$1(exec);
|
|
2392
2650
|
const name = args.name;
|
|
@@ -2444,7 +2702,7 @@ CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design doc
|
|
|
2444
2702
|
|
|
2445
2703
|
## Reference
|
|
2446
2704
|
https://github.com/tellmewhattodo/theory-eap`;
|
|
2447
|
-
function renderText$
|
|
2705
|
+
function renderText$9(value) {
|
|
2448
2706
|
return [{
|
|
2449
2707
|
type: "text",
|
|
2450
2708
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2464,7 +2722,7 @@ defineTool({
|
|
|
2464
2722
|
} },
|
|
2465
2723
|
output: {
|
|
2466
2724
|
schema: { type: "string" },
|
|
2467
|
-
render: (_args, value) => renderText$
|
|
2725
|
+
render: (_args, value) => renderText$9(value)
|
|
2468
2726
|
},
|
|
2469
2727
|
async execute(args) {
|
|
2470
2728
|
if (args.section === "variables") return EAP_CONTENT.split("## Pre-Output Self-Check Checklist")[0];
|
|
@@ -2505,7 +2763,7 @@ Requirements → Scope → Solution → Interface → Implementation
|
|
|
2505
2763
|
2. Wait for confirmation or correction (small step)
|
|
2506
2764
|
3. After confirmation, record the decision (into session SESSION.md or a design doc)
|
|
2507
2765
|
4. Move to the next decision point`;
|
|
2508
|
-
function renderText$
|
|
2766
|
+
function renderText$8(value) {
|
|
2509
2767
|
return [{
|
|
2510
2768
|
type: "text",
|
|
2511
2769
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2521,7 +2779,7 @@ defineTool({
|
|
|
2521
2779
|
} },
|
|
2522
2780
|
output: {
|
|
2523
2781
|
schema: { type: "string" },
|
|
2524
|
-
render: (_args, value) => renderText$
|
|
2782
|
+
render: (_args, value) => renderText$8(value)
|
|
2525
2783
|
},
|
|
2526
2784
|
async execute(args) {
|
|
2527
2785
|
if (args.section === "rules") return NEAT_CONTENT.match(/## Four Iron Rules[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
|
|
@@ -2584,7 +2842,7 @@ CCE answers "how structured knowledge should keep evolving across time without l
|
|
|
2584
2842
|
## Relationship with Serenity
|
|
2585
2843
|
Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
|
|
2586
2844
|
the behavioral constraints embedded in CCC system prompts come from CCE.`;
|
|
2587
|
-
function renderText$
|
|
2845
|
+
function renderText$7(value) {
|
|
2588
2846
|
return [{
|
|
2589
2847
|
type: "text",
|
|
2590
2848
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2605,7 +2863,7 @@ defineTool({
|
|
|
2605
2863
|
} },
|
|
2606
2864
|
output: {
|
|
2607
2865
|
schema: { type: "string" },
|
|
2608
|
-
render: (_args, value) => renderText$
|
|
2866
|
+
render: (_args, value) => renderText$7(value)
|
|
2609
2867
|
},
|
|
2610
2868
|
async execute(args) {
|
|
2611
2869
|
const section = args.section;
|
|
@@ -2651,7 +2909,7 @@ const PRAXIS_INDEX = `praxis — 可实践理论注入(认知框架,按需
|
|
|
2651
2909
|
praxis cce(认知连续性工程——容器 5 行为约束 + H_op 操作熵)
|
|
2652
2910
|
无参返回本目录;详情以对应框架原文为准(单一真相源)。
|
|
2653
2911
|
`;
|
|
2654
|
-
function renderText$
|
|
2912
|
+
function renderText$6(value) {
|
|
2655
2913
|
return [{
|
|
2656
2914
|
type: "text",
|
|
2657
2915
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2671,7 +2929,7 @@ const praxisTool = defineTool({
|
|
|
2671
2929
|
} },
|
|
2672
2930
|
output: {
|
|
2673
2931
|
schema: { type: "string" },
|
|
2674
|
-
render: (_args, value) => renderText$
|
|
2932
|
+
render: (_args, value) => renderText$6(value)
|
|
2675
2933
|
},
|
|
2676
2934
|
async execute(args) {
|
|
2677
2935
|
if (args.section === "eap") return EAP_CONTENT;
|
|
@@ -2725,10 +2983,10 @@ function handymanPresetInheritance(parentCtx) {
|
|
|
2725
2983
|
* preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
|
|
2726
2984
|
* subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
|
|
2727
2985
|
*/
|
|
2728
|
-
function agentCwd$
|
|
2986
|
+
function agentCwd$6(exec) {
|
|
2729
2987
|
return (exec.agent?.session)?.header?.cwd ?? process.cwd();
|
|
2730
2988
|
}
|
|
2731
|
-
function renderText$
|
|
2989
|
+
function renderText$5(value) {
|
|
2732
2990
|
return [{
|
|
2733
2991
|
type: "text",
|
|
2734
2992
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2931,11 +3189,11 @@ function createHandymanTool(ctx) {
|
|
|
2931
3189
|
},
|
|
2932
3190
|
output: {
|
|
2933
3191
|
schema: { type: "json" },
|
|
2934
|
-
render: (_args, value) => renderText$
|
|
3192
|
+
render: (_args, value) => renderText$5(value)
|
|
2935
3193
|
},
|
|
2936
3194
|
async execute(args, exec) {
|
|
2937
3195
|
if (args.guide) return { guide: HANDYMAN_GUIDE };
|
|
2938
|
-
const root = findSerenityRoot(agentCwd$
|
|
3196
|
+
const root = findSerenityRoot(agentCwd$6(exec));
|
|
2939
3197
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2940
3198
|
const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
2941
3199
|
if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
|
|
@@ -2996,7 +3254,7 @@ function createHandymanTool(ctx) {
|
|
|
2996
3254
|
* 同步把当前 dsh 会话重命名为该 SESSION 目录名**(sessionTitle.rename,user source
|
|
2997
3255
|
* pin 住标题)。非创建时预命名。
|
|
2998
3256
|
*/
|
|
2999
|
-
function agentCwd$
|
|
3257
|
+
function agentCwd$5(exec) {
|
|
3000
3258
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3001
3259
|
}
|
|
3002
3260
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
@@ -3112,7 +3370,7 @@ function renameDshSessionForActive(ctx, exec, info, summary) {
|
|
|
3112
3370
|
console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
|
|
3113
3371
|
}
|
|
3114
3372
|
}
|
|
3115
|
-
function renderText$
|
|
3373
|
+
function renderText$4(value) {
|
|
3116
3374
|
return [{
|
|
3117
3375
|
type: "text",
|
|
3118
3376
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3258,10 +3516,10 @@ function createSessionTool(ctx) {
|
|
|
3258
3516
|
},
|
|
3259
3517
|
output: {
|
|
3260
3518
|
schema: { type: "json" },
|
|
3261
|
-
render: (args, value) => renderText$
|
|
3519
|
+
render: (args, value) => renderText$4(value)
|
|
3262
3520
|
},
|
|
3263
3521
|
async execute(args, exec) {
|
|
3264
|
-
const root = findSerenityRoot(agentCwd$
|
|
3522
|
+
const root = findSerenityRoot(agentCwd$5(exec));
|
|
3265
3523
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3266
3524
|
const entries = loadMsmEntries(root);
|
|
3267
3525
|
const hasSessionTool = entries.some((e) => e.name === "session-tool");
|
|
@@ -3367,7 +3625,7 @@ function createSessionTool(ctx) {
|
|
|
3367
3625
|
root,
|
|
3368
3626
|
note: args.note,
|
|
3369
3627
|
summary: args.summary,
|
|
3370
|
-
agentCwd: agentCwd$
|
|
3628
|
+
agentCwd: agentCwd$5(exec),
|
|
3371
3629
|
dshSessionId
|
|
3372
3630
|
});
|
|
3373
3631
|
return {
|
|
@@ -3392,7 +3650,7 @@ function createSessionTool(ctx) {
|
|
|
3392
3650
|
* 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
|
|
3393
3651
|
* doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
|
|
3394
3652
|
*/
|
|
3395
|
-
const ACTIONS = [
|
|
3653
|
+
const ACTIONS$1 = [
|
|
3396
3654
|
"list",
|
|
3397
3655
|
"get",
|
|
3398
3656
|
"set",
|
|
@@ -3400,10 +3658,10 @@ const ACTIONS = [
|
|
|
3400
3658
|
"show",
|
|
3401
3659
|
"doc"
|
|
3402
3660
|
];
|
|
3403
|
-
function agentCwd$
|
|
3661
|
+
function agentCwd$4(exec) {
|
|
3404
3662
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3405
3663
|
}
|
|
3406
|
-
function renderText$
|
|
3664
|
+
function renderText$3(value) {
|
|
3407
3665
|
return [{
|
|
3408
3666
|
type: "text",
|
|
3409
3667
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3415,7 +3673,7 @@ const localstoreTool = defineTool({
|
|
|
3415
3673
|
parameters: {
|
|
3416
3674
|
action: {
|
|
3417
3675
|
type: "string",
|
|
3418
|
-
enum: [...ACTIONS],
|
|
3676
|
+
enum: [...ACTIONS$1],
|
|
3419
3677
|
required: true,
|
|
3420
3678
|
description: "Subcommand: list/get/set/unset/show/doc"
|
|
3421
3679
|
},
|
|
@@ -3435,10 +3693,10 @@ const localstoreTool = defineTool({
|
|
|
3435
3693
|
},
|
|
3436
3694
|
output: {
|
|
3437
3695
|
schema: { type: "json" },
|
|
3438
|
-
render: (args, value) => renderText$
|
|
3696
|
+
render: (args, value) => renderText$3(value)
|
|
3439
3697
|
},
|
|
3440
3698
|
async execute(args, exec) {
|
|
3441
|
-
const root = findSerenityRoot(agentCwd$
|
|
3699
|
+
const root = findSerenityRoot(agentCwd$4(exec));
|
|
3442
3700
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3443
3701
|
return runLocalStore(root, args);
|
|
3444
3702
|
}
|
|
@@ -3594,13 +3852,13 @@ function applySkiffConfig(root) {
|
|
|
3594
3852
|
*
|
|
3595
3853
|
* 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
|
|
3596
3854
|
*/
|
|
3597
|
-
function agentCwd$
|
|
3855
|
+
function agentCwd$3(exec) {
|
|
3598
3856
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3599
3857
|
}
|
|
3600
3858
|
function agentSessionId(exec) {
|
|
3601
3859
|
return exec.agent?.session?.id ?? "";
|
|
3602
3860
|
}
|
|
3603
|
-
function renderText$
|
|
3861
|
+
function renderText$2(value) {
|
|
3604
3862
|
return [{
|
|
3605
3863
|
type: "text",
|
|
3606
3864
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3655,10 +3913,10 @@ const containerAdminTool = defineTool({
|
|
|
3655
3913
|
},
|
|
3656
3914
|
output: {
|
|
3657
3915
|
schema: { type: "json" },
|
|
3658
|
-
render: (args, value) => renderText$
|
|
3916
|
+
render: (args, value) => renderText$2(value)
|
|
3659
3917
|
},
|
|
3660
3918
|
async execute(args, exec) {
|
|
3661
|
-
const root = findSerenityRoot(agentCwd$
|
|
3919
|
+
const root = findSerenityRoot(agentCwd$3(exec));
|
|
3662
3920
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3663
3921
|
const sessionId = agentSessionId(exec);
|
|
3664
3922
|
const domain = args.domain;
|
|
@@ -4363,10 +4621,10 @@ const AUTOPILOT_ACTIONS = [
|
|
|
4363
4621
|
"guide",
|
|
4364
4622
|
"diag-live"
|
|
4365
4623
|
];
|
|
4366
|
-
function agentCwd$
|
|
4624
|
+
function agentCwd$2(exec) {
|
|
4367
4625
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
4368
4626
|
}
|
|
4369
|
-
function renderText(value) {
|
|
4627
|
+
function renderText$1(value) {
|
|
4370
4628
|
return [{
|
|
4371
4629
|
type: "text",
|
|
4372
4630
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -4405,11 +4663,11 @@ function createAutopilotTool(ctx) {
|
|
|
4405
4663
|
} },
|
|
4406
4664
|
output: {
|
|
4407
4665
|
schema: { type: "json" },
|
|
4408
|
-
render: (_args, value) => renderText(value)
|
|
4666
|
+
render: (_args, value) => renderText$1(value)
|
|
4409
4667
|
},
|
|
4410
4668
|
async execute(args, exec) {
|
|
4411
4669
|
if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
|
|
4412
|
-
const root = findSerenityRoot(agentCwd$
|
|
4670
|
+
const root = findSerenityRoot(agentCwd$2(exec));
|
|
4413
4671
|
const result = {};
|
|
4414
4672
|
if (!EXP_SCRIPT) {
|
|
4415
4673
|
result.error = "autopilot-trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
|
|
@@ -5889,7 +6147,7 @@ function codeModeAdaptationLine(ctx, scope) {
|
|
|
5889
6147
|
* 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
|
|
5890
6148
|
* 回退 process.cwd()——对齐 context.ts,否则这些 agent 系统提示词注入为空
|
|
5891
6149
|
* (v1.18.6:workflow subagent 无宁静号上下文 bug) */
|
|
5892
|
-
function agentCwd(context) {
|
|
6150
|
+
function agentCwd$1(context) {
|
|
5893
6151
|
const agent = context.agent;
|
|
5894
6152
|
if (!agent) return void 0;
|
|
5895
6153
|
return agent.session?.header?.cwd ?? process.cwd();
|
|
@@ -5909,7 +6167,7 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
5909
6167
|
name: "serenity-entry",
|
|
5910
6168
|
order: -50,
|
|
5911
6169
|
text: (context) => {
|
|
5912
|
-
const cwd = agentCwd(context);
|
|
6170
|
+
const cwd = agentCwd$1(context);
|
|
5913
6171
|
if (!cwd) return "";
|
|
5914
6172
|
const root = findSerenityRoot(cwd);
|
|
5915
6173
|
if (!root) return "";
|
|
@@ -6116,6 +6374,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
6116
6374
|
injected.add(key);
|
|
6117
6375
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
6118
6376
|
syncSafeModeRestriction(agent, root);
|
|
6377
|
+
try {
|
|
6378
|
+
syncImBridgeVisibility(agent, root);
|
|
6379
|
+
} catch {}
|
|
6119
6380
|
};
|
|
6120
6381
|
if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
|
|
6121
6382
|
try {
|
|
@@ -6133,6 +6394,9 @@ function registerContext(ctx, opts = {}) {
|
|
|
6133
6394
|
try {
|
|
6134
6395
|
syncSafeModeRestriction(agent, root);
|
|
6135
6396
|
} catch {}
|
|
6397
|
+
try {
|
|
6398
|
+
syncImBridgeVisibility(agent, root);
|
|
6399
|
+
} catch {}
|
|
6136
6400
|
registerEntrySkillSection(agent, root);
|
|
6137
6401
|
}
|
|
6138
6402
|
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
@@ -6621,11 +6885,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6621
6885
|
sendJson$2(res, 400, { error: chk.error });
|
|
6622
6886
|
return;
|
|
6623
6887
|
}
|
|
6624
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6888
|
+
const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6625
6889
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6626
6890
|
const settings = readWeixinSettings(chk.root);
|
|
6627
6891
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
6628
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6892
|
+
const { readWeixinCredential } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6629
6893
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
6630
6894
|
accountId: a.accountId,
|
|
6631
6895
|
name: a.name ?? void 0,
|
|
@@ -6654,10 +6918,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6654
6918
|
}
|
|
6655
6919
|
const root = chk.root;
|
|
6656
6920
|
if (body.action === "login-start") {
|
|
6657
|
-
const { fetchQRCode } = await import("./weixin-api-
|
|
6921
|
+
const { fetchQRCode } = await import("./weixin-api-PjEhmjlZ.js").then((n) => n.u);
|
|
6658
6922
|
const { randomUUID } = await import("node:crypto");
|
|
6659
6923
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
6660
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6924
|
+
const { readWeixinSettings } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6661
6925
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
6662
6926
|
const loginKey = randomUUID();
|
|
6663
6927
|
weixinLogins.set(loginKey, {
|
|
@@ -6678,7 +6942,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6678
6942
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
6679
6943
|
return;
|
|
6680
6944
|
}
|
|
6681
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6945
|
+
const { removeWeixinAccount } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6682
6946
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6683
6947
|
removeWeixinAccount(root, body.accountId);
|
|
6684
6948
|
syncCccBridge(ctx, root);
|
|
@@ -6691,7 +6955,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6691
6955
|
sendJson$2(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
6692
6956
|
return;
|
|
6693
6957
|
}
|
|
6694
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
6958
|
+
const { saveWeixinRoutes } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6695
6959
|
const { readSkiffRoles } = await import("./skiff-role-BYvNnwv1.js").then((n) => n.u);
|
|
6696
6960
|
const roles = readSkiffRoles(root);
|
|
6697
6961
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
@@ -6704,7 +6968,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6704
6968
|
}
|
|
6705
6969
|
if (body.action === "set-enabled") {
|
|
6706
6970
|
const enabled = body.enabled === true;
|
|
6707
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6971
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6708
6972
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6709
6973
|
const settings = readWeixinSettings(root);
|
|
6710
6974
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -6750,8 +7014,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6750
7014
|
sendJson$2(res, 200, { status: "expired" });
|
|
6751
7015
|
return;
|
|
6752
7016
|
}
|
|
6753
|
-
const { pollQRStatus } = await import("./weixin-api-
|
|
6754
|
-
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);
|
|
6755
7019
|
const settings = readWeixinSettings(login.root);
|
|
6756
7020
|
const status = await pollQRStatus({
|
|
6757
7021
|
baseUrl: void 0,
|
|
@@ -6766,7 +7030,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6766
7030
|
});
|
|
6767
7031
|
return;
|
|
6768
7032
|
}
|
|
6769
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
7033
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-wWk4AIwV.js").then((n) => n.u);
|
|
6770
7034
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
6771
7035
|
const accountId = nextWeixinAccountId(settings);
|
|
6772
7036
|
upsertWeixinAccount(login.root, {
|
|
@@ -9239,19 +9503,31 @@ function sessionIdOf$1(value) {
|
|
|
9239
9503
|
const id = v?.session?.id ?? v?.id;
|
|
9240
9504
|
return typeof id === "string" && id !== "" ? id : null;
|
|
9241
9505
|
}
|
|
9242
|
-
/**
|
|
9506
|
+
/**
|
|
9507
|
+
* 工具调用是否为「成功的微信发送」。
|
|
9508
|
+
*
|
|
9509
|
+
* v1.31.0 起有两种合法形态(标记与闸门同时认,迁移期并存):
|
|
9510
|
+
* - `im-bridge({channel:"weixin", action:"send"|"send-file", …})` —— ACC 工具(推荐)
|
|
9511
|
+
* - `msm("weixin-send", ["send"|"send-file", …])` —— CCC MSM(v1.30.x 通道,逐步退役)
|
|
9512
|
+
*/
|
|
9243
9513
|
function isSuccessfulWeixinSend(exec, result) {
|
|
9244
9514
|
const e = exec;
|
|
9245
|
-
if (
|
|
9246
|
-
|
|
9247
|
-
|
|
9515
|
+
if (result?.isError === true) return false;
|
|
9516
|
+
const args = e?.arguments;
|
|
9517
|
+
if (e?.name === "im-bridge") {
|
|
9518
|
+
if ((typeof args?.channel === "string" ? args.channel : "") !== "weixin") return false;
|
|
9519
|
+
const action = typeof args?.action === "string" ? args.action : "";
|
|
9520
|
+
return action === "send" || action === "send-file";
|
|
9521
|
+
}
|
|
9522
|
+
if (e?.name === "msm") return args?.name === "weixin-send";
|
|
9523
|
+
return false;
|
|
9248
9524
|
}
|
|
9249
9525
|
/** 打回消息(机制事实 + 标记;怎么做由 CCC 角色提示词定义) */
|
|
9250
9526
|
function buildManualOutputRebuke(session) {
|
|
9251
9527
|
return [
|
|
9252
9528
|
"[serenity:weixin-manual-output] no send detected in this turn — the user has received NOTHING.",
|
|
9253
9529
|
`ccc=${session.root} account=${session.accountId} user=${session.userId}`,
|
|
9254
|
-
`
|
|
9530
|
+
`im-bridge({channel:"weixin", action:"send", account:"${session.accountId}", user:"${session.userId}", text:"<回复>"})`
|
|
9255
9531
|
].join("\n");
|
|
9256
9532
|
}
|
|
9257
9533
|
/**
|
|
@@ -9358,7 +9634,8 @@ function buildOutgoingHookEvent(input) {
|
|
|
9358
9634
|
sessionId: input.sessionId,
|
|
9359
9635
|
role: input.role,
|
|
9360
9636
|
reply: input.reply,
|
|
9361
|
-
...input.source && input.source !== "reply" ? { source: input.source } : {}
|
|
9637
|
+
...input.source && input.source !== "reply" ? { source: input.source } : {},
|
|
9638
|
+
...input.file ? { file: input.file } : {}
|
|
9362
9639
|
};
|
|
9363
9640
|
}
|
|
9364
9641
|
/**
|
|
@@ -9474,6 +9751,7 @@ var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
|
|
|
9474
9751
|
handleIncoming: () => handleIncoming,
|
|
9475
9752
|
manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
|
|
9476
9753
|
registerWeixinBridge: () => registerWeixinBridge,
|
|
9754
|
+
resolveWeixinAccount: () => resolveWeixinAccount,
|
|
9477
9755
|
sendProactiveText: () => sendProactiveText,
|
|
9478
9756
|
stopAllBridges: () => stopAllBridges,
|
|
9479
9757
|
stopCccBridge: () => stopCccBridge,
|
|
@@ -9571,14 +9849,14 @@ async function runAccountLoop(ctx, root, accountId, cred, loop) {
|
|
|
9571
9849
|
* "约束不够,LLM 不听"——且措辞迭代要动插件代码 + 发版 + 重启;把措辞放 CCC 才能快速迭代,
|
|
9572
9850
|
* 也符合"ACC 管机制、CCC 管内容"的归属二分。
|
|
9573
9851
|
*
|
|
9574
|
-
* 标记格式(稳定,供 CCC
|
|
9852
|
+
* 标记格式(稳定,供 CCC 提示词引用;v1.31.0 起为 ACC 工具形态):
|
|
9575
9853
|
* ```
|
|
9576
9854
|
* [serenity:weixin-manual-output]
|
|
9577
|
-
*
|
|
9855
|
+
* im-bridge({channel:"weixin", action:"send", account:"<account>", user:"<user>", text:"<回复>"})
|
|
9578
9856
|
* ```
|
|
9579
9857
|
*/
|
|
9580
9858
|
function weixinManualOutputMarker(root, accountId, userId) {
|
|
9581
|
-
return ["[serenity:weixin-manual-output]", `
|
|
9859
|
+
return ["[serenity:weixin-manual-output]", `im-bridge({channel:"weixin", action:"send", account:"${accountId}", user:"${userId}", text:"<回复>"})`].join("\n");
|
|
9582
9860
|
}
|
|
9583
9861
|
/**
|
|
9584
9862
|
* 手动输出模式的**兜底判定**(纯函数,v1.30.17——把决策矩阵从装配里抽出来,可独立测试)。
|
|
@@ -9840,22 +10118,12 @@ function stopAllBridges() {
|
|
|
9840
10118
|
bridges.clear();
|
|
9841
10119
|
}
|
|
9842
10120
|
/**
|
|
9843
|
-
*
|
|
9844
|
-
*
|
|
9845
|
-
*
|
|
9846
|
-
*
|
|
9847
|
-
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
9848
|
-
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
9849
|
-
*
|
|
9850
|
-
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
9851
|
-
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
9852
|
-
*
|
|
9853
|
-
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
9854
|
-
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
9855
|
-
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
10121
|
+
* 解析该 CCC 的发送账号 + 凭据(v1.31.0:文本与文件发送的**单一账号选择真相源**)。
|
|
10122
|
+
* 顺序:桥启用 → 有启用账号 → 指定账号存在 → 凭据已绑定(扫码)。
|
|
10123
|
+
* @param root CCC 根
|
|
10124
|
+
* @param accountId 指定账号(缺省 = 第一个启用账号)
|
|
9856
10125
|
*/
|
|
9857
|
-
|
|
9858
|
-
const { root, toUserId } = input;
|
|
10126
|
+
function resolveWeixinAccount(root, accountId) {
|
|
9859
10127
|
const settings = readWeixinSettings(root);
|
|
9860
10128
|
if (!settings.enabled) return {
|
|
9861
10129
|
ok: false,
|
|
@@ -9870,20 +10138,32 @@ async function sendProactiveText(input) {
|
|
|
9870
10138
|
error: "微信桥未配置任何启用账号",
|
|
9871
10139
|
remediation: "在设置面板「微信桥」扫码绑定账号"
|
|
9872
10140
|
};
|
|
9873
|
-
const
|
|
9874
|
-
if (!accounts.some((a) => a.accountId ===
|
|
10141
|
+
const id = accountId ?? accounts[0].accountId;
|
|
10142
|
+
if (!accounts.some((a) => a.accountId === id)) return {
|
|
9875
10143
|
ok: false,
|
|
9876
10144
|
code: "ACCOUNT_NOT_FOUND",
|
|
9877
|
-
error: `账号不存在或未启用: ${
|
|
10145
|
+
error: `账号不存在或未启用: ${id}`,
|
|
9878
10146
|
remediation: `可用账号: ${accounts.map((a) => a.accountId).join(", ")}`
|
|
9879
10147
|
};
|
|
9880
|
-
const cred = readWeixinCredential(root,
|
|
10148
|
+
const cred = readWeixinCredential(root, id);
|
|
9881
10149
|
if (!cred) return {
|
|
9882
10150
|
ok: false,
|
|
9883
10151
|
code: "ACCOUNT_NOT_BOUND",
|
|
9884
|
-
error: `账号未绑定(无凭据): ${
|
|
10152
|
+
error: `账号未绑定(无凭据): ${id}`,
|
|
9885
10153
|
remediation: "在设置面板重新扫码绑定"
|
|
9886
10154
|
};
|
|
10155
|
+
return {
|
|
10156
|
+
ok: true,
|
|
10157
|
+
accountId: id,
|
|
10158
|
+
cred
|
|
10159
|
+
};
|
|
10160
|
+
}
|
|
10161
|
+
async function sendProactiveText(input) {
|
|
10162
|
+
const { root, toUserId } = input;
|
|
10163
|
+
const settings = readWeixinSettings(root);
|
|
10164
|
+
const account = resolveWeixinAccount(root, input.accountId);
|
|
10165
|
+
if (!account.ok) return account;
|
|
10166
|
+
const { accountId, cred } = account;
|
|
9887
10167
|
const sessionId = weixinSessionIdFor(toUserId);
|
|
9888
10168
|
const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
|
|
9889
10169
|
try {
|
|
@@ -9959,7 +10239,9 @@ function registerWeixinBridge(ctx) {
|
|
|
9959
10239
|
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
9960
10240
|
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
9961
10241
|
*
|
|
9962
|
-
* 调用者 =
|
|
10242
|
+
* 调用者 = **非 agent 的调用者**(外部脚本/集成/运维)。**agent 侧已不走这里**:v1.31.0 起
|
|
10243
|
+
* agent 统一调 `im-bridge` 工具(进程内直调 `sendProactiveText`,无 HTTP 一跳),旧 CCC MSM
|
|
10244
|
+
* `weixin-send` 已退役。本入口保留 = 给容器外的进程一个稳定的本机发送面。
|
|
9963
10245
|
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
9964
10246
|
*
|
|
9965
10247
|
* 契约:
|
|
@@ -10325,6 +10607,9 @@ function cleanupSessionState(sessionId) {
|
|
|
10325
10607
|
try {
|
|
10326
10608
|
forgetManualOutputSession(sessionId);
|
|
10327
10609
|
} catch {}
|
|
10610
|
+
try {
|
|
10611
|
+
forgetImBridgeVisibility(sessionId);
|
|
10612
|
+
} catch {}
|
|
10328
10613
|
}
|
|
10329
10614
|
/**
|
|
10330
10615
|
* 装配生命周期订阅与资源拆卸。
|
|
@@ -10548,6 +10833,243 @@ async function registerWebFetchProvider(ctx) {
|
|
|
10548
10833
|
}
|
|
10549
10834
|
}
|
|
10550
10835
|
//#endregion
|
|
10836
|
+
//#region src/im-weixin.ts
|
|
10837
|
+
/**
|
|
10838
|
+
* im-weixin.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
|
|
10839
|
+
*
|
|
10840
|
+
* 能力归 ACC(S142 用户洞察):本文件吸收原 CCC MSM `.opencode/skills/home-serenity/scripts/
|
|
10841
|
+
* weixin-send.ts` 的 `send` / `send-file` / `users` 语义——协议、凭据、账号选择、别名解析、
|
|
10842
|
+
* 记录全部落在 ACC 侧;CCC 只保留「何时发 / 发什么 / 怎么写」。
|
|
10843
|
+
*
|
|
10844
|
+
* 通道内部契约:
|
|
10845
|
+
* - `isEnabled(root)` = 该 CCC 的 `weixin.enabled`(可见性判据,im-bridge 用它决定工具是否可见)
|
|
10846
|
+
* - `send` 复用 `sendProactiveText`(唯一文本发送实现 + 唯一记录点)
|
|
10847
|
+
* - `sendFile` 走 iLink 上传(`weixin-api.sendFileMessage`)+ 同一 outgoing hook(带 `file` 元数据)
|
|
10848
|
+
*/
|
|
10849
|
+
/**
|
|
10850
|
+
* 别名 → CCC localstore 凭据键(与 v1.30.9 CCC MSM 逐字一致,迁移零认知成本)。
|
|
10851
|
+
* 别名解析属"数据填值",归 ACC(D23)。
|
|
10852
|
+
*/
|
|
10853
|
+
const WEIXIN_ALIAS_MAP = {
|
|
10854
|
+
yh: "WEIXIN_USER_YH",
|
|
10855
|
+
danica: "WEIXIN_USER_DANICA",
|
|
10856
|
+
xiaowang: "WEIXIN_USER_DANICA"
|
|
10857
|
+
};
|
|
10858
|
+
/** 读该 CCC 的一个凭据值(空/缺失 → null) */
|
|
10859
|
+
function credentialValue(root, key) {
|
|
10860
|
+
try {
|
|
10861
|
+
const value = readStore(root, "credential")[key];
|
|
10862
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
10863
|
+
} catch {
|
|
10864
|
+
return null;
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
/** 把通道层的失败码转成一句可读错误(工具面统一映射为 SEND_FAILED) */
|
|
10868
|
+
function channelError(code, error, remediation) {
|
|
10869
|
+
return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
|
|
10870
|
+
}
|
|
10871
|
+
const weixinChannel = {
|
|
10872
|
+
id: "weixin",
|
|
10873
|
+
isEnabled(root) {
|
|
10874
|
+
return readWeixinSettings(root).enabled === true;
|
|
10875
|
+
},
|
|
10876
|
+
resolveUser(root, input) {
|
|
10877
|
+
const raw = input.trim();
|
|
10878
|
+
if (raw === "") return null;
|
|
10879
|
+
const alias = raw.toLowerCase();
|
|
10880
|
+
const key = WEIXIN_ALIAS_MAP[alias];
|
|
10881
|
+
if (key) {
|
|
10882
|
+
const id = credentialValue(root, key);
|
|
10883
|
+
return id ? {
|
|
10884
|
+
alias,
|
|
10885
|
+
id
|
|
10886
|
+
} : null;
|
|
10887
|
+
}
|
|
10888
|
+
if (raw.includes("@")) return {
|
|
10889
|
+
alias: raw,
|
|
10890
|
+
id: raw
|
|
10891
|
+
};
|
|
10892
|
+
return null;
|
|
10893
|
+
},
|
|
10894
|
+
listUsers(root) {
|
|
10895
|
+
const out = [];
|
|
10896
|
+
for (const [alias, key] of Object.entries(WEIXIN_ALIAS_MAP)) {
|
|
10897
|
+
const id = credentialValue(root, key);
|
|
10898
|
+
if (id) out.push({
|
|
10899
|
+
alias,
|
|
10900
|
+
id
|
|
10901
|
+
});
|
|
10902
|
+
}
|
|
10903
|
+
return out;
|
|
10904
|
+
},
|
|
10905
|
+
async send(input) {
|
|
10906
|
+
const result = await sendProactiveText({
|
|
10907
|
+
root: input.root,
|
|
10908
|
+
toUserId: input.userId,
|
|
10909
|
+
text: input.text,
|
|
10910
|
+
...input.accountId ? { accountId: input.accountId } : {}
|
|
10911
|
+
});
|
|
10912
|
+
if (!result.ok) throw channelError(result.code, result.error, result.remediation);
|
|
10913
|
+
return {
|
|
10914
|
+
accountId: result.accountId,
|
|
10915
|
+
userId: result.userId,
|
|
10916
|
+
sessionId: result.sessionId,
|
|
10917
|
+
role: result.role
|
|
10918
|
+
};
|
|
10919
|
+
},
|
|
10920
|
+
async sendFile(input) {
|
|
10921
|
+
const settings = readWeixinSettings(input.root);
|
|
10922
|
+
if (!settings.enabled) throw channelError("BRIDGE_DISABLED", `微信桥未启用(ccc=${input.root})`);
|
|
10923
|
+
const account = resolveWeixinAccount(input.root, input.accountId);
|
|
10924
|
+
if (!account.ok) throw channelError(account.code, account.error, account.remediation);
|
|
10925
|
+
const sent = await sendFileMessage({
|
|
10926
|
+
baseUrl: account.cred.baseUrl,
|
|
10927
|
+
token: account.cred.token,
|
|
10928
|
+
toUserId: input.userId,
|
|
10929
|
+
data: input.data,
|
|
10930
|
+
fileName: input.fileName
|
|
10931
|
+
});
|
|
10932
|
+
if (input.caption) await sendTextMessage({
|
|
10933
|
+
baseUrl: account.cred.baseUrl,
|
|
10934
|
+
token: account.cred.token,
|
|
10935
|
+
toUserId: input.userId,
|
|
10936
|
+
text: input.caption
|
|
10937
|
+
});
|
|
10938
|
+
if (settings.hook) {
|
|
10939
|
+
const role = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
10940
|
+
invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
10941
|
+
cccRoot: input.root,
|
|
10942
|
+
accountId: account.accountId,
|
|
10943
|
+
userId: input.userId,
|
|
10944
|
+
sessionId: weixinSessionIdFor(input.userId),
|
|
10945
|
+
role,
|
|
10946
|
+
reply: input.caption ? `[文件] ${sent.fileName} — ${input.caption}` : `[文件] ${sent.fileName}`,
|
|
10947
|
+
source: "proactive",
|
|
10948
|
+
file: {
|
|
10949
|
+
name: sent.fileName,
|
|
10950
|
+
size: sent.size,
|
|
10951
|
+
...input.caption ? { caption: input.caption } : {}
|
|
10952
|
+
}
|
|
10953
|
+
})).catch((err) => {
|
|
10954
|
+
console.log(`[serenity-hooks] weixin hook outgoing(file) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
10955
|
+
});
|
|
10956
|
+
}
|
|
10957
|
+
return sent;
|
|
10958
|
+
},
|
|
10959
|
+
/**
|
|
10960
|
+
* 通道健康快照(`status` 动作)。
|
|
10961
|
+
*
|
|
10962
|
+
* 显式映射为 JSON 安全形状(而非把内部结构直接返回):内部类型含 `undefined`
|
|
10963
|
+
* 可选字段与 interface(无隐式索引签名)→ 直接返回既过不了 `ImJson` 类型门,
|
|
10964
|
+
* 也会让工具返回值出现不可序列化字段。缺省用条件展开去掉 undefined。
|
|
10965
|
+
*/
|
|
10966
|
+
status(root) {
|
|
10967
|
+
const settings = readWeixinSettings(root);
|
|
10968
|
+
const accounts = weixinBridgeStatus().filter((entry) => entry.ccc === root).flatMap((entry) => entry.accounts.map((account) => ({
|
|
10969
|
+
accountId: account.accountId,
|
|
10970
|
+
lastPollAt: account.lastPollAt,
|
|
10971
|
+
...account.lastError ? { lastError: account.lastError } : {}
|
|
10972
|
+
})));
|
|
10973
|
+
const routes = (settings.routes ?? []).map((route) => ({
|
|
10974
|
+
user: route.user,
|
|
10975
|
+
role: route.role
|
|
10976
|
+
}));
|
|
10977
|
+
return {
|
|
10978
|
+
enabled: settings.enabled === true,
|
|
10979
|
+
autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
|
|
10980
|
+
fallbackOnNoSend: settings.fallbackOnNoSend === true,
|
|
10981
|
+
routes,
|
|
10982
|
+
accounts
|
|
10983
|
+
};
|
|
10984
|
+
}
|
|
10985
|
+
};
|
|
10986
|
+
//#endregion
|
|
10987
|
+
//#region src/tools/im-bridge.ts
|
|
10988
|
+
/**
|
|
10989
|
+
* im-bridge.ts(工具面)— IM 消息发送工具(v1.31.0)
|
|
10990
|
+
*
|
|
10991
|
+
* 用户洞察(S142):微信桥是 ACC 提供的 → **发送能力也应是 ACC 的工具**;
|
|
10992
|
+
* 「当用户配置了微信桥则可用,不配置则不可见」。
|
|
10993
|
+
*
|
|
10994
|
+
* 本文件只做工具面三件事:① 从 agent 会话解析**本会话 CCC**(R4:不接受目标 CCC)
|
|
10995
|
+
* ② 把参数交给 `im-bridge.ts`(能力层)分发 ③ 渲染结果/错误。
|
|
10996
|
+
*
|
|
10997
|
+
* 可见性不在这里判定:`seams/context.ts` 在会话就绪时按 CCC 配置
|
|
10998
|
+
* `agent.ctx.tools.restrict({ deny: ['im-bridge'] })` 把本工具从**未配置通道**的
|
|
10999
|
+
* agent 工具清单中移除(未配置 = 模型看不到,而非"看得到但被拒")。
|
|
11000
|
+
*/
|
|
11001
|
+
const ACTIONS = [
|
|
11002
|
+
"send",
|
|
11003
|
+
"send-file",
|
|
11004
|
+
"users",
|
|
11005
|
+
"status"
|
|
11006
|
+
];
|
|
11007
|
+
function agentCwd(exec) {
|
|
11008
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
11009
|
+
}
|
|
11010
|
+
function renderText(value) {
|
|
11011
|
+
return [{
|
|
11012
|
+
type: "text",
|
|
11013
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
11014
|
+
}];
|
|
11015
|
+
}
|
|
11016
|
+
/**
|
|
11017
|
+
* IM 发送工具(`im-bridge`)。通道维度为将来其他 IM 预留(R3)——
|
|
11018
|
+
* 新增通道只注册实现,不改工具名与参数形状。
|
|
11019
|
+
*/
|
|
11020
|
+
function createImBridgeTool() {
|
|
11021
|
+
return defineTool({
|
|
11022
|
+
name: "im-bridge",
|
|
11023
|
+
description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. yh, danica) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
|
|
11024
|
+
parameters: {
|
|
11025
|
+
channel: {
|
|
11026
|
+
type: "string",
|
|
11027
|
+
description: `IM channel id (e.g. "weixin"); available: ${imChannelIds().join(", ") || "(none)"}`,
|
|
11028
|
+
required: true
|
|
11029
|
+
},
|
|
11030
|
+
action: {
|
|
11031
|
+
type: "string",
|
|
11032
|
+
enum: [...ACTIONS],
|
|
11033
|
+
required: true,
|
|
11034
|
+
description: "send / send-file / users / status"
|
|
11035
|
+
},
|
|
11036
|
+
user: {
|
|
11037
|
+
type: "string",
|
|
11038
|
+
description: "Recipient: configured alias (yh, danica) or raw channel user id (send / send-file)"
|
|
11039
|
+
},
|
|
11040
|
+
text: {
|
|
11041
|
+
type: "string",
|
|
11042
|
+
description: "Message text, plain text only (send)"
|
|
11043
|
+
},
|
|
11044
|
+
file: {
|
|
11045
|
+
type: "string",
|
|
11046
|
+
description: "File path inside this container, relative to the container root (send-file)"
|
|
11047
|
+
},
|
|
11048
|
+
caption: {
|
|
11049
|
+
type: "string",
|
|
11050
|
+
description: "Optional caption sent as a separate message (send-file)"
|
|
11051
|
+
},
|
|
11052
|
+
account: {
|
|
11053
|
+
type: "string",
|
|
11054
|
+
description: "Channel account id (default: the first enabled, bound account)"
|
|
11055
|
+
}
|
|
11056
|
+
},
|
|
11057
|
+
output: {
|
|
11058
|
+
schema: { type: "json" },
|
|
11059
|
+
render: (_args, value) => renderText(value)
|
|
11060
|
+
},
|
|
11061
|
+
async execute(args, exec) {
|
|
11062
|
+
const root = findSerenityRoot(agentCwd(exec));
|
|
11063
|
+
if (!root) return {
|
|
11064
|
+
ok: false,
|
|
11065
|
+
code: "CCC_UNRESOLVED",
|
|
11066
|
+
error: "this session is not inside a Serenity container (no .serenity marker found from the session cwd)"
|
|
11067
|
+
};
|
|
11068
|
+
return runImBridge(root, args);
|
|
11069
|
+
}
|
|
11070
|
+
});
|
|
11071
|
+
}
|
|
11072
|
+
//#endregion
|
|
10551
11073
|
//#region src/index.ts
|
|
10552
11074
|
const name = "dsh-serenity-hooks";
|
|
10553
11075
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
|
|
@@ -10597,6 +11119,7 @@ function apply(ctx, config) {
|
|
|
10597
11119
|
const report = probeHostContract(ctx, readDshVersion());
|
|
10598
11120
|
if (report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
|
|
10599
11121
|
} catch {}
|
|
11122
|
+
registerImChannel(weixinChannel);
|
|
10600
11123
|
if (config.tools) {
|
|
10601
11124
|
ctx.tools.register(ccFsTool);
|
|
10602
11125
|
ctx.tools.register(createSessionTool(ctx));
|
|
@@ -10608,6 +11131,7 @@ function apply(ctx, config) {
|
|
|
10608
11131
|
ctx.tools.register(localstoreTool);
|
|
10609
11132
|
ctx.tools.register(containerAdminTool);
|
|
10610
11133
|
ctx.tools.register(createAutopilotTool(ctx));
|
|
11134
|
+
ctx.tools.register(createImBridgeTool());
|
|
10611
11135
|
}
|
|
10612
11136
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|
|
10613
11137
|
if (config.keeper) registerKeeper(ctx, {
|