@shgroup/dsh-serenity-hooks 1.71.0 → 1.72.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 +2 -2
- package/lib/im/accounts.d.ts +105 -0
- package/lib/im/route.d.ts +24 -2
- package/lib/im/weixin-account.d.ts +45 -0
- package/lib/index.js +360 -72
- package/lib/kernel/ccc.d.ts +76 -0
- package/lib/{route-CPneN5iv.js → route-f7JlinIt.js} +77 -31
- package/lib/seams/system-prompt.d.ts +3 -1
- package/lib/trajectory/prompt.d.ts +101 -0
- package/package.json +2 -2
package/dsh.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.72.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/认知轨迹唤醒注册表。skiff = 目录下有 skiff.json
|
|
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/认知轨迹唤醒注册表。skiff = 目录下有 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": {
|
|
7
7
|
"dsh": ">=0.1.7-rc.1"
|
|
8
8
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* im/accounts.ts — **账号实现派发表**(P2,2026-10-19)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是"账号"这一层(owner 定向,逐字)
|
|
5
|
+
*
|
|
6
|
+
* > 「飞书继承可以很方便的加到当前的体系里,**和微信一样,只不过是 account 方实现不同**。
|
|
7
|
+
* > **要整合到统一的配置中**。」
|
|
8
|
+
*
|
|
9
|
+
* 微信与飞书的差别**只在账号侧**:怎么连、怎么收、怎么发。而它们的**身份、路由、会话**
|
|
10
|
+
* 从来就是通道无关的:
|
|
11
|
+
* - 路由表 = `user → 轨迹编码`(不认渠道)
|
|
12
|
+
* - 会话 id = `skiff-weixin-<轨迹编码>`(由轨迹决定,不认渠道)
|
|
13
|
+
* - 一账号一 poller 的归属判据 = `(root, accountId, identity)`(不含渠道概念)
|
|
14
|
+
*
|
|
15
|
+
* ⇒ 抽象点是 `account.channel`,**不是**新造一个平行的"通道接口"。
|
|
16
|
+
* 本文件的全部职责:**channel 字符串 → 那个实现**。
|
|
17
|
+
*
|
|
18
|
+
* ## 这不是新设计(源码里早就留了口)
|
|
19
|
+
*
|
|
20
|
+
* `bridge.ts` 的机器级装配处原本写着:
|
|
21
|
+
* *「只有已实现的渠道能起 poller(**P2 才加第二个实现**)⇒ 未实现的出声但不静默跳过」*
|
|
22
|
+
* 且账号行 `HumanChannelAccountSettings` **本来就有 `channel` 字段**。
|
|
23
|
+
* ⇒ 本文件就是把那个"P2 预留位"填上。
|
|
24
|
+
*
|
|
25
|
+
* ## 为什么用「注册表 + 未命中响亮」而不是 switch
|
|
26
|
+
*
|
|
27
|
+
* switch 会把"支持哪些渠道"写死在装配逻辑里;注册表让**新增一个渠道实现 = 新增一个文件
|
|
28
|
+
* + 一次 register**,装配处一行不改(R3 家族式扩展)。而未命中**必须出声**——
|
|
29
|
+
* 静默跳过会表现成"某个账号莫名其妙不工作",那是本轨反复踩过的一类最难查的故障。
|
|
30
|
+
*/
|
|
31
|
+
/** 一条账号的运行期凭据(各实现自解释;`weixin` 用 token/baseUrl/userId) */
|
|
32
|
+
export interface ImAccountCredential {
|
|
33
|
+
token?: string;
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
userId?: string;
|
|
36
|
+
/** 飞书等:appId(凭据本体仍在 localstore) */
|
|
37
|
+
appId?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 账号实现的**最小面**(只放装配处真正需要的东西)。
|
|
41
|
+
*
|
|
42
|
+
* 🔴 刻意**不**把"收消息"放进来:接收循环的形状(长轮询 vs WebSocket 回调)
|
|
43
|
+
* 两家根本不同,硬塞进一个签名只会让两边都别扭。装配处需要的是:
|
|
44
|
+
* ① 这个账号该不该起(凭据齐不齐)② 起起来 ③ 停 ④ 健康快照。
|
|
45
|
+
*/
|
|
46
|
+
export interface ImAccountImpl {
|
|
47
|
+
/** 实现名(= `account.channel` 的取值,如 `weixin`) */
|
|
48
|
+
readonly channel: string;
|
|
49
|
+
/**
|
|
50
|
+
* 凭据是否齐全到**可以起**(缺失 ⇒ 装配处跳过并出声,**不静默**)。
|
|
51
|
+
* @param cred 已从 localstore 读好的凭据(实现自己判断哪个字段必需)
|
|
52
|
+
*/
|
|
53
|
+
isReady(cred: ImAccountCredential): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 凭据不全时**给人看的原因**(装配处的告警文案,可含 CC 特定措辞)。
|
|
56
|
+
* 缺省 ⇒ 装配处用通用文案。存在的理由:微信的文案是「无 token(未绑定)」,
|
|
57
|
+
* 那是有既有判据钉住的措辞(测试与排查手册都按它找),不该被本层统一措辞冲掉。
|
|
58
|
+
*/
|
|
59
|
+
notReadyReason?(cred: ImAccountCredential): string;
|
|
60
|
+
/**
|
|
61
|
+
* 起这条账号的接收循环(fire-and-forget:实现内部自行循环,不得阻塞装配)。
|
|
62
|
+
* @returns 该账号的循环控制对象(装配处用它做停止与快照)
|
|
63
|
+
*/
|
|
64
|
+
startLoop(input: ImAccountLoopInput): ImAccountLoop;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 起循环的输入。
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ `root` 在**机器级模式**下是 `MACHINE_SCOPE` 哨兵(不是一个目录)——
|
|
70
|
+
* 这与既有语义一致(投到哪个 CCC 由**每条消息**的订阅表决定,不由账号决定)。
|
|
71
|
+
*/
|
|
72
|
+
export interface ImAccountLoopInput {
|
|
73
|
+
ctx: unknown;
|
|
74
|
+
root: string;
|
|
75
|
+
accountId: string;
|
|
76
|
+
cred: ImAccountCredential;
|
|
77
|
+
/** 真 = 机器级 poller(目标由订阅表逐条决定) */
|
|
78
|
+
machine: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 循环控制(装配处持有;`weixinBridgeStatus` 读它出健康快照)。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 `lastPollAt` / `lastError` 是**面板读数面**的既有字段,名字不能改
|
|
84
|
+
* (`im-bridge status` 与设置面板都按这个形状读)。
|
|
85
|
+
*/
|
|
86
|
+
export interface ImAccountLoop {
|
|
87
|
+
stopped: boolean;
|
|
88
|
+
lastPollAt: number;
|
|
89
|
+
lastError?: string;
|
|
90
|
+
}
|
|
91
|
+
/** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
|
|
92
|
+
export declare function registerImAccountImpl(impl: ImAccountImpl): void;
|
|
93
|
+
/** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
|
|
94
|
+
export declare function getImAccountImpl(channel: string): ImAccountImpl | null;
|
|
95
|
+
/** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
|
|
96
|
+
export declare function imAccountChannels(): string[];
|
|
97
|
+
/** 测试辅助:清空派发表(生产零调用) */
|
|
98
|
+
export declare function __resetImAccountImplsForTest(): void;
|
|
99
|
+
/**
|
|
100
|
+
* 账号行的渠道归一(**单一处**)。
|
|
101
|
+
*
|
|
102
|
+
* 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
|
|
103
|
+
* 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
|
|
104
|
+
*/
|
|
105
|
+
export declare function normalizeAccountChannel(channel: string | undefined): string;
|
package/lib/im/route.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 配置归属(S142 用户拍板):**CCC 级**——serenity.json 结构 + localstore 凭据分离;
|
|
12
12
|
* 不绑定具体 role(用户自选路由目标)。
|
|
13
13
|
*/
|
|
14
|
-
import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig } from '../kernel/ccc.js';
|
|
14
|
+
import { type WeixinSettings, type WeixinRouteConfig, type WeixinAccountConfig, type ImSettings } from '../kernel/ccc.js';
|
|
15
15
|
import type { WeixinMediaRef, WeixinMessageItem } from './ilink.js';
|
|
16
16
|
/** 判定 sessionId 是否为微信桥会话(**命名形态**判定;权力面判据见 `isSkiffSession`) */
|
|
17
17
|
export declare function isWeixinSessionId(sessionId: string | undefined): boolean;
|
|
@@ -39,9 +39,31 @@ export declare function isWeixinSessionId(sessionId: string | undefined): boolea
|
|
|
39
39
|
* 见 `privacy-scan` 的 `weixin-session-pseudonym` 规则 —— 新形态连这个问题都没有了)。
|
|
40
40
|
*/
|
|
41
41
|
export declare function weixinSessionIdForTrajectory(code: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* **统一 IM 配置的单一读取入口**(2026-10-19 owner 定向:微信与飞书只是账号实现不同,
|
|
44
|
+
* 整合到统一配置)。
|
|
45
|
+
*
|
|
46
|
+
* 读取顺序(**确定性,只有这一处**):
|
|
47
|
+
* ① `im` 段 —— 但**判据是 `accounts` 是数组**,不是"段存在"(见下 🔴)
|
|
48
|
+
* ② 旧 `weixin` 段 —— 存量 CCC 零迁移
|
|
49
|
+
* ③ 都没有 ⇒ 归一默认(`enabled:false`、空数组)
|
|
50
|
+
*
|
|
51
|
+
* 🔴 **为什么判据是「`accounts` 是数组」**:本 CCC 的配置里**曾经**有过一个 `im` 段
|
|
52
|
+
* (退役的 ACP 形态:`{channel:"acp", acp:{enabled, accounts:[…]}}`)。若只判"段存在",
|
|
53
|
+
* 那份残留会被当成新配置读 ⇒ 得到一个没有 `accounts` 数组 / 或 `accounts` 形状完全不同的对象
|
|
54
|
+
* ⇒ 表现为**"配置没生效"且不报错**。⇒ 用可判别的形状把它排除掉(退役残留继续被静默忽略,
|
|
55
|
+
* 与 v1.59.0 的裁决一致)。
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ 本函数**只做归一**,不判断"这个通道实现有没有"——那是装配处的事
|
|
58
|
+
* (`syncMachinePollers` 里有"未实现的 channel 出声但不静默跳过")。
|
|
59
|
+
*/
|
|
60
|
+
export declare function readImSettings(root: string, paths?: string[]): ImSettings;
|
|
42
61
|
/**
|
|
43
62
|
* 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
|
|
44
|
-
*
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
|
|
65
|
+
* 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
|
|
66
|
+
* 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
|
|
45
67
|
*/
|
|
46
68
|
export declare function readWeixinSettings(root: string, paths?: string[]): WeixinSettings;
|
|
47
69
|
/** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* im/weixin-account.ts — 微信账号实现(P2:把既有微信轮询包成 `ImAccountImpl`)
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **纯移动,零逻辑改动**。`runAccountLoop` 的函数体逐字来自 `bridge.ts`;
|
|
5
|
+
* 唯一新增的是把它**登记成"一种账号实现"**,这样装配处不必再写死 `channel === 'weixin'`。
|
|
6
|
+
*
|
|
7
|
+
* 为什么值得单独成一个文件(而不是留在 bridge.ts 里):
|
|
8
|
+
* 装配处从此只认 `ImAccountImpl` 接口;微信与飞书是**并列的两个实现**,
|
|
9
|
+
* 而不是"微信是主角、别的往里塞"。这正是 owner「只不过是 account 方实现不同」的结构表达。
|
|
10
|
+
*/
|
|
11
|
+
import type { WeixinAccountCredential } from './route.js';
|
|
12
|
+
import { type ImAccountImpl } from './accounts.js';
|
|
13
|
+
/**
|
|
14
|
+
* 把实现所需的最小注入面收进来。
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ 这些是**微信侧的运行依赖**(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)。
|
|
17
|
+
* 用注入而不是 import,是为了让 `bridge.ts` 不因本文件而反向依赖 —— 否则两个文件互相
|
|
18
|
+
* import 会成环(本轨已有先例:`rebuild ⇄ tools/trajectory` 那个人工发现的环)。
|
|
19
|
+
*/
|
|
20
|
+
export interface WeixinLoopDeps {
|
|
21
|
+
getUpdates: (input: {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
token: string;
|
|
24
|
+
getUpdatesBuf: string;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
}) => Promise<{
|
|
27
|
+
get_updates_buf?: string;
|
|
28
|
+
msgs?: Array<Record<string, unknown>>;
|
|
29
|
+
}>;
|
|
30
|
+
handleIncoming: (ctx: unknown, root: string, accountId: string, cred: WeixinAccountCredential, msg: never) => Promise<void>;
|
|
31
|
+
handleMachineIncoming: (ctx: unknown, accountId: string, cred: WeixinAccountCredential, msg: never) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** 装配时注入依赖(`bridge.ts` 在模块初始化末尾调用一次;见 `installWeixinAccountImpl`) */
|
|
34
|
+
export declare function setWeixinLoopDeps(d: WeixinLoopDeps): void;
|
|
35
|
+
/** 微信账号实现(登记的 channel 名 = `weixin`;与配置里的取值一致) */
|
|
36
|
+
export declare const weixinAccountImpl: ImAccountImpl;
|
|
37
|
+
/**
|
|
38
|
+
* 注册微信实现**并注入依赖**(`bridge.ts` 调用;幂等——重复注册覆盖同名 channel)。
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ 必须在首次 `syncHumanChannel` / `syncCccBridge` **之前**发生。
|
|
41
|
+
* `bridge.ts` 在**模块求值时**调用它一次(不是等到 `registerWeixinBridge`)——
|
|
42
|
+
* 因为测试与面板会**直接**调 `syncCccBridge`,那时装配入口还没跑过;
|
|
43
|
+
* 若依赖装配入口注册,那些路径会全部走进"未实现"分支(本轮实测:12 例红,根因就是它)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function installWeixinAccountImpl(d: WeixinLoopDeps): void;
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
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-jZYR2GVE.js";
|
|
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, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-Dkoa2oW3.js";
|
|
3
|
+
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-Dkoa2oW3.js";
|
|
4
4
|
import { A as sessionEvents$1, C as listSessions, D as readActiveSessionMd, E as parseTrajectoryCode, F as useSession, I as isoLocal, L as localFileStamp, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as localHuman, S as listActiveTrajectories, T as parseSessionContextFromEvents, _ as createSession, a as readSkiffProfileAt, b as getActiveSessionInfo, c as registerSkiffSession$1, d as unregisterSkiffSession$1, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, l as skiffSessionInfo$1, m as UID_FILENAME, n as isSkiffSession, o as readSkiffProfileByCode, p as DEFAULT_SESSION_SCOPE, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as skiffTrajectoryDirNameFor, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as localIdStamp } from "./profile-DyMJ_bfr.js";
|
|
5
|
-
import { a as readWeixinCredential, c as sanitizeFileName$1, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinInboundDir, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, t as extractWeixinMedia, u as weixinSessionIdForTrajectory } from "./route-
|
|
5
|
+
import { a as readWeixinCredential, c as sanitizeFileName$1, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinInboundDir, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, t as extractWeixinMedia, u as weixinSessionIdForTrajectory } from "./route-f7JlinIt.js";
|
|
6
6
|
import { _ as resolveProfileSystemPrompt, a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as buildSkiffBasePrompt, f as createProfilePromptReader, g as profileToolWhitelist, h as profileSystemPromptSource, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as profileMsmWhitelist, n as readSimpleSettings, o as MAIN_WEB_PORT, p as profileCognitiveTrajectory, r as registerSettingsSection, s as MECHANISM_PORTS, u as SKIFF_PROFILE_HINT, v as ACC_VERSION, y as findPackageRoot } from "./settings-lt1i3OZv.js";
|
|
7
7
|
import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-kVoNK0C5.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
@@ -1823,6 +1823,141 @@ async function runMsmAsync(root, args) {
|
|
|
1823
1823
|
}
|
|
1824
1824
|
}
|
|
1825
1825
|
//#endregion
|
|
1826
|
+
//#region src/trajectory/prompt.ts
|
|
1827
|
+
/**
|
|
1828
|
+
* trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
|
|
1829
|
+
*
|
|
1830
|
+
* ## 概念(owner 2026-10-19 定向,逐字)
|
|
1831
|
+
*
|
|
1832
|
+
* > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
|
|
1833
|
+
*
|
|
1834
|
+
* **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
|
|
1835
|
+
* 三个关键词各是一条判据:
|
|
1836
|
+
* - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
|
|
1837
|
+
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `skiff.json` 白名单与工具 schema)
|
|
1838
|
+
* - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
|
|
1839
|
+
*
|
|
1840
|
+
* ## 为什么取代「人格」
|
|
1841
|
+
*
|
|
1842
|
+
* 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
|
|
1843
|
+
* (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
|
|
1844
|
+
* 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
|
|
1845
|
+
*
|
|
1846
|
+
* ## 落点(为什么在轨迹目录里)
|
|
1847
|
+
*
|
|
1848
|
+
* `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
|
|
1849
|
+
*
|
|
1850
|
+
* - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
|
|
1851
|
+
* 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
|
|
1852
|
+
* - **每条轨迹都能有**:不只是 skiff。普通轨迹同样需要"我是谁、按什么章程"。
|
|
1853
|
+
* - 与 `SESSION.md`(身体:延续/日志)、`skiff.json`(约束面:能力)三件并列,
|
|
1854
|
+
* 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
|
|
1855
|
+
*
|
|
1856
|
+
* ## 兼容(不静默坏)
|
|
1857
|
+
*
|
|
1858
|
+
* `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。skiff 的旧字段 `systemPromptFile`
|
|
1859
|
+
* 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
|
|
1860
|
+
*/
|
|
1861
|
+
/** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
|
|
1862
|
+
const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
|
|
1863
|
+
/** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
|
|
1864
|
+
function trajectoryPromptPath(root, dirName) {
|
|
1865
|
+
return join(sessionsRoot(root), dirName, TRAJECTORY_PROMPT_FILENAME);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* 该轨迹**有没有**轨迹提示词(单一判据)。
|
|
1869
|
+
*
|
|
1870
|
+
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,skiff 旧字段 `systemPromptFile`
|
|
1871
|
+
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-skiff` 段)。
|
|
1872
|
+
* 🔴 判据只有这一处:别在 skiff 那边自己拼路径再判(两处会漂)。
|
|
1873
|
+
*/
|
|
1874
|
+
function hasTrajectoryPrompt(root, dirName) {
|
|
1875
|
+
try {
|
|
1876
|
+
return existsSync(trajectoryPromptPath(root, dirName));
|
|
1877
|
+
} catch {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
|
|
1883
|
+
*
|
|
1884
|
+
* ## 为什么返回函数而不是字符串(R↓,实证代价)
|
|
1885
|
+
*
|
|
1886
|
+
* 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
|
|
1887
|
+
* (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
|
|
1888
|
+
* **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
|
|
1889
|
+
*
|
|
1890
|
+
* ## 三条失败语义(各治一个失败模式)
|
|
1891
|
+
*
|
|
1892
|
+
* - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
|
|
1893
|
+
* 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
|
|
1894
|
+
* - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
|
|
1895
|
+
* 并按去重标签告警一次
|
|
1896
|
+
* - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
|
|
1897
|
+
*
|
|
1898
|
+
* @param root CCC 根
|
|
1899
|
+
* @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
|
|
1900
|
+
*/
|
|
1901
|
+
function createTrajectoryPromptReader(root, dirName) {
|
|
1902
|
+
const absPath = trajectoryPromptPath(root, dirName);
|
|
1903
|
+
let cachedMtime = -1;
|
|
1904
|
+
let cachedSize = -1;
|
|
1905
|
+
let cachedText = "";
|
|
1906
|
+
let warned = false;
|
|
1907
|
+
return () => {
|
|
1908
|
+
let st;
|
|
1909
|
+
try {
|
|
1910
|
+
st = statSync(absPath);
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
const code = err?.code;
|
|
1913
|
+
cachedMtime = -1;
|
|
1914
|
+
cachedSize = -1;
|
|
1915
|
+
cachedText = "";
|
|
1916
|
+
if (code !== "ENOENT" && !warned) {
|
|
1917
|
+
warned = true;
|
|
1918
|
+
console.warn(`[serenity-hooks] ✗ 轨迹提示词不可读(${absPath})——按"没有章程"处理: ${String(err?.message ?? err)}`);
|
|
1919
|
+
}
|
|
1920
|
+
return "";
|
|
1921
|
+
}
|
|
1922
|
+
try {
|
|
1923
|
+
if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
|
|
1924
|
+
const text = readUtf8(absPath).trim();
|
|
1925
|
+
const isReload = cachedMtime !== -1;
|
|
1926
|
+
cachedMtime = st.mtimeMs;
|
|
1927
|
+
cachedSize = st.size;
|
|
1928
|
+
cachedText = text;
|
|
1929
|
+
warned = false;
|
|
1930
|
+
if (isReload) console.info(`[serenity-hooks] ↻ 轨迹提示词已热重载(${TRAJECTORY_PROMPT_FILENAME},${text.length} 字符)`);
|
|
1931
|
+
return text;
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
if (!warned) {
|
|
1934
|
+
warned = true;
|
|
1935
|
+
console.warn(`[serenity-hooks] ✗ 轨迹提示词读取失败(${absPath})——沿用上次成功内容: ${String(err?.message ?? err)}`);
|
|
1936
|
+
}
|
|
1937
|
+
return cachedText;
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
|
|
1943
|
+
*
|
|
1944
|
+
* 标记头 `=== Serenity Trajectory Prompt ===`:
|
|
1945
|
+
* ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
|
|
1946
|
+
* ② 让诊断可以按块名 grep。
|
|
1947
|
+
*
|
|
1948
|
+
* 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
|
|
1949
|
+
*/
|
|
1950
|
+
function trajectoryPromptBlock(text) {
|
|
1951
|
+
const body = text.trim();
|
|
1952
|
+
if (body === "") return "";
|
|
1953
|
+
return [
|
|
1954
|
+
"",
|
|
1955
|
+
"=== Serenity Trajectory Prompt ===",
|
|
1956
|
+
body,
|
|
1957
|
+
""
|
|
1958
|
+
].join("\n");
|
|
1959
|
+
}
|
|
1960
|
+
//#endregion
|
|
1826
1961
|
//#region src/role/worker/idle.ts
|
|
1827
1962
|
/**
|
|
1828
1963
|
* 等待 agent 空闲或销毁。
|
|
@@ -3441,7 +3576,7 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
3441
3576
|
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
3442
3577
|
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
3443
3578
|
*/
|
|
3444
|
-
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
3579
|
+
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
|
|
3445
3580
|
const persona = readPersonaSettings();
|
|
3446
3581
|
const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
|
|
3447
3582
|
const parts = [
|
|
@@ -3449,12 +3584,18 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
3449
3584
|
metaphorBlock(),
|
|
3450
3585
|
principlesBlock(root, personaOn),
|
|
3451
3586
|
cceBlock(),
|
|
3452
|
-
|
|
3587
|
+
eapBlock()
|
|
3453
3588
|
];
|
|
3589
|
+
if (personaOn) {
|
|
3590
|
+
const expr = personaBlock(persona.mode, persona.overrideText);
|
|
3591
|
+
if (expr !== "") parts.push(expr);
|
|
3592
|
+
}
|
|
3454
3593
|
const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
|
|
3455
3594
|
if (state) parts.push(state);
|
|
3456
3595
|
const skill = entrySkillSectionText(root);
|
|
3457
3596
|
if (skill) parts.push(skill);
|
|
3597
|
+
const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
|
|
3598
|
+
if (trajPrompt !== "") parts.push(trajPrompt);
|
|
3458
3599
|
parts.push(toolsBlock());
|
|
3459
3600
|
const session = sessionBlock(root, scope);
|
|
3460
3601
|
if (session) parts.push(session);
|
|
@@ -3501,6 +3642,23 @@ function agentScope$2(context) {
|
|
|
3501
3642
|
return (context.agent?.session)?.id ?? "default";
|
|
3502
3643
|
}
|
|
3503
3644
|
/**
|
|
3645
|
+
* 该会话**绑定的轨迹目录名**(有 ⇒ 注入第 ⑧ 段「轨迹提示词」;无 ⇒ `undefined`,不注入)。
|
|
3646
|
+
*
|
|
3647
|
+
* 🔴 为什么用**目录名**而不是编码或 label:`PROMPT.md` 的落盘位置是目录,
|
|
3648
|
+
* 而编码只是目录名里的一段。用 label 拼路径 = 把模糊匹配当精确匹配用(本轨已踩过)。
|
|
3649
|
+
*
|
|
3650
|
+
* ⚠️ 每次装配都重读绑定(**不缓存**):绑定切换后提示词必须立刻跟上,
|
|
3651
|
+
* 这与 `serenity-trajectory-skills` section 的口径一致(动态注入,owner 令 2026-09-20)。
|
|
3652
|
+
*/
|
|
3653
|
+
function boundDirNameFor(session) {
|
|
3654
|
+
try {
|
|
3655
|
+
const name = readLastBound(session)?.dirName;
|
|
3656
|
+
return typeof name === "string" && name !== "" ? name : void 0;
|
|
3657
|
+
} catch {
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3504
3662
|
* 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
|
|
3505
3663
|
* text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
|
|
3506
3664
|
* 非 CCC / 无 agent 返回空(不注入)。
|
|
@@ -3515,9 +3673,7 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
3515
3673
|
if (!cwd) return "";
|
|
3516
3674
|
const root = cccRootForCwd(cwd);
|
|
3517
3675
|
if (!root) return "";
|
|
3518
|
-
const
|
|
3519
|
-
if (isSkiffSession(root, scope)) return "";
|
|
3520
|
-
const base = serenitySystemPrompt(root, scope);
|
|
3676
|
+
const base = serenitySystemPrompt(root, agentScope$2(context), boundDirNameFor(context.agent?.session));
|
|
3521
3677
|
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
3522
3678
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3523
3679
|
}
|
|
@@ -3549,8 +3705,7 @@ function registerEntrySkillSection(agent, root) {
|
|
|
3549
3705
|
name: "serenity-entry",
|
|
3550
3706
|
order: -50,
|
|
3551
3707
|
text: (context) => {
|
|
3552
|
-
|
|
3553
|
-
const base = serenitySystemPrompt(root, scope);
|
|
3708
|
+
const base = serenitySystemPrompt(root, scope, boundDirNameFor(agent.session));
|
|
3554
3709
|
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
3555
3710
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
3556
3711
|
}
|
|
@@ -3903,7 +4058,10 @@ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
|
|
|
3903
4058
|
agent.ctx.systemPrompt.section({
|
|
3904
4059
|
name: "serenity-skiff",
|
|
3905
4060
|
order: -60,
|
|
3906
|
-
text: () =>
|
|
4061
|
+
text: () => {
|
|
4062
|
+
const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
|
|
4063
|
+
return [buildSkiffBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
|
|
4064
|
+
}
|
|
3907
4065
|
});
|
|
3908
4066
|
} catch (err) {
|
|
3909
4067
|
console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
|
|
@@ -12671,11 +12829,14 @@ function validateSkiffConfig(root) {
|
|
|
12671
12829
|
const hc = readCheapWorkerConfig(root);
|
|
12672
12830
|
const allowedModels = hc ? new Set(hc.models) : null;
|
|
12673
12831
|
for (const p of profiles) {
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12832
|
+
if (!(hasTrajectoryPrompt(root, p.dirName) || (() => {
|
|
12833
|
+
try {
|
|
12834
|
+
return resolveProfileSystemPrompt(root, p).trim() !== "";
|
|
12835
|
+
} catch (err) {
|
|
12836
|
+
issues.push(`trajectory "${p.code}": ${String(err?.message ?? err)}`);
|
|
12837
|
+
return true;
|
|
12838
|
+
}
|
|
12839
|
+
})())) issues.push(`trajectory "${p.code}": no trajectory prompt (add ${TRAJECTORY_PROMPT_FILENAME} in the trajectory dir — recommended — or the legacy systemPromptFile/systemPrompt)`);
|
|
12679
12840
|
for (const m of p.msms) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
|
|
12680
12841
|
if (p.model && allowedModels && !allowedModels.has(p.model)) issues.push(`trajectory "${p.code}": model "${p.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
|
|
12681
12842
|
}
|
|
@@ -15413,11 +15574,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15413
15574
|
sendJson$3(res, 400, { error: chk.error });
|
|
15414
15575
|
return;
|
|
15415
15576
|
}
|
|
15416
|
-
const { readWeixinSettings } = await import("./route-
|
|
15577
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15417
15578
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
|
|
15418
15579
|
const settings = readWeixinSettings(chk.root);
|
|
15419
15580
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15420
|
-
const { readWeixinCredential } = await import("./route-
|
|
15581
|
+
const { readWeixinCredential } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15421
15582
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
15422
15583
|
accountId: a.accountId,
|
|
15423
15584
|
name: a.name ?? void 0,
|
|
@@ -15449,7 +15610,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15449
15610
|
const { fetchQRCode } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
|
|
15450
15611
|
const { randomUUID } = await import("node:crypto");
|
|
15451
15612
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15452
|
-
const { readWeixinSettings } = await import("./route-
|
|
15613
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15453
15614
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15454
15615
|
const loginKey = randomUUID();
|
|
15455
15616
|
weixinLogins.set(loginKey, {
|
|
@@ -15470,7 +15631,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15470
15631
|
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15471
15632
|
return;
|
|
15472
15633
|
}
|
|
15473
|
-
const { removeWeixinAccount } = await import("./route-
|
|
15634
|
+
const { removeWeixinAccount } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15474
15635
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15475
15636
|
removeWeixinAccount(root, body.accountId);
|
|
15476
15637
|
syncCccBridge(ctx, root);
|
|
@@ -15496,7 +15657,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15496
15657
|
trajectory: code
|
|
15497
15658
|
});
|
|
15498
15659
|
}
|
|
15499
|
-
const { saveWeixinRoutes } = await import("./route-
|
|
15660
|
+
const { saveWeixinRoutes } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15500
15661
|
const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
|
|
15501
15662
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
15502
15663
|
sendJson$3(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
@@ -15508,7 +15669,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15508
15669
|
}
|
|
15509
15670
|
if (body.action === "set-enabled") {
|
|
15510
15671
|
const enabled = body.enabled === true;
|
|
15511
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./route-
|
|
15672
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15512
15673
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15513
15674
|
const settings = readWeixinSettings(root);
|
|
15514
15675
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -15552,7 +15713,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15552
15713
|
return;
|
|
15553
15714
|
}
|
|
15554
15715
|
const { pollQRStatus } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
|
|
15555
|
-
const { readWeixinSettings } = await import("./route-
|
|
15716
|
+
const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15556
15717
|
const settings = readWeixinSettings(login.root);
|
|
15557
15718
|
const status = await pollQRStatus({
|
|
15558
15719
|
baseUrl: void 0,
|
|
@@ -15567,7 +15728,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15567
15728
|
});
|
|
15568
15729
|
return;
|
|
15569
15730
|
}
|
|
15570
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-
|
|
15731
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-f7JlinIt.js").then((n) => n.s);
|
|
15571
15732
|
const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
|
|
15572
15733
|
const accountId = nextWeixinAccountId(settings);
|
|
15573
15734
|
upsertWeixinAccount(login.root, {
|
|
@@ -20721,6 +20882,105 @@ function invokeWeixinHook(root, hookRel, event) {
|
|
|
20721
20882
|
return activeRunner(root, hookRel, event);
|
|
20722
20883
|
}
|
|
20723
20884
|
//#endregion
|
|
20885
|
+
//#region src/im/accounts.ts
|
|
20886
|
+
const impls = /* @__PURE__ */ new Map();
|
|
20887
|
+
/** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
|
|
20888
|
+
function registerImAccountImpl(impl) {
|
|
20889
|
+
impls.set(impl.channel, impl);
|
|
20890
|
+
}
|
|
20891
|
+
/** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
|
|
20892
|
+
function getImAccountImpl(channel) {
|
|
20893
|
+
return impls.get(channel) ?? null;
|
|
20894
|
+
}
|
|
20895
|
+
/** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
|
|
20896
|
+
function imAccountChannels() {
|
|
20897
|
+
return [...impls.keys()];
|
|
20898
|
+
}
|
|
20899
|
+
/**
|
|
20900
|
+
* 账号行的渠道归一(**单一处**)。
|
|
20901
|
+
*
|
|
20902
|
+
* 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
|
|
20903
|
+
* 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
|
|
20904
|
+
*/
|
|
20905
|
+
function normalizeAccountChannel(channel) {
|
|
20906
|
+
const v = (channel ?? "").trim();
|
|
20907
|
+
return v === "" ? "weixin" : v;
|
|
20908
|
+
}
|
|
20909
|
+
//#endregion
|
|
20910
|
+
//#region src/im/weixin-account.ts
|
|
20911
|
+
/** 轮询间隔(getupdates 失败后重试;成功 = 立即续,长轮询本身 hold 35s) */
|
|
20912
|
+
const POLL_RETRY_MS = 3e3;
|
|
20913
|
+
/**
|
|
20914
|
+
* 微信的凭据就绪判据:**有 token** 才算绑定(baseUrl 留空 = 走官方默认)。
|
|
20915
|
+
* 与既有装配处逐字同义(原 `if (token === '') { …跳过… }`)。
|
|
20916
|
+
*/
|
|
20917
|
+
function weixinCredentialReady(cred) {
|
|
20918
|
+
return typeof cred.token === "string" && cred.token.trim() !== "";
|
|
20919
|
+
}
|
|
20920
|
+
let deps = null;
|
|
20921
|
+
/** 装配时注入依赖(`bridge.ts` 在模块初始化末尾调用一次;见 `installWeixinAccountImpl`) */
|
|
20922
|
+
function setWeixinLoopDeps(d) {
|
|
20923
|
+
deps = d;
|
|
20924
|
+
}
|
|
20925
|
+
/** 单账号轮询循环(**函数体逐字照搬自 bridge.ts 的 `runAccountLoop`,仅改类型与依赖来源**) */
|
|
20926
|
+
async function runWeixinAccountLoop(input, loop) {
|
|
20927
|
+
if (!deps) {
|
|
20928
|
+
loop.lastError = "weixin 实现未注入依赖(装配顺序错误)";
|
|
20929
|
+
return;
|
|
20930
|
+
}
|
|
20931
|
+
const d = deps;
|
|
20932
|
+
const ctx = input.ctx;
|
|
20933
|
+
const cred = input.cred;
|
|
20934
|
+
let buf = "";
|
|
20935
|
+
while (!loop.stopped) {
|
|
20936
|
+
loop.lastPollAt = Date.now();
|
|
20937
|
+
try {
|
|
20938
|
+
const resp = await d.getUpdates({
|
|
20939
|
+
baseUrl: cred.baseUrl,
|
|
20940
|
+
token: cred.token,
|
|
20941
|
+
getUpdatesBuf: buf,
|
|
20942
|
+
timeoutMs: 35e3
|
|
20943
|
+
});
|
|
20944
|
+
buf = resp.get_updates_buf ?? buf;
|
|
20945
|
+
for (const msg of resp.msgs ?? []) {
|
|
20946
|
+
if (loop.stopped) break;
|
|
20947
|
+
if (msg.message_type !== 1) continue;
|
|
20948
|
+
if (input.machine) await d.handleMachineIncoming(ctx, input.accountId, cred, msg);
|
|
20949
|
+
else await d.handleIncoming(ctx, input.root, input.accountId, cred, msg);
|
|
20950
|
+
}
|
|
20951
|
+
} catch (err) {
|
|
20952
|
+
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
20953
|
+
await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
|
|
20954
|
+
}
|
|
20955
|
+
}
|
|
20956
|
+
}
|
|
20957
|
+
/** 微信账号实现(登记的 channel 名 = `weixin`;与配置里的取值一致) */
|
|
20958
|
+
const weixinAccountImpl = {
|
|
20959
|
+
channel: "weixin",
|
|
20960
|
+
isReady: weixinCredentialReady,
|
|
20961
|
+
notReadyReason: () => "**无 token**(未绑定)",
|
|
20962
|
+
startLoop(input) {
|
|
20963
|
+
const loop = {
|
|
20964
|
+
stopped: false,
|
|
20965
|
+
lastPollAt: 0
|
|
20966
|
+
};
|
|
20967
|
+
runWeixinAccountLoop(input, loop);
|
|
20968
|
+
return loop;
|
|
20969
|
+
}
|
|
20970
|
+
};
|
|
20971
|
+
/**
|
|
20972
|
+
* 注册微信实现**并注入依赖**(`bridge.ts` 调用;幂等——重复注册覆盖同名 channel)。
|
|
20973
|
+
*
|
|
20974
|
+
* ⚠️ 必须在首次 `syncHumanChannel` / `syncCccBridge` **之前**发生。
|
|
20975
|
+
* `bridge.ts` 在**模块求值时**调用它一次(不是等到 `registerWeixinBridge`)——
|
|
20976
|
+
* 因为测试与面板会**直接**调 `syncCccBridge`,那时装配入口还没跑过;
|
|
20977
|
+
* 若依赖装配入口注册,那些路径会全部走进"未实现"分支(本轮实测:12 例红,根因就是它)。
|
|
20978
|
+
*/
|
|
20979
|
+
function installWeixinAccountImpl(d) {
|
|
20980
|
+
setWeixinLoopDeps(d);
|
|
20981
|
+
registerImAccountImpl(weixinAccountImpl);
|
|
20982
|
+
}
|
|
20983
|
+
//#endregion
|
|
20724
20984
|
//#region src/im/bridge.ts
|
|
20725
20985
|
var bridge_exports = /* @__PURE__ */ __exportAll({
|
|
20726
20986
|
handleIncoming: () => handleIncoming,
|
|
@@ -20747,8 +21007,6 @@ const bridges = /* @__PURE__ */ new Map();
|
|
|
20747
21007
|
const MACHINE_SCOPE = "<machine>";
|
|
20748
21008
|
/** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
|
|
20749
21009
|
const machineLoops = /* @__PURE__ */ new Map();
|
|
20750
|
-
/** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
|
|
20751
|
-
const POLL_RETRY_MS = 3e3;
|
|
20752
21010
|
/** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
|
|
20753
21011
|
const MEDIA_MAX_BYTES = 20971520;
|
|
20754
21012
|
/** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
|
|
@@ -20799,31 +21057,16 @@ async function sendTypingStop(cred, accountId, fromUserId) {
|
|
|
20799
21057
|
console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
20800
21058
|
}
|
|
20801
21059
|
}
|
|
20802
|
-
/**
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
timeoutMs: 35e3
|
|
20813
|
-
});
|
|
20814
|
-
buf = resp.get_updates_buf ?? buf;
|
|
20815
|
-
for (const msg of resp.msgs ?? []) {
|
|
20816
|
-
if (loop.stopped) break;
|
|
20817
|
-
if (msg.message_type !== 1) continue;
|
|
20818
|
-
if (machine) await handleMachineIncoming(ctx, accountId, cred, msg);
|
|
20819
|
-
else await handleIncoming(ctx, root, accountId, cred, msg);
|
|
20820
|
-
}
|
|
20821
|
-
} catch (err) {
|
|
20822
|
-
loop.lastError = err instanceof Error ? err.message : String(err);
|
|
20823
|
-
await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
|
|
20824
|
-
}
|
|
20825
|
-
}
|
|
20826
|
-
}
|
|
21060
|
+
/**
|
|
21061
|
+
* 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
|
|
21062
|
+
* `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
|
|
21063
|
+
*
|
|
21064
|
+
* 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
|
|
21065
|
+
* owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
|
|
21066
|
+
* 归**账号自己的实现**,装配处只按 `channel` 查派发表。
|
|
21067
|
+
* ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
|
|
21068
|
+
* `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
|
|
21069
|
+
*/
|
|
20827
21070
|
/**
|
|
20828
21071
|
* 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
|
|
20829
21072
|
* 要让 CCC 定义"重构)。
|
|
@@ -21225,10 +21468,23 @@ function syncCccBridge(ctx, root) {
|
|
|
21225
21468
|
if (!settings.enabled) return;
|
|
21226
21469
|
const requests = [];
|
|
21227
21470
|
const creds = /* @__PURE__ */ new Map();
|
|
21471
|
+
/** 账号 id → 它的实现(P2:按 `channel` 查派发表) */
|
|
21472
|
+
const implByAccount = /* @__PURE__ */ new Map();
|
|
21228
21473
|
for (const account of settings.accounts ?? []) {
|
|
21229
21474
|
if (account.enabled === false) continue;
|
|
21475
|
+
const channel = normalizeAccountChannel(account.channel);
|
|
21476
|
+
const impl = getImAccountImpl(channel);
|
|
21477
|
+
if (!impl) {
|
|
21478
|
+
if (!warnedChannels.has(`${account.accountId}:${channel}`)) {
|
|
21479
|
+
warnedChannels.add(`${account.accountId}:${channel}`);
|
|
21480
|
+
console.warn(`[serenity-hooks] ⚠ weixin-bridge: 账号 ${account.accountId}(ccc=${root})的渠道 "${channel}" **本机未实现** ⇒ 不起循环(已实现:${imAccountChannels().join(", ") || "(无)"})`);
|
|
21481
|
+
}
|
|
21482
|
+
continue;
|
|
21483
|
+
}
|
|
21230
21484
|
const cred = readWeixinCredential(root, account.accountId);
|
|
21231
21485
|
if (!cred) continue;
|
|
21486
|
+
if (!impl.isReady(cred)) continue;
|
|
21487
|
+
implByAccount.set(account.accountId, impl);
|
|
21232
21488
|
creds.set(account.accountId, cred);
|
|
21233
21489
|
requests.push({
|
|
21234
21490
|
root,
|
|
@@ -21244,13 +21500,16 @@ function syncCccBridge(ctx, root) {
|
|
|
21244
21500
|
};
|
|
21245
21501
|
for (const claim of plan.claims) {
|
|
21246
21502
|
const cred = creds.get(claim.accountId);
|
|
21247
|
-
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
|
|
21503
|
+
const impl = implByAccount.get(claim.accountId);
|
|
21504
|
+
if (!cred || !impl) continue;
|
|
21505
|
+
const loop = impl.startLoop({
|
|
21506
|
+
ctx,
|
|
21507
|
+
root,
|
|
21508
|
+
accountId: claim.accountId,
|
|
21509
|
+
cred,
|
|
21510
|
+
machine: false
|
|
21511
|
+
});
|
|
21252
21512
|
bridge.loops.set(claim.accountId, loop);
|
|
21253
|
-
runAccountLoop(ctx, root, claim.accountId, cred, loop);
|
|
21254
21513
|
}
|
|
21255
21514
|
commitPollerClaims(plan);
|
|
21256
21515
|
if (bridge.loops.size > 0) {
|
|
@@ -21301,6 +21560,8 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21301
21560
|
stopCccBridges();
|
|
21302
21561
|
const requests = [];
|
|
21303
21562
|
const creds = /* @__PURE__ */ new Map();
|
|
21563
|
+
/** 账号 id → 它的实现(P2:**按 `channel` 查派发表**,不再写死 weixin) */
|
|
21564
|
+
const implByAccount = /* @__PURE__ */ new Map();
|
|
21304
21565
|
for (const account of hc.accounts) {
|
|
21305
21566
|
const id = account.id;
|
|
21306
21567
|
if (account.enabled === false) continue;
|
|
@@ -21308,23 +21569,27 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21308
21569
|
console.warn("[serenity-hooks] ⚠ human-channel: 机器级账号表有一行**缺 `id`** ⇒ 跳过(该行无法被订阅表引用)");
|
|
21309
21570
|
continue;
|
|
21310
21571
|
}
|
|
21311
|
-
|
|
21312
|
-
|
|
21313
|
-
|
|
21314
|
-
|
|
21572
|
+
const channel = normalizeAccountChannel(account.channel);
|
|
21573
|
+
const impl = getImAccountImpl(channel);
|
|
21574
|
+
if (!impl) {
|
|
21575
|
+
if (!warnedChannels.has(`${id}:${channel}`)) {
|
|
21576
|
+
warnedChannels.add(`${id}:${channel}`);
|
|
21577
|
+
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} 的渠道 "${account.channel ?? "(未填)"}" **本机未实现** ⇒ 不起 poller(已实现:${imAccountChannels().join(", ") || "(无)"})`);
|
|
21315
21578
|
}
|
|
21316
21579
|
continue;
|
|
21317
21580
|
}
|
|
21318
21581
|
const token = (account.token ?? "").trim();
|
|
21319
|
-
|
|
21320
|
-
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} **无 token**(未绑定)⇒ 不起 poller`);
|
|
21321
|
-
continue;
|
|
21322
|
-
}
|
|
21323
|
-
creds.set(id, {
|
|
21582
|
+
const cred = {
|
|
21324
21583
|
token,
|
|
21325
21584
|
baseUrl: "",
|
|
21326
21585
|
userId: account.userId
|
|
21327
|
-
}
|
|
21586
|
+
};
|
|
21587
|
+
if (!impl.isReady(cred)) {
|
|
21588
|
+
console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
|
|
21589
|
+
continue;
|
|
21590
|
+
}
|
|
21591
|
+
implByAccount.set(id, impl);
|
|
21592
|
+
creds.set(id, cred);
|
|
21328
21593
|
requests.push({
|
|
21329
21594
|
root: MACHINE_SCOPE,
|
|
21330
21595
|
accountId: id,
|
|
@@ -21336,13 +21601,16 @@ function syncMachinePollers(ctx, hc) {
|
|
|
21336
21601
|
stopMachineLoops();
|
|
21337
21602
|
for (const claim of plan.claims) {
|
|
21338
21603
|
const cred = creds.get(claim.accountId);
|
|
21339
|
-
|
|
21340
|
-
|
|
21341
|
-
|
|
21342
|
-
|
|
21343
|
-
|
|
21604
|
+
const impl = implByAccount.get(claim.accountId);
|
|
21605
|
+
if (!cred || !impl) continue;
|
|
21606
|
+
const loop = impl.startLoop({
|
|
21607
|
+
ctx,
|
|
21608
|
+
root: MACHINE_SCOPE,
|
|
21609
|
+
accountId: claim.accountId,
|
|
21610
|
+
cred,
|
|
21611
|
+
machine: true
|
|
21612
|
+
});
|
|
21344
21613
|
machineLoops.set(claim.accountId, loop);
|
|
21345
|
-
runAccountLoop(ctx, MACHINE_SCOPE, claim.accountId, cred, loop, true);
|
|
21346
21614
|
}
|
|
21347
21615
|
commitPollerClaims(plan);
|
|
21348
21616
|
if (machineLoops.size > 0) console.log(`[serenity-hooks] ✓ human-channel: 机器级 poller accounts=${[...machineLoops.keys()].join(",")}(与 CCC 无关)`);
|
|
@@ -21494,6 +21762,26 @@ function registerWeixinBridge(ctx) {
|
|
|
21494
21762
|
ctx.on("session/created", () => syncHumanChannel(ctx));
|
|
21495
21763
|
} catch {}
|
|
21496
21764
|
}
|
|
21765
|
+
/**
|
|
21766
|
+
* 🔴 **P2 装配:微信实现登记进"账号实现派发表"**(模块求值时执行,不是等 `apply`)。
|
|
21767
|
+
*
|
|
21768
|
+
* ## 为什么必须在模块求值时做(本轮实测教训)
|
|
21769
|
+
*
|
|
21770
|
+
* 首版把这句放在 `registerWeixinBridge` 里 ⇒ 全量测试 **12 例红**,报的全是
|
|
21771
|
+
* 「`weixinBridgeStatus()` 空数组」。根因:测试与设置面板会**直接**调 `syncCccBridge` /
|
|
21772
|
+
* `syncHumanChannel`,那时装配入口还没跑过 ⇒ 派发表是空的 ⇒ 每条账号都走进
|
|
21773
|
+
* 「渠道未实现」分支(那分支是给**真正**未实现的渠道用的)。
|
|
21774
|
+
*
|
|
21775
|
+
* ⇒ 判据:**"谁能被调"决定"注册要在哪儿发生"**,不能按"装配流程"想当然。
|
|
21776
|
+
*
|
|
21777
|
+
* 注入的是本模块自己的函数(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)——
|
|
21778
|
+
* 不需要 `weixin-account.ts import 回 bridge.ts`,避免两个文件互相 import 成环。
|
|
21779
|
+
*/
|
|
21780
|
+
installWeixinAccountImpl({
|
|
21781
|
+
getUpdates,
|
|
21782
|
+
handleIncoming,
|
|
21783
|
+
handleMachineIncoming
|
|
21784
|
+
});
|
|
21497
21785
|
//#endregion
|
|
21498
21786
|
//#region src/im/send.ts
|
|
21499
21787
|
/** 请求体上限(文本消息远小于此;防滥用) */
|
package/lib/kernel/ccc.d.ts
CHANGED
|
@@ -136,6 +136,12 @@ interface SerenityConfig {
|
|
|
136
136
|
* 未配置或 enabled=false → 该 CCC 桥完全不启动(零资源占用)。
|
|
137
137
|
*/
|
|
138
138
|
weixin?: WeixinSettings;
|
|
139
|
+
/**
|
|
140
|
+
* 🔴 **统一 IM 配置(2026-10-19,owner 定向)**:微信与飞书**只是账号实现不同**,
|
|
141
|
+
* 配置整合到这一段。存在且 `accounts` 是数组 ⇒ 优先于旧 `weixin` 段。
|
|
142
|
+
* 见 `ImSettings` 的注释(含"为什么判据不是'段存在'"的坑)。
|
|
143
|
+
*/
|
|
144
|
+
im?: ImSettings;
|
|
139
145
|
/**
|
|
140
146
|
* 专属工具声明(v1.33,S142 §32.11):**ACC 注册但默认对所有 CCC 隐藏**的工具,
|
|
141
147
|
* 只有在自己这里声明的 CCC 可见(例:`["acc-diag"]` = ACC 负责人的运行态诊断)。
|
|
@@ -145,6 +151,73 @@ interface SerenityConfig {
|
|
|
145
151
|
*/
|
|
146
152
|
exclusiveTools?: string[];
|
|
147
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* 🔴 **统一 IM 配置(2026-10-19 owner 定向:「飞书和微信一样,只不过是 account 方实现不同。
|
|
156
|
+
* 要整合到统一的配置中」)。**
|
|
157
|
+
*
|
|
158
|
+
* ## 为什么是"账号"这一层,不是"通道"这一层
|
|
159
|
+
*
|
|
160
|
+
* 微信与飞书的差别**只在账号侧**:怎么连、怎么收、怎么发。而它们的**身份、路由、会话**
|
|
161
|
+
* 从来就是通道无关的(`routes: user → 轨迹编码`;会话 id 由轨迹编码派生)。
|
|
162
|
+
* ⇒ 抽象点是 `account.channel`(这个字段本来就有,装配处也早写着"等第二个实现"),
|
|
163
|
+
* 不是新造一个平行的"通道接口"。
|
|
164
|
+
*
|
|
165
|
+
* ## 与旧 `weixin` 段的关系(向后兼容是硬要求)
|
|
166
|
+
*
|
|
167
|
+
* - `im` 段**存在且 `accounts` 是数组** ⇒ 用它;
|
|
168
|
+
* - 否则 ⇒ **回退读旧 `weixin` 段**(存量 CCC 零迁移,行为逐字不变)。
|
|
169
|
+
* - 旧段**不删不改**:回滚 = 删掉 `im` 段即可。
|
|
170
|
+
*
|
|
171
|
+
* 🔴 **为什么判据是「`accounts` 是数组」而不是「有没有 im 段」**:
|
|
172
|
+
* 本 CCC 的配置里**曾经**有过一个 `im` 段(退役的 ACP:`{channel:"acp", acp:{…}}`)。
|
|
173
|
+
* 若只判"段存在",那份残留会被当成新配置读 ⇒ **读到一个没有 accounts 的对象** ⇒
|
|
174
|
+
* 表现为"配置没生效"且**不报错**。⇒ 形状判据必须能区分这两种 `im`。
|
|
175
|
+
*/
|
|
176
|
+
export interface ImSettings {
|
|
177
|
+
/** 总开关(缺省 false——未开零资源占用) */
|
|
178
|
+
enabled?: boolean;
|
|
179
|
+
/** 账号表:**每条账号自带 `channel`**(`weixin` / `feishu`),凭据在 localstore */
|
|
180
|
+
accounts?: ImAccountConfig[];
|
|
181
|
+
/** 消息路由:**通道用户 id → 认知轨迹编码**(通道无关;`*` 通配兜底) */
|
|
182
|
+
routes?: ImRouteConfig[];
|
|
183
|
+
/** 用户别名表:`{ "<别名>": "<localstore 凭据键>" }` */
|
|
184
|
+
aliases?: Record<string, string>;
|
|
185
|
+
/** 消息记录 hook(相对 CCC 根的脚本路径) */
|
|
186
|
+
hook?: string;
|
|
187
|
+
/** 桥是否自动回发 agent 的最终文本(缺省 true;微信侧语义见旧注释) */
|
|
188
|
+
autoReplyWithLastMessage?: boolean;
|
|
189
|
+
/** 手动输出模式的兜底(缺省 false = 严格静默) */
|
|
190
|
+
fallbackOnNoSend?: boolean;
|
|
191
|
+
/** iLink 专属:bot_type 取码参数(飞书账号忽略它) */
|
|
192
|
+
botType?: string;
|
|
193
|
+
}
|
|
194
|
+
/** 统一账号配置:**`channel` 决定用哪个实现**;其余字段各实现自查 */
|
|
195
|
+
export interface ImAccountConfig {
|
|
196
|
+
/** 本地唯一键(如 wechat-1 / feishu-1) */
|
|
197
|
+
accountId: string;
|
|
198
|
+
/**
|
|
199
|
+
* 账号实现(缺省 `weixin`——旧配置平移到 `im` 段时不必补这个字段)。
|
|
200
|
+
* 未实现的 channel ⇒ **出声但不静默跳过**(装配处已有该分支)。
|
|
201
|
+
*/
|
|
202
|
+
channel?: string;
|
|
203
|
+
/** 展示名(可选) */
|
|
204
|
+
name?: string;
|
|
205
|
+
/** 该账号开关(缺省 true) */
|
|
206
|
+
enabled?: boolean;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 统一路由配置:**通道用户 id → 认知轨迹编码**。
|
|
210
|
+
*
|
|
211
|
+
* 与旧 `WeixinRouteConfig` 语义完全相同(那是它的超集形态);保留别名仅为读侧兼容。
|
|
212
|
+
*/
|
|
213
|
+
export interface ImRouteConfig {
|
|
214
|
+
/** 通道用户 id(微信 `xxx@im.wechat` / 飞书 `open_id`);`*` = 通配兜底 */
|
|
215
|
+
user: string;
|
|
216
|
+
/** 目标认知轨迹编码 */
|
|
217
|
+
trajectory: string;
|
|
218
|
+
/** @deprecated 过渡别名(语义等同 `trajectory`) */
|
|
219
|
+
role?: string;
|
|
220
|
+
}
|
|
148
221
|
/**
|
|
149
222
|
* 微信桥配置(CCC 级;S142 用户拍板):
|
|
150
223
|
* - 结构/路由/开关在 .opencode/serenity.json(git 管可重建)
|
|
@@ -152,6 +225,9 @@ interface SerenityConfig {
|
|
|
152
225
|
* 扫码登录后插件自动写入(面板只显示"已绑定"),永不进 git 明文面
|
|
153
226
|
* - 路由 user → role:exact 匹配优先,通配 `*` 兜底;role 必须是该 CCC skiff.roles 之一
|
|
154
227
|
* - 多账号:accounts[] 每项 { accountId, name?, enabled }(本地唯一键,如 wechat-1)
|
|
228
|
+
*
|
|
229
|
+
* 🔴 2026-10-19:**本段已被统一 `im` 段取代**(见上方 `ImSettings`)。此处保留是因为
|
|
230
|
+
* 存量 CCC 的配置里写的就是它——读侧继续认,**不强制迁移**。
|
|
155
231
|
*/
|
|
156
232
|
export interface WeixinSettings {
|
|
157
233
|
/** 总开关(缺省 false——默认关,未开零资源占用) */
|
|
@@ -302,6 +302,7 @@ var route_exports = /* @__PURE__ */ __exportAll({
|
|
|
302
302
|
hasVoiceItem: () => hasVoiceItem,
|
|
303
303
|
matchWeixinRoute: () => matchWeixinRoute,
|
|
304
304
|
nextWeixinAccountId: () => nextWeixinAccountId,
|
|
305
|
+
readImSettings: () => readImSettings,
|
|
305
306
|
readWeixinCredential: () => readWeixinCredential,
|
|
306
307
|
readWeixinSettings: () => readWeixinSettings,
|
|
307
308
|
removeWeixinAccount: () => removeWeixinAccount,
|
|
@@ -367,43 +368,88 @@ function routeTarget(row) {
|
|
|
367
368
|
return "";
|
|
368
369
|
}
|
|
369
370
|
/**
|
|
370
|
-
*
|
|
371
|
-
*
|
|
371
|
+
* **统一 IM 配置的单一读取入口**(2026-10-19 owner 定向:微信与飞书只是账号实现不同,
|
|
372
|
+
* 整合到统一配置)。
|
|
373
|
+
*
|
|
374
|
+
* 读取顺序(**确定性,只有这一处**):
|
|
375
|
+
* ① `im` 段 —— 但**判据是 `accounts` 是数组**,不是"段存在"(见下 🔴)
|
|
376
|
+
* ② 旧 `weixin` 段 —— 存量 CCC 零迁移
|
|
377
|
+
* ③ 都没有 ⇒ 归一默认(`enabled:false`、空数组)
|
|
378
|
+
*
|
|
379
|
+
* 🔴 **为什么判据是「`accounts` 是数组」**:本 CCC 的配置里**曾经**有过一个 `im` 段
|
|
380
|
+
* (退役的 ACP 形态:`{channel:"acp", acp:{enabled, accounts:[…]}}`)。若只判"段存在",
|
|
381
|
+
* 那份残留会被当成新配置读 ⇒ 得到一个没有 `accounts` 数组 / 或 `accounts` 形状完全不同的对象
|
|
382
|
+
* ⇒ 表现为**"配置没生效"且不报错**。⇒ 用可判别的形状把它排除掉(退役残留继续被静默忽略,
|
|
383
|
+
* 与 v1.59.0 的裁决一致)。
|
|
384
|
+
*
|
|
385
|
+
* ⚠️ 本函数**只做归一**,不判断"这个通道实现有没有"——那是装配处的事
|
|
386
|
+
* (`syncMachinePollers` 里有"未实现的 channel 出声但不静默跳过")。
|
|
372
387
|
*/
|
|
373
|
-
function
|
|
388
|
+
function readImSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
389
|
+
const empty = () => ({
|
|
390
|
+
enabled: false,
|
|
391
|
+
accounts: [],
|
|
392
|
+
routes: []
|
|
393
|
+
});
|
|
374
394
|
try {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
enabled: w.enabled === true,
|
|
383
|
-
botType: typeof w.botType === "string" && w.botType !== "" ? w.botType : void 0,
|
|
384
|
-
hook: typeof w.hook === "string" && w.hook.trim() !== "" ? w.hook.trim() : void 0,
|
|
385
|
-
autoReplyWithLastMessage: w.autoReplyWithLastMessage !== false,
|
|
386
|
-
fallbackOnNoSend: w.fallbackOnNoSend === true,
|
|
387
|
-
accounts: Array.isArray(w.accounts) ? w.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.accountId === "string" && a.accountId !== "").map((a) => ({
|
|
388
|
-
accountId: a.accountId,
|
|
389
|
-
name: typeof a.name === "string" ? a.name : void 0,
|
|
390
|
-
enabled: a.enabled !== false
|
|
391
|
-
})) : [],
|
|
392
|
-
routes: Array.isArray(w.routes) ? w.routes.filter((r) => typeof r === "object" && r !== null && typeof r.user === "string" && r.user !== "" && typeof routeTarget(r) === "string").map((r) => ({
|
|
393
|
-
user: r.user,
|
|
394
|
-
trajectory: routeTarget(r)
|
|
395
|
-
})) : [],
|
|
396
|
-
aliases: readAliases(w)
|
|
397
|
-
};
|
|
395
|
+
const cfg = loadSerenityConfig(root, paths);
|
|
396
|
+
const unified = cfg.im;
|
|
397
|
+
if (isUnifiedImSection(unified)) return normalizeImSection(unified, empty());
|
|
398
|
+
const legacy = cfg.weixin;
|
|
399
|
+
if (!legacy || typeof legacy !== "object") return empty();
|
|
400
|
+
return normalizeImSection(legacy, empty());
|
|
398
401
|
} catch {
|
|
399
|
-
return
|
|
400
|
-
enabled: false,
|
|
401
|
-
accounts: [],
|
|
402
|
-
routes: []
|
|
403
|
-
};
|
|
402
|
+
return empty();
|
|
404
403
|
}
|
|
405
404
|
}
|
|
406
405
|
/**
|
|
406
|
+
* 判别一个 `im` 段是不是**新的统一形态**。
|
|
407
|
+
*
|
|
408
|
+
* 判据 = `accounts` 是数组 **且** 每行(若有)不带退役 ACP 的 `accountId` 之外的 ACP 形状;
|
|
409
|
+
* 退役 ACP 形态的特征是顶层有 `channel:"acp"` 或 `acp` 对象。
|
|
410
|
+
* ⇒ 两者**任一命中**即判为"退役残留",不认。
|
|
411
|
+
*/
|
|
412
|
+
function isUnifiedImSection(v) {
|
|
413
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
414
|
+
const obj = v;
|
|
415
|
+
if (obj.acp !== void 0 && (typeof obj.acp === "object" || typeof obj.acp === "string")) return false;
|
|
416
|
+
if (obj.channel === "acp") return false;
|
|
417
|
+
return Array.isArray(obj.accounts);
|
|
418
|
+
}
|
|
419
|
+
/** 段 → 归一形态(统一段与旧段**共用同一套归一语义**,避免两条读法各漂) */
|
|
420
|
+
function normalizeImSection(w, d) {
|
|
421
|
+
const src = w;
|
|
422
|
+
const accountsIn = Array.isArray(src.accounts) ? src.accounts : [];
|
|
423
|
+
return {
|
|
424
|
+
enabled: src.enabled === true,
|
|
425
|
+
botType: typeof src.botType === "string" && src.botType !== "" ? src.botType : void 0,
|
|
426
|
+
hook: typeof src.hook === "string" && src.hook.trim() !== "" ? src.hook.trim() : void 0,
|
|
427
|
+
autoReplyWithLastMessage: src.autoReplyWithLastMessage !== false,
|
|
428
|
+
fallbackOnNoSend: src.fallbackOnNoSend === true,
|
|
429
|
+
accounts: accountsIn.filter((a) => typeof a === "object" && a !== null && typeof a.accountId === "string" && a.accountId !== "").map((a) => ({
|
|
430
|
+
accountId: a.accountId,
|
|
431
|
+
channel: typeof a.channel === "string" && a.channel.trim() !== "" ? a.channel.trim() : "weixin",
|
|
432
|
+
name: typeof a.name === "string" ? a.name : void 0,
|
|
433
|
+
enabled: a.enabled !== false
|
|
434
|
+
})),
|
|
435
|
+
routes: Array.isArray(src.routes) ? src.routes.filter((r) => typeof r === "object" && r !== null && typeof r.user === "string" && r.user !== "" && typeof routeTarget(r) === "string").map((r) => ({
|
|
436
|
+
user: r.user,
|
|
437
|
+
trajectory: routeTarget(r)
|
|
438
|
+
})) : [],
|
|
439
|
+
aliases: readAliases(src)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 读取 CCC 微信桥配置(未配置/未启用 → 归一默认)。
|
|
444
|
+
*
|
|
445
|
+
* ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
|
|
446
|
+
* 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
|
|
447
|
+
* 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
|
|
448
|
+
*/
|
|
449
|
+
function readWeixinSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
450
|
+
return readImSettings(root, paths);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
407
453
|
* 别名表清洗(v1.56.10):键归一为小写、值必须是非空字符串;非法项静默丢弃。
|
|
408
454
|
* 空/缺省 → `{}`(此时别名解析不可用,只能用裸 iLink 用户 id)。
|
|
409
455
|
*/
|
|
@@ -109,7 +109,9 @@ export declare function sessionBlock(root: string, scope?: string): string;
|
|
|
109
109
|
* Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
|
|
110
110
|
* 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
|
|
111
111
|
*/
|
|
112
|
-
export declare function serenitySystemPrompt(root: string, scope?: string
|
|
112
|
+
export declare function serenitySystemPrompt(root: string, scope?: string,
|
|
113
|
+
/** 活跃轨迹目录名(给了才有第 ⑧ 段;未绑定轨迹 ⇒ undefined,不注入) */
|
|
114
|
+
dirName?: string): string;
|
|
113
115
|
/**
|
|
114
116
|
* Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
|
|
115
117
|
* ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
|
|
3
|
+
*
|
|
4
|
+
* ## 概念(owner 2026-10-19 定向,逐字)
|
|
5
|
+
*
|
|
6
|
+
* > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
|
|
7
|
+
*
|
|
8
|
+
* **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
|
|
9
|
+
* 三个关键词各是一条判据:
|
|
10
|
+
* - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
|
|
11
|
+
* - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `skiff.json` 白名单与工具 schema)
|
|
12
|
+
* - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
|
|
13
|
+
*
|
|
14
|
+
* ## 为什么取代「人格」
|
|
15
|
+
*
|
|
16
|
+
* 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
|
|
17
|
+
* (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
|
|
18
|
+
* 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
|
|
19
|
+
*
|
|
20
|
+
* ## 落点(为什么在轨迹目录里)
|
|
21
|
+
*
|
|
22
|
+
* `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
|
|
23
|
+
*
|
|
24
|
+
* - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
|
|
25
|
+
* 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
|
|
26
|
+
* - **每条轨迹都能有**:不只是 skiff。普通轨迹同样需要"我是谁、按什么章程"。
|
|
27
|
+
* - 与 `SESSION.md`(身体:延续/日志)、`skiff.json`(约束面:能力)三件并列,
|
|
28
|
+
* 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
|
|
29
|
+
*
|
|
30
|
+
* ## 兼容(不静默坏)
|
|
31
|
+
*
|
|
32
|
+
* `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。skiff 的旧字段 `systemPromptFile`
|
|
33
|
+
* 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
|
|
34
|
+
*/
|
|
35
|
+
import type { SkiffProfile } from '../role/skiff/profile.js';
|
|
36
|
+
/** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
|
|
37
|
+
export declare const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
|
|
38
|
+
/** 一次解析的结果(`source` 说明正文**从哪来**,便于诊断与「为什么我改的不生效」) */
|
|
39
|
+
export interface TrajectoryPromptResolution {
|
|
40
|
+
/** 提示词正文(已 trim);无来源 ⇒ 空串 */
|
|
41
|
+
text: string;
|
|
42
|
+
/** 来源:轨迹目录的 PROMPT.md / skiff 旧字段 / 无 */
|
|
43
|
+
source: 'trajectory-file' | 'skiff-legacy' | 'none';
|
|
44
|
+
/** 实际读取的绝对路径(`none` ⇒ null) */
|
|
45
|
+
path: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
|
|
48
|
+
export declare function trajectoryPromptPath(root: string, dirName: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* 该轨迹**有没有**轨迹提示词(单一判据)。
|
|
51
|
+
*
|
|
52
|
+
* 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,skiff 旧字段 `systemPromptFile`
|
|
53
|
+
* 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-skiff` 段)。
|
|
54
|
+
* 🔴 判据只有这一处:别在 skiff 那边自己拼路径再判(两处会漂)。
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasTrajectoryPrompt(root: string, dirName: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* 解析一条轨迹的**轨迹提示词**(一次性读;`create` / 诊断 / 预览用)。
|
|
59
|
+
*
|
|
60
|
+
* 🔴 热路径**不要**用它(那会每次工具调用都读盘)——热路径用 {@link createTrajectoryPromptReader}。
|
|
61
|
+
*
|
|
62
|
+
* 优先级(**确定性,只有这一条**):
|
|
63
|
+
* ① `<轨迹目录>/PROMPT.md` —— 新形态,**所有轨迹**都能有
|
|
64
|
+
* ② skiff 的 `systemPromptFile` —— **兼容期**旧形态(存量 CCC 不必同批迁移)
|
|
65
|
+
* ③ 无 ⇒ 空串(**不是错误**:没有章程的轨迹是常态)
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ 读失败(权限/坏文件)⇒ 空串而不是抛错:提示词读不到不该阻断会话创建;
|
|
68
|
+
* 但**会告警**(见 reader 版本),不静默。
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveTrajectoryPrompt(root: string, dirName: string, legacyProfile?: SkiffProfile | null): TrajectoryPromptResolution;
|
|
71
|
+
/**
|
|
72
|
+
* 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
|
|
73
|
+
*
|
|
74
|
+
* ## 为什么返回函数而不是字符串(R↓,实证代价)
|
|
75
|
+
*
|
|
76
|
+
* 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
|
|
77
|
+
* (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
|
|
78
|
+
* **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
|
|
79
|
+
*
|
|
80
|
+
* ## 三条失败语义(各治一个失败模式)
|
|
81
|
+
*
|
|
82
|
+
* - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
|
|
83
|
+
* 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
|
|
84
|
+
* - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
|
|
85
|
+
* 并按去重标签告警一次
|
|
86
|
+
* - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
|
|
87
|
+
*
|
|
88
|
+
* @param root CCC 根
|
|
89
|
+
* @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
|
|
90
|
+
*/
|
|
91
|
+
export declare function createTrajectoryPromptReader(root: string, dirName: string): () => string;
|
|
92
|
+
/**
|
|
93
|
+
* 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
|
|
94
|
+
*
|
|
95
|
+
* 标记头 `=== Serenity Trajectory Prompt ===`:
|
|
96
|
+
* ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
|
|
97
|
+
* ② 让诊断可以按块名 grep。
|
|
98
|
+
*
|
|
99
|
+
* 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
|
|
100
|
+
*/
|
|
101
|
+
export declare function trajectoryPromptBlock(text: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/认知轨迹唤醒注册表。skiff = 目录下有 skiff.json
|
|
3
|
+
"version": "1.72.0",
|
|
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 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/微信桥/认知轨迹唤醒注册表。skiff = 目录下有 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": {
|
|
7
7
|
"type": "git",
|