hiwork-core 0.0.10 → 0.0.16

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.
@@ -0,0 +1,218 @@
1
+ /**
2
+ * 「管理员下发的技能」客户端取数层:经 `/hiwork-managed-skills` loopback RPC 拿到清单与
3
+ * 按需取回的包体,交给技能域(hiwork-capabilities)去校验并落盘。
4
+ *
5
+ * 铁律:
6
+ * - 客户端**不持有**会话 cookie,也**不直接 fetch 网关**——一切经 Host 频道;包体由 Host
7
+ * 代取、以 base64 过来(网关会话 cookie 绝不离开 Host,spec §4.5);
8
+ * - 客户端**不解包、不落盘**:本层交出去的只有「清单 + 一条分配的包体字节」,
9
+ * `sha256(字节) === contentSha256` 的完整性判定与受管目录的写入都在技能域
10
+ * (spec §4.6 的 `skills.apply-managed`、§6 的冲突表);
11
+ * - **只有 {@link ManagedSkillsStore.refresh} 成功返回的清单才允许用作删除本地受管技能的
12
+ * 依据**;{@link ManagedSkillsStore.snapshot} 可能与界面展示共享,但它可能是上一次
13
+ * 成功的结果(管理员刚撤掉的分配还在里面),拿它做删除决策就会把「撤销」误做成
14
+ * 「没同步上」——反过来,拉取失败时它又是唯一的真相来源,所以两个入口必须分开。
15
+ * (`warm` 同理:它可能命中 Host 的预热缓存,比服务端现状旧。)
16
+ *
17
+ * client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import
18
+ * Host 代码(类型按结构重声明,字段名与契约一致)。
19
+ */
20
+ /** RPC 频道;必须与 Host `src/managed-skills.ts` 的 `MANAGED_SKILLS_RPC_CHANNEL` 一致(由测试锁定)。 */
21
+ export declare const MANAGED_SKILLS_CHANNEL = "/hiwork-managed-skills";
22
+ /** 端点白名单;必须与 Host `MANAGED_SKILLS_RPC_ENDPOINTS` 一致(由测试锁定)。 */
23
+ export declare const MANAGED_SKILLS_ENDPOINTS: readonly ["snapshot", "refresh", "package"];
24
+ export type ManagedSkillsEndpoint = (typeof MANAGED_SKILLS_ENDPOINTS)[number];
25
+ /**
26
+ * 本地镜像的「陈旧」阈值:5 分钟。
27
+ * 触发时机里的「打开能力中心 · 技能页且 >5min stale 就同步一次」用它判定——短于它只会让
28
+ * 用户每次切页都打一遍网关,长于它则界面上的清单明显落后于管理员的推送。
29
+ */
30
+ export declare const MANAGED_SKILLS_STALE_MS: number;
31
+ /** 一条分配(对齐 ai-hub spec §4.1 的冻结契约)。 */
32
+ export interface ManagedSkillAssignment {
33
+ assignmentId: string;
34
+ revision: number;
35
+ slug: string;
36
+ displayName: string;
37
+ /**
38
+ * **服务端权威下发**的技能名:模型侧 `/<名>`、受管目录名的唯一来源(spec §2 决策表)。
39
+ * 客户端**不参与命名**,也**不得**替它改名——技能名是模型侧标识符,漂了就什么都对不上。
40
+ */
41
+ skillName: string;
42
+ description: string;
43
+ /** 要不要重装的**唯一**依据(spec §4.1):内容寻址;不得拿 `version` 比大小。 */
44
+ contentSha256: string;
45
+ sizeBytes: number;
46
+ /** 只给人看的版本标签(服务端派生),空串表示没有。 */
47
+ version: string;
48
+ updatedAt: string;
49
+ }
50
+ /**
51
+ * 一份清单快照。
52
+ *
53
+ * `stale` 为 true 表示「这是上一次成功的结果,最近一次拉取失败了」——调用方可以照常展示,
54
+ * 但**不得**把它用作删除本地受管技能的依据(那条路只允许走成功响应,spec §6)。
55
+ */
56
+ export interface ManagedSkillsSnapshot {
57
+ /** 清单级序号(服务端任何影响清单的写入都 +1),客户端先比它再决定要不要深比对。 */
58
+ revision: number;
59
+ fetchedAt: string;
60
+ assignments: ManagedSkillAssignment[];
61
+ stale: boolean;
62
+ lastError?: {
63
+ code: string;
64
+ message: string;
65
+ };
66
+ }
67
+ /**
68
+ * `package` 的返回值(对齐 spec §4.5 的 Host 过面):包体字节的 base64 + 服务端声明的身份。
69
+ * 客户端拿 `contentSha256` 去校验**解码后的字节**,不符的那条不落盘(spec §6 完整性那一行)。
70
+ */
71
+ export interface ManagedSkillPackage {
72
+ assignmentId: string;
73
+ skillName: string;
74
+ contentSha256: string;
75
+ archiveBase64: string;
76
+ }
77
+ /**
78
+ * 「管理员下发的技能」面(spec §4.5 逐字冻结的六项)。
79
+ *
80
+ * 它挂在 `HiWorkCoreClientFace` 上作为**可选成员** `managedSkills?`:老 core 没有这个面,
81
+ * 老 capabilities 也不会来取;两侧因此不必同发一个版本(老 core 遇新 capabilities 就是
82
+ * 「没有受管技能那一块」,能力中心照常工作)。
83
+ */
84
+ export interface ManagedSkillsFace {
85
+ /** 本地镜像(同步读,可能是上一次成功的结果)。**不要拿它做删除决策**。 */
86
+ readonly snapshot: () => ManagedSkillsSnapshot | undefined;
87
+ /** 本地镜像是否陈旧(或从未同步过)。 */
88
+ readonly isStale: (ttlMs?: number) => boolean;
89
+ /** 用 Host 的预热缓存拿一份清单(命中缓存时零网络请求)。 */
90
+ readonly warm: (signal?: AbortSignal) => Promise<ManagedSkillsSnapshot>;
91
+ /** 强制拉一次,返回**刚拉到的**清单;失败抛错。只有它的返回值保证是当下的真相。 */
92
+ readonly refresh: (signal?: AbortSignal) => Promise<ManagedSkillsSnapshot>;
93
+ /** 取一条分配的技能包(base64 字节 + 服务端声明的身份)。失败抛错;本层不缓存包体。 */
94
+ readonly package: (assignmentId: string, signal?: AbortSignal) => Promise<ManagedSkillPackage>;
95
+ /** 订阅镜像变化(登出 / 换账号时会被清空)。返回退订函数。 */
96
+ readonly subscribe: (listener: () => void) => () => void;
97
+ }
98
+ /** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
99
+ export interface ManagedSkillsRpcPort {
100
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
101
+ }
102
+ /** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
103
+ export declare class ManagedSkillsRpcError extends Error {
104
+ readonly code: string;
105
+ constructor(code: string, message: string);
106
+ }
107
+ /**
108
+ * 解开 DSH Connection 的结果信封。
109
+ * 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
110
+ * 通用失败,绝不把未知结构当数据用。
111
+ *
112
+ * `ok:false` 一律**抛错**——这是「拉清单失败」与「拉到一份空清单」之间的那道闸:
113
+ * 把它读成 `value`,一次网络抖动就会变成「管理员把技能全撤了」。返回 `value` 的唯一
114
+ * 前提是 `ok === true`。
115
+ */
116
+ export declare function unwrapManagedSkillsResult<T>(value: unknown): T;
117
+ /** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
118
+ export declare function managedSkillsErrorOf(error: unknown): {
119
+ code: string;
120
+ message: string;
121
+ };
122
+ /**
123
+ * 受管技能的取数仓库。
124
+ *
125
+ * 本地镜像(`snapshot()` 同步读)是有必要的:DSH 的 client 半边在不同插件之间只通过
126
+ * 这个面同步取状态,而清单本身是异步来的。镜像**只由成功响应覆写**,失败时不改动它。
127
+ */
128
+ export declare class ManagedSkillsStore {
129
+ private readonly rpc;
130
+ private current;
131
+ private readonly listeners;
132
+ /**
133
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
134
+ *
135
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,
136
+ * 它的清单属于上一个人。复用它等于让新账号的同步拿到旧账号的清单,而 `refresh` 的返回值
137
+ * 是技能域唯一被允许用来删本地受管技能的依据(spec §6 的唯一安全不变量)⇒ 会把新账号
138
+ * 被分配的技能**硬删掉**。所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
139
+ */
140
+ private inFlight;
141
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
142
+ private generation;
143
+ constructor(rpc: ManagedSkillsRpcPort);
144
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做删除决策**(见文件头)。 */
145
+ snapshot(): ManagedSkillsSnapshot | undefined;
146
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
147
+ isStale(ttlMs?: number, now?: number): boolean;
148
+ /**
149
+ * 强制同步一次并返回**刚拉到的**清单。
150
+ *
151
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次
152
+ * 网络请求成功了」,用它做删除决策才是安全的;读镜像可能读到上一次的结果。
153
+ * 失败时抛错且**不动镜像**——调用方据此保持现有的受管技能一条不删(spec §6 的
154
+ * 唯一安全不变量)。
155
+ */
156
+ refresh(signal?: AbortSignal): Promise<ManagedSkillsSnapshot>;
157
+ /**
158
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
159
+ *
160
+ * 与 {@link refresh} 的分工:这里是「启动时先把界面填上」的便宜入口——命中缓存时
161
+ * 一次网络请求都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功
162
+ * 拉取的结果」,但**可能比服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它
163
+ * 够不够权威,不够就再 `refresh()` 一次。
164
+ */
165
+ warm(signal?: AbortSignal): Promise<ManagedSkillsSnapshot>;
166
+ /**
167
+ * 取一条分配的技能包(base64 字节)。
168
+ *
169
+ * **本层不缓存包体、也不解码**:它是一条分配的内容,不是「服务端此刻认什么」的投影;
170
+ * 缓存下来等于把每次同步装过的所有技能包都钉在内存里(base64 后每条约 5 MiB)。
171
+ * 校验与落盘在技能域(spec §4.6)。
172
+ */
173
+ package(assignmentId: string, signal?: AbortSignal): Promise<ManagedSkillPackage>;
174
+ /** 订阅镜像变化(技能域据此重渲染 / 重排同步)。返回退订函数。 */
175
+ subscribe(listener: () => void): () => void;
176
+ /**
177
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
178
+ *
179
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的
180
+ * 清单属于上一个人——清空之后到来的调用者若复用了它,就会拿旧账号的清单去对账本地受管
181
+ * 技能,把新账号被分配的技能全删掉(`refresh` 的返回值是唯一被允许用来删的那条路,
182
+ * spec §6)。所以世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
183
+ *
184
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把
185
+ * **新**发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据
186
+ * 天然避开这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/managed-skills.ts`
187
+ * 的 `load()` 同形)。
188
+ *
189
+ * ⚠️ 这条修复**刻意只落在技能这半**:`src/client/managed-mcp.ts` 是逐字同形的既有缺口,
190
+ * **用户已裁定 MCP 那半本轮不动**。别为了「两半对齐」把这里改回原样。
191
+ */
192
+ clear(): void;
193
+ /**
194
+ * 并发去重:`warm` 与 `refresh` 撞在一起(启动同步与定时同步常常同拍)时共用一次
195
+ * 请求,且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
196
+ *
197
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
198
+ * 于是删除得更少——偏差落在「该删的这次没删」这一侧,正是 spec §6 允许的那一侧。
199
+ *
200
+ * 上面这条讲的是**新旧次序**,管不到**身份**:并发合并还有第二个维度——「这几次调用是不是
201
+ * 同一个人的」。世代号({@link clear})负责的就是这一格:换账号之后账上那条属于上一个人,
202
+ * 谁都不许复用,必须各自重新发起。
203
+ *
204
+ * 传给 `task` 的 `generation` 是**发起这一次请求时**的世代号,由它决定这次结果能不能写进镜像
205
+ * (见 {@link adopt})——晚到的旧世代结果照旧返回给它自己的调用方,但不许覆盖新世代的镜像。
206
+ */
207
+ private dedupe;
208
+ /**
209
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
210
+ *
211
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
212
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步
213
+ * 就靠 `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示
214
+ * 新账号的技能,也不触发同步。
215
+ */
216
+ private adopt;
217
+ private emit;
218
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 「工作台应用清单」客户端取数层:经 `/hiwork-workbench` loopback RPC 拿到管理员登记并分配
3
+ * 给当前账号的应用,交给工作台插件(hiwork-workbench)的货架页渲染。
4
+ *
5
+ * 契约见 spec `docs/superpowers/specs/2026-09-17-hiwork-workbench-design.md` §4.1(清单 JSON)
6
+ * 与 §4.5(Host 面)。本文件与 `src/client/managed-skills.ts` 同款,是同一副骨架的**第三条链**,
7
+ * 也是最短的一条:M0 只做「看得到货架」,所以端点只有 `snapshot` / `refresh` 两个。
8
+ *
9
+ * 铁律:
10
+ * - 客户端**不持有**会话 cookie,也**不直接 fetch 网关**——一切经 Host 频道(spec D6 的硬约束:
11
+ * 网关会话 cookie 与虚拟 Key 都只存在于 core 的 Host 半边);
12
+ * - **只有 {@link ManagedWorkbenchStore.refresh} 成功返回的清单才允许用作「这个应用被下架了」的
13
+ * 依据**;{@link ManagedWorkbenchStore.snapshot} 可能与界面展示共享,但它可能是上一次成功的结果
14
+ * (管理员刚撤掉的条目还在里面),拿它做删除决策就会把「撤销」误做成「没同步上」——反过来,
15
+ * 拉取失败时它又是唯一的真相来源(离线态显示上次成功缓存),所以两个入口必须分开。
16
+ * (`warm` 同理:它可能命中 Host 的预热缓存,比服务端现状旧。)
17
+ * - **M0 不落盘、不写工作区、不搬字节**:本层交出去的只有一份清单(plan T5 的边界;
18
+ * `submit` / `poll` / `cancel` / `materialize` 属 M1,不许在这里提前出现)。
19
+ *
20
+ * client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import
21
+ * Host 代码(类型按结构重声明,字段名与契约一致)。
22
+ */
23
+ /** RPC 频道;必须与 Host `src/workbench-bridge.ts` 的 `WORKBENCH_RPC_CHANNEL` 一致(由测试锁定)。 */
24
+ export declare const MANAGED_WORKBENCH_CHANNEL = "/hiwork-workbench";
25
+ /**
26
+ * 端点白名单;必须与 Host `WORKBENCH_RPC_ENDPOINTS` 一致(由测试锁定)。
27
+ * 只有两个:`snapshot`(拿缓存,冷启动才拉一次)与 `refresh`(现在去拉一次)。
28
+ */
29
+ export declare const MANAGED_WORKBENCH_ENDPOINTS: readonly ["snapshot", "refresh"];
30
+ export type ManagedWorkbenchEndpoint = (typeof MANAGED_WORKBENCH_ENDPOINTS)[number];
31
+ /**
32
+ * 本地镜像的「陈旧」阈值:5 分钟。
33
+ *
34
+ * 货架页的节奏是「挂载时 `warm()`,`isStale()` 超出 TTL 时 `refresh()`」(plan T7)。
35
+ * 短于它只会让用户每次切到工作台都打一遍网关,长于它则界面上的清单明显落后于管理员的改动。
36
+ */
37
+ export declare const MANAGED_WORKBENCH_STALE_MS: number;
38
+ /**
39
+ * 清单里的一条应用(**冻结契约**,字段名与 spec §4.1 一一对应,改名即破坏跨仓契约)。
40
+ *
41
+ * 两处开放的形状(演进纪律见 spec §4.1,消费方按它降级):
42
+ * - `executor.kind` 是开放集合:**不认识的 kind 必须原样出现在这里**,由消费方置灰 +
43
+ * 「请升级 HiWork 客户端」,绝不因为它不认识就把整份清单判为无效(spec D2);
44
+ * - `inputs[].type` 同理(一期只允许 `file` / `text` / `number` / `enum` / `boolean`,
45
+ * 未知 type → 该应用置灰)。
46
+ */
47
+ export interface WorkbenchAppView {
48
+ code: string;
49
+ displayName: string;
50
+ summary: string;
51
+ /** 网关线名是 `icon`,Host 规范化成 `iconUrl`(客户端只认这一个名字)。 */
52
+ iconUrl?: string;
53
+ category: {
54
+ code: string;
55
+ name: string;
56
+ };
57
+ tags: string[];
58
+ featured: boolean;
59
+ revision: number;
60
+ executor: {
61
+ kind: string;
62
+ mode?: string;
63
+ } & Record<string, unknown>;
64
+ inputs: ReadonlyArray<{
65
+ key: string;
66
+ type: string;
67
+ label: string;
68
+ required: boolean;
69
+ } & Record<string, unknown>>;
70
+ outputs: ReadonlyArray<{
71
+ key: string;
72
+ type: string;
73
+ } & Record<string, unknown>>;
74
+ effects: string[];
75
+ }
76
+ /**
77
+ * 一份清单快照。
78
+ *
79
+ * `stale` 为 true 表示「这是上一次成功的结果,最近一次拉取失败了」——调用方可以照常展示
80
+ * (离线态 + 「可能不是最新」的提示),但**不得**把它用作「应用被下架」的依据。
81
+ */
82
+ export interface WorkbenchCatalogSnapshot {
83
+ revision: number;
84
+ fetchedAt: string;
85
+ apps: WorkbenchAppView[];
86
+ stale: boolean;
87
+ lastError?: {
88
+ code: string;
89
+ message: string;
90
+ };
91
+ }
92
+ /**
93
+ * 「工作台应用清单」面(spec §4.5 在 M0 范围内冻结的六项)。
94
+ *
95
+ * 它挂在 `HiWorkCoreClientFace` 上作为**可选成员** `managedWorkbench?`:老 core 没有这个面,
96
+ * 老 workbench 也不会来取;两侧因此不必同发一个版本——老 core 遇新 workbench 就是
97
+ * 「本部署尚未启用工作台」(spec §10 的降级矩阵)。
98
+ */
99
+ export interface ManagedWorkbenchFace {
100
+ /** 本地镜像(同步读,可能是上一次成功的结果)。**不要拿它做下架决策**。 */
101
+ readonly snapshot: () => WorkbenchCatalogSnapshot | undefined;
102
+ /** 本地镜像是否陈旧(或从未同步过)。 */
103
+ readonly isStale: (ttlMs?: number) => boolean;
104
+ /** 用 Host 的预热缓存拿一份清单(命中缓存时零网络请求)。 */
105
+ readonly warm: (signal?: AbortSignal) => Promise<WorkbenchCatalogSnapshot>;
106
+ /** 强制拉一次,返回**刚拉到的**清单;失败抛错。只有它的返回值保证是当下的真相。 */
107
+ readonly refresh: (signal?: AbortSignal) => Promise<WorkbenchCatalogSnapshot>;
108
+ /** 订阅镜像变化(登出 / 换账号时会被清空)。返回退订函数。 */
109
+ readonly subscribe: (listener: () => void) => () => void;
110
+ }
111
+ /** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
112
+ export interface ManagedWorkbenchRpcPort {
113
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
114
+ }
115
+ /** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
116
+ export declare class ManagedWorkbenchRpcError extends Error {
117
+ readonly code: string;
118
+ constructor(code: string, message: string);
119
+ }
120
+ /**
121
+ * 解开 DSH Connection 的结果信封。
122
+ * 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
123
+ * 通用失败,绝不把未知结构当数据用。
124
+ *
125
+ * `ok:false` 一律**抛错**——这是「拉清单失败」与「拉到一份空清单」之间的那道闸:
126
+ * 把它读成 `value`,一次网络抖动就会变成「管理员把应用全撤了」(货架清空)。返回 `value`
127
+ * 的唯一前提是 `ok === true`。
128
+ */
129
+ export declare function unwrapManagedWorkbenchResult<T>(value: unknown): T;
130
+ /** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
131
+ export declare function managedWorkbenchErrorOf(error: unknown): {
132
+ code: string;
133
+ message: string;
134
+ };
135
+ /**
136
+ * 工作台应用清单的取数仓库。
137
+ *
138
+ * 本地镜像(`snapshot()` 同步读)是有必要的:DSH 的 client 半边在不同插件之间只通过
139
+ * 这个面同步取状态,而清单本身是异步来的。镜像**只由成功响应覆写**,失败时不改动它。
140
+ */
141
+ export declare class ManagedWorkbenchStore {
142
+ private readonly rpc;
143
+ private current;
144
+ private readonly listeners;
145
+ /**
146
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
147
+ *
148
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,它的清单
149
+ * 属于上一个人。复用它等于让新账号的货架显示旧账号的应用(管理员给谁分配是按人的)。
150
+ * 所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
151
+ */
152
+ private inFlight;
153
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
154
+ private generation;
155
+ constructor(rpc: ManagedWorkbenchRpcPort);
156
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做下架决策**(见文件头)。 */
157
+ snapshot(): WorkbenchCatalogSnapshot | undefined;
158
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
159
+ isStale(ttlMs?: number, now?: number): boolean;
160
+ /**
161
+ * 强制同步一次并返回**刚拉到的**清单。
162
+ *
163
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次网络请求成功了」,
164
+ * 用它判断「这个应用还在不在」才是安全的;读镜像可能读到上一次的结果。失败时抛错且**不动镜像**
165
+ * ——调用方据此保持货架原样(离线态显示上次成功缓存 + 「可能不是最新」)。
166
+ */
167
+ refresh(signal?: AbortSignal): Promise<WorkbenchCatalogSnapshot>;
168
+ /**
169
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
170
+ *
171
+ * 与 {@link refresh} 的分工:这里是「挂载时先把界面填上」的便宜入口——命中缓存时一次网络请求
172
+ * 都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功拉取的结果」,但**可能比
173
+ * 服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它够不够权威,不够就再 `refresh()`。
174
+ */
175
+ warm(signal?: AbortSignal): Promise<WorkbenchCatalogSnapshot>;
176
+ /** 订阅镜像变化(货架页据此重渲染)。返回退订函数。 */
177
+ subscribe(listener: () => void): () => void;
178
+ /**
179
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
180
+ *
181
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的清单
182
+ * 属于上一个人——清空之后到来的调用者若复用了它,新账号的货架就会显示旧账号的应用。所以
183
+ * 世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
184
+ *
185
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把**新**
186
+ * 发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据天然避开
187
+ * 这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/workbench-bridge.ts`
188
+ * 的 `load()` 同形,也与 `src/client/managed-skills.ts` 的同名方法逐字同款)。
189
+ */
190
+ clear(): void;
191
+ /**
192
+ * 并发去重:`warm` 与 `refresh` 撞在一起(挂载预热与手动刷新常常同拍)时共用一次请求,
193
+ * 且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
194
+ *
195
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
196
+ * 于是「该消失的这次没消失」——偏差落在保守的那一侧。
197
+ *
198
+ * 上面这条讲的是**新旧次序**,管不到**身份**:世代号({@link clear})负责的就是这一格——
199
+ * 换账号之后账上那条属于上一个人,谁都不许复用,必须各自重新发起。
200
+ */
201
+ private dedupe;
202
+ /**
203
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
204
+ *
205
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
206
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步就靠
207
+ * `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示新账号的
208
+ * 应用,也不触发同步。
209
+ */
210
+ private adopt;
211
+ private emit;
212
+ }
@@ -92,6 +92,33 @@ export declare const frameCss: string;
92
92
  * 见 dsh-session-log-export/lib/client.js)。整钮隐藏即满足同一产品要求。
93
93
  */
94
94
  export declare const hostHideCss: string;
95
+ /**
96
+ * 会话输入框(composer)的描边色。
97
+ *
98
+ * 宿主那枚卡片(`dsh-client-ui-conversation` 的 `InputBar`,`[data-composer-card]`)**不画
99
+ * border**:`border: 0` + `box-shadow: var(--dsw-elevation-soft)`,而 soft 的第一层就是
100
+ * `--dsw-elevation-stroke` = `0 0 0 .5px var(--dsw-elevation-stroke-color)`。卡片自己把
101
+ * 那个变量覆盖成 `--dsw-alias-border-l2`(亮色 #0000001a / 暗色 #ffffff1f,约一成的
102
+ * 半透明),所以整条会话里最大的一块输入区反而比别处的输入框(`.editor`、`.modalInput`
103
+ * 等用 `border-l4`)更淡——真机上看着就是「边框不明显」。
104
+ *
105
+ * 这里只改**颜色**,不动 .5px 的描边宽度,也不碰圆角/底色:非激活提到 `border-l4`
106
+ * (与全站其他输入框同一档),激活态用品牌蓝——宿主自己的输入框聚焦就是这个色
107
+ * (`dsh-client-ui-conversation` 的 `_search:focus-within{border-color:...business-primary}`),
108
+ * 与 `interactionCss` 里 hiwork-core 自己的输入框也一致。
109
+ *
110
+ * 两条都挂在 `:has([data-composer-input])` 上:那把 contentEditable 文本域只在**真能打字**
111
+ * 时渲染,而「请先选工作区」那种 inert 态渲染的是 `workspaceTrigger` 变体(编辑器为 null、
112
+ * 卡片自带虚线描边、描边变量被设为 transparent)。不筛掉它,我的实线会和那圈虚线叠在一起。
113
+ * `:has()` 在 Electron 43(Chromium 130+)与 jsdom 30 上都可用,仓库里已有先例
114
+ * (`frameSurfaceSelector`)。
115
+ *
116
+ * 第三条是给**卡内浮层**的:`--dsw-elevation-stroke-color` 会往下继承,触发词菜单/模型
117
+ * 选择面板(`InputBar` 的 `.panel`,`box-shadow: var(--dsw-elevation-prominent)`)就长在卡片
118
+ * 里面,聚焦时会把那道蓝边一起继承过去。把它在卡片的直接子节点上复位成全站默认的 l4
119
+ * (子节点整棵子树跟着复位),浮层照旧用中性描边,只有卡片本体吃品牌蓝。
120
+ */
121
+ export declare const composerCss: string;
95
122
  /**
96
123
  * 品牌区样式:词标资产只有深色版,暗色主题下整体反白(纯色 logo 反转无损;
97
124
  * body[data-ds-dark-theme] 为 dsh 暗色选择器,与 hiwork-dsh-ui 同款)。
@@ -1,5 +1,37 @@
1
1
  /** 稳定错误码,供 settings card 分支渲染。 */
2
- export type LoginErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'GATEWAY_UNREACHABLE' | 'NO_KEY_ASSIGNED' | 'SESSION_EXPIRED' | 'KEY_DISABLED' | 'KEY_INVALID' | 'NOT_FOUND';
2
+ export type LoginErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'GATEWAY_UNREACHABLE' | 'NO_KEY_ASSIGNED' | 'SESSION_EXPIRED' | 'KEY_DISABLED' | 'KEY_INVALID' | 'NOT_FOUND'
3
+ /** 该分配已被管理员撤销(或从未属于当前账号):取断言时 404。 */
4
+ | 'MCP_ASSIGNMENT_GONE'
5
+ /** 服务或分配已停用(或被改成了不可下发的凭证方式):取断言时 409。 */
6
+ | 'MCP_NOT_DELIVERABLE'
7
+ /** 本部署未配置断言签名私钥(`mcp.assertion_key`):取断言时 503。 */
8
+ | 'MCP_ASSERTION_DISABLED'
9
+ /** 该分配已被管理员撤销(或从未属于当前账号):取包体时 404。 */
10
+ | 'SKILL_ASSIGNMENT_GONE'
11
+ /** 本部署未启用技能下发:取包体时 503。 */
12
+ | 'SKILL_DISTRIBUTION_DISABLED'
13
+ /** 本部署未配置 OnlyOffice 集成(`office.document_server_url` /
14
+ * `office.jwt_secret`):取配置时 503。 */
15
+ | 'OFFICE_NOT_CONFIGURED'
16
+ /** 本部署未启用文档中继(`office.relay_enabled` 未开):中继端点 404。 */
17
+ | 'RELAY_DISABLED'
18
+ /** 中继会话已过期或被清理(12h 硬过期 / 回调后空转):410。 */
19
+ | 'RELAY_EXPIRED'
20
+ /** 文档超过中继大小上限(`office.relay_max_bytes`):413。 */
21
+ | 'RELAY_TOO_LARGE'
22
+ /** 文档类型不在中继白名单:415。 */
23
+ | 'RELAY_EXTENSION'
24
+ /** 单用户并发编辑会话过多:429。 */
25
+ | 'RELAY_QUOTA'
26
+ /**
27
+ * 本部署未启用工作台(网关侧没有配置 `workbench`,`/api/me/workbench-apps`
28
+ * 这条路由压根没注册):取清单时 404。
29
+ *
30
+ * ⚠️ 与技能清单那半的差别是**服务端语义**的差别:技能清单的 404 只能是「路径没被路由」,
31
+ * 收敛成 `GATEWAY_UNREACHABLE`;工作台的 404 是一个明确的功能状态(spec §10 的降级矩阵),
32
+ * 消费方据此显示「本部署尚未启用工作台」而不是报错。别顺手把两边对齐。
33
+ */
34
+ | 'WORKBENCH_DISABLED';
3
35
  /** 一个稳定的、可安全展示的登录/刷新错误。 */
4
36
  export interface LoginError {
5
37
  code: LoginErrorCode;