@shgroup/dsh-serenity-hooks 1.84.0 → 1.86.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/dsh.plugin.json +1 -1
- package/lib/{ccc-roots-4LwGLCXa.js → ccc-roots-BI_4UVcj.js} +1 -1
- package/lib/client.js +150 -95
- package/lib/im/channels.d.ts +33 -0
- package/lib/im/hook.d.ts +17 -0
- package/lib/im/log-scan.d.ts +53 -0
- package/lib/im/route.d.ts +8 -1
- package/lib/index.js +465 -38
- package/lib/{message-source-CfqdGIi2.js → message-source-DRkQwfgh.js} +2 -2
- package/lib/{profile-CiRWh2a2.js → profile-vHheCJiU.js} +17 -6
- package/lib/{route-Dr0SFD8A.js → route-GtYJdR06.js} +7 -0
- package/lib/seams/context.d.ts +13 -0
- package/lib/{session-BF4C3hJL.js → session-DZXSLLEx.js} +3 -3
- package/lib/{settings-Dj4_ppTC.js → settings-H7TVDayZ.js} +1 -1
- package/lib/tools/im-docs.d.ts +20 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
|
|
2
|
-
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-
|
|
2
|
+
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-BI_4UVcj.js";
|
|
3
3
|
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
|
|
4
|
-
import { A as
|
|
4
|
+
import { A as resolveSessionByTitle, B as readTrajectoryFile, C as listActiveTrajectories, D as parseTrajectoryCode, E as parseSessionContextFromEvents, F as summarize, H as localFileStamp, I as trajectoryDescOf, L as useSession, M as sessionsRoot, N as setActiveSessionInfo, P as showSession, R as TRAJECTORY_FILENAME, T as namingTitleFor, U as localHuman, V as isoLocal, W as localIdStamp, _ as clearActiveSessionInfo, b as findSession, c as replacesDefaultIdentity, g as archiveSession, h as UID_FILENAME, i as listTrajectoryConstraints, j as sessionEvents, k as readTrajectoryUid, l as trajectoryConstraintForSession, n as findDirNameByCode, o as readTrajectoryConstraintAt, p as COGNITIVE_TRAJECTORY_ACTIONS, r as isTrajectoryConstrained, s as readTrajectoryConstraintByCode, t as TRAJECTORY_CONSTRAINT_FILENAME, v as createSession, w as listSessions, x as getActiveSessionInfo, y as extractSessionMdPathFromText, z as TRAJECTORY_LEGACY_FILENAMES } from "./profile-vHheCJiU.js";
|
|
5
5
|
import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
|
|
6
|
-
import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-
|
|
7
|
-
import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-
|
|
6
|
+
import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-DRkQwfgh.js";
|
|
7
|
+
import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-DZXSLLEx.js";
|
|
8
8
|
import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
|
|
9
|
-
import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-
|
|
9
|
+
import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-H7TVDayZ.js";
|
|
10
|
+
import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-GtYJdR06.js";
|
|
10
11
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
11
|
-
import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-Dr0SFD8A.js";
|
|
12
12
|
import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
|
|
13
13
|
import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
@@ -9346,7 +9346,8 @@ async function sendToCognitiveTrajectory(ctx, root, target, message, sender) {
|
|
|
9346
9346
|
}],
|
|
9347
9347
|
source: PLUGIN_SOURCE
|
|
9348
9348
|
});
|
|
9349
|
-
|
|
9349
|
+
const freshCarrier = createdNote !== "";
|
|
9350
|
+
if (freshCarrier || agent.status === "idle") {
|
|
9350
9351
|
try {
|
|
9351
9352
|
agent.followup(make());
|
|
9352
9353
|
} catch (err) {
|
|
@@ -9357,7 +9358,7 @@ async function sendToCognitiveTrajectory(ctx, root, target, message, sender) {
|
|
|
9357
9358
|
}
|
|
9358
9359
|
return {
|
|
9359
9360
|
ok: true,
|
|
9360
|
-
detail: `已即时投递 ${t.dirName}(${acquired.how}
|
|
9361
|
+
detail: `已即时投递 ${t.dirName}(${acquired.how};${freshCarrier ? "刚建的载体 ⇒ followup 排队,锚定先于任务" : "目标空闲 ⇒ followup 立即起轮"})${createdNote}`
|
|
9361
9362
|
};
|
|
9362
9363
|
}
|
|
9363
9364
|
try {
|
|
@@ -11047,6 +11048,152 @@ const TRAJECTORY_SELF_GUIDE = [
|
|
|
11047
11048
|
⚠️ 以上四项都是**读取当前真实状态**,不是"保存成功"的回执。保存成功 ≠ 生效(本仓反复踩过)。`
|
|
11048
11049
|
].join("\n");
|
|
11049
11050
|
//#endregion
|
|
11051
|
+
//#region src/tools/im-docs.ts
|
|
11052
|
+
/**
|
|
11053
|
+
* tools/im-docs.ts — 「IM 桥怎么用、怎么配路由」的**手册正文**(2026-10-22)
|
|
11054
|
+
*
|
|
11055
|
+
* ## 为什么需要这一章(owner 原话)
|
|
11056
|
+
*
|
|
11057
|
+
* 「我在测试飞书,发现 ACC 没提供 IM 桥使用说明和账户查询方法,**导致 CCC 没办法自己配路由**,
|
|
11058
|
+
* 这个要完善下」+「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
|
|
11059
|
+
* 需要有个能力让 CCC 查询用户配好的账户」。
|
|
11060
|
+
*
|
|
11061
|
+
* ⇒ 两件事被同时指出来:**没有说明面**(只能猜配置形状)+ **没有查询面**(拿不到对方 id)。
|
|
11062
|
+
* 后者已由 `im-bridge action=contacts` 兑现;本章补前者。
|
|
11063
|
+
*
|
|
11064
|
+
* ## 🔴 写这一章时守的两条纪律
|
|
11065
|
+
*
|
|
11066
|
+
* 1. **只描述已经建成的东西**(D179):本章出现的每个字段、每个动作,都必须在实现里存在。
|
|
11067
|
+
* 写"打算做"的功能进手册 = 教用户按图施工然后把自己锁死。
|
|
11068
|
+
* 2. **指针从读者真正打开的那个工具发出**(D178):读这一章的人是 **CCC 的 agent**,
|
|
11069
|
+
* 它手边常用的入口是 `im-bridge`(工具描述已指向本章)。⛔ 不能只藏在 container_admin 里。
|
|
11070
|
+
*/
|
|
11071
|
+
const IM_BRIDGE_GUIDE = `# IM 桥(im-bridge)—— 怎么用、怎么配、怎么查
|
|
11072
|
+
|
|
11073
|
+
## 一、它是什么
|
|
11074
|
+
|
|
11075
|
+
一个 CCC 通过 IM 渠道(微信 / 飞书)跟家人收发消息的能力。
|
|
11076
|
+
**只作用于本容器**——没有"目标容器"参数(跨 CCC 发送不在能力面)。
|
|
11077
|
+
|
|
11078
|
+
工具在**未配置任何 IM 渠道时根本不可见**(不是"看得到但被拒")。
|
|
11079
|
+
⇒ 你看到这个工具,说明本 CCC 已配了至少一个渠道。
|
|
11080
|
+
|
|
11081
|
+
## 二、五个动作
|
|
11082
|
+
|
|
11083
|
+
| 动作 | 用途 | 关键参数 |
|
|
11084
|
+
|---|---|---|
|
|
11085
|
+
| \`send\` | 发一条文本 | \`user\`(对方)+ \`text\` |
|
|
11086
|
+
| \`send-file\` | 发本容器内的一个文件 | \`user\` + \`file\`(**CCC 根相对路径**)+ 可选 \`caption\` |
|
|
11087
|
+
| \`users\` | 已配的**别名**表 | — |
|
|
11088
|
+
| \`status\` | 渠道健康 + **账号** + **当前路由**(含每条路由的目标是否存在) | — |
|
|
11089
|
+
| \`contacts\` | **跟本容器有过往来的人**(含对方 id)+ 未配路由的 / 配了却没往来的 | — |
|
|
11090
|
+
|
|
11091
|
+
⚠️ 渠道不一定支持全部动作(例如飞书目前不支持 \`send-file\`,会如实返回 \`ACTION_UNSUPPORTED\`)。
|
|
11092
|
+
**如实返回"不支持"比假装支持好**——后者会得到一条"发出去但对方收不到"的静默故障。
|
|
11093
|
+
|
|
11094
|
+
## 三、🔴 配路由:先 \`contacts\`,再写,再 \`status\` 自检
|
|
11095
|
+
|
|
11096
|
+
路由的形状(\`.opencode/serenity.json\` 的 \`im.routes\`):
|
|
11097
|
+
|
|
11098
|
+
\`\`\`json
|
|
11099
|
+
{
|
|
11100
|
+
"im": {
|
|
11101
|
+
"enabled": true,
|
|
11102
|
+
"accounts": [
|
|
11103
|
+
{ "accountId": "wechat-1", "channel": "weixin", "enabled": true },
|
|
11104
|
+
{ "accountId": "feishu-1", "channel": "feishu", "enabled": true }
|
|
11105
|
+
],
|
|
11106
|
+
"routes": [
|
|
11107
|
+
{ "user": "o9cq…@im.wechat", "trajectory": "zhaocai-xingzai" },
|
|
11108
|
+
{ "user": "ou_xxxxxxxx", "trajectory": "llm-intel" }
|
|
11109
|
+
]
|
|
11110
|
+
}
|
|
11111
|
+
}
|
|
11112
|
+
\`\`\`
|
|
11113
|
+
|
|
11114
|
+
- \`user\` = **对方在该渠道内的 id**(不是我们的账号,也不是别名)
|
|
11115
|
+
· 微信:形如 \`xxx@im.wechat\`
|
|
11116
|
+
· 飞书:形如 \`ou_…\`(open_id)
|
|
11117
|
+
- \`trajectory\` = **认知轨迹编码**(目录名里那一段,如 \`S142\` / \`llm-intel\`)
|
|
11118
|
+
- \`*\` 可作为 \`user\` 的**通配兜底**(exact 优先)
|
|
11119
|
+
|
|
11120
|
+
### 那个 \`user\` id 从哪里来 —— 这是本节存在的原因
|
|
11121
|
+
|
|
11122
|
+
**你不知道对方 id,而它也不在任何配置文件里。** 它只出现在聊天记录中。
|
|
11123
|
+
⇒ 用 \`im-bridge({channel:"weixin", action:"contacts"})\`,它会给出:
|
|
11124
|
+
|
|
11125
|
+
\`\`\`json
|
|
11126
|
+
{
|
|
11127
|
+
"contacts": [
|
|
11128
|
+
{ "channel": "weixin", "userId": "o9cq…@im.wechat", "accountIds": ["wechat-1"],
|
|
11129
|
+
"route": null, "firstSeen": "2026-09-04T…", "lastSeen": "2026-10-22T…", "messages": 1284 }
|
|
11130
|
+
],
|
|
11131
|
+
"unrouted": ["o9cq…@im.wechat"],
|
|
11132
|
+
"orphanRoutes": ["ou_typo_never_seen"],
|
|
11133
|
+
"sources": ["AGENT_SESSIONS/_im-logs/weixin/2026-10-22.jsonl", "…"]
|
|
11134
|
+
}
|
|
11135
|
+
\`\`\`
|
|
11136
|
+
|
|
11137
|
+
三行输出**各有用途,缺一行这个动作就只剩"列名字"**:
|
|
11138
|
+
|
|
11139
|
+
| 字段 | 含义 | 你要做什么 |
|
|
11140
|
+
|---|---|---|
|
|
11141
|
+
| \`contacts[].userId\` | 对方 id | **复制它**填进 \`routes[].user\` |
|
|
11142
|
+
| \`unrouted\` | ⚠️ 有往来、但**没配路由** | 这些人的消息现在**无处可去** ⇒ 这就是该配的 |
|
|
11143
|
+
| \`orphanRoutes\` | ❓ 配了路由、但记录里**从没出现过** | 大概率 **id 写错**(或从未有人发过) |
|
|
11144
|
+
| \`sources\` | 实际读过的文件 | **否定性结论的可信度上限** = 这个列表;它空着就别下"没人"的结论 |
|
|
11145
|
+
|
|
11146
|
+
### 写完怎么自检
|
|
11147
|
+
|
|
11148
|
+
\`im-bridge({channel:"weixin", action:"status"})\` 的 \`routes[]\` 每条都带 **\`targetExists\`**:
|
|
11149
|
+
|
|
11150
|
+
- \`true\` ⇒ 该轨迹目录真实存在,路由**有机会**生效
|
|
11151
|
+
- \`false\` ⇒ **轨迹编码写错了**(或那条轨迹已被归档/删除)
|
|
11152
|
+
|
|
11153
|
+
🔴 这一格是"配了不生效"的**唯一机械信号**:写错一个编码**不会有任何报错**,
|
|
11154
|
+
消息进来找不到目标就静默不投递,你只会看到"没回音"。
|
|
11155
|
+
|
|
11156
|
+
## 四、多账号 / 多渠道
|
|
11157
|
+
|
|
11158
|
+
一个 CCC 绑多个账号是常态(owner 2026-10-22:「一个 CCC 绑定多个账户很常见」)。
|
|
11159
|
+
|
|
11160
|
+
- **账号**在 \`im.accounts[]\`,每条自带 \`channel\`(缺省 \`weixin\`)
|
|
11161
|
+
- **路由是通道无关的**(\`user → trajectory\`);同一轨迹可以被多个渠道的对端指向
|
|
11162
|
+
- \`contacts\` 的每条带 \`accountIds\`(该对端被哪些账号收过)——**多账号靠它区分**
|
|
11163
|
+
- 发消息时可用 \`account\` 参数指定用哪个账号(缺省:第一个启用且已绑定的)
|
|
11164
|
+
|
|
11165
|
+
## 五、聊天记录落在哪
|
|
11166
|
+
|
|
11167
|
+
\`\`\`
|
|
11168
|
+
AGENT_SESSIONS/_im-logs/<channel>/<YYYY-MM-DD>.jsonl
|
|
11169
|
+
\`\`\`
|
|
11170
|
+
|
|
11171
|
+
- 按**渠道**分目录(\`weixin\` / \`feishu\`),按**本地自然天**分文件,逐行 JSON 追加
|
|
11172
|
+
- 老路径 \`AGENT_SESSIONS/_weixin-logs/\` 是**同一族的前代**(2026-10-22 之前的微信记录);
|
|
11173
|
+
读侧**两个都认**,⛔ 但**只往新路径写**
|
|
11174
|
+
- 每条事件带 \`channel\` 字段 ⇒ 飞书事件不会再混进微信目录(此前会,目录名会说谎)
|
|
11175
|
+
- 媒体的持久副本在 \`_im-logs/<channel>/media/<日期>/\`
|
|
11176
|
+
|
|
11177
|
+
⚠️ **记录归 CCC**(\`im.hook\` 指向本容器的脚本),ACC 不认识存储格式 —— 这层分离是有意的。
|
|
11178
|
+
|
|
11179
|
+
## 六、出错时
|
|
11180
|
+
|
|
11181
|
+
工具返回**稳定错误码 + 可行动提示**,常见的:
|
|
11182
|
+
|
|
11183
|
+
| 码 | 意思 | 怎么改 |
|
|
11184
|
+
|---|---|---|
|
|
11185
|
+
| \`CHANNEL_NOT_CONFIGURED\` | 本 CCC 没启用该渠道 | 看提示里的**那一半**(总开关 vs 缺账号,两者改法不同) |
|
|
11186
|
+
| \`ALIAS_UNKNOWN\` | \`user\` 既不是已配别名、也不是合法的裸 id | 先 \`action=users\` 看别名,或用 \`contacts\` 拿裸 id |
|
|
11187
|
+
| \`ACTION_UNSUPPORTED\` | 该渠道没实现这个动作 | 换动作;⛔ 不要重试 |
|
|
11188
|
+
| \`FILE_ESCAPE\` | \`file\` 跑出了 CCC 根 | 用容器内相对路径 |
|
|
11189
|
+
|
|
11190
|
+
## 七、边界(明确不做)
|
|
11191
|
+
|
|
11192
|
+
- ⛔ 跨 CCC 发送(只作用于本容器)
|
|
11193
|
+
- ⛔ 修改配置(\`contacts\` 只读;加路由要人/CCC 自己写 \`serenity.json\`)
|
|
11194
|
+
- ⛔ 判断"对方读没读"——记录只证明**已发出**
|
|
11195
|
+
`;
|
|
11196
|
+
//#endregion
|
|
11050
11197
|
//#region src/tools/container-admin.ts
|
|
11051
11198
|
/**
|
|
11052
11199
|
* container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
|
|
@@ -11197,6 +11344,11 @@ const DOCS_CHAPTERS = [
|
|
|
11197
11344
|
id: "trajectory-constraint",
|
|
11198
11345
|
summary: "轨迹约束(只讲限制面):trajectory.json 的双白名单 / 模型 / 纪律参与项 / IM 路由",
|
|
11199
11346
|
render: () => TRAJECTORY_CONSTRAINT_GUIDE
|
|
11347
|
+
},
|
|
11348
|
+
{
|
|
11349
|
+
id: "im-bridge",
|
|
11350
|
+
summary: "IM 桥怎么用、怎么配路由、怎么查对方 id —— send/send-file/users/status/contacts 五个动作 + 路由三步法(contacts → 写 → status 自检)",
|
|
11351
|
+
render: () => IM_BRIDGE_GUIDE
|
|
11200
11352
|
}
|
|
11201
11353
|
];
|
|
11202
11354
|
/**
|
|
@@ -11207,7 +11359,7 @@ const DOCS_CHAPTERS = [
|
|
|
11207
11359
|
function createContainerAdminTool(ctx) {
|
|
11208
11360
|
return defineTool({
|
|
11209
11361
|
name: "container_admin",
|
|
11210
|
-
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). TRAJECTORY SELF-DOCS: a cognitive trajectory describing ITSELF — charter (PROMPT.md) / resident declaration / self-restriction (whitelists) ⇒ config docs trajectory-self; the restriction side only ⇒ config docs trajectory-constraint. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
11362
|
+
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): config — CCC configuration: docs (usage-manual index) / docs <chapter> (one chapter in full) / view (current values) + 轨迹约束 (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference). TRAJECTORY SELF-DOCS: a cognitive trajectory describing ITSELF — charter (PROMPT.md) / resident declaration / self-restriction (whitelists) ⇒ config docs trajectory-self; the restriction side only ⇒ config docs trajectory-constraint. IM BRIDGE: how to send / configure routes / look up the other party's channel user id — send / send-file / users / status / contacts + the three-step route recipe ⇒ config docs im-bridge. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
11211
11363
|
parameters: {
|
|
11212
11364
|
domain: {
|
|
11213
11365
|
type: "string",
|
|
@@ -11320,6 +11472,138 @@ function createContainerAdminTool(ctx) {
|
|
|
11320
11472
|
});
|
|
11321
11473
|
}
|
|
11322
11474
|
//#endregion
|
|
11475
|
+
//#region src/im/log-scan.ts
|
|
11476
|
+
/**
|
|
11477
|
+
* im/log-scan.ts — 读聊天记录落盘,汇总出「与谁有过往来」(`contacts` 的数据源)
|
|
11478
|
+
*
|
|
11479
|
+
* ## 这个文件解决什么
|
|
11480
|
+
*
|
|
11481
|
+
* owner 2026-10-22:「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
|
|
11482
|
+
* 需要有个能力让 CCC 查询用户配好的账户」。这里的"账户"= **通道用户**(对方是谁),
|
|
11483
|
+
* 不是 IM 账号(我们自己登录的身份,`status` 早就能查)。
|
|
11484
|
+
*
|
|
11485
|
+
* 那个 id(微信 `xxx@im.wechat` / 飞书 `ou_…`)不在任何配置文件里 ——
|
|
11486
|
+
* 它**只在聊天记录里出现过**。⇒ 本文件把记录读成联系人表。
|
|
11487
|
+
*
|
|
11488
|
+
* ## 🔴 两条硬边界
|
|
11489
|
+
*
|
|
11490
|
+
* 1. **只读**:本文件不写任何东西。真相源仍是那些 jsonl(append-only 事件流),
|
|
11491
|
+
* 本文件只是它的一次投影 —— ⛔ 不产生第二真相源。
|
|
11492
|
+
* 2. **不猜渠道**:事件的 `channel` 字段为准;缺它(存量事件)⇒ 按
|
|
11493
|
+
* `accountId → im.accounts[].channel` 回填,仍取不到才落 `weixin`
|
|
11494
|
+
* (那是该字段存在之前唯一的渠道)。⛔ **绝不按 userId 的形状猜**
|
|
11495
|
+
* (`ou_` 前缀 / `@im.wechat` 后缀都是命名习惯,不是判据 —— D167 认族不认名)。
|
|
11496
|
+
*/
|
|
11497
|
+
/** 日志根目录族(**新旧两代并存**:`_im-logs/<channel>/` 与存量 `_weixin-logs/`) */
|
|
11498
|
+
const IM_LOG_DIR_NEW = "_im-logs";
|
|
11499
|
+
const IM_LOG_DIR_LEGACY = "_weixin-logs";
|
|
11500
|
+
/** 某 CCC 下**存在**的日志目录(相对根),新代在前 */
|
|
11501
|
+
function imLogRoots(root) {
|
|
11502
|
+
const out = [];
|
|
11503
|
+
const newBase = join(root, "AGENT_SESSIONS", IM_LOG_DIR_NEW);
|
|
11504
|
+
const legacy = join(root, "AGENT_SESSIONS", IM_LOG_DIR_LEGACY);
|
|
11505
|
+
if (existsSync(newBase)) out.push(join("AGENT_SESSIONS", IM_LOG_DIR_NEW));
|
|
11506
|
+
if (existsSync(legacy)) out.push(join("AGENT_SESSIONS", IM_LOG_DIR_LEGACY));
|
|
11507
|
+
return out;
|
|
11508
|
+
}
|
|
11509
|
+
/** 收集某个日志根下所有 `*.jsonl`(含 `_im-logs/<channel>/` 一层;按文件名排序) */
|
|
11510
|
+
function jsonlFilesIn(root, rel) {
|
|
11511
|
+
const abs = join(root, rel);
|
|
11512
|
+
const out = [];
|
|
11513
|
+
let entries = [];
|
|
11514
|
+
try {
|
|
11515
|
+
entries = readdirSync(abs, { withFileTypes: true }).map((d) => d.name);
|
|
11516
|
+
} catch {
|
|
11517
|
+
return out;
|
|
11518
|
+
}
|
|
11519
|
+
for (const name of entries.sort()) {
|
|
11520
|
+
if (name.startsWith(".")) continue;
|
|
11521
|
+
if (name.endsWith(".jsonl")) {
|
|
11522
|
+
out.push(join(rel, name));
|
|
11523
|
+
continue;
|
|
11524
|
+
}
|
|
11525
|
+
if (rel === join("AGENT_SESSIONS", "_im-logs")) {
|
|
11526
|
+
const sub = join(rel, name);
|
|
11527
|
+
try {
|
|
11528
|
+
for (const child of readdirSync(join(root, sub)).sort()) if (child.endsWith(".jsonl")) out.push(join(sub, child));
|
|
11529
|
+
} catch {}
|
|
11530
|
+
}
|
|
11531
|
+
}
|
|
11532
|
+
return out;
|
|
11533
|
+
}
|
|
11534
|
+
/** 从文件名 / 所在目录猜渠道(**只作为回填的最后一级**,不是判据) */
|
|
11535
|
+
function channelFromPath(rel) {
|
|
11536
|
+
const parts = rel.split("/");
|
|
11537
|
+
const idx = parts.indexOf(IM_LOG_DIR_NEW);
|
|
11538
|
+
const candidate = idx >= 0 && parts.length > idx + 2 ? parts[idx + 1] : void 0;
|
|
11539
|
+
return candidate !== void 0 && candidate !== "" ? candidate : null;
|
|
11540
|
+
}
|
|
11541
|
+
/**
|
|
11542
|
+
* 扫描本 CCC 的全部聊天记录,按 `(channel, userId)` 汇总。
|
|
11543
|
+
*
|
|
11544
|
+
* @param root CCC 根
|
|
11545
|
+
* @param accountChannel 存量事件缺 `channel` 时的回填表(`accountId → channel`)
|
|
11546
|
+
* @returns 汇总结果 + **扫描域** + 读取错误(三者都不可省)
|
|
11547
|
+
*/
|
|
11548
|
+
function scanImContacts(root, accountChannel = {}) {
|
|
11549
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
11550
|
+
const sources = [];
|
|
11551
|
+
const errors = [];
|
|
11552
|
+
for (const rel of imLogRoots(root)) for (const fileRel of jsonlFilesIn(root, rel)) {
|
|
11553
|
+
sources.push(fileRel);
|
|
11554
|
+
let text = "";
|
|
11555
|
+
try {
|
|
11556
|
+
text = readFileSync(join(root, fileRel), "utf8");
|
|
11557
|
+
} catch (err) {
|
|
11558
|
+
errors.push(`${fileRel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11559
|
+
continue;
|
|
11560
|
+
}
|
|
11561
|
+
const pathChannel = channelFromPath(fileRel);
|
|
11562
|
+
for (const line of text.split("\n")) {
|
|
11563
|
+
const trimmed = line.trim();
|
|
11564
|
+
if (trimmed === "") continue;
|
|
11565
|
+
let ev;
|
|
11566
|
+
try {
|
|
11567
|
+
ev = JSON.parse(trimmed);
|
|
11568
|
+
} catch {
|
|
11569
|
+
errors.push(`${fileRel}: 有 1 行不是合法 JSON(已跳过)`);
|
|
11570
|
+
continue;
|
|
11571
|
+
}
|
|
11572
|
+
const userId = typeof ev.userId === "string" ? ev.userId.trim() : "";
|
|
11573
|
+
if (userId === "") continue;
|
|
11574
|
+
const accountId = typeof ev.accountId === "string" ? ev.accountId.trim() : "";
|
|
11575
|
+
const channel = (typeof ev.channel === "string" && ev.channel.trim() !== "" ? ev.channel.trim() : null) ?? pathChannel ?? (accountId !== "" ? accountChannel[accountId] ?? null : null) ?? "weixin";
|
|
11576
|
+
const key = `${channel}\u0000${userId}`;
|
|
11577
|
+
let entry = byKey.get(key);
|
|
11578
|
+
if (!entry) {
|
|
11579
|
+
entry = {
|
|
11580
|
+
channel,
|
|
11581
|
+
userId,
|
|
11582
|
+
accountIds: [],
|
|
11583
|
+
roleAtEvent: null,
|
|
11584
|
+
firstSeen: null,
|
|
11585
|
+
lastSeen: null,
|
|
11586
|
+
messages: 0
|
|
11587
|
+
};
|
|
11588
|
+
byKey.set(key, entry);
|
|
11589
|
+
}
|
|
11590
|
+
if (accountId !== "" && !entry.accountIds.includes(accountId)) entry.accountIds.push(accountId);
|
|
11591
|
+
if (typeof ev.role === "string" && ev.role.trim() !== "") entry.roleAtEvent = ev.role.trim();
|
|
11592
|
+
const ts = typeof ev.ts === "number" && Number.isFinite(ev.ts) ? ev.ts : null;
|
|
11593
|
+
if (ts !== null) {
|
|
11594
|
+
if (entry.firstSeen === null || ts < entry.firstSeen) entry.firstSeen = ts;
|
|
11595
|
+
if (entry.lastSeen === null || ts > entry.lastSeen) entry.lastSeen = ts;
|
|
11596
|
+
}
|
|
11597
|
+
entry.messages += 1;
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
return {
|
|
11601
|
+
contacts: [...byKey.values()].sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0)),
|
|
11602
|
+
sources,
|
|
11603
|
+
errors
|
|
11604
|
+
};
|
|
11605
|
+
}
|
|
11606
|
+
//#endregion
|
|
11323
11607
|
//#region src/im/channels.ts
|
|
11324
11608
|
/**
|
|
11325
11609
|
* im/channels.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
@@ -11463,6 +11747,39 @@ async function runImBridge(root, req) {
|
|
|
11463
11747
|
action,
|
|
11464
11748
|
detail: { users: channel.listUsers(root) }
|
|
11465
11749
|
};
|
|
11750
|
+
if (action === "contacts") {
|
|
11751
|
+
if (!channel.listContacts) return fail("ACTION_UNSUPPORTED", `channel "${channelId}" has no record source for contacts`);
|
|
11752
|
+
const settings = readWeixinSettings(root);
|
|
11753
|
+
const accountChannel = {};
|
|
11754
|
+
for (const account of settings.accounts ?? []) if (typeof account.accountId === "string" && account.accountId !== "") accountChannel[account.accountId] = typeof account.channel === "string" && account.channel !== "" ? account.channel : "weixin";
|
|
11755
|
+
const scanned = scanImContacts(root, accountChannel);
|
|
11756
|
+
const mine = scanned.contacts.filter((c) => c.channel === channelId);
|
|
11757
|
+
/** 当前路由表(`user → trajectory`);同一 user 多条时后者覆盖 */
|
|
11758
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
11759
|
+
for (const route of settings.routes ?? []) if (typeof route.user === "string" && route.user !== "") routeMap.set(route.user, typeof route.trajectory === "string" ? route.trajectory : "");
|
|
11760
|
+
const contacts = mine.map((c) => ({
|
|
11761
|
+
channel: c.channel,
|
|
11762
|
+
userId: c.userId,
|
|
11763
|
+
accountIds: c.accountIds,
|
|
11764
|
+
route: routeMap.get(c.userId) ?? null,
|
|
11765
|
+
firstSeen: c.firstSeen === null ? null : new Date(c.firstSeen).toISOString(),
|
|
11766
|
+
lastSeen: c.lastSeen === null ? null : new Date(c.lastSeen).toISOString(),
|
|
11767
|
+
messages: c.messages
|
|
11768
|
+
}));
|
|
11769
|
+
const seenUsers = new Set(mine.map((c) => c.userId));
|
|
11770
|
+
return {
|
|
11771
|
+
ok: true,
|
|
11772
|
+
channel: channelId,
|
|
11773
|
+
action,
|
|
11774
|
+
detail: {
|
|
11775
|
+
contacts,
|
|
11776
|
+
unrouted: mine.filter((c) => !routeMap.has(c.userId)).map((c) => c.userId),
|
|
11777
|
+
orphanRoutes: [...routeMap.keys()].filter((user) => !seenUsers.has(user)),
|
|
11778
|
+
sources: scanned.sources,
|
|
11779
|
+
...scanned.errors.length > 0 ? { errors: scanned.errors } : {}
|
|
11780
|
+
}
|
|
11781
|
+
};
|
|
11782
|
+
}
|
|
11466
11783
|
if (action === "status") return {
|
|
11467
11784
|
ok: true,
|
|
11468
11785
|
channel: channelId,
|
|
@@ -13351,11 +13668,65 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
13351
13668
|
source: PLUGIN_SOURCE
|
|
13352
13669
|
});
|
|
13353
13670
|
}
|
|
13354
|
-
/**
|
|
13671
|
+
/**
|
|
13672
|
+
* 每 agent 是否已注入过 —— **只作为本进程内的快路径**,⛔ 不是判据。
|
|
13673
|
+
*
|
|
13674
|
+
* 🔴🔴 2026-10-22(owner 报「send-now 创建的会话消息注入两次」的取证结论):
|
|
13675
|
+
* 此前**它就是判据**,于是 `pre-step` 那道门拿它回答"**这条会话**注入过没有"——
|
|
13676
|
+
* 而它实际回答的是"**本进程**注入过没有"。进程一换(`restart-web` / 看门狗拉起 /
|
|
13677
|
+
* 崩溃重启)它就清空 ⇒ 老会话被冷载入时 `pre-step` 判"没注入过" ⇒ **再注入一遍**。
|
|
13678
|
+
*
|
|
13679
|
+
* 实测铁证(同一会话两条 `[ACC] … active` 横幅,相隔 18.6 小时):
|
|
13680
|
+
* 只有**跨过重启**的会话有两条,没跨的只有一条 ⇒ 重复的量 = 跨了几个进程。
|
|
13681
|
+
*
|
|
13682
|
+
* ⇒ 判据改为 {@link hasAccBanner}(读**会话历史**)。本 Set 降级为"同进程免重复扫"的优化:
|
|
13683
|
+
* 命中它就直接返回(省一次扫),⛔ 它为空**不代表**没注入过。
|
|
13684
|
+
*/
|
|
13355
13685
|
const injected = /* @__PURE__ */ new Set();
|
|
13356
13686
|
function agentKey(agent) {
|
|
13357
13687
|
return agent.session.id ?? "global";
|
|
13358
13688
|
}
|
|
13689
|
+
/**
|
|
13690
|
+
* ACC 身份横幅在**会话历史**里的识别特征(结构判据,不依赖措辞顺序)。
|
|
13691
|
+
*
|
|
13692
|
+
* 🔴 为什么认 `source.kind` + 正文前缀两件:
|
|
13693
|
+
* - 只认正文 ⇒ 将来文案一改就静默失效(D151 同族:判据不能长在措辞上);
|
|
13694
|
+
* - 只认 `source.kind` ⇒ 本插件注入的**所有**消息都算(`skill-catalog` 块、假首轮横幅…)
|
|
13695
|
+
* ⇒ 会把"别的注入出现过"误当成"身份横幅出现过",导致**永不再注入**。
|
|
13696
|
+
* ⇒ 两者与:由本插件发出 **且** 正文以 ACC 横幅首词开头。
|
|
13697
|
+
*/
|
|
13698
|
+
const ACC_BANNER_PREFIX = "[ACC] Serenity cognitive container active";
|
|
13699
|
+
/**
|
|
13700
|
+
* 这条会话的历史里**是否已经有** ACC 身份横幅(= 判据的唯一权威)。
|
|
13701
|
+
*
|
|
13702
|
+
* 🔴 这是本条修复的核心:**"注入过没有"是会话的属性,不是进程的属性。**
|
|
13703
|
+
* 会话历史随会话走(落盘、重启后仍在),进程内状态随进程死。
|
|
13704
|
+
*
|
|
13705
|
+
* ⚠️ 读不到 events(受限宿主 / 投影未就绪)⇒ 返回 `false`(退回"注入一次")。
|
|
13706
|
+
* 这是**偏保守**的选择:多注入一条横幅(可见、可清理)远好于
|
|
13707
|
+
* **永远不再注入**(陈旧会话里 ACC 身份彻底缺失,且零信号)。
|
|
13708
|
+
*
|
|
13709
|
+
* @param session 目标会话(`sessionEvents` 能读的形状)
|
|
13710
|
+
*/
|
|
13711
|
+
function hasAccBanner(session) {
|
|
13712
|
+
return sessionEvents(session).some((event) => {
|
|
13713
|
+
const e = event;
|
|
13714
|
+
if (e.type !== "user/message") return false;
|
|
13715
|
+
if (e.data?.source?.kind !== PLUGIN_SOURCE.kind) return false;
|
|
13716
|
+
return textOfUserMessage(e.data)?.trimStart().startsWith(ACC_BANNER_PREFIX) === true;
|
|
13717
|
+
});
|
|
13718
|
+
}
|
|
13719
|
+
/** 从 `user/message` 的 content 块数组里拼出纯文本(无 text 块 ⇒ undefined) */
|
|
13720
|
+
function textOfUserMessage(data) {
|
|
13721
|
+
const content = data?.content;
|
|
13722
|
+
if (!Array.isArray(content)) return void 0;
|
|
13723
|
+
const parts = [];
|
|
13724
|
+
for (const block of content) {
|
|
13725
|
+
const b = block;
|
|
13726
|
+
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
13727
|
+
}
|
|
13728
|
+
return parts.length === 0 ? void 0 : parts.join("");
|
|
13729
|
+
}
|
|
13359
13730
|
function agentScope(agent) {
|
|
13360
13731
|
return agent.session.id ?? "default";
|
|
13361
13732
|
}
|
|
@@ -13455,7 +13826,10 @@ function registerContext(ctx, opts = {}) {
|
|
|
13455
13826
|
}
|
|
13456
13827
|
}
|
|
13457
13828
|
} catch {}
|
|
13458
|
-
if (injected.has(key))
|
|
13829
|
+
if (injected.has(key) || hasAccBanner(agent.session)) {
|
|
13830
|
+
injected.add(key);
|
|
13831
|
+
return;
|
|
13832
|
+
}
|
|
13459
13833
|
injected.add(key);
|
|
13460
13834
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
13461
13835
|
syncSafeModeRestriction(agent, root);
|
|
@@ -13511,7 +13885,11 @@ function registerContext(ctx, opts = {}) {
|
|
|
13511
13885
|
registerEntrySkillSection(agent, root);
|
|
13512
13886
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
13513
13887
|
}
|
|
13514
|
-
if (!root ||
|
|
13888
|
+
if (!root || downstream.kind !== "enter") return downstream;
|
|
13889
|
+
if (injected.has(key) || hasAccBanner(agent.session)) {
|
|
13890
|
+
injected.add(key);
|
|
13891
|
+
return downstream;
|
|
13892
|
+
}
|
|
13515
13893
|
injected.add(key);
|
|
13516
13894
|
return {
|
|
13517
13895
|
kind: "enter",
|
|
@@ -14100,11 +14478,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14100
14478
|
sendJson$2(res, 400, { error: chk.error });
|
|
14101
14479
|
return;
|
|
14102
14480
|
}
|
|
14103
|
-
const { readWeixinSettings } = await import("./route-
|
|
14481
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14104
14482
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
14105
14483
|
const settings = readWeixinSettings(chk.root);
|
|
14106
14484
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
14107
|
-
const { imCredentialBound } = await import("./route-
|
|
14485
|
+
const { imCredentialBound } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14108
14486
|
const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
|
|
14109
14487
|
const accounts = (settings.accounts ?? []).map((a) => {
|
|
14110
14488
|
const channel = normalizeAccountChannel(a.channel);
|
|
@@ -14141,7 +14519,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14141
14519
|
const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
14142
14520
|
const { randomUUID } = await import("node:crypto");
|
|
14143
14521
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
14144
|
-
const { readWeixinSettings } = await import("./route-
|
|
14522
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14145
14523
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
14146
14524
|
const loginKey = randomUUID();
|
|
14147
14525
|
weixinLogins.set(loginKey, {
|
|
@@ -14162,7 +14540,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14162
14540
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
14163
14541
|
return;
|
|
14164
14542
|
}
|
|
14165
|
-
const { removeWeixinAccount } = await import("./route-
|
|
14543
|
+
const { removeWeixinAccount } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14166
14544
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14167
14545
|
removeWeixinAccount(root, body.accountId);
|
|
14168
14546
|
syncCccBridge(ctx, root);
|
|
@@ -14188,8 +14566,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14188
14566
|
trajectory: code
|
|
14189
14567
|
});
|
|
14190
14568
|
}
|
|
14191
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
14192
|
-
const { findDirNameByCode } = await import("./profile-
|
|
14569
|
+
const { saveWeixinRoutes } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14570
|
+
const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
|
|
14193
14571
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
14194
14572
|
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
14195
14573
|
return;
|
|
@@ -14200,7 +14578,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14200
14578
|
}
|
|
14201
14579
|
if (body.action === "set-enabled") {
|
|
14202
14580
|
const enabled = body.enabled === true;
|
|
14203
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
14581
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14204
14582
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14205
14583
|
const settings = readWeixinSettings(root);
|
|
14206
14584
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -14223,7 +14601,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14223
14601
|
sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
|
|
14224
14602
|
return;
|
|
14225
14603
|
}
|
|
14226
|
-
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-
|
|
14604
|
+
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14227
14605
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14228
14606
|
const before = readWeixinSettings(root);
|
|
14229
14607
|
const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
|
|
@@ -14273,7 +14651,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14273
14651
|
return;
|
|
14274
14652
|
}
|
|
14275
14653
|
const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
14276
|
-
const { readWeixinSettings } = await import("./route-
|
|
14654
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14277
14655
|
const settings = readWeixinSettings(login.root);
|
|
14278
14656
|
const status = await pollQRStatus({
|
|
14279
14657
|
baseUrl: void 0,
|
|
@@ -14288,7 +14666,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14288
14666
|
});
|
|
14289
14667
|
return;
|
|
14290
14668
|
}
|
|
14291
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
14669
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14292
14670
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14293
14671
|
const accountId = nextWeixinAccountId(settings);
|
|
14294
14672
|
upsertWeixinAccount(login.root, {
|
|
@@ -18824,12 +19202,15 @@ function appendHumanChannelAudit(event) {
|
|
|
18824
19202
|
const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
|
|
18825
19203
|
/** hook stderr/stdout 日志截断(防脚本刷屏) */
|
|
18826
19204
|
const HOOK_LOG_MAX = 500;
|
|
19205
|
+
/** 存量调用点未传 `channel` 时的缺省值(本字段存在之前唯一的渠道) */
|
|
19206
|
+
const DEFAULT_HOOK_CHANNEL = "weixin";
|
|
18827
19207
|
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
18828
19208
|
function buildIncomingHookEvent(input) {
|
|
18829
19209
|
return {
|
|
18830
19210
|
event: "incoming",
|
|
18831
19211
|
ts: Date.now(),
|
|
18832
19212
|
cccRoot: input.cccRoot,
|
|
19213
|
+
channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
|
|
18833
19214
|
accountId: input.accountId,
|
|
18834
19215
|
userId: input.userId,
|
|
18835
19216
|
sessionId: input.sessionId,
|
|
@@ -18846,6 +19227,7 @@ function buildOutgoingHookEvent(input) {
|
|
|
18846
19227
|
event: "outgoing",
|
|
18847
19228
|
ts: Date.now(),
|
|
18848
19229
|
cccRoot: input.cccRoot,
|
|
19230
|
+
channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
|
|
18849
19231
|
accountId: input.accountId,
|
|
18850
19232
|
userId: input.userId,
|
|
18851
19233
|
sessionId: input.sessionId,
|
|
@@ -19228,7 +19610,7 @@ const MEDIA_MAX_BYTES = 20971520;
|
|
|
19228
19610
|
*/
|
|
19229
19611
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
19230
19612
|
try {
|
|
19231
|
-
const { listCccs } = await import("./ccc-roots-
|
|
19613
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
19232
19614
|
const entries = await listCccs(ctx);
|
|
19233
19615
|
const byName = entries.find((e) => e.name === alias);
|
|
19234
19616
|
if (byName) return byName.root;
|
|
@@ -19296,7 +19678,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
19296
19678
|
});
|
|
19297
19679
|
return null;
|
|
19298
19680
|
}
|
|
19299
|
-
const { findDirNameByCode } = await import("./profile-
|
|
19681
|
+
const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
|
|
19300
19682
|
if (findDirNameByCode(root, target.role) === null) {
|
|
19301
19683
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
19302
19684
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -19525,6 +19907,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19525
19907
|
const hookRel = settings.hook;
|
|
19526
19908
|
if (hookRel) invokeWeixinHook(root, hookRel, buildIncomingHookEvent({
|
|
19527
19909
|
cccRoot: root,
|
|
19910
|
+
channel,
|
|
19528
19911
|
accountId,
|
|
19529
19912
|
userId: fromUserId,
|
|
19530
19913
|
sessionId,
|
|
@@ -19551,6 +19934,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19551
19934
|
console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
|
|
19552
19935
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
19553
19936
|
cccRoot: root,
|
|
19937
|
+
channel,
|
|
19554
19938
|
accountId,
|
|
19555
19939
|
userId: fromUserId,
|
|
19556
19940
|
sessionId,
|
|
@@ -19571,6 +19955,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19571
19955
|
});
|
|
19572
19956
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
19573
19957
|
cccRoot: root,
|
|
19958
|
+
channel,
|
|
19574
19959
|
accountId,
|
|
19575
19960
|
userId: fromUserId,
|
|
19576
19961
|
sessionId,
|
|
@@ -20246,7 +20631,7 @@ function syncHumanChannel(ctx) {
|
|
|
20246
20631
|
(async () => {
|
|
20247
20632
|
try {
|
|
20248
20633
|
stopMachineLoops();
|
|
20249
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20634
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
20250
20635
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
20251
20636
|
} catch {}
|
|
20252
20637
|
})();
|
|
@@ -20343,6 +20728,7 @@ async function sendProactiveText(input) {
|
|
|
20343
20728
|
}
|
|
20344
20729
|
if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
|
|
20345
20730
|
cccRoot: root,
|
|
20731
|
+
channel: "weixin",
|
|
20346
20732
|
accountId,
|
|
20347
20733
|
userId: toUserId,
|
|
20348
20734
|
sessionId,
|
|
@@ -20537,7 +20923,7 @@ function matchCcc(input, candidates) {
|
|
|
20537
20923
|
}
|
|
20538
20924
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
20539
20925
|
async function collectCandidates(ctx) {
|
|
20540
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20926
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
20541
20927
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
20542
20928
|
const seen = /* @__PURE__ */ new Set();
|
|
20543
20929
|
const out = [];
|
|
@@ -21752,6 +22138,24 @@ function credentialValue$1(root, key) {
|
|
|
21752
22138
|
function channelError$1(code, error, remediation) {
|
|
21753
22139
|
return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
|
|
21754
22140
|
}
|
|
22141
|
+
/**
|
|
22142
|
+
* 路由目标轨迹**是否存在**(`status` 的自检列)。
|
|
22143
|
+
*
|
|
22144
|
+
* 🔴 复用既有判据 `findDirNameByCode`(它扫活跃区并按编码解析)—— ⛔ 不在这里另写一套目录扫描,
|
|
22145
|
+
* 两份判据会漂(一处认归档、另一处不认,就会出现"这里说有、投递时说没有")。
|
|
22146
|
+
*
|
|
22147
|
+
* ⚠️ 解析失败(`findDirNameByCode` 在**重码**时会抛)⇒ 返回 `false` 但**不吞掉原因**:
|
|
22148
|
+
* 重码本身也是"这个路由不可信"的一种形态,`status` 里会表现为 `targetExists:false`,
|
|
22149
|
+
* 配合 `cognitive_trajectory list` 可定位。不额外造一个错误码(键名已经够直白)。
|
|
22150
|
+
*/
|
|
22151
|
+
function trajectoryDirExists(root, code) {
|
|
22152
|
+
if (typeof code !== "string" || code.trim() === "") return false;
|
|
22153
|
+
try {
|
|
22154
|
+
return findDirNameByCode(root, code.trim()) !== null;
|
|
22155
|
+
} catch {
|
|
22156
|
+
return false;
|
|
22157
|
+
}
|
|
22158
|
+
}
|
|
21755
22159
|
const weixinChannel = {
|
|
21756
22160
|
id: "weixin",
|
|
21757
22161
|
isEnabled(root) {
|
|
@@ -21830,6 +22234,7 @@ const weixinChannel = {
|
|
|
21830
22234
|
const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
21831
22235
|
invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
21832
22236
|
cccRoot: input.root,
|
|
22237
|
+
channel: "weixin",
|
|
21833
22238
|
accountId: account.accountId,
|
|
21834
22239
|
userId: input.userId,
|
|
21835
22240
|
sessionId: weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown"),
|
|
@@ -21861,10 +22266,14 @@ const weixinChannel = {
|
|
|
21861
22266
|
lastPollAt: account.lastPollAt,
|
|
21862
22267
|
...account.lastError ? { lastError: account.lastError } : {}
|
|
21863
22268
|
})));
|
|
21864
|
-
const routes = (settings.routes ?? []).map((route) =>
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
22269
|
+
const routes = (settings.routes ?? []).map((route) => {
|
|
22270
|
+
const code = route.trajectory;
|
|
22271
|
+
return {
|
|
22272
|
+
user: route.user,
|
|
22273
|
+
role: code,
|
|
22274
|
+
targetExists: trajectoryDirExists(root, code)
|
|
22275
|
+
};
|
|
22276
|
+
});
|
|
21868
22277
|
return {
|
|
21869
22278
|
enabled: settings.enabled === true,
|
|
21870
22279
|
autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
|
|
@@ -21905,12 +22314,17 @@ const weixinChannel = {
|
|
|
21905
22314
|
* ⇒ 校验**值的形状**(飞书 open_id 以 `ou_` 开头):错配在**解析处**就失败,
|
|
21906
22315
|
* 错误码是 `ALIAS_UNKNOWN` + 可行动提示。
|
|
21907
22316
|
*
|
|
21908
|
-
* ## 🔴
|
|
22317
|
+
* ## 🔴 记录 hook:曾经不 fire,2026-10-22 起 fire(A83 已裁)
|
|
21909
22318
|
*
|
|
21910
|
-
*
|
|
21911
|
-
* 把事件追加进 `AGENT_SESSIONS/_weixin-logs/<日期>.jsonl
|
|
21912
|
-
*
|
|
21913
|
-
*
|
|
22319
|
+
* **当时的理由**:`settings.hook` 的契约与消费方都是**微信的**,CCC 的
|
|
22320
|
+
* `scripts/weixin-message-hook.ts` 把事件追加进 `AGENT_SESSIONS/_weixin-logs/<日期>.jsonl`。
|
|
22321
|
+
* 飞书事件若流进去,那个目录就**同时装两种渠道的数据而名字只说一种**(D151/D179 同族)。
|
|
22322
|
+
*
|
|
22323
|
+
* **为什么现在可以 fire**:同批给 hook 事件加了 **`channel` 字段**(owner 2026-10-22:
|
|
22324
|
+
* 「一个 CCC 绑定多个账户很常见,聊天记录落盘要完善」)⇒ CCC 侧 hook 按渠道分流到
|
|
22325
|
+
* `_im-logs/<channel>/`,**目录名不再需要撒谎**。
|
|
22326
|
+
* ⇒ 所以这不是"决定记录",而是"**让记录不再有害**之后才记录"。
|
|
22327
|
+
* ⚠️ 顺序有意义:**先有渠道字段,再有记录**。反过来做就会先造出一批分不清渠道的数据。
|
|
21914
22328
|
*/
|
|
21915
22329
|
/**
|
|
21916
22330
|
* 飞书 open_id 的形状前缀(官方形态:`ou_` + 一串)。
|
|
@@ -22065,6 +22479,18 @@ const feishuChannel = {
|
|
|
22065
22479
|
const settings = readWeixinSettings(input.root);
|
|
22066
22480
|
const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
22067
22481
|
const sessionId = weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown");
|
|
22482
|
+
if (settings.hook) invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
22483
|
+
cccRoot: input.root,
|
|
22484
|
+
channel: "feishu",
|
|
22485
|
+
accountId: account.accountId,
|
|
22486
|
+
userId: input.userId,
|
|
22487
|
+
sessionId,
|
|
22488
|
+
role: trajectory,
|
|
22489
|
+
reply: markdownToPlainText(input.text),
|
|
22490
|
+
source: "proactive"
|
|
22491
|
+
})).catch((err) => {
|
|
22492
|
+
console.log(`[serenity-hooks] im hook outgoing(feishu) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
22493
|
+
});
|
|
22068
22494
|
return {
|
|
22069
22495
|
accountId: account.accountId,
|
|
22070
22496
|
userId: input.userId,
|
|
@@ -22118,7 +22544,8 @@ const ACTIONS = [
|
|
|
22118
22544
|
"send",
|
|
22119
22545
|
"send-file",
|
|
22120
22546
|
"users",
|
|
22121
|
-
"status"
|
|
22547
|
+
"status",
|
|
22548
|
+
"contacts"
|
|
22122
22549
|
];
|
|
22123
22550
|
function renderText(value) {
|
|
22124
22551
|
return [{
|
|
@@ -22133,7 +22560,7 @@ function renderText(value) {
|
|
|
22133
22560
|
function createImBridgeTool() {
|
|
22134
22561
|
return defineTool({
|
|
22135
22562
|
name: "im-bridge",
|
|
22136
|
-
description: `Send messages to family members through an IM channel configured for THIS container. 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 (
|
|
22563
|
+
description: `Send messages to family members through an IM channel configured for THIS container. 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 (configured aliases) / status (channel health, accounts + current routes) / contacts (people this container has exchanged messages with — use this to find the \`user\` id when adding a route, since that id exists only in the chat logs). Not every channel supports every action (e.g. send-file may return ACTION_UNSUPPORTED). user accepts a configured alias or a raw channel user id. Docs: container_admin config docs im-bridge (how to add a route, where the other party's id comes from). Failures return a stable code plus a remediation hint.`,
|
|
22137
22564
|
parameters: {
|
|
22138
22565
|
channel: {
|
|
22139
22566
|
type: "string",
|
|
@@ -22144,7 +22571,7 @@ function createImBridgeTool() {
|
|
|
22144
22571
|
type: "string",
|
|
22145
22572
|
enum: [...ACTIONS],
|
|
22146
22573
|
required: true,
|
|
22147
|
-
description: "send / send-file / users / status"
|
|
22574
|
+
description: "send / send-file / users / status / contacts"
|
|
22148
22575
|
},
|
|
22149
22576
|
user: {
|
|
22150
22577
|
type: "string",
|