hiwork-core 0.0.11 → 0.0.17

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,169 @@
1
+ /**
2
+ * `/hiwork-plugin-updates` loopback RPC 的 Host 实现:把**桌面端**的 `desktopPlugins`
3
+ * (受管插件清单的只读面 + 唯一的回收入口)搬到渲染端够得着的地方。
4
+ *
5
+ * 为什么必须有这条 RPC(spec §4.5 的「数据来源」一段):`desktopPlugins` 是桌面端的
6
+ * `desktop-bridge.mts` 用 `ctx.provide('desktopPlugins', …)` 提供到 **DSH host** ctx 上的,
7
+ * 而 hiwork-core 的客户端跑在 **renderer**——两个进程。客户端现有的全部数据层(usage /
8
+ * session-cost / daily / managed-mcp / managed-skills / managed-workbench / app-version)
9
+ * 无一例外都经 `ctx.get('connection').rpc` 走 `/hiwork-*` loopback RPC,**没有任何
10
+ * client→host 的服务直连通道**。而「重启并更新」是个**点击**,它发生在客户端。所以取数与
11
+ * 触发都得过这条 RPC。
12
+ *
13
+ * 骨架照 `src/workbench-bridge.ts`(同 `src/managed-skills.ts` / `src/app-version-rpc.ts`):
14
+ * 导出频道名 + 端点白名单(`as const`)+ 结构化 deps 端口(**不 import cordis 运行时**,
15
+ * 便于单测直接伪造)+ 一个 `register…Rpc(...)` 返回 disposer。
16
+ *
17
+ * 三条本频道特有的纪律:
18
+ * - ⚠️ **`desktopPlugins` 是可选的**(不变量:依赖必须是可选的)。老桌面端上没有它 —— 那正是
19
+ * 「新 core + 老桌面端」这条共存路径。拿不到时 `snapshot` 回**成功信封 + `null`**
20
+ * (「不可用」**不是错误**,与 `app-version-rpc.ts` 的「桌面端没推过不是错误」同一条口径),
21
+ * 客户端据此**不渲染任何东西**。抛错会把共存路径变成一条报错日志。
22
+ * - ⚠️ **只搬运 `packages` / `revision` / `fetchedAt` 这三样**,**不搬 `held`**(plan T11
23
+ * Step 0)。它们三个是真的:落在 `.dsh-pending-updates.json` 里,跨进程可用。`held` /
24
+ * `from` / `displayName` 在生产路径上是空的——根因是 `desktop-bridge.mts` 调
25
+ * `createDesktopHostServices()` 时**没传 `pluginManifest`**,于是 `desktop-host.ts` 里那条
26
+ * `memory = options.pluginManifest?.current()` 恒 `undefined`、`held` 退到 `?? []`。
27
+ * 那条通道要修得补一条**主进程 → 子进程**的路(新 IPC 信封或 sidecar 文件),而**本 RPC 是
28
+ * client→host 的**,它跨的是「renderer↔DSH」,**跨不过「DSH↔主进程」这一层**。所以别「顺手把
29
+ * `held` 也传过去」——传过去也永远是空的,只会让下一个读这段的人以为服务端没下发降级项。
30
+ * - ⚠️ **`apply` 不是「更新成功」的回执**:它只把触发递下去(`desktopPlugins.applyNow()` →
31
+ * `APPLY_PLUGIN_UPDATES_IPC` → 主进程的 `recycleDshForPluginUpdate()`),重启由**主进程**执行、
32
+ * 客户端那一刻就没了。按钮语义是「已请求重启」。所以返回值是 `{ requested }`,
33
+ * **不是** `{ applied: true }`。
34
+ *
35
+ * 回收红线(spec §6.1 不变量②):本文件的 `apply` 是**唯一**允许触发回收的新入口,且只能由
36
+ * 用户点击到达(客户端只在「重启并更新」按钮的 onClick 里调它)。`snapshot` **绝不触碰**
37
+ * `applyNow()`——清单拉取、轮询、启动流程都不许触发回收。
38
+ */
39
+ /** RPC 频道;必须与 client 半边 `src/client/plugin-updates.ts` 的 `PLUGIN_UPDATES_CHANNEL` 一致(由测试锁定)。 */
40
+ export declare const PLUGIN_UPDATES_RPC_CHANNEL = "/hiwork-plugin-updates";
41
+ /**
42
+ * 端点白名单:只有两个。
43
+ *
44
+ * `snapshot` 是「现在有没有待生效的更新」(桌面端读 pending 文件,**不拉清单、不写文件、
45
+ * 不触发回收**),`apply` 是「现在就去重启并应用」——它只在用户点击时被调用。
46
+ * 白名单是客户端可用的能力边界,多一项就等于先发布了一个没有实现的承诺。
47
+ */
48
+ export declare const PLUGIN_UPDATES_RPC_ENDPOINTS: readonly ["snapshot", "apply"];
49
+ export type PluginUpdatesRpcEndpoint = (typeof PLUGIN_UPDATES_RPC_ENDPOINTS)[number];
50
+ /**
51
+ * 一条待生效的更新(**冻结契约**:与桌面端 `ManagedPluginUpdate` 逐字同形)。
52
+ *
53
+ * ⚠️ `from` / `displayName` **不得用于渲染任何东西**(spec §4.3 的 ⚠️):生产路径上 `from` 恒空串、
54
+ * `displayName` 退化成包名(`pendingUpdatesFromDisk()`)。保留它们是为了贴合桌面端契约,
55
+ * 不是因为它们可用——界面只认 `packages.length > 0`。
56
+ */
57
+ export interface PendingPluginUpdate {
58
+ packageName: string;
59
+ displayName: string;
60
+ from: string;
61
+ to: string;
62
+ }
63
+ /**
64
+ * 一次待更新快照。
65
+ *
66
+ * `revision` 供提示条去重「关过一次的提示」(spec §4.5):**服务端没给就是 `null`**,
67
+ * 客户端把 `null` 当作一个独立的桶(关过它就不再提示,换了值就重新提示)。
68
+ *
69
+ * **本对象里没有 `held`**:见文件头第二条纪律。
70
+ */
71
+ export interface PluginUpdatesSnapshot {
72
+ packages: PendingPluginUpdate[];
73
+ /** 桌面端最近一次清单的时刻(epoch ms);拿不到就是 0。 */
74
+ fetchedAt: number;
75
+ /** 清单级版本号;服务端没给就是 `null`。 */
76
+ revision: number | null;
77
+ }
78
+ /** `snapshot` 端点的响应:`null` = 不可用(老桌面端没有 `desktopPlugins`)。**不是错误**。 */
79
+ export type PluginUpdatesSnapshotValue = PluginUpdatesSnapshot | null;
80
+ /** `apply` 端点的响应:`requested` = 触发是否递下去了(**不是**「更新是否成功」)。 */
81
+ export interface PluginUpdatesApplyValue {
82
+ requested: boolean;
83
+ }
84
+ /** 客户端可安全展示的失败。 */
85
+ export interface PluginUpdatesFailure {
86
+ code: string;
87
+ message: string;
88
+ /** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
89
+ details: object;
90
+ }
91
+ /** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
92
+ export type PluginUpdatesRpcResult<T> = {
93
+ readonly ok: true;
94
+ readonly value: T;
95
+ } | {
96
+ readonly ok: false;
97
+ readonly error: PluginUpdatesFailure;
98
+ };
99
+ /** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
100
+ export declare class PluginUpdatesRequestError extends Error {
101
+ readonly code: 'bad-request';
102
+ constructor(code: 'bad-request', message: string);
103
+ }
104
+ /** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
105
+ export interface PluginUpdatesRpcConnection {
106
+ rpc: {
107
+ handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
108
+ authority: 'loopback';
109
+ }): () => Promise<void> | void;
110
+ };
111
+ }
112
+ export interface PluginUpdatesRpcContext {
113
+ readonly connection: PluginUpdatesRpcConnection;
114
+ readonly logger: {
115
+ warn(message: string): void;
116
+ };
117
+ }
118
+ /**
119
+ * 桌面端 `desktopPlugins` 服务在本频道里真正用到的那两个成员(结构兼容,便于单测伪造)。
120
+ *
121
+ * 返回值一律按 `unknown` 收:它来自**另一个包**在运行时 provide 的对象,本文件不假设它的形状
122
+ * ——形状由 {@link toSnapshot} 兜底(这正是「不变量:拿不到就当不可用」在取值一侧的落实)。
123
+ */
124
+ export interface DesktopPluginsPort {
125
+ /** 待生效的更新;没有待生效项(含只有 `held`)时返回 `undefined`。 */
126
+ pending(): Promise<unknown>;
127
+ /** 等价于用户点插件市场的「更新」:立刻回收。**只能由用户点击触发**。 */
128
+ applyNow(): Promise<unknown>;
129
+ }
130
+ /** Host 侧取数依赖:桌面端的 `desktopPlugins` 服务(老桌面端上没有它)。 */
131
+ export interface PluginUpdatesRpcDeps {
132
+ /**
133
+ * 现取 `ctx.get('desktopPlugins')`;拿不到返回 `undefined`(老桌面端)。
134
+ *
135
+ * 做成**函数**而不是值:cordis 的服务可能在插件生命周期中途才被 provide / 被撤下,
136
+ * 一次性取到的值会把「还没 provide」误判成「永远没有」。
137
+ */
138
+ desktopPlugins(): DesktopPluginsPort | undefined;
139
+ /** Host 日志出口(只记事件与错误类型)。 */
140
+ log?: ((message: string) => void) | undefined;
141
+ }
142
+ /** 通用失败文案:不暴露任何桌面端细节。 */
143
+ export declare const PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE = "\u63D2\u4EF6\u66F4\u65B0\u72B6\u6001\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
144
+ /** 是否为白名单端点。 */
145
+ export declare function isPluginUpdatesRpcEndpoint(endpoint: string): endpoint is PluginUpdatesRpcEndpoint;
146
+ /**
147
+ * payload 校验:两个端点都**只放行空对象**(本频道没有任何入参)。
148
+ *
149
+ * 与 workbench / app-version 的空入参分支逐字同款:出现任何字段都是客户端实现缺陷,直接
150
+ * `bad-request`——一个 `{ force: true }` 被静默忽略,会让调用方以为「强制重启」生效了。
151
+ */
152
+ export declare function parsePluginUpdatesPayload(endpoint: PluginUpdatesRpcEndpoint, raw: unknown): Record<string, never>;
153
+ /**
154
+ * 把 `desktopPlugins.pending()` 的返回值规范化成 {@link PluginUpdatesSnapshot}。
155
+ *
156
+ * `undefined` / 非对象 → `undefined`(= 没有待生效项,界面不渲染)。**只有降级项(`held` 非空、
157
+ * `packages` 为空)时同样落成空 `packages`**,由客户端据此不渲染——回退对员工是要解释的事,
158
+ * 不该混在「更新已就绪」里(spec §4.5)。`held` 本身不搬运,理由见文件头。
159
+ */
160
+ export declare function toSnapshot(raw: unknown): PluginUpdatesSnapshot | undefined;
161
+ /**
162
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
163
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
164
+ */
165
+ export declare function handlePluginUpdatesEndpoint(endpoint: string, rawPayload: unknown, deps: PluginUpdatesRpcDeps, signal?: AbortSignal): Promise<PluginUpdatesRpcResult<unknown>>;
166
+ /**
167
+ * 注册 `/hiwork-plugin-updates` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
168
+ */
169
+ export declare function registerPluginUpdatesRpc(ctx: PluginUpdatesRpcContext, deps: PluginUpdatesRpcDeps): () => Promise<void>;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `/hiwork-workbench` loopback RPC 的 Host 实现:管理员登记并分配给当前账号的**工作台应用清单**。
3
+ *
4
+ * 契约见 spec `docs/superpowers/specs/2026-09-17-hiwork-workbench-design.md` §4.1(清单 JSON)与
5
+ * §4.5(Host 面)。本文件是 `src/managed-skills.ts` 同一副骨架的**第三条链**,也是三条里最短的一条:
6
+ * 工作台 M0 只做「看得到货架」,因此
7
+ * - 端点只有两个:`snapshot`(你现在有什么)/ `refresh`(现在去拉一次)。**没有 `package` 那一档**
8
+ * (清单里没有大字节,一条 GET 全量下发);
9
+ * - **M1 的四个端点(`submit` / `poll` / `cancel` / `materialize`)刻意不在本文件里**:它们要动
10
+ * 工作区落盘与上传字节,属于任务运行时的范围(plan T5 的「M0 的边界」)。别为了「一次写完」
11
+ * 顺手加上——那会把 M0 的验收面撑成 M1。
12
+ *
13
+ * 与 MCP / 技能逐字同款的三条铁律:
14
+ * - **网关会话 cookie 绝不离开 Host**:cookie 只在 `resolveSession()` 内部出现,客户端拿到的是
15
+ * 清单(经 loopback RPC 过去,里面没有任何令牌);
16
+ * - **拉清单失败绝不清空上一次成功的清单**:失败返回「保留旧值 + 标注失败原因」,于是断网时
17
+ * 界面还能显示上次成功缓存 + 「可能不是最新」(spec §10 的离线那一行);
18
+ * - **`snapshot` 可能陈旧、只有 `refresh` 的成功返回值可作真相**:下架 / 撤销分配后卡片消失
19
+ * 只能基于 `refresh` 的返回值判定(spec §4.5)。
20
+ *
21
+ * 与技能清单不同的一处:**远端会回 404**(网关没配置工作台时那条路由压根没注册,spec §10),
22
+ * 它被映射成 `WORKBENCH_DISABLED` 而非「网关不可达」——这是可展示的功能状态,不是故障。
23
+ *
24
+ * 演进纪律(spec §4.1):**未知字段忽略;未知 `executor.kind` / `inputs[].type` 原样透传**。
25
+ * 本文件的规范化只做「形状兜底 + 已知字段改名(`icon` → `iconUrl`)」,绝不按白名单过滤语义
26
+ * ——老客户端遇到新 kind 由客户端置灰 + 提示升级(spec D2),Host 不替它做丢弃决策。
27
+ *
28
+ * 本模块不 import cordis 运行时,只依赖结构化的 {@link WorkbenchRpcDeps} 端口,便于单测直接伪造。
29
+ */
30
+ import { type GatewayWorkbenchAppsView } from './ai-router-client.ts';
31
+ /** RPC 频道;必须与 client 半边 `src/client/managed-workbench.ts` 的 `MANAGED_WORKBENCH_CHANNEL` 一致(由测试锁定)。 */
32
+ export declare const WORKBENCH_RPC_CHANNEL = "/hiwork-workbench";
33
+ /**
34
+ * 端点白名单:M0 只有「拿清单」这一件事的两个入口。
35
+ *
36
+ * `snapshot` 是「你现在有什么」(有缓存就直接给,冷启动才拉一次),`refresh` 是「现在去拉一次」。
37
+ * 分两个端点的理由与 MCP / 技能同款:同步策略(启动 / 超 TTL / 手动刷新)由消费方自己定,
38
+ * Host 不该替它决定什么时候必须联网。
39
+ *
40
+ * ⚠️ `submit` / `poll` / `cancel` / `materialize` 属 M1(spec §4.5),**不许提前登记**:
41
+ * 白名单是客户端可用的能力边界,多一项就等于先发布了一个没有实现的承诺。
42
+ */
43
+ export declare const WORKBENCH_RPC_ENDPOINTS: readonly ["snapshot", "refresh"];
44
+ export type WorkbenchRpcEndpoint = (typeof WORKBENCH_RPC_ENDPOINTS)[number];
45
+ /**
46
+ * Host 侧后台预热间隔:30 分钟(与 MCP / 技能两份同量级)。
47
+ * 它**不是**权威节奏——只为让客户端首次 `snapshot` 不必等一次网络往返。
48
+ */
49
+ export declare const WORKBENCH_REFRESH_INTERVAL_MS: number;
50
+ /**
51
+ * 清单里的一条应用(**冻结契约**,字段名与 spec §4.1 一一对应,改名即破坏跨仓契约)。
52
+ *
53
+ * 两处规范化的说明:
54
+ * - `iconUrl` 是契约面的名字,网关线名是 `icon`(§4.1 的 JSON)——改名发生在 Host 这一层,
55
+ * 所以客户端只需要认 `iconUrl` 一个名字;
56
+ * - `executor` 与 `inputs[i]` / `outputs[i]` 都带 `& Record<string, unknown>`:**未知键原样透传**。
57
+ * `executor.kind` 是开放集合(`link` / `http-task`,二期 `session-task` / `embedded`),
58
+ * 客户端认不认识由它自己决定,Host 只保证「存在且是字符串」。
59
+ */
60
+ export interface WorkbenchAppView {
61
+ code: string;
62
+ displayName: string;
63
+ summary: string;
64
+ iconUrl?: string;
65
+ category: {
66
+ code: string;
67
+ name: string;
68
+ };
69
+ tags: string[];
70
+ featured: boolean;
71
+ revision: number;
72
+ /** 未知 kind / 未知键必须原样透传(spec §4.1 的演进纪律)。 */
73
+ executor: {
74
+ kind: string;
75
+ mode?: string;
76
+ } & Record<string, unknown>;
77
+ /** `type` 一期只有 `file` / `text` / `number` / `enum` / `boolean`;未知 type 同样透传。 */
78
+ inputs: ReadonlyArray<{
79
+ key: string;
80
+ type: string;
81
+ label: string;
82
+ required: boolean;
83
+ } & Record<string, unknown>>;
84
+ outputs: ReadonlyArray<{
85
+ key: string;
86
+ type: string;
87
+ } & Record<string, unknown>>;
88
+ /** `[]` | `['external']` | `['metered']`。 */
89
+ effects: string[];
90
+ }
91
+ /**
92
+ * 一份清单快照。
93
+ *
94
+ * `stale` 为 true 表示「这是上一次成功的结果,最近一次拉取失败了」——调用方可以照常展示
95
+ * (离线态),但**不得**用它做出「这个应用已被下架」的判断(那条路只允许走 `refresh` 的成功返回值)。
96
+ */
97
+ export interface WorkbenchCatalogSnapshot {
98
+ /** 清单级序号(服务端任何影响清单的写入都 +1),消费方先比它再决定要不要深比对。 */
99
+ revision: number;
100
+ /** 本次成功拉取的时刻(ISO 8601)。 */
101
+ fetchedAt: string;
102
+ apps: WorkbenchAppView[];
103
+ stale: boolean;
104
+ /** 最近一次拉取失败的原因(成功时缺省)。 */
105
+ lastError?: WorkbenchFailure;
106
+ }
107
+ /** 客户端可安全展示的失败。 */
108
+ export interface WorkbenchFailure {
109
+ code: string;
110
+ message: string;
111
+ /** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
112
+ details: object;
113
+ }
114
+ /** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
115
+ export type WorkbenchRpcResult<T> = {
116
+ readonly ok: true;
117
+ readonly value: T;
118
+ } | {
119
+ readonly ok: false;
120
+ readonly error: WorkbenchFailure;
121
+ };
122
+ /** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
123
+ export declare class WorkbenchRequestError extends Error {
124
+ readonly code: 'bad-request';
125
+ constructor(code: 'bad-request', message: string);
126
+ }
127
+ /** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
128
+ export interface WorkbenchRpcConnection {
129
+ rpc: {
130
+ handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
131
+ authority: 'loopback';
132
+ }): () => Promise<void> | void;
133
+ };
134
+ }
135
+ export interface WorkbenchRpcContext {
136
+ readonly connection: WorkbenchRpcConnection;
137
+ readonly logger: {
138
+ warn(message: string): void;
139
+ };
140
+ }
141
+ /** 网关客户端的工作台子集:只声明本频道真正调用的方法(结构兼容 `GatewayClient`)。 */
142
+ export interface WorkbenchGatewayPort {
143
+ getMyWorkbenchApps(sessionCookie: string): Promise<GatewayWorkbenchAppsView>;
144
+ }
145
+ /** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关客户端。 */
146
+ export interface WorkbenchRpcDeps {
147
+ /**
148
+ * 解析网关会话 cookie(`AI_ROUTER_SESSION`)。未登录 / 会话缺失返回 undefined。
149
+ * **返回值只在本模块内部使用,绝不出现在 RPC 响应里。**
150
+ */
151
+ resolveSession(): Promise<string | undefined>;
152
+ /** 当前 baseURL 对应的网关客户端;baseURL 未配置时返回 undefined。 */
153
+ gateway(): WorkbenchGatewayPort | undefined;
154
+ /** Host 日志出口(只记事件与错误类型,不记会话值)。 */
155
+ log?: ((message: string) => void) | undefined;
156
+ }
157
+ /** 通用失败文案:不暴露任何网关细节。 */
158
+ export declare const WORKBENCH_GENERIC_FAILURE_MESSAGE = "\u5DE5\u4F5C\u53F0\u5E94\u7528\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
159
+ /** 未登录 / 会话失效的统一文案(与用量、每日一句、MCP / 技能清单几个频道同口径)。 */
160
+ export declare const WORKBENCH_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
161
+ /** 本部署未启用工作台(网关 404)的文案:spec §10 的降级矩阵里这一行要给人可行动的提示。 */
162
+ export declare const WORKBENCH_DISABLED_MESSAGE = "\u672C\u90E8\u7F72\u672A\u542F\u7528\u5DE5\u4F5C\u53F0\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458";
163
+ /** 工作台未启用时的稳定 code(与网关客户端的映射一致,消费方按它走降级文案)。 */
164
+ export declare const WORKBENCH_DISABLED_CODE = "WORKBENCH_DISABLED";
165
+ /**
166
+ * 网关 message 消毒(与 `usage-rpc.ts` 的 `displayGatewayMessage` 同实现,MCP / 技能那两半逐字同款):
167
+ * 含 baseURL / HTTP 状态码 / URL 的原始消息一律换成稳定文案,其余(网关自己写好的中文业务文案)
168
+ * 原样保留。code 不受影响,始终原样透传。
169
+ */
170
+ export declare function displayWorkbenchMessage(code: string, message: unknown): string;
171
+ /**
172
+ * payload 校验:两个端点都**只放行空对象**(M0 没有任何入参)。
173
+ *
174
+ * 与技能清单的空入参分支逐字同款:出现任何字段都是客户端实现缺陷,直接 `bad-request`
175
+ * ——一个 `{ code: 'pdf-to-word' }` 被静默忽略,会让调用方以为「按 code 过滤」生效了。
176
+ */
177
+ export declare function parseWorkbenchPayload(endpoint: WorkbenchRpcEndpoint, raw: unknown): Record<string, never>;
178
+ /** 是否为白名单端点。 */
179
+ export declare function isWorkbenchRpcEndpoint(endpoint: string): endpoint is WorkbenchRpcEndpoint;
180
+ /**
181
+ * 清单缓存:**只有成功响应才覆写**,失败只记原因;且缓存**按会话绑定**(见 {@link WorkbenchStore.bindSession})。
182
+ *
183
+ * 「成功」的判据里含**响应形状**:200 但响应不是「带数组 `apps` 的对象」按**取数失败**处理
184
+ * (见 {@link WorkbenchStore.fetchOnce})——那种响应是中间层给的,不是服务端说「你没有应用」,
185
+ * 读成空清单会让货架在反代维护页面前清空,用户以为管理员把应用全撤了(spec §10 的离线态本意
186
+ * 是「显示上次成功缓存 + 可能不是最新」,不是「什么都不显示」)。
187
+ *
188
+ * 进程内的(不落盘)是有意的:清单是「服务端此刻认什么」的投影,跨重启的旧清单没有价值
189
+ * ——重启后消费方会立刻 `warm()` 一次,拿到新的比拿到旧的更安全。
190
+ */
191
+ export declare class WorkbenchStore {
192
+ private readonly deps;
193
+ private readonly now;
194
+ private cached;
195
+ private lastError;
196
+ /** 产生 `cached` 的那个会话 cookie(只在内存里)。会话绑定的全部依据就是它。 */
197
+ private cachedSession;
198
+ /**
199
+ * 在飞请求连同它所用的会话一起记账:并发合并**只允许发生在同一会话内**。
200
+ * 合并跨会话等于把 A 那次拉取的结果交给 B(登出 / 换账号后货架会显示上一个人的应用)。
201
+ */
202
+ private inFlight;
203
+ constructor(deps: WorkbenchRpcDeps, now?: () => number);
204
+ /**
205
+ * 当前缓存(同步读,不发请求)。从没成功过返回 undefined;
206
+ * 最近一次失败会以 `stale + lastError` 标注在上一次成功的结果上。
207
+ *
208
+ * ⚠️ **本方法不做会话绑定**——它是 spec §4.5 冻结的同步读,而当前会话只能异步解析(见
209
+ * {@link peekForCurrentSession}),同步函数里根本判不了「这份缓存还算不算我的」。
210
+ * 所以模块内唯一的正确用法是:先 `await store.peekForCurrentSession()`,再(或由它内部)`peek()`。
211
+ * 直接调 `peek()` 只适合诊断与测试断言内存状态。
212
+ */
213
+ peek(): WorkbenchCatalogSnapshot | undefined;
214
+ /**
215
+ * 「当前会话下的缓存」= 解析当前会话 → 剪枝 → {@link peek}。
216
+ *
217
+ * 与 `src/managed-skills.ts` 的同名方法逐字同款:`peek()` 是 spec §4.5 冻结的**同步**接口,而
218
+ * 「当前是谁」只有 `deps.resolveSession()` 异步拿得到,于是分工只能是「async 的一侧先绑会话,
219
+ * sync 的 `peek()` 再读」。端点实现必须走它、不许绕过。
220
+ */
221
+ peekForCurrentSession(): Promise<WorkbenchCatalogSnapshot | undefined>;
222
+ /**
223
+ * 会话绑定:缓存只属于产生它的那个会话;当前会话不是它(含**当前没有会话**)→ 立刻丢弃。
224
+ *
225
+ * 与技能清单那半逐字同款(那里写了为什么**不许**为了「两半对齐」改回 MCP 的写法):
226
+ * 绑会话**自带**——不依赖任何调用方、不依赖谁记得清缓存,登出后(当前会话为空)与换账号后
227
+ * 都不会再吐出上一个人的清单。`clear()` 也没有任何调用方(端点白名单够不到 Host 内存),
228
+ * 把安全不变量挂在一个没人调的钩子上正是已经失败过一次的做法。
229
+ *
230
+ * 它也**不**触碰「失败不覆写缓存」那条铁律:剪枝的触发条件是**会话变了**,不是「这一次拉取
231
+ * 失败了」。失败路径仍然原样保留本会话上一次成功的清单(`stale + lastError`)。
232
+ */
233
+ private bindSession;
234
+ /**
235
+ * 去网关拉一次清单并更新缓存。失败时抛异常,且**本会话的缓存原样保留**。
236
+ *
237
+ * 并发调用合并成一次请求:后台预热与消费方的手动刷新撞在一起时不必打两遍网关。
238
+ * 合并**只在同一会话内**——两个会话合并到一起,就等于把 A 的结果发给 B。
239
+ */
240
+ load(): Promise<WorkbenchCatalogSnapshot>;
241
+ /**
242
+ * 清空内存清单(登出 / 换账号调用)。
243
+ *
244
+ * ⚠️ **当前没有任何调用方**:这是 spec §4.5 冻结要求提供的接口(与技能清单那半同款),而它的
245
+ * 用途已经被**会话绑定**覆盖——缓存按会话剪枝,登出 / 换账号后再也吐不出上一个人的清单。
246
+ * 保留它是为了不改冻结接口;真要删得先改 spec §4.5。冻结接口里的成员不要悄悄删掉。
247
+ */
248
+ clear(): void;
249
+ private fetchOnce;
250
+ }
251
+ /**
252
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
253
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
254
+ */
255
+ export declare function handleWorkbenchEndpoint(endpoint: string, rawPayload: unknown, deps: WorkbenchRpcDeps, store: WorkbenchStore, signal?: AbortSignal): Promise<WorkbenchRpcResult<unknown>>;
256
+ /**
257
+ * 注册 `/hiwork-workbench` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
258
+ */
259
+ export declare function registerWorkbenchRpc(ctx: WorkbenchRpcContext, deps: WorkbenchRpcDeps, store: WorkbenchStore): () => Promise<void>;