@shgroup/dsh-serenity-hooks 1.59.0 → 1.59.2
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/acp-auth.d.ts +156 -39
- package/lib/acp-face.d.ts +82 -3
- package/lib/ccc.d.ts +28 -5
- package/lib/client.js +305 -5
- package/lib/index.js +633 -69
- package/lib/localstore-ops-DMs_ovHo.js +282 -0
- package/lib/weixin-route-Aheyq3fN.js +281 -0
- package/package.json +1 -1
- package/lib/weixin-route-CSDBGLwl.js +0 -558
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.59.
|
|
3
|
+
"version": "1.59.2",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1 与 0.2.0-rc.1(双档,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/acp-auth.d.ts
CHANGED
|
@@ -34,25 +34,32 @@
|
|
|
34
34
|
*
|
|
35
35
|
* 本模块**零 DSH 依赖**(只 `node:crypto` / `node:fs` / `node:path` 与本仓纯逻辑)⇒ 可独立单测。
|
|
36
36
|
*/
|
|
37
|
-
import { type ImSettings, type ImAcpSettings } from './ccc.js';
|
|
38
|
-
/**
|
|
39
|
-
export declare
|
|
37
|
+
import { type ImSettings, type ImAcpSettings, type ImAcpAccount } from './ccc.js';
|
|
38
|
+
/** 该账户的凭据键(`IM_ACP_TOKEN_<ACCOUNT_ID>`);`accountId` 必填 */
|
|
39
|
+
export declare function acpTokenKeyFor(accountId: string): string;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* 该**账户**的 ACP token(**读**;不存在返回 null)。
|
|
42
42
|
*
|
|
43
|
-
* 🔴 **不自动生成** ——
|
|
44
|
-
*
|
|
43
|
+
* 🔴 **不自动生成** —— 生成是**写操作**,只在显式要求时做(`ensureAcpToken`)。
|
|
44
|
+
* 读路径永远无副作用:否则每次判定都会改 localstore。
|
|
45
|
+
*
|
|
46
|
+
* @param accountId 账户键(必填 —— 凭据按账户分离,没有"CCC 默认 token"这回事)
|
|
45
47
|
*/
|
|
46
|
-
export declare function readAcpToken(root: string): string | null;
|
|
48
|
+
export declare function readAcpToken(root: string, accountId: string): string | null;
|
|
47
49
|
/**
|
|
48
50
|
* 生成(或返回既有)ACP token 并落该 CCC 的 localstore。
|
|
49
51
|
*
|
|
50
52
|
* ⚠️ **幂等**:已有 token 时**原样返回**(不轮换)—— 轮换会让在用的客户端立刻断线,
|
|
51
53
|
* 必须是一次显式动作(`resetAcpToken`)。
|
|
54
|
+
*
|
|
55
|
+
* @param accountId 多账户时指明哪个账户;省略 = 单账户简写键
|
|
56
|
+
*/
|
|
57
|
+
export declare function ensureAcpToken(root: string, accountId: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* 轮换 token(显式动作;返回新值)。旧 token 立即失效。
|
|
60
|
+
* @param accountId 账户键
|
|
52
61
|
*/
|
|
53
|
-
export declare function
|
|
54
|
-
/** 轮换 token(显式动作;返回新值)。旧 token 立即失效。 */
|
|
55
|
-
export declare function resetAcpToken(root: string): string;
|
|
62
|
+
export declare function resetAcpToken(root: string, accountId: string): string;
|
|
56
63
|
/**
|
|
57
64
|
* 常时比较(防时序侧信道)。
|
|
58
65
|
*
|
|
@@ -60,15 +67,36 @@ export declare function resetAcpToken(root: string): string;
|
|
|
60
67
|
* ⚠️ 那一步本身**泄漏长度**(可接受:token 长度固定且公开)。
|
|
61
68
|
*/
|
|
62
69
|
export declare function safeEqual(a: string, b: string): boolean;
|
|
63
|
-
/** 一枚 token 的归属(用于"谁拥有这个 token"的反查与排重) */
|
|
64
|
-
interface TokenOwner {
|
|
65
|
-
root: string;
|
|
66
|
-
token: string;
|
|
67
|
-
/** token 的 SHA-256(**日志与诊断里只出现它**,绝不出现 token 明文) */
|
|
68
|
-
fingerprint: string;
|
|
69
|
-
}
|
|
70
70
|
/** token 指纹(诊断面用;不可逆) */
|
|
71
71
|
export declare function tokenFingerprint(token: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* 该账户的**主会话**键(`IM_ACP_SESSION_<ACCOUNT_ID>`)。
|
|
74
|
+
*
|
|
75
|
+
* 🔴 为什么放在 **credential** 节而不是 config 节:它跟 token 一样是**每账户一值的键控数据**
|
|
76
|
+
* (键由 `accountId` 派生、随账户生命周期存亡)。放 config 节要另造一层 `section.key`
|
|
77
|
+
* 结构,而这一层的唯一读者就是本模块 ⇒ 多一层结构 = 多一处会漂的形状。
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 它**不是凭据**(会话 id 不是秘密)—— 共享 credential 节只是为了复用它既有的
|
|
80
|
+
* 「按 accountId 派生键 + 删账户时一并清理」这套语义,不引入第二套键规则。
|
|
81
|
+
*/
|
|
82
|
+
export declare function acpSessionKeyFor(accountId: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* 读该账户的主会话 id(不存在 ⇒ `null`)。
|
|
85
|
+
*
|
|
86
|
+
* 🔴 **读路径无副作用**(与 `readAcpToken` 同纪律):不存在的含义是「这个账户还没有主会话」,
|
|
87
|
+
* 而那时的正确行为是**让 App 去建**(`session/list` 回空数组),不是替它编一个。
|
|
88
|
+
*/
|
|
89
|
+
export declare function readAcpMainSession(root: string, accountId: string): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* 记下该账户的主会话 id(**写操作**;由 `session/new` 的出站响应触发)。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **后写覆盖先写**(last-wins):owner 要的是"一条长期对话",而"再建一条"是用户
|
|
94
|
+
* 在 App 里显式点「新对话」的结果 ⇒ 那时他要的就是**新的那条**。
|
|
95
|
+
* ⛔ **被否的两种做法**:① 已有主会话就拒建(用户会看到"点了没反应");
|
|
96
|
+
* ② 把 `session/new` 改写成 `session/resume`(响应形状不同,要让客户端认一条
|
|
97
|
+
* **不存在于协议里**的响应 —— 那是拿协议兼容性换一个我们本来就有的语义)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeAcpMainSession(root: string, accountId: string, sessionId: string): void;
|
|
72
100
|
/**
|
|
73
101
|
* 从一次 HTTP upgrade 请求里取出候选 token。
|
|
74
102
|
*
|
|
@@ -80,17 +108,62 @@ export declare function tokenFingerprint(token: string): string;
|
|
|
80
108
|
*/
|
|
81
109
|
export declare function extractToken(headers: Record<string, unknown>, url: string | undefined): string | null;
|
|
82
110
|
/**
|
|
83
|
-
*
|
|
111
|
+
* 一个"可接受 token 的账户"——**认证闸真正比对的单元**。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 v1.59.1 的关键抽象变更:原先是"CCC ↔ 一枚 token",现在是
|
|
114
|
+
* **"(CCC, 账户)↔ 一枚 token ↔ 一个角色"**。同一个 CCC 可以有多个账户
|
|
115
|
+
* (一台手机、一个脚本各一枚),彼此**互不影响**:撤一个不动另一个。
|
|
116
|
+
*/
|
|
117
|
+
export interface AcpAccount {
|
|
118
|
+
/** 归属 CCC 根 */
|
|
119
|
+
root: string;
|
|
120
|
+
/** 账户键(单账户简写 ⇒ `''`) */
|
|
121
|
+
accountId: string;
|
|
122
|
+
/** 凭据键(localstore) */
|
|
123
|
+
tokenKey: string;
|
|
124
|
+
/** 该账户的 token(**读时现取**;不缓存) */
|
|
125
|
+
token: string;
|
|
126
|
+
/** 该账户绑定的 skiff 角色名 */
|
|
127
|
+
role: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 把该 CCC 的配置**展开成账户列表**(读时现算,**无快照**)。
|
|
131
|
+
*
|
|
132
|
+
* 🔴 **`im.acp.accounts` 是必填项** —— 没有它就没有账户,**一个都认证不了**。
|
|
133
|
+
* 刻意**不提供**"单账户简写"退回路径(v1.59.1):那会多出一条永不生效的分支,
|
|
134
|
+
* 而它的唯一作用是把"配置写错了"伪装成"用了简写"。
|
|
135
|
+
*
|
|
136
|
+
* 判据(三条都由调用方以 fail-closed 消费):
|
|
137
|
+
* - `enabled !== false`(缺省 true)
|
|
138
|
+
* - `role` 非空(空 ⇒ 该账户**不出现**在列表里 ⇒ 必然登录失败)
|
|
139
|
+
* - token 存在(不存在 ⇒ 同样不出现)
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ 本函数**纯读,绝不生成 token** —— 生成是写操作,归 `ensureAcpChannelTokens`。
|
|
142
|
+
* 这样 upgrade 路径可以随时现算而不冒"每次连接都写 localstore"的风险。
|
|
143
|
+
*/
|
|
144
|
+
export declare function expandAcpAccounts(root: string, paths?: string[]): AcpAccount[];
|
|
145
|
+
/**
|
|
146
|
+
* **让配置里声明的账户都拥有 token**(写操作;幂等;由装配层与配置变更时调用)。
|
|
147
|
+
*
|
|
148
|
+
* 🔴 owner 令「**随即生成**认证用的 token」的落地点。与 v1.59.0 的差别:
|
|
149
|
+
* 那时只在**启动**时跑一次 ⇒ 加一台设备要重启;现在**任何一次判定前都可以跑**,
|
|
150
|
+
* 且撤账户后不会被"复活"(只对**当前声明里**的账户生成)。
|
|
151
|
+
*
|
|
152
|
+
* @returns 本次**新生成**的账户键列表(已存在的原样返回,不轮换)
|
|
153
|
+
*/
|
|
154
|
+
export declare function ensureAcpChannelTokens(root: string, paths?: string[]): string[];
|
|
155
|
+
/**
|
|
156
|
+
* 校验一枚候选 token,返回它归属的**账户**(未命中返回 null)。
|
|
84
157
|
*
|
|
85
158
|
* @param candidate 客户端出示的 token
|
|
86
159
|
* @param roots 候选 CCC 根集合(由调用方枚举——本模块不认识"CCC 在哪")
|
|
87
160
|
*/
|
|
88
|
-
export declare function
|
|
161
|
+
export declare function resolveTokenAccount(candidate: string, roots: readonly string[], paths?: string[]): AcpAccount | null;
|
|
89
162
|
/**
|
|
90
|
-
*
|
|
163
|
+
* 枚举**有可用 ACP 账户**的 CCC(供准入名单用)。
|
|
91
164
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
165
|
+
* 判据:`im.channel === 'acp'` ∧ `im.acp.enabled === true` ∧ **至少一个账户有 token 且配了角色**。
|
|
166
|
+
* ⚠️ 缺一即不算(fail-closed:宁可连不上,不可放行一个半配置的通道)。
|
|
94
167
|
*/
|
|
95
168
|
export declare function listAcpRoots(roots: readonly string[], paths?: string[]): string[];
|
|
96
169
|
/** 读该 CCC 的 `im` 段(未配置 ⇒ 归一缺省;永不 undefined) */
|
|
@@ -105,8 +178,6 @@ export declare function readImSettings(root: string, paths?: string[]): Required
|
|
|
105
178
|
* 让 owner 能**选好 ACP 但暂时关着**(不用删配置),与既有各面的 `enabled` 范式一致。
|
|
106
179
|
*/
|
|
107
180
|
export declare function acpChannelEnabled(root: string, paths?: string[]): boolean;
|
|
108
|
-
/** 该 CCC 绑定的 skiff 角色名(未配置 ⇒ null) */
|
|
109
|
-
export declare function acpRoleFor(root: string, paths?: string[]): string | null;
|
|
110
181
|
/**
|
|
111
182
|
* 客户端 `session/new` 传来的 `cwd` 是否落在该 token 归属的 CCC 内。
|
|
112
183
|
*
|
|
@@ -118,21 +189,36 @@ export declare function acpRoleFor(root: string, paths?: string[]): string | nul
|
|
|
118
189
|
*/
|
|
119
190
|
export declare function cwdInsideRoot(cwd: string, root: string): boolean;
|
|
120
191
|
/**
|
|
121
|
-
* 一次性解析:候选 token → 归属
|
|
192
|
+
* 一次性解析:候选 token → 归属 **账户**(含 CCC 根、账户键、角色)。
|
|
122
193
|
*
|
|
123
|
-
* @returns
|
|
194
|
+
* @returns 账户;token 无效 / 通道未启用 / 无角色 → null(**调用方一律拒绝连接**)
|
|
124
195
|
*/
|
|
125
|
-
export declare function authorizeAcpConnection(candidate: string | null, roots: readonly string[], paths?: string[]):
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
196
|
+
export declare function authorizeAcpConnection(candidate: string | null, roots: readonly string[], paths?: string[]): AcpAccount | null;
|
|
197
|
+
/** 单个账户的对外可见形态(**绝不含 token 明文**) */
|
|
198
|
+
export interface AcpAccountStatus {
|
|
199
|
+
accountId: string;
|
|
200
|
+
name: string | null;
|
|
201
|
+
role: string | null;
|
|
202
|
+
enabled: boolean;
|
|
203
|
+
hasToken: boolean;
|
|
204
|
+
tokenFingerprint: string | null;
|
|
205
|
+
/**
|
|
206
|
+
* 该账户的**主会话** id(owner「每账户固定一条长期对话」的那一条;`null` = 还没建)。
|
|
207
|
+
*
|
|
208
|
+
* 🔴 会话 id **不是秘密**,故这里给明文(与 token 不同)—— 它的用途是**可诊断**:
|
|
209
|
+
* 面板/排障时要能回答"这台手机此刻钉的是哪条对话",否则出问题时只能靠猜。
|
|
210
|
+
*/
|
|
211
|
+
mainSession: string | null;
|
|
212
|
+
/** 该账户此刻**能不能被认证闸接受**(三条判据全真) */
|
|
213
|
+
usable: boolean;
|
|
214
|
+
/** 不能用的原因(能用 ⇒ null)—— 直接指向该改哪个字段 */
|
|
215
|
+
blockedBy: string | null;
|
|
216
|
+
}
|
|
129
217
|
/** 该 CCC 的 ACP 通道对外**可见形态**(诊断面用;**绝不含 token 明文**) */
|
|
130
218
|
export declare function acpChannelStatus(root: string, paths?: string[]): {
|
|
131
219
|
channel: 'weixin' | 'acp';
|
|
132
220
|
enabled: boolean;
|
|
133
|
-
|
|
134
|
-
hasToken: boolean;
|
|
135
|
-
tokenFingerprint: string | null;
|
|
221
|
+
accounts: AcpAccountStatus[];
|
|
136
222
|
};
|
|
137
223
|
/** 只读:该 CCC 的 localstore 是否存在(避免对不存在的 CCC 白跑 token 反查) */
|
|
138
224
|
export declare function hasLocalstore(root: string): boolean;
|
|
@@ -143,14 +229,45 @@ export declare function localstoreDir(root: string): string;
|
|
|
143
229
|
* ⚠️ 与 `acpChannelEnabled` 分开:本函数**不读配置**(只判文件存在),用于快速排除。
|
|
144
230
|
*/
|
|
145
231
|
export declare function looksLikeCcc(root: string): boolean;
|
|
146
|
-
/** 排重后的 token 归属表(诊断用;同一 token
|
|
232
|
+
/** 排重后的 token 归属表(诊断用;同一 token 出现在两个账户 ⇒ 响亮暴露,不静默取第一个) */
|
|
147
233
|
export declare function tokenOwnerAudit(roots: readonly string[]): {
|
|
148
|
-
owners:
|
|
234
|
+
owners: Array<{
|
|
235
|
+
root: string;
|
|
236
|
+
accountId: string;
|
|
237
|
+
fingerprint: string;
|
|
238
|
+
}>;
|
|
149
239
|
duplicates: Array<{
|
|
150
240
|
fingerprint: string;
|
|
151
|
-
|
|
241
|
+
owners: string[];
|
|
152
242
|
}>;
|
|
153
243
|
};
|
|
154
|
-
/** 测试辅助:读 store
|
|
155
|
-
export declare function __rawTokenForTest(root: string): string | undefined;
|
|
156
|
-
|
|
244
|
+
/** 测试辅助:读 store 里该账户键的原始值(绕开 getEntry 的归一) */
|
|
245
|
+
export declare function __rawTokenForTest(root: string, accountId: string): string | undefined;
|
|
246
|
+
/**
|
|
247
|
+
* 写回 `im.acp.accounts`(保留文件里其余所有字段)。
|
|
248
|
+
*
|
|
249
|
+
* 🔴 **只碰这一个键**:`im.channel` / `im.acp.enabled` 归面板总开关与人工配置,
|
|
250
|
+
* 本函数不越权改它们(否则"加一台设备"会顺手把通道开关也改了)。
|
|
251
|
+
*/
|
|
252
|
+
export declare function saveAcpAccounts(root: string, accounts: ImAcpAccount[], paths?: string[]): ImAcpAccount[];
|
|
253
|
+
/**
|
|
254
|
+
* 规范化一条账户声明(面板传入的任意形状 → 合法声明;非法 ⇒ null)。
|
|
255
|
+
*
|
|
256
|
+
* 🔴 返回类型里 `role` 是**必填**(不是 `ImAcpAccount` 的可选形态)——
|
|
257
|
+
* 本函数的契约就是"要么给一个角色齐备的账户,要么给 null"。
|
|
258
|
+
* 这样调用方**不必再判一次** `role` 有没有(那正是会漂的第二处判据)。
|
|
259
|
+
*/
|
|
260
|
+
export declare function normalizeAcpAccount(input: unknown): (ImAcpAccount & {
|
|
261
|
+
accountId: string;
|
|
262
|
+
role: string;
|
|
263
|
+
}) | null;
|
|
264
|
+
/** 增/改一条账户(按 accountId 定位;写入后返回全表) */
|
|
265
|
+
export declare function upsertAcpAccount(root: string, account: ImAcpAccount, paths?: string[]): ImAcpAccount[];
|
|
266
|
+
/**
|
|
267
|
+
* 删一条账户(**同时清掉它的 token + 主会话**,避免留死键)。
|
|
268
|
+
*
|
|
269
|
+
* 🔴 主会话键**必须一起清**:否则日后用同一个 `accountId` 重建账户时,它会**静默继承**
|
|
270
|
+
* 那条旧会话 —— 而那条会话的 cwd/角色都是上一代账户的。这个失效**没有任何信号**
|
|
271
|
+
* (列表上它看着就是一条正常会话)。
|
|
272
|
+
*/
|
|
273
|
+
export declare function removeAcpAccount(root: string, accountId: string, paths?: string[]): ImAcpAccount[];
|
package/lib/acp-face.d.ts
CHANGED
|
@@ -48,16 +48,27 @@
|
|
|
48
48
|
*/
|
|
49
49
|
import type { Context } from 'cordis';
|
|
50
50
|
import { type FaceHandle } from './face-host.js';
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* 刷新"本机有哪些 CCC"(async,枚举宿主三来源)。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **失败必须响亮**:v1.59.0 这里是 `void refresh(...)` 且没有 catch ——
|
|
55
|
+
* 枚举失败会让名单清空 ⇒ **所有连接一律 401**,而现场只有一行 stdout 看不到的日志。
|
|
56
|
+
* 这正是本轨记过的「调用没报错 ≠ 已验证」。
|
|
57
|
+
*
|
|
58
|
+
* @returns 本次枚举到的根数(0 = 一个都没发现,日志会说为什么)
|
|
59
|
+
*/
|
|
52
60
|
export declare function refreshAcpRoots(ctx: Context): Promise<number>;
|
|
53
|
-
/**
|
|
61
|
+
/** 当前准入名单(诊断用只读快照;**现算**,不是缓存值) */
|
|
54
62
|
export declare function acpRootsSnapshot(): readonly string[];
|
|
55
63
|
/**
|
|
56
|
-
*
|
|
64
|
+
* 测试注入钩子(**生产零调用**):直接置"本机有哪些 CCC 候选根"。
|
|
57
65
|
*
|
|
58
66
|
* ⚠️ 为什么需要:`refreshAcpRoots()` 依赖 `listCccs(ctx)`(要真 CCC 环境)。
|
|
59
67
|
* 而认证闸本身是**纯逻辑**,测试要能单独驱动它 —— 否则"闸真的拦得住吗"这件事
|
|
60
68
|
* 只能靠集成环境验,而集成环境恰恰是最不容易跑的那种。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 v1.59.1:**置的是候选根,不是"放行名单"** —— 每个根会不会被放行,
|
|
71
|
+
* 仍由它自己的 `serenity.json` + localstore **现算**决定(测试照样要走真配置)。
|
|
61
72
|
*/
|
|
62
73
|
export declare function __setAcpRootsForTest(roots: readonly string[] | null): void;
|
|
63
74
|
export declare function acpFaceActive(): boolean;
|
|
@@ -68,6 +79,73 @@ export declare function acpFacePort(): number | null;
|
|
|
68
79
|
* 由装配层用它决定"起还是停"。
|
|
69
80
|
*/
|
|
70
81
|
export declare function acpFaceEnabled(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* 把客户端给的 `cwd` 归一化成宿主要求的**绝对路径**(v1.59.1)。
|
|
84
|
+
*
|
|
85
|
+
* ## 为什么需要
|
|
86
|
+
* 宿主 `dsh-acp` 在每次 `session/new` 都有这一行(`lib/index.js` 逐字):
|
|
87
|
+
* ```js
|
|
88
|
+
* if (!isAbsolute(params.cwd)) throw invalidParams(`cwd must be an absolute path: ${params.cwd}`)
|
|
89
|
+
* ```
|
|
90
|
+
* 而**连接归属哪个 CCC 已由 token 决定**(服务端权威)⇒ 让用户再手打一遍绝对路径
|
|
91
|
+
* 是**同一个信息说两遍**,且那第二遍正是最容易打错的一遍。
|
|
92
|
+
*
|
|
93
|
+
* ## 规则(唯一一条)
|
|
94
|
+
* **非绝对路径 ⇒ 一律解释为「本连接归属 CCC 的根」**。
|
|
95
|
+
*
|
|
96
|
+
* | 客户端填 | 送宿主 |
|
|
97
|
+
* |---|---|
|
|
98
|
+
* | `serenity` / `a` / `.` / 空 | `/…/home-serenity`(该 token 的 CCC 根) |
|
|
99
|
+
* | `/…/home-serenity`(已绝对且在根内) | 原样 |
|
|
100
|
+
* | `/…/home-serenity/sub` | 原样(仍在根内) |
|
|
101
|
+
* | `/etc`(绝对但在根外) | **原样透传,交给 `cwdInsideRoot` 拒** |
|
|
102
|
+
*
|
|
103
|
+
* 🔴 **最后一行为什么不在这里直接拒**:越权判据必须**只有一处**(`cwdInsideRoot`,
|
|
104
|
+
* 它也管着角色认领),在这里再判一次就是**两份会漂的实现**。归一化只负责"把非绝对变成绝对"。
|
|
105
|
+
*
|
|
106
|
+
* @param raw 客户端原样给的 `cwd`(可能是 `undefined`)
|
|
107
|
+
* @param cccRoot 本连接归属的 CCC 根
|
|
108
|
+
*/
|
|
109
|
+
export declare function normalizeClientCwd(raw: unknown, cccRoot: string): string;
|
|
110
|
+
/**
|
|
111
|
+
* 一条连接的**会话钉住**状态(owner「每账户固定一条」的执行单元)。
|
|
112
|
+
*
|
|
113
|
+
* 生命周期 = 一条 WS 连接。⚠️ **跨连接的真相在 localstore**(`IM_ACP_SESSION_<ACCOUNT_ID>`);
|
|
114
|
+
* 本结构只是"这条连接上,请求 id 对应哪个方法"的记账 + 那两个方法的处理口径。
|
|
115
|
+
*/
|
|
116
|
+
interface AcpSessionPin {
|
|
117
|
+
/** 本连接归属的 CCC 根(合成 `SessionInfo.cwd` 时用 —— 那也是我们当初钉的值) */
|
|
118
|
+
root: string;
|
|
119
|
+
/** 本连接归属的账户(主会话键由它派生) */
|
|
120
|
+
accountId: string;
|
|
121
|
+
/** 在飞的请求:`id` → 方法名(**出站改写唯一的方法来源**) */
|
|
122
|
+
pending: Map<string, string>;
|
|
123
|
+
}
|
|
124
|
+
/** 建一条连接的钉住状态 */
|
|
125
|
+
export declare function createAcpSessionPin(root: string, accountId: string): AcpSessionPin;
|
|
126
|
+
/**
|
|
127
|
+
* 入站记账:把"这条请求的方法"记住,供出站按 `id` 反查。
|
|
128
|
+
*
|
|
129
|
+
* 判据:**既要有 `id`,又要有 `method`** —— 只满足一个的是响应(`id` 无 `method`)
|
|
130
|
+
* 或通知(`method` 无 `id`)⇒ 与我们无关,不记。
|
|
131
|
+
*/
|
|
132
|
+
export declare function recordAcpPending(pin: AcpSessionPin, msg: unknown): void;
|
|
133
|
+
/**
|
|
134
|
+
* 出站改写(本功能的落点)。返回**要发给客户端的那条消息**。
|
|
135
|
+
*
|
|
136
|
+
* | 原响应 | 改写后 |
|
|
137
|
+
* |---|---|
|
|
138
|
+
* | `session/list` | `{sessions:[该账户那一条]}`;**没有主会话 ⇒ `{sessions:[]}`** |
|
|
139
|
+
* | `session/new` | 原样,**同时**把 `result.sessionId` 记为该账户的主会话 |
|
|
140
|
+
* | 其余(含所有 error 响应、所有通知) | 原样 |
|
|
141
|
+
*
|
|
142
|
+
* 🔴 **没有主会话时回空数组,绝不回退成全量列表** —— 退回去正是 owner 说
|
|
143
|
+
* 「23 个对我没什么用」的那个东西。空数组会让 App 引导用户新建,而新建即建立主会话。
|
|
144
|
+
*
|
|
145
|
+
* 🔴 `nextCursor` **一律丢掉**:收窄到一条之后,分页游标失去意义;
|
|
146
|
+
* 留着它会让客户端继续翻页(而后续页仍会被收窄成同一条 ⇒ 前端可能出现重复项)。
|
|
147
|
+
*/
|
|
148
|
+
export declare function rewriteAcpResponse(pin: AcpSessionPin, msg: unknown): unknown;
|
|
71
149
|
/** 面的运行统计(`dashboard` / 诊断面用;**只读快照**) */
|
|
72
150
|
export interface AcpFaceStatus {
|
|
73
151
|
active: boolean;
|
|
@@ -163,3 +241,4 @@ export declare function resolveAcpSelection(ctx: Context): {
|
|
|
163
241
|
* 绝不抛出(抛出会让整个插件加载失败,而它只是一个可选面)。
|
|
164
242
|
*/
|
|
165
243
|
export declare function registerAcpFace(ctx: Context): void;
|
|
244
|
+
export {};
|
package/lib/ccc.d.ts
CHANGED
|
@@ -270,16 +270,39 @@ export interface ImSettings {
|
|
|
270
270
|
}
|
|
271
271
|
/** ACP 通道参数(CCC 级) */
|
|
272
272
|
export interface ImAcpSettings {
|
|
273
|
-
/**
|
|
274
|
-
* 绑定的 skiff 角色名(**必须 ∈ 本 CCC 的 `skiff.roles`**)。
|
|
275
|
-
* 缺省 / 角色不存在 ⇒ **连接被拒**(fail-closed:宁可连不上,不可给出一个无身份、无约束的裸 agent)。
|
|
276
|
-
*/
|
|
277
|
-
role?: string;
|
|
278
273
|
/**
|
|
279
274
|
* 该 CCC 的 ACP 面开关(**与 `channel: "acp"` 是「与」关系**:两者都真才算启用)。
|
|
280
275
|
* 缺省 false。
|
|
281
276
|
*/
|
|
282
277
|
enabled?: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* **账户声明 —— 必填**(v1.59.1,owner 2026-10-17 令「token 生成和 skiff 绑定都做配置上」)。
|
|
280
|
+
*
|
|
281
|
+
* 🔴 **声明式,照 `weixin.accounts[]` 的形态**:结构(账户与角色)放本文件,
|
|
282
|
+
* 凭据(token)由 `accountId` 派生键 `IM_ACP_TOKEN_<ACCOUNT_ID>` 落 localstore。
|
|
283
|
+
*
|
|
284
|
+
* 判据(关键):**配置是真相源,localstore 只是它的派生凭据**。
|
|
285
|
+
* 声明了账户却还没有 token ⇒ **下次判定前当场生成**(不依赖启动那一次)。
|
|
286
|
+
* 撤销账户 ⇒ 该账户的 token **不再被接受**(即使 localstore 里还留着,也只是死键)。
|
|
287
|
+
*
|
|
288
|
+
* ⚠️ **没有简写退回**:不写本项 ⇒ **一个账户都没有 ⇒ 谁都认证不了**。
|
|
289
|
+
* 刻意如此 —— 多一条永不生效的简写分支,只会把"配置写错了"伪装成"用了简写"。
|
|
290
|
+
*/
|
|
291
|
+
accounts?: ImAcpAccount[];
|
|
292
|
+
}
|
|
293
|
+
/** ACP 账户声明(一台客户端一条;形状对齐 `weixin.accounts[]`) */
|
|
294
|
+
export interface ImAcpAccount {
|
|
295
|
+
/**
|
|
296
|
+
* 本地唯一键(如 `agmente-phone`;凭据键 `IM_ACP_TOKEN_<ACCOUNT_ID>`)。
|
|
297
|
+
* ⚠️ 归一为 **UPPER_SNAKE**(`agmente-phone` → `AGMENTE_PHONE`),与微信桥同规则。
|
|
298
|
+
*/
|
|
299
|
+
accountId: string;
|
|
300
|
+
/** 该账户绑定的 skiff 角色名(**必须 ∈ `skiff.roles`**;缺省 = 拒绝该账户) */
|
|
301
|
+
role?: string;
|
|
302
|
+
/** 该账户开关(缺省 true)——撤一台设备用它,不必删条目 */
|
|
303
|
+
enabled?: boolean;
|
|
304
|
+
/** 展示名(可选,仅用于诊断面) */
|
|
305
|
+
name?: string;
|
|
283
306
|
}
|
|
284
307
|
/** 微信桥账号配置(serenity.json 内;凭据部分在 localstore) */
|
|
285
308
|
export interface WeixinAccountConfig {
|