@shgroup/dsh-serenity-hooks 1.85.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-VCJ3aYJA.js → ccc-roots-BI_4UVcj.js} +1 -1
- 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 +462 -36
- package/lib/{message-source-D7dVNDIh.js → message-source-DRkQwfgh.js} +2 -2
- package/lib/{profile-DnLSb7yv.js → profile-vHheCJiU.js} +1 -1
- package/lib/{route-Dr0SFD8A.js → route-GtYJdR06.js} +7 -0
- package/lib/seams/context.d.ts +13 -0
- package/lib/{session-I6SM2GT6.js → session-DZXSLLEx.js} +3 -3
- package/lib/{settings-4ag1jtR0.js → settings-H7TVDayZ.js} +1 -1
- package/lib/tools/im-docs.d.ts +20 -0
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.86.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
|
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
|
|
|
227
227
|
if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
|
|
228
228
|
let rolesOf = null;
|
|
229
229
|
if (opts.withRoles === true) {
|
|
230
|
-
const { listTrajectoryConstraints } = await import("./profile-
|
|
230
|
+
const { listTrajectoryConstraints } = await import("./profile-vHheCJiU.js").then((n) => n.a);
|
|
231
231
|
rolesOf = (root) => {
|
|
232
232
|
try {
|
|
233
233
|
return listTrajectoryConstraints(root).map((p) => p.code);
|
package/lib/im/channels.d.ts
CHANGED
|
@@ -87,6 +87,39 @@ export interface ImChannel {
|
|
|
87
87
|
}>;
|
|
88
88
|
/** 通道健康快照(`status` 动作)——必须可 JSON 化(工具返回值契约) */
|
|
89
89
|
status(root: string): ImJson;
|
|
90
|
+
/**
|
|
91
|
+
* 列出**与这条 CCC 有过往来的对端用户**(`contacts` 动作,2026-10-22 新增)。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **它存在的理由**(owner 原话:「配路由需要的是账户到轨迹,轨迹 CCC 知道,
|
|
94
|
+
* 账户 CCC 不知道,需要有个能力让 CCC 查询用户配好的账户」):
|
|
95
|
+
* 路由要写 `{ user: "<通道用户 id>", trajectory: "<轨迹编码>" }`。轨迹编码 CCC 知道,
|
|
96
|
+
* **但"对方是谁"它不知道** —— 那个 id 只存在于聊天记录里。
|
|
97
|
+
* ⇒ 没有本动作,CCC 配路由时只能去翻日志或猜。
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ **可选**:某通道若没有可扫的记录来源(例如未实现记录落盘),不实现即可,
|
|
100
|
+
* 工具会如实返回 `ACTION_UNSUPPORTED` —— 比返回空数组好(空数组会被读成"确实没有人")。
|
|
101
|
+
*/
|
|
102
|
+
listContacts?(root: string): ImContact[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 一个"有过往来的对端"(`contacts` 动作的条目)。
|
|
106
|
+
*
|
|
107
|
+
* 字段刻意**全部可 JSON 化**(工具返回值契约),且不引用内部类型。
|
|
108
|
+
*/
|
|
109
|
+
export interface ImContact {
|
|
110
|
+
/** 该对端属于哪个渠道(`weixin` / `feishu`) */
|
|
111
|
+
channel: string;
|
|
112
|
+
/** 对端在该通道内的稳定 id(微信 `xxx@im.wechat` / 飞书 `ou_…`) */
|
|
113
|
+
userId: string;
|
|
114
|
+
/** 收发过它的账号(本地唯一键,如 `wechat-1`)—— 多账号 CCC 靠它区分 */
|
|
115
|
+
accountIds: string[];
|
|
116
|
+
/** 当前路由到的轨迹编码;未配路由 ⇒ `null` */
|
|
117
|
+
route: string | null;
|
|
118
|
+
/** 首次/最近出现时间(ISO8601;取不到为 `null`) */
|
|
119
|
+
firstSeen: string | null;
|
|
120
|
+
lastSeen: string | null;
|
|
121
|
+
/** 见过的消息条数 */
|
|
122
|
+
messages: number;
|
|
90
123
|
}
|
|
91
124
|
/** 注册通道(index.ts apply 调用;重复 id 覆盖——测试与热重载友好) */
|
|
92
125
|
export declare function registerImChannel(channel: ImChannel): void;
|
package/lib/im/hook.d.ts
CHANGED
|
@@ -27,6 +27,19 @@ interface WeixinHookEventBase {
|
|
|
27
27
|
ts: number;
|
|
28
28
|
/** 触发 CCC 根(多 CCC 一进程可区分) */
|
|
29
29
|
cccRoot: string;
|
|
30
|
+
/**
|
|
31
|
+
* 渠道 id(`weixin` / `feishu`)。
|
|
32
|
+
*
|
|
33
|
+
* 🔴 2026-10-22 新增(owner:「一个 CCC 绑定多个账户很常见,聊天记录落盘要完善」)。
|
|
34
|
+
* 此前**没有这个字段** ⇒ 三个后果:① 飞书事件落进同一份 jsonl 后**无法区分渠道**
|
|
35
|
+
* (只能靠 `ou_` / `@im.wechat` 的形状猜 = D167 违反:认族不认名);② 一个 CCC 配多个
|
|
36
|
+
* 账号时**不知是哪个渠道**收发的(`accountId` 是本地命名习惯,不携带渠道信息);
|
|
37
|
+
* ③ 按渠道分组统计在字段层就做不到。
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ **向后兼容**:存量事件没有它 ⇒ 读侧按 `accountId → im.accounts[].channel`
|
|
40
|
+
* 回填,仍取不到则按 `weixin`(那是本字段存在之前唯一的渠道)。
|
|
41
|
+
*/
|
|
42
|
+
channel: string;
|
|
30
43
|
/** 接收账号(incoming)/ 发送账号(outgoing) */
|
|
31
44
|
accountId: string;
|
|
32
45
|
/** 对端用户(from_user_id,形如 userA@im.wechat) */
|
|
@@ -71,6 +84,8 @@ type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
|
|
|
71
84
|
/** incoming 事件构造输入(bridge 提供实参) */
|
|
72
85
|
interface IncomingHookInput {
|
|
73
86
|
cccRoot: string;
|
|
87
|
+
/** 渠道 id(`weixin` / `feishu`);缺省 `weixin`(存量调用点零改动) */
|
|
88
|
+
channel?: string;
|
|
74
89
|
accountId: string;
|
|
75
90
|
userId: string;
|
|
76
91
|
sessionId: string;
|
|
@@ -81,6 +96,8 @@ interface IncomingHookInput {
|
|
|
81
96
|
/** outgoing 事件构造输入(bridge 提供实参) */
|
|
82
97
|
interface OutgoingHookInput {
|
|
83
98
|
cccRoot: string;
|
|
99
|
+
/** 渠道 id(`weixin` / `feishu`);缺省 `weixin` */
|
|
100
|
+
channel?: string;
|
|
84
101
|
accountId: string;
|
|
85
102
|
userId: string;
|
|
86
103
|
sessionId: string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* im/log-scan.ts — 读聊天记录落盘,汇总出「与谁有过往来」(`contacts` 的数据源)
|
|
3
|
+
*
|
|
4
|
+
* ## 这个文件解决什么
|
|
5
|
+
*
|
|
6
|
+
* owner 2026-10-22:「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
|
|
7
|
+
* 需要有个能力让 CCC 查询用户配好的账户」。这里的"账户"= **通道用户**(对方是谁),
|
|
8
|
+
* 不是 IM 账号(我们自己登录的身份,`status` 早就能查)。
|
|
9
|
+
*
|
|
10
|
+
* 那个 id(微信 `xxx@im.wechat` / 飞书 `ou_…`)不在任何配置文件里 ——
|
|
11
|
+
* 它**只在聊天记录里出现过**。⇒ 本文件把记录读成联系人表。
|
|
12
|
+
*
|
|
13
|
+
* ## 🔴 两条硬边界
|
|
14
|
+
*
|
|
15
|
+
* 1. **只读**:本文件不写任何东西。真相源仍是那些 jsonl(append-only 事件流),
|
|
16
|
+
* 本文件只是它的一次投影 —— ⛔ 不产生第二真相源。
|
|
17
|
+
* 2. **不猜渠道**:事件的 `channel` 字段为准;缺它(存量事件)⇒ 按
|
|
18
|
+
* `accountId → im.accounts[].channel` 回填,仍取不到才落 `weixin`
|
|
19
|
+
* (那是该字段存在之前唯一的渠道)。⛔ **绝不按 userId 的形状猜**
|
|
20
|
+
* (`ou_` 前缀 / `@im.wechat` 后缀都是命名习惯,不是判据 —— D167 认族不认名)。
|
|
21
|
+
*/
|
|
22
|
+
/** 日志根目录族(**新旧两代并存**:`_im-logs/<channel>/` 与存量 `_weixin-logs/`) */
|
|
23
|
+
export declare const IM_LOG_DIR_NEW = "_im-logs";
|
|
24
|
+
export declare const IM_LOG_DIR_LEGACY = "_weixin-logs";
|
|
25
|
+
/** 汇总结果(可 JSON 化) */
|
|
26
|
+
export interface ScannedContact {
|
|
27
|
+
channel: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
accountIds: string[];
|
|
30
|
+
/** 事件里记录的路由目标(`role` 字段;它是事件发生当时的轨迹编码,未必等于**当前**路由) */
|
|
31
|
+
roleAtEvent: string | null;
|
|
32
|
+
firstSeen: number | null;
|
|
33
|
+
lastSeen: number | null;
|
|
34
|
+
messages: number;
|
|
35
|
+
}
|
|
36
|
+
/** 扫描结果(含**扫描域**——否定性结论必须附它) */
|
|
37
|
+
export interface ScanResult {
|
|
38
|
+
contacts: ScannedContact[];
|
|
39
|
+
/** 实际读过的文件(相对 CCC 根) */
|
|
40
|
+
sources: string[];
|
|
41
|
+
/** 读失败的文件及其原因(不静默) */
|
|
42
|
+
errors: string[];
|
|
43
|
+
}
|
|
44
|
+
/** 某 CCC 下**存在**的日志目录(相对根),新代在前 */
|
|
45
|
+
export declare function imLogRoots(root: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* 扫描本 CCC 的全部聊天记录,按 `(channel, userId)` 汇总。
|
|
48
|
+
*
|
|
49
|
+
* @param root CCC 根
|
|
50
|
+
* @param accountChannel 存量事件缺 `channel` 时的回填表(`accountId → channel`)
|
|
51
|
+
* @returns 汇总结果 + **扫描域** + 读取错误(三者都不可省)
|
|
52
|
+
*/
|
|
53
|
+
export declare function scanImContacts(root: string, accountChannel?: Record<string, string>): ScanResult;
|
package/lib/im/route.d.ts
CHANGED
|
@@ -64,8 +64,15 @@ export declare function readImSettings(root: string, paths?: string[]): ImSettin
|
|
|
64
64
|
* ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
|
|
65
65
|
* 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
|
|
66
66
|
* 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
|
|
67
|
+
*
|
|
68
|
+
* 🔴 2026-10-22:**返回类型由 `WeixinSettings` 改为 `ImSettings`**。
|
|
69
|
+
* 此前实现返回的就是 `readImSettings(...)`(已是归一形态:`accounts[].channel` 必填),
|
|
70
|
+
* 却把它断言成旧类型 —— 于是类型层"看不见"`channel` 字段,
|
|
71
|
+
* 调用方写 `a.channel` 会报 TS2339,而**运行时它一直在那儿**。
|
|
72
|
+
* ⇒ 那是**类型声明在撒谎**,不是调用方写错。改成真实类型后,多账户/多渠道的读侧才敢用它。
|
|
73
|
+
* ⚠️ 附带效应:`WeixinSettings` 的旧形状调用方若用到旧字段,会在此处暴露(好事,早暴露)。
|
|
67
74
|
*/
|
|
68
|
-
export declare function readWeixinSettings(root: string, paths?: string[]):
|
|
75
|
+
export declare function readWeixinSettings(root: string, paths?: string[]): ImSettings;
|
|
69
76
|
/** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
|
|
70
77
|
export interface WeixinAccountCredential {
|
|
71
78
|
token: string;
|
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";
|
|
@@ -11048,6 +11048,152 @@ const TRAJECTORY_SELF_GUIDE = [
|
|
|
11048
11048
|
⚠️ 以上四项都是**读取当前真实状态**,不是"保存成功"的回执。保存成功 ≠ 生效(本仓反复踩过)。`
|
|
11049
11049
|
].join("\n");
|
|
11050
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
|
|
11051
11197
|
//#region src/tools/container-admin.ts
|
|
11052
11198
|
/**
|
|
11053
11199
|
* container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
|
|
@@ -11198,6 +11344,11 @@ const DOCS_CHAPTERS = [
|
|
|
11198
11344
|
id: "trajectory-constraint",
|
|
11199
11345
|
summary: "轨迹约束(只讲限制面):trajectory.json 的双白名单 / 模型 / 纪律参与项 / IM 路由",
|
|
11200
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
|
|
11201
11352
|
}
|
|
11202
11353
|
];
|
|
11203
11354
|
/**
|
|
@@ -11208,7 +11359,7 @@ const DOCS_CHAPTERS = [
|
|
|
11208
11359
|
function createContainerAdminTool(ctx) {
|
|
11209
11360
|
return defineTool({
|
|
11210
11361
|
name: "container_admin",
|
|
11211
|
-
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.",
|
|
11212
11363
|
parameters: {
|
|
11213
11364
|
domain: {
|
|
11214
11365
|
type: "string",
|
|
@@ -11321,6 +11472,138 @@ function createContainerAdminTool(ctx) {
|
|
|
11321
11472
|
});
|
|
11322
11473
|
}
|
|
11323
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
|
|
11324
11607
|
//#region src/im/channels.ts
|
|
11325
11608
|
/**
|
|
11326
11609
|
* im/channels.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
|
|
@@ -11464,6 +11747,39 @@ async function runImBridge(root, req) {
|
|
|
11464
11747
|
action,
|
|
11465
11748
|
detail: { users: channel.listUsers(root) }
|
|
11466
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
|
+
}
|
|
11467
11783
|
if (action === "status") return {
|
|
11468
11784
|
ok: true,
|
|
11469
11785
|
channel: channelId,
|
|
@@ -13352,11 +13668,65 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
13352
13668
|
source: PLUGIN_SOURCE
|
|
13353
13669
|
});
|
|
13354
13670
|
}
|
|
13355
|
-
/**
|
|
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
|
+
*/
|
|
13356
13685
|
const injected = /* @__PURE__ */ new Set();
|
|
13357
13686
|
function agentKey(agent) {
|
|
13358
13687
|
return agent.session.id ?? "global";
|
|
13359
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
|
+
}
|
|
13360
13730
|
function agentScope(agent) {
|
|
13361
13731
|
return agent.session.id ?? "default";
|
|
13362
13732
|
}
|
|
@@ -13456,7 +13826,10 @@ function registerContext(ctx, opts = {}) {
|
|
|
13456
13826
|
}
|
|
13457
13827
|
}
|
|
13458
13828
|
} catch {}
|
|
13459
|
-
if (injected.has(key))
|
|
13829
|
+
if (injected.has(key) || hasAccBanner(agent.session)) {
|
|
13830
|
+
injected.add(key);
|
|
13831
|
+
return;
|
|
13832
|
+
}
|
|
13460
13833
|
injected.add(key);
|
|
13461
13834
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
13462
13835
|
syncSafeModeRestriction(agent, root);
|
|
@@ -13512,7 +13885,11 @@ function registerContext(ctx, opts = {}) {
|
|
|
13512
13885
|
registerEntrySkillSection(agent, root);
|
|
13513
13886
|
registerCognitiveTrajectorySkillSection(agent, root);
|
|
13514
13887
|
}
|
|
13515
|
-
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
|
+
}
|
|
13516
13893
|
injected.add(key);
|
|
13517
13894
|
return {
|
|
13518
13895
|
kind: "enter",
|
|
@@ -14101,11 +14478,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14101
14478
|
sendJson$2(res, 400, { error: chk.error });
|
|
14102
14479
|
return;
|
|
14103
14480
|
}
|
|
14104
|
-
const { readWeixinSettings } = await import("./route-
|
|
14481
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14105
14482
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
14106
14483
|
const settings = readWeixinSettings(chk.root);
|
|
14107
14484
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
14108
|
-
const { imCredentialBound } = await import("./route-
|
|
14485
|
+
const { imCredentialBound } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14109
14486
|
const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
|
|
14110
14487
|
const accounts = (settings.accounts ?? []).map((a) => {
|
|
14111
14488
|
const channel = normalizeAccountChannel(a.channel);
|
|
@@ -14142,7 +14519,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14142
14519
|
const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
14143
14520
|
const { randomUUID } = await import("node:crypto");
|
|
14144
14521
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
14145
|
-
const { readWeixinSettings } = await import("./route-
|
|
14522
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14146
14523
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
14147
14524
|
const loginKey = randomUUID();
|
|
14148
14525
|
weixinLogins.set(loginKey, {
|
|
@@ -14163,7 +14540,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14163
14540
|
sendJson$2(res, 400, { error: "missing accountId" });
|
|
14164
14541
|
return;
|
|
14165
14542
|
}
|
|
14166
|
-
const { removeWeixinAccount } = await import("./route-
|
|
14543
|
+
const { removeWeixinAccount } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14167
14544
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14168
14545
|
removeWeixinAccount(root, body.accountId);
|
|
14169
14546
|
syncCccBridge(ctx, root);
|
|
@@ -14189,8 +14566,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14189
14566
|
trajectory: code
|
|
14190
14567
|
});
|
|
14191
14568
|
}
|
|
14192
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
14193
|
-
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);
|
|
14194
14571
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
14195
14572
|
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
14196
14573
|
return;
|
|
@@ -14201,7 +14578,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14201
14578
|
}
|
|
14202
14579
|
if (body.action === "set-enabled") {
|
|
14203
14580
|
const enabled = body.enabled === true;
|
|
14204
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
14581
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14205
14582
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14206
14583
|
const settings = readWeixinSettings(root);
|
|
14207
14584
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -14224,7 +14601,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14224
14601
|
sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
|
|
14225
14602
|
return;
|
|
14226
14603
|
}
|
|
14227
|
-
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-
|
|
14604
|
+
const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14228
14605
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14229
14606
|
const before = readWeixinSettings(root);
|
|
14230
14607
|
const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
|
|
@@ -14274,7 +14651,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14274
14651
|
return;
|
|
14275
14652
|
}
|
|
14276
14653
|
const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
|
|
14277
|
-
const { readWeixinSettings } = await import("./route-
|
|
14654
|
+
const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14278
14655
|
const settings = readWeixinSettings(login.root);
|
|
14279
14656
|
const status = await pollQRStatus({
|
|
14280
14657
|
baseUrl: void 0,
|
|
@@ -14289,7 +14666,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14289
14666
|
});
|
|
14290
14667
|
return;
|
|
14291
14668
|
}
|
|
14292
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
14669
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
|
|
14293
14670
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
14294
14671
|
const accountId = nextWeixinAccountId(settings);
|
|
14295
14672
|
upsertWeixinAccount(login.root, {
|
|
@@ -18825,12 +19202,15 @@ function appendHumanChannelAudit(event) {
|
|
|
18825
19202
|
const WEIXIN_HOOK_TIMEOUT_MS = 15e3;
|
|
18826
19203
|
/** hook stderr/stdout 日志截断(防脚本刷屏) */
|
|
18827
19204
|
const HOOK_LOG_MAX = 500;
|
|
19205
|
+
/** 存量调用点未传 `channel` 时的缺省值(本字段存在之前唯一的渠道) */
|
|
19206
|
+
const DEFAULT_HOOK_CHANNEL = "weixin";
|
|
18828
19207
|
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
18829
19208
|
function buildIncomingHookEvent(input) {
|
|
18830
19209
|
return {
|
|
18831
19210
|
event: "incoming",
|
|
18832
19211
|
ts: Date.now(),
|
|
18833
19212
|
cccRoot: input.cccRoot,
|
|
19213
|
+
channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
|
|
18834
19214
|
accountId: input.accountId,
|
|
18835
19215
|
userId: input.userId,
|
|
18836
19216
|
sessionId: input.sessionId,
|
|
@@ -18847,6 +19227,7 @@ function buildOutgoingHookEvent(input) {
|
|
|
18847
19227
|
event: "outgoing",
|
|
18848
19228
|
ts: Date.now(),
|
|
18849
19229
|
cccRoot: input.cccRoot,
|
|
19230
|
+
channel: input.channel ?? DEFAULT_HOOK_CHANNEL,
|
|
18850
19231
|
accountId: input.accountId,
|
|
18851
19232
|
userId: input.userId,
|
|
18852
19233
|
sessionId: input.sessionId,
|
|
@@ -19229,7 +19610,7 @@ const MEDIA_MAX_BYTES = 20971520;
|
|
|
19229
19610
|
*/
|
|
19230
19611
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
19231
19612
|
try {
|
|
19232
|
-
const { listCccs } = await import("./ccc-roots-
|
|
19613
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
19233
19614
|
const entries = await listCccs(ctx);
|
|
19234
19615
|
const byName = entries.find((e) => e.name === alias);
|
|
19235
19616
|
if (byName) return byName.root;
|
|
@@ -19297,7 +19678,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
19297
19678
|
});
|
|
19298
19679
|
return null;
|
|
19299
19680
|
}
|
|
19300
|
-
const { findDirNameByCode } = await import("./profile-
|
|
19681
|
+
const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
|
|
19301
19682
|
if (findDirNameByCode(root, target.role) === null) {
|
|
19302
19683
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
19303
19684
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -19526,6 +19907,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19526
19907
|
const hookRel = settings.hook;
|
|
19527
19908
|
if (hookRel) invokeWeixinHook(root, hookRel, buildIncomingHookEvent({
|
|
19528
19909
|
cccRoot: root,
|
|
19910
|
+
channel,
|
|
19529
19911
|
accountId,
|
|
19530
19912
|
userId: fromUserId,
|
|
19531
19913
|
sessionId,
|
|
@@ -19552,6 +19934,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19552
19934
|
console.warn(`[serenity-hooks] ⚠ im 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
|
|
19553
19935
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
19554
19936
|
cccRoot: root,
|
|
19937
|
+
channel,
|
|
19555
19938
|
accountId,
|
|
19556
19939
|
userId: fromUserId,
|
|
19557
19940
|
sessionId,
|
|
@@ -19572,6 +19955,7 @@ async function handleInboundEvent(ctx, root, accountId, io, event, preset) {
|
|
|
19572
19955
|
});
|
|
19573
19956
|
if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
|
|
19574
19957
|
cccRoot: root,
|
|
19958
|
+
channel,
|
|
19575
19959
|
accountId,
|
|
19576
19960
|
userId: fromUserId,
|
|
19577
19961
|
sessionId,
|
|
@@ -20247,7 +20631,7 @@ function syncHumanChannel(ctx) {
|
|
|
20247
20631
|
(async () => {
|
|
20248
20632
|
try {
|
|
20249
20633
|
stopMachineLoops();
|
|
20250
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20634
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
20251
20635
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
20252
20636
|
} catch {}
|
|
20253
20637
|
})();
|
|
@@ -20344,6 +20728,7 @@ async function sendProactiveText(input) {
|
|
|
20344
20728
|
}
|
|
20345
20729
|
if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
|
|
20346
20730
|
cccRoot: root,
|
|
20731
|
+
channel: "weixin",
|
|
20347
20732
|
accountId,
|
|
20348
20733
|
userId: toUserId,
|
|
20349
20734
|
sessionId,
|
|
@@ -20538,7 +20923,7 @@ function matchCcc(input, candidates) {
|
|
|
20538
20923
|
}
|
|
20539
20924
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
20540
20925
|
async function collectCandidates(ctx) {
|
|
20541
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20926
|
+
const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
|
|
20542
20927
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
20543
20928
|
const seen = /* @__PURE__ */ new Set();
|
|
20544
20929
|
const out = [];
|
|
@@ -21753,6 +22138,24 @@ function credentialValue$1(root, key) {
|
|
|
21753
22138
|
function channelError$1(code, error, remediation) {
|
|
21754
22139
|
return /* @__PURE__ */ new Error(`${code}: ${error}${remediation ? ` — ${remediation}` : ""}`);
|
|
21755
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
|
+
}
|
|
21756
22159
|
const weixinChannel = {
|
|
21757
22160
|
id: "weixin",
|
|
21758
22161
|
isEnabled(root) {
|
|
@@ -21831,6 +22234,7 @@ const weixinChannel = {
|
|
|
21831
22234
|
const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
21832
22235
|
invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
|
|
21833
22236
|
cccRoot: input.root,
|
|
22237
|
+
channel: "weixin",
|
|
21834
22238
|
accountId: account.accountId,
|
|
21835
22239
|
userId: input.userId,
|
|
21836
22240
|
sessionId: weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown"),
|
|
@@ -21862,10 +22266,14 @@ const weixinChannel = {
|
|
|
21862
22266
|
lastPollAt: account.lastPollAt,
|
|
21863
22267
|
...account.lastError ? { lastError: account.lastError } : {}
|
|
21864
22268
|
})));
|
|
21865
|
-
const routes = (settings.routes ?? []).map((route) =>
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
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
|
+
});
|
|
21869
22277
|
return {
|
|
21870
22278
|
enabled: settings.enabled === true,
|
|
21871
22279
|
autoReplyWithLastMessage: settings.autoReplyWithLastMessage !== false,
|
|
@@ -21906,12 +22314,17 @@ const weixinChannel = {
|
|
|
21906
22314
|
* ⇒ 校验**值的形状**(飞书 open_id 以 `ou_` 开头):错配在**解析处**就失败,
|
|
21907
22315
|
* 错误码是 `ALIAS_UNKNOWN` + 可行动提示。
|
|
21908
22316
|
*
|
|
21909
|
-
* ## 🔴
|
|
22317
|
+
* ## 🔴 记录 hook:曾经不 fire,2026-10-22 起 fire(A83 已裁)
|
|
21910
22318
|
*
|
|
21911
|
-
*
|
|
21912
|
-
* 把事件追加进 `AGENT_SESSIONS/_weixin-logs/<日期>.jsonl
|
|
21913
|
-
*
|
|
21914
|
-
*
|
|
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
|
+
* ⚠️ 顺序有意义:**先有渠道字段,再有记录**。反过来做就会先造出一批分不清渠道的数据。
|
|
21915
22328
|
*/
|
|
21916
22329
|
/**
|
|
21917
22330
|
* 飞书 open_id 的形状前缀(官方形态:`ou_` + 一串)。
|
|
@@ -22066,6 +22479,18 @@ const feishuChannel = {
|
|
|
22066
22479
|
const settings = readWeixinSettings(input.root);
|
|
22067
22480
|
const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
|
|
22068
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
|
+
});
|
|
22069
22494
|
return {
|
|
22070
22495
|
accountId: account.accountId,
|
|
22071
22496
|
userId: input.userId,
|
|
@@ -22119,7 +22544,8 @@ const ACTIONS = [
|
|
|
22119
22544
|
"send",
|
|
22120
22545
|
"send-file",
|
|
22121
22546
|
"users",
|
|
22122
|
-
"status"
|
|
22547
|
+
"status",
|
|
22548
|
+
"contacts"
|
|
22123
22549
|
];
|
|
22124
22550
|
function renderText(value) {
|
|
22125
22551
|
return [{
|
|
@@ -22134,7 +22560,7 @@ function renderText(value) {
|
|
|
22134
22560
|
function createImBridgeTool() {
|
|
22135
22561
|
return defineTool({
|
|
22136
22562
|
name: "im-bridge",
|
|
22137
|
-
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.`,
|
|
22138
22564
|
parameters: {
|
|
22139
22565
|
channel: {
|
|
22140
22566
|
type: "string",
|
|
@@ -22145,7 +22571,7 @@ function createImBridgeTool() {
|
|
|
22145
22571
|
type: "string",
|
|
22146
22572
|
enum: [...ACTIONS],
|
|
22147
22573
|
required: true,
|
|
22148
|
-
description: "send / send-file / users / status"
|
|
22574
|
+
description: "send / send-file / users / status / contacts"
|
|
22149
22575
|
},
|
|
22150
22576
|
user: {
|
|
22151
22577
|
type: "string",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as cccRootForCwd } from "./ccc-roots-
|
|
1
|
+
import { r as cccRootForCwd } from "./ccc-roots-BI_4UVcj.js";
|
|
2
2
|
import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
|
|
3
|
-
import {
|
|
3
|
+
import { D as parseTrajectoryCode, M as sessionsRoot, S as getLastActiveSessionInfo, j as sessionEvents, x as getActiveSessionInfo } from "./profile-vHheCJiU.js";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -1222,4 +1222,4 @@ function replacesDefaultIdentity(profile) {
|
|
|
1222
1222
|
return hasFile || hasInline;
|
|
1223
1223
|
}
|
|
1224
1224
|
//#endregion
|
|
1225
|
-
export {
|
|
1225
|
+
export { resolveSessionByTitle as A, readTrajectoryFile as B, listActiveTrajectories as C, parseTrajectoryCode as D, parseSessionContextFromEvents as E, summarize as F, localFileStamp as H, trajectoryDescOf as I, useSession as L, sessionsRoot as M, setActiveSessionInfo as N, readActiveSessionMd as O, showSession as P, TRAJECTORY_FILENAME as R, getLastActiveSessionInfo as S, namingTitleFor as T, localHuman as U, isoLocal as V, localIdStamp as W, clearActiveSessionInfo as _, profile_exports as a, findSession as b, replacesDefaultIdentity as c, trajectoryDirNameForSession as d, unregisterTrajectorySession as f, archiveSession as g, UID_FILENAME as h, listTrajectoryConstraints as i, sessionEvents as j, readTrajectoryUid as k, trajectoryConstraintForSession as l, DEFAULT_SESSION_SCOPE as m, findDirNameByCode as n, readTrajectoryConstraintAt as o, COGNITIVE_TRAJECTORY_ACTIONS as p, isTrajectoryConstrained as r, readTrajectoryConstraintByCode as s, TRAJECTORY_CONSTRAINT_FILENAME as t, registerTrajectorySession as u, createSession as v, listSessions as w, getActiveSessionInfo as x, extractSessionMdPathFromText as y, TRAJECTORY_LEGACY_FILENAMES as z };
|
|
@@ -172,6 +172,13 @@ function normalizeImSection(w, d) {
|
|
|
172
172
|
* ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
|
|
173
173
|
* 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
|
|
174
174
|
* 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
|
|
175
|
+
*
|
|
176
|
+
* 🔴 2026-10-22:**返回类型由 `WeixinSettings` 改为 `ImSettings`**。
|
|
177
|
+
* 此前实现返回的就是 `readImSettings(...)`(已是归一形态:`accounts[].channel` 必填),
|
|
178
|
+
* 却把它断言成旧类型 —— 于是类型层"看不见"`channel` 字段,
|
|
179
|
+
* 调用方写 `a.channel` 会报 TS2339,而**运行时它一直在那儿**。
|
|
180
|
+
* ⇒ 那是**类型声明在撒谎**,不是调用方写错。改成真实类型后,多账户/多渠道的读侧才敢用它。
|
|
181
|
+
* ⚠️ 附带效应:`WeixinSettings` 的旧形状调用方若用到旧字段,会在此处暴露(好事,早暴露)。
|
|
175
182
|
*/
|
|
176
183
|
function readWeixinSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
177
184
|
return readImSettings(root, paths);
|
package/lib/seams/context.d.ts
CHANGED
|
@@ -21,6 +21,19 @@ export declare function accIdentityText(root: string, configPaths?: string[], en
|
|
|
21
21
|
* 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
|
|
22
22
|
*/
|
|
23
23
|
export declare function accMessage(root: string, configPaths: string[], entrySkillMaxChars: number): UserMessage;
|
|
24
|
+
/**
|
|
25
|
+
* 这条会话的历史里**是否已经有** ACC 身份横幅(= 判据的唯一权威)。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 这是本条修复的核心:**"注入过没有"是会话的属性,不是进程的属性。**
|
|
28
|
+
* 会话历史随会话走(落盘、重启后仍在),进程内状态随进程死。
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ 读不到 events(受限宿主 / 投影未就绪)⇒ 返回 `false`(退回"注入一次")。
|
|
31
|
+
* 这是**偏保守**的选择:多注入一条横幅(可见、可清理)远好于
|
|
32
|
+
* **永远不再注入**(陈旧会话里 ACC 身份彻底缺失,且零信号)。
|
|
33
|
+
*
|
|
34
|
+
* @param session 目标会话(`sessionEvents` 能读的形状)
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasAccBanner(session: unknown): boolean;
|
|
24
37
|
/**
|
|
25
38
|
* 重启恢复的根会话判定(S134 需求):
|
|
26
39
|
* 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-
|
|
1
|
+
import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-BI_4UVcj.js";
|
|
2
2
|
import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
|
|
3
|
-
import {
|
|
3
|
+
import { M as sessionsRoot, O as readActiveSessionMd, V as isoLocal, d as trajectoryDirNameForSession, f as unregisterTrajectorySession$1, l as trajectoryConstraintForSession, m as DEFAULT_SESSION_SCOPE, o as readTrajectoryConstraintAt, u as registerTrajectorySession$1 } from "./profile-vHheCJiU.js";
|
|
4
4
|
import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
|
|
5
|
-
import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-
|
|
5
|
+
import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-DRkQwfgh.js";
|
|
6
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/im-docs.ts — 「IM 桥怎么用、怎么配路由」的**手册正文**(2026-10-22)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这一章(owner 原话)
|
|
5
|
+
*
|
|
6
|
+
* 「我在测试飞书,发现 ACC 没提供 IM 桥使用说明和账户查询方法,**导致 CCC 没办法自己配路由**,
|
|
7
|
+
* 这个要完善下」+「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
|
|
8
|
+
* 需要有个能力让 CCC 查询用户配好的账户」。
|
|
9
|
+
*
|
|
10
|
+
* ⇒ 两件事被同时指出来:**没有说明面**(只能猜配置形状)+ **没有查询面**(拿不到对方 id)。
|
|
11
|
+
* 后者已由 `im-bridge action=contacts` 兑现;本章补前者。
|
|
12
|
+
*
|
|
13
|
+
* ## 🔴 写这一章时守的两条纪律
|
|
14
|
+
*
|
|
15
|
+
* 1. **只描述已经建成的东西**(D179):本章出现的每个字段、每个动作,都必须在实现里存在。
|
|
16
|
+
* 写"打算做"的功能进手册 = 教用户按图施工然后把自己锁死。
|
|
17
|
+
* 2. **指针从读者真正打开的那个工具发出**(D178):读这一章的人是 **CCC 的 agent**,
|
|
18
|
+
* 它手边常用的入口是 `im-bridge`(工具描述已指向本章)。⛔ 不能只藏在 container_admin 里。
|
|
19
|
+
*/
|
|
20
|
+
export declare const IM_BRIDGE_GUIDE = "# IM \u6865\uFF08im-bridge\uFF09\u2014\u2014 \u600E\u4E48\u7528\u3001\u600E\u4E48\u914D\u3001\u600E\u4E48\u67E5\n\n## \u4E00\u3001\u5B83\u662F\u4EC0\u4E48\n\n\u4E00\u4E2A CCC \u901A\u8FC7 IM \u6E20\u9053\uFF08\u5FAE\u4FE1 / \u98DE\u4E66\uFF09\u8DDF\u5BB6\u4EBA\u6536\u53D1\u6D88\u606F\u7684\u80FD\u529B\u3002\n**\u53EA\u4F5C\u7528\u4E8E\u672C\u5BB9\u5668**\u2014\u2014\u6CA1\u6709\"\u76EE\u6807\u5BB9\u5668\"\u53C2\u6570\uFF08\u8DE8 CCC \u53D1\u9001\u4E0D\u5728\u80FD\u529B\u9762\uFF09\u3002\n\n\u5DE5\u5177\u5728**\u672A\u914D\u7F6E\u4EFB\u4F55 IM \u6E20\u9053\u65F6\u6839\u672C\u4E0D\u53EF\u89C1**\uFF08\u4E0D\u662F\"\u770B\u5F97\u5230\u4F46\u88AB\u62D2\"\uFF09\u3002\n\u21D2 \u4F60\u770B\u5230\u8FD9\u4E2A\u5DE5\u5177\uFF0C\u8BF4\u660E\u672C CCC \u5DF2\u914D\u4E86\u81F3\u5C11\u4E00\u4E2A\u6E20\u9053\u3002\n\n## \u4E8C\u3001\u4E94\u4E2A\u52A8\u4F5C\n\n| \u52A8\u4F5C | \u7528\u9014 | \u5173\u952E\u53C2\u6570 |\n|---|---|---|\n| `send` | \u53D1\u4E00\u6761\u6587\u672C | `user`\uFF08\u5BF9\u65B9\uFF09\uFF0B `text` |\n| `send-file` | \u53D1\u672C\u5BB9\u5668\u5185\u7684\u4E00\u4E2A\u6587\u4EF6 | `user` \uFF0B `file`\uFF08**CCC \u6839\u76F8\u5BF9\u8DEF\u5F84**\uFF09\uFF0B \u53EF\u9009 `caption` |\n| `users` | \u5DF2\u914D\u7684**\u522B\u540D**\u8868 | \u2014 |\n| `status` | \u6E20\u9053\u5065\u5EB7 \uFF0B **\u8D26\u53F7** \uFF0B **\u5F53\u524D\u8DEF\u7531**\uFF08\u542B\u6BCF\u6761\u8DEF\u7531\u7684\u76EE\u6807\u662F\u5426\u5B58\u5728\uFF09 | \u2014 |\n| `contacts` | **\u8DDF\u672C\u5BB9\u5668\u6709\u8FC7\u5F80\u6765\u7684\u4EBA**\uFF08\u542B\u5BF9\u65B9 id\uFF09\uFF0B \u672A\u914D\u8DEF\u7531\u7684 \uFF0F \u914D\u4E86\u5374\u6CA1\u5F80\u6765\u7684 | \u2014 |\n\n\u26A0\uFE0F \u6E20\u9053\u4E0D\u4E00\u5B9A\u652F\u6301\u5168\u90E8\u52A8\u4F5C\uFF08\u4F8B\u5982\u98DE\u4E66\u76EE\u524D\u4E0D\u652F\u6301 `send-file`\uFF0C\u4F1A\u5982\u5B9E\u8FD4\u56DE `ACTION_UNSUPPORTED`\uFF09\u3002\n**\u5982\u5B9E\u8FD4\u56DE\"\u4E0D\u652F\u6301\"\u6BD4\u5047\u88C5\u652F\u6301\u597D**\u2014\u2014\u540E\u8005\u4F1A\u5F97\u5230\u4E00\u6761\"\u53D1\u51FA\u53BB\u4F46\u5BF9\u65B9\u6536\u4E0D\u5230\"\u7684\u9759\u9ED8\u6545\u969C\u3002\n\n## \u4E09\u3001\uD83D\uDD34 \u914D\u8DEF\u7531\uFF1A\u5148 `contacts`\uFF0C\u518D\u5199\uFF0C\u518D `status` \u81EA\u68C0\n\n\u8DEF\u7531\u7684\u5F62\u72B6\uFF08`.opencode/serenity.json` \u7684 `im.routes`\uFF09\uFF1A\n\n```json\n{\n \"im\": {\n \"enabled\": true,\n \"accounts\": [\n { \"accountId\": \"wechat-1\", \"channel\": \"weixin\", \"enabled\": true },\n { \"accountId\": \"feishu-1\", \"channel\": \"feishu\", \"enabled\": true }\n ],\n \"routes\": [\n { \"user\": \"o9cq\u2026@im.wechat\", \"trajectory\": \"zhaocai-xingzai\" },\n { \"user\": \"ou_xxxxxxxx\", \"trajectory\": \"llm-intel\" }\n ]\n }\n}\n```\n\n- `user` = **\u5BF9\u65B9\u5728\u8BE5\u6E20\u9053\u5185\u7684 id**\uFF08\u4E0D\u662F\u6211\u4EEC\u7684\u8D26\u53F7\uFF0C\u4E5F\u4E0D\u662F\u522B\u540D\uFF09\n \u00B7 \u5FAE\u4FE1\uFF1A\u5F62\u5982 `xxx@im.wechat`\n \u00B7 \u98DE\u4E66\uFF1A\u5F62\u5982 `ou_\u2026`\uFF08open_id\uFF09\n- `trajectory` = **\u8BA4\u77E5\u8F68\u8FF9\u7F16\u7801**\uFF08\u76EE\u5F55\u540D\u91CC\u90A3\u4E00\u6BB5\uFF0C\u5982 `S142` / `llm-intel`\uFF09\n- `*` \u53EF\u4F5C\u4E3A `user` \u7684**\u901A\u914D\u515C\u5E95**\uFF08exact \u4F18\u5148\uFF09\n\n### \u90A3\u4E2A `user` id \u4ECE\u54EA\u91CC\u6765 \u2014\u2014 \u8FD9\u662F\u672C\u8282\u5B58\u5728\u7684\u539F\u56E0\n\n**\u4F60\u4E0D\u77E5\u9053\u5BF9\u65B9 id\uFF0C\u800C\u5B83\u4E5F\u4E0D\u5728\u4EFB\u4F55\u914D\u7F6E\u6587\u4EF6\u91CC\u3002** \u5B83\u53EA\u51FA\u73B0\u5728\u804A\u5929\u8BB0\u5F55\u4E2D\u3002\n\u21D2 \u7528 `im-bridge({channel:\"weixin\", action:\"contacts\"})`\uFF0C\u5B83\u4F1A\u7ED9\u51FA\uFF1A\n\n```json\n{\n \"contacts\": [\n { \"channel\": \"weixin\", \"userId\": \"o9cq\u2026@im.wechat\", \"accountIds\": [\"wechat-1\"],\n \"route\": null, \"firstSeen\": \"2026-09-04T\u2026\", \"lastSeen\": \"2026-10-22T\u2026\", \"messages\": 1284 }\n ],\n \"unrouted\": [\"o9cq\u2026@im.wechat\"],\n \"orphanRoutes\": [\"ou_typo_never_seen\"],\n \"sources\": [\"AGENT_SESSIONS/_im-logs/weixin/2026-10-22.jsonl\", \"\u2026\"]\n}\n```\n\n\u4E09\u884C\u8F93\u51FA**\u5404\u6709\u7528\u9014\uFF0C\u7F3A\u4E00\u884C\u8FD9\u4E2A\u52A8\u4F5C\u5C31\u53EA\u5269\"\u5217\u540D\u5B57\"**\uFF1A\n\n| \u5B57\u6BB5 | \u542B\u4E49 | \u4F60\u8981\u505A\u4EC0\u4E48 |\n|---|---|---|\n| `contacts[].userId` | \u5BF9\u65B9 id | **\u590D\u5236\u5B83**\u586B\u8FDB `routes[].user` |\n| `unrouted` | \u26A0\uFE0F \u6709\u5F80\u6765\u3001\u4F46**\u6CA1\u914D\u8DEF\u7531** | \u8FD9\u4E9B\u4EBA\u7684\u6D88\u606F\u73B0\u5728**\u65E0\u5904\u53EF\u53BB** \u21D2 \u8FD9\u5C31\u662F\u8BE5\u914D\u7684 |\n| `orphanRoutes` | \u2753 \u914D\u4E86\u8DEF\u7531\u3001\u4F46\u8BB0\u5F55\u91CC**\u4ECE\u6CA1\u51FA\u73B0\u8FC7** | \u5927\u6982\u7387 **id \u5199\u9519**\uFF08\u6216\u4ECE\u672A\u6709\u4EBA\u53D1\u8FC7\uFF09 |\n| `sources` | \u5B9E\u9645\u8BFB\u8FC7\u7684\u6587\u4EF6 | **\u5426\u5B9A\u6027\u7ED3\u8BBA\u7684\u53EF\u4FE1\u5EA6\u4E0A\u9650** = \u8FD9\u4E2A\u5217\u8868\uFF1B\u5B83\u7A7A\u7740\u5C31\u522B\u4E0B\"\u6CA1\u4EBA\"\u7684\u7ED3\u8BBA |\n\n### \u5199\u5B8C\u600E\u4E48\u81EA\u68C0\n\n`im-bridge({channel:\"weixin\", action:\"status\"})` \u7684 `routes[]` \u6BCF\u6761\u90FD\u5E26 **`targetExists`**\uFF1A\n\n- `true` \u21D2 \u8BE5\u8F68\u8FF9\u76EE\u5F55\u771F\u5B9E\u5B58\u5728\uFF0C\u8DEF\u7531**\u6709\u673A\u4F1A**\u751F\u6548\n- `false` \u21D2 **\u8F68\u8FF9\u7F16\u7801\u5199\u9519\u4E86**\uFF08\u6216\u90A3\u6761\u8F68\u8FF9\u5DF2\u88AB\u5F52\u6863/\u5220\u9664\uFF09\n\n\uD83D\uDD34 \u8FD9\u4E00\u683C\u662F\"\u914D\u4E86\u4E0D\u751F\u6548\"\u7684**\u552F\u4E00\u673A\u68B0\u4FE1\u53F7**\uFF1A\u5199\u9519\u4E00\u4E2A\u7F16\u7801**\u4E0D\u4F1A\u6709\u4EFB\u4F55\u62A5\u9519**\uFF0C\n\u6D88\u606F\u8FDB\u6765\u627E\u4E0D\u5230\u76EE\u6807\u5C31\u9759\u9ED8\u4E0D\u6295\u9012\uFF0C\u4F60\u53EA\u4F1A\u770B\u5230\"\u6CA1\u56DE\u97F3\"\u3002\n\n## \u56DB\u3001\u591A\u8D26\u53F7 / \u591A\u6E20\u9053\n\n\u4E00\u4E2A CCC \u7ED1\u591A\u4E2A\u8D26\u53F7\u662F\u5E38\u6001\uFF08owner 2026-10-22\uFF1A\u300C\u4E00\u4E2A CCC \u7ED1\u5B9A\u591A\u4E2A\u8D26\u6237\u5F88\u5E38\u89C1\u300D\uFF09\u3002\n\n- **\u8D26\u53F7**\u5728 `im.accounts[]`\uFF0C\u6BCF\u6761\u81EA\u5E26 `channel`\uFF08\u7F3A\u7701 `weixin`\uFF09\n- **\u8DEF\u7531\u662F\u901A\u9053\u65E0\u5173\u7684**\uFF08`user \u2192 trajectory`\uFF09\uFF1B\u540C\u4E00\u8F68\u8FF9\u53EF\u4EE5\u88AB\u591A\u4E2A\u6E20\u9053\u7684\u5BF9\u7AEF\u6307\u5411\n- `contacts` \u7684\u6BCF\u6761\u5E26 `accountIds`\uFF08\u8BE5\u5BF9\u7AEF\u88AB\u54EA\u4E9B\u8D26\u53F7\u6536\u8FC7\uFF09\u2014\u2014**\u591A\u8D26\u53F7\u9760\u5B83\u533A\u5206**\n- \u53D1\u6D88\u606F\u65F6\u53EF\u7528 `account` \u53C2\u6570\u6307\u5B9A\u7528\u54EA\u4E2A\u8D26\u53F7\uFF08\u7F3A\u7701\uFF1A\u7B2C\u4E00\u4E2A\u542F\u7528\u4E14\u5DF2\u7ED1\u5B9A\u7684\uFF09\n\n## \u4E94\u3001\u804A\u5929\u8BB0\u5F55\u843D\u5728\u54EA\n\n```\nAGENT_SESSIONS/_im-logs/<channel>/<YYYY-MM-DD>.jsonl\n```\n\n- \u6309**\u6E20\u9053**\u5206\u76EE\u5F55\uFF08`weixin` / `feishu`\uFF09\uFF0C\u6309**\u672C\u5730\u81EA\u7136\u5929**\u5206\u6587\u4EF6\uFF0C\u9010\u884C JSON \u8FFD\u52A0\n- \u8001\u8DEF\u5F84 `AGENT_SESSIONS/_weixin-logs/` \u662F**\u540C\u4E00\u65CF\u7684\u524D\u4EE3**\uFF082026-10-22 \u4E4B\u524D\u7684\u5FAE\u4FE1\u8BB0\u5F55\uFF09\uFF1B\n \u8BFB\u4FA7**\u4E24\u4E2A\u90FD\u8BA4**\uFF0C\u26D4 \u4F46**\u53EA\u5F80\u65B0\u8DEF\u5F84\u5199**\n- \u6BCF\u6761\u4E8B\u4EF6\u5E26 `channel` \u5B57\u6BB5 \u21D2 \u98DE\u4E66\u4E8B\u4EF6\u4E0D\u4F1A\u518D\u6DF7\u8FDB\u5FAE\u4FE1\u76EE\u5F55\uFF08\u6B64\u524D\u4F1A\uFF0C\u76EE\u5F55\u540D\u4F1A\u8BF4\u8C0E\uFF09\n- \u5A92\u4F53\u7684\u6301\u4E45\u526F\u672C\u5728 `_im-logs/<channel>/media/<\u65E5\u671F>/`\n\n\u26A0\uFE0F **\u8BB0\u5F55\u5F52 CCC**\uFF08`im.hook` \u6307\u5411\u672C\u5BB9\u5668\u7684\u811A\u672C\uFF09\uFF0CACC \u4E0D\u8BA4\u8BC6\u5B58\u50A8\u683C\u5F0F \u2014\u2014 \u8FD9\u5C42\u5206\u79BB\u662F\u6709\u610F\u7684\u3002\n\n## \u516D\u3001\u51FA\u9519\u65F6\n\n\u5DE5\u5177\u8FD4\u56DE**\u7A33\u5B9A\u9519\u8BEF\u7801 + \u53EF\u884C\u52A8\u63D0\u793A**\uFF0C\u5E38\u89C1\u7684\uFF1A\n\n| \u7801 | \u610F\u601D | \u600E\u4E48\u6539 |\n|---|---|---|\n| `CHANNEL_NOT_CONFIGURED` | \u672C CCC \u6CA1\u542F\u7528\u8BE5\u6E20\u9053 | \u770B\u63D0\u793A\u91CC\u7684**\u90A3\u4E00\u534A**\uFF08\u603B\u5F00\u5173 vs \u7F3A\u8D26\u53F7\uFF0C\u4E24\u8005\u6539\u6CD5\u4E0D\u540C\uFF09 |\n| `ALIAS_UNKNOWN` | `user` \u65E2\u4E0D\u662F\u5DF2\u914D\u522B\u540D\u3001\u4E5F\u4E0D\u662F\u5408\u6CD5\u7684\u88F8 id | \u5148 `action=users` \u770B\u522B\u540D\uFF0C\u6216\u7528 `contacts` \u62FF\u88F8 id |\n| `ACTION_UNSUPPORTED` | \u8BE5\u6E20\u9053\u6CA1\u5B9E\u73B0\u8FD9\u4E2A\u52A8\u4F5C | \u6362\u52A8\u4F5C\uFF1B\u26D4 \u4E0D\u8981\u91CD\u8BD5 |\n| `FILE_ESCAPE` | `file` \u8DD1\u51FA\u4E86 CCC \u6839 | \u7528\u5BB9\u5668\u5185\u76F8\u5BF9\u8DEF\u5F84 |\n\n## \u4E03\u3001\u8FB9\u754C\uFF08\u660E\u786E\u4E0D\u505A\uFF09\n\n- \u26D4 \u8DE8 CCC \u53D1\u9001\uFF08\u53EA\u4F5C\u7528\u4E8E\u672C\u5BB9\u5668\uFF09\n- \u26D4 \u4FEE\u6539\u914D\u7F6E\uFF08`contacts` \u53EA\u8BFB\uFF1B\u52A0\u8DEF\u7531\u8981\u4EBA/CCC \u81EA\u5DF1\u5199 `serenity.json`\uFF09\n- \u26D4 \u5224\u65AD\"\u5BF9\u65B9\u8BFB\u6CA1\u8BFB\"\u2014\u2014\u8BB0\u5F55\u53EA\u8BC1\u660E**\u5DF2\u53D1\u51FA**\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.86.0",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|