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.
- package/README.md +6 -0
- package/lib/ai-router-client.js +310 -1
- package/lib/client.js +698 -5
- package/lib/index.js +1292 -2
- package/lib/managed-mcp.js +334 -0
- package/lib/managed-skills.js +446 -0
- package/lib/office-credentials.js +113 -0
- package/lib/office-relay.js +113 -0
- package/lib/types/ai-router-client.d.ts +314 -0
- package/lib/types/client/contracts.d.ts +63 -0
- package/lib/types/client/managed-mcp.d.ts +133 -0
- package/lib/types/client/managed-skills.d.ts +218 -0
- package/lib/types/client/managed-workbench.d.ts +212 -0
- package/lib/types/client/styles.d.ts +27 -0
- package/lib/types/client/types.d.ts +33 -1
- package/lib/types/managed-mcp.d.ts +177 -0
- package/lib/types/managed-skills.d.ts +261 -0
- package/lib/types/office-credentials.d.ts +99 -0
- package/lib/types/office-relay.d.ts +105 -0
- package/lib/types/workbench-bridge.d.ts +259 -0
- package/lib/workbench-bridge.js +447 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文档中继的 host 侧服务 `hiworkOfficeRelay`(设计见 ai-gateway 的
|
|
3
|
+
* `docs/superpowers/specs/2026-09-16-office-document-relay-design.md` §5)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它:在线编辑要求 Document Server 主动回连文档 URL;用户走 VPN / DS 在云上时
|
|
6
|
+
* 这条路径不通(2026-09-16 现场:DS 拉 http://192.168.16.105:54426 超时)。中继把文档
|
|
7
|
+
* 暂存到 DS 能访问的服务器侧,桌面端只做出站调用——**用户侧不再需要任何入向可达性**。
|
|
8
|
+
*
|
|
9
|
+
* 与 `office-credentials.ts` 同一套形态:
|
|
10
|
+
* - **网关会话 cookie 绝不离开 Host**:只在 resolveSession() 内部出现;
|
|
11
|
+
* - 失败不抛异常,返回稳定 `{ ok, code, message }`(message 已消毒,可直接展示);
|
|
12
|
+
* - 服务名 `hiworkOfficeRelay`,经 ctx.provide 发布;hiwork-office 用 ctx.get 探测,
|
|
13
|
+
* 老 core 上没有它 → 插件回落本机模式。
|
|
14
|
+
*
|
|
15
|
+
* 本模块不 import cordis 运行时:provide 走结构化的 {@link OfficeRelayContext} 端口。
|
|
16
|
+
*/
|
|
17
|
+
import { GatewayError } from "./ai-router-client.js";
|
|
18
|
+
/** cordis 服务名(hiwork-office 的 Host 半边按此名探测)。 */
|
|
19
|
+
export const OFFICE_RELAY_SERVICE = 'hiworkOfficeRelay';
|
|
20
|
+
/** 各稳定错误码的用户可见文案(网关 message 含实现细节时用它兜底)。 */
|
|
21
|
+
const OFFICE_RELAY_MESSAGE_FALLBACK = {
|
|
22
|
+
SESSION_EXPIRED: '登录状态已过期,请重新登录',
|
|
23
|
+
GATEWAY_UNREACHABLE: '网关暂时不可用,请稍后重试',
|
|
24
|
+
RELAY_DISABLED: '本部署未启用文档中继,请联系管理员',
|
|
25
|
+
RELAY_EXPIRED: '编辑会话已过期,请重新打开文档',
|
|
26
|
+
RELAY_TOO_LARGE: '文档超过在线编辑的大小上限',
|
|
27
|
+
RELAY_EXTENSION: '该文档类型不支持在线编辑',
|
|
28
|
+
RELAY_QUOTA: '同时打开的编辑会话过多,请稍后再试',
|
|
29
|
+
};
|
|
30
|
+
/** 通用失败文案:不暴露任何网关细节。 */
|
|
31
|
+
export const OFFICE_RELAY_GENERIC_FAILURE_MESSAGE = '文档中继暂时不可用,请稍后重试';
|
|
32
|
+
/** 未登录 / 会话失效的统一文案(与其余频道同口径)。 */
|
|
33
|
+
export const OFFICE_RELAY_SESSION_EXPIRED_MESSAGE = '登录状态已过期,请重新登录';
|
|
34
|
+
/**
|
|
35
|
+
* 网关 message 消毒(与 `office-credentials.ts` 同实现,各频道自带一份)。
|
|
36
|
+
*/
|
|
37
|
+
export function displayOfficeRelayMessage(code, message) {
|
|
38
|
+
const text = typeof message === 'string' ? message.trim() : '';
|
|
39
|
+
if (text === '')
|
|
40
|
+
return OFFICE_RELAY_MESSAGE_FALLBACK[code] ?? OFFICE_RELAY_GENERIC_FAILURE_MESSAGE;
|
|
41
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text)
|
|
42
|
+
|| /\bHTTP\s*\d{3}\b/i.test(text)
|
|
43
|
+
|| /\bbaseURL\b/i.test(text);
|
|
44
|
+
if (leaksImplementationDetail)
|
|
45
|
+
return OFFICE_RELAY_MESSAGE_FALLBACK[code] ?? OFFICE_RELAY_GENERIC_FAILURE_MESSAGE;
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
/** 凭据取数(无缓存;缓存策略是消费方的事)。 */
|
|
49
|
+
export class OfficeRelayBroker {
|
|
50
|
+
deps;
|
|
51
|
+
constructor(deps) {
|
|
52
|
+
this.deps = deps;
|
|
53
|
+
}
|
|
54
|
+
async withSession(run) {
|
|
55
|
+
try {
|
|
56
|
+
const sessionCookie = await this.deps.resolveSession();
|
|
57
|
+
if (sessionCookie === undefined || sessionCookie === '') {
|
|
58
|
+
return { ok: false, code: 'SESSION_EXPIRED', message: OFFICE_RELAY_SESSION_EXPIRED_MESSAGE };
|
|
59
|
+
}
|
|
60
|
+
const gateway = this.deps.gateway();
|
|
61
|
+
if (gateway === undefined) {
|
|
62
|
+
return { ok: false, code: 'GATEWAY_UNREACHABLE', message: '网关地址未配置,请先在「网关模型」里完成登录' };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: await run(sessionCookie, gateway) };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof GatewayError) {
|
|
68
|
+
return { ok: false, code: error.code, message: displayOfficeRelayMessage(error.code, error.message) };
|
|
69
|
+
}
|
|
70
|
+
this.deps.log?.(`hiwork-core: 文档中继调用失败(${error instanceof Error ? error.name : typeof error})`);
|
|
71
|
+
return { ok: false, code: 'internal', message: OFFICE_RELAY_GENERIC_FAILURE_MESSAGE };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async upload(input, signal) {
|
|
75
|
+
if (signal?.aborted === true)
|
|
76
|
+
return { ok: false, code: 'cancelled', message: '上传已取消。' };
|
|
77
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
78
|
+
const created = await gateway.createOfficeRelayDocument(cookie, input.name, input.bytes, input.mode);
|
|
79
|
+
return {
|
|
80
|
+
id: created.id,
|
|
81
|
+
documentKey: created.documentKey,
|
|
82
|
+
readUrl: created.readUrl,
|
|
83
|
+
callbackUrl: created.callbackUrl,
|
|
84
|
+
expiresAt: created.expiresAt,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async status(id, _signal) {
|
|
89
|
+
return await this.withSession(async (cookie, gateway) => await gateway.getOfficeRelayStatus(cookie, id));
|
|
90
|
+
}
|
|
91
|
+
async download(id, revision, _signal) {
|
|
92
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
93
|
+
const content = await gateway.getOfficeRelayContent(cookie, id, revision);
|
|
94
|
+
return content.notModified
|
|
95
|
+
? { notModified: true }
|
|
96
|
+
: { notModified: false, ...(content.bytes === undefined ? {} : { bytes: content.bytes }) };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async close(id, _signal) {
|
|
100
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
101
|
+
await gateway.closeOfficeRelayDocument(cookie, id);
|
|
102
|
+
return {};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 提供 host 侧服务 `hiworkOfficeRelay`;返回 disposer(交给 `ctx.effect`)。
|
|
108
|
+
*/
|
|
109
|
+
export function provideOfficeRelay(ctx, deps) {
|
|
110
|
+
const service = new OfficeRelayBroker(deps);
|
|
111
|
+
const dispose = ctx.provide(OFFICE_RELAY_SERVICE, service);
|
|
112
|
+
return typeof dispose === 'function' ? dispose : () => { };
|
|
113
|
+
}
|
|
@@ -209,6 +209,185 @@ export interface GatewayDailyQuoteResponse {
|
|
|
209
209
|
export interface GatewayDailyQuoteRandomResponse {
|
|
210
210
|
item: GatewayDailyQuoteItem | null;
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* 一条下发给当前账号的 MCP 分配(`GET /api/me/mcp-assignments` 的 `assignments[]`)。
|
|
214
|
+
*
|
|
215
|
+
* **字段名是跨仓冻结契约**(ai-hub spec §4.1):四仓(visio-mcp / ai-hub / hiwork-core /
|
|
216
|
+
* hiwork-capabilities)唯一的接口面,改一个字段名要先改 spec 并同步四仓注释。
|
|
217
|
+
*/
|
|
218
|
+
export interface GatewayMcpAssignment {
|
|
219
|
+
/** 服务端分配 id:客户端按它派生连接 key(`admin-<assignmentId>`),因此必须稳定。 */
|
|
220
|
+
assignmentId: string;
|
|
221
|
+
/** 单条版本号:管理员每次推送 +1,客户端据此判「这条要不要重新落地」。 */
|
|
222
|
+
revision: number;
|
|
223
|
+
/** 服务的 slug(ai-hub 侧唯一名,也是断言的 audience)。 */
|
|
224
|
+
slug: string;
|
|
225
|
+
displayName: string;
|
|
226
|
+
/** 服务端权威下发的装载名(工具前缀 `mcp__<serverName>__` 跨版本稳定的锚点)。 */
|
|
227
|
+
serverName: string;
|
|
228
|
+
/** `streamable-http | stdio`(与 capabilities 的传输判别联合同源)。 */
|
|
229
|
+
transport: string;
|
|
230
|
+
url: string;
|
|
231
|
+
/** 非凭证的业务请求头;`{}` 表示没有。 */
|
|
232
|
+
extraHeaders: Record<string, string>;
|
|
233
|
+
auth: GatewayMcpAssignmentAuth;
|
|
234
|
+
updatedAt: string;
|
|
235
|
+
}
|
|
236
|
+
/** 取凭证的入口:去哪换(mintUrl)、去哪还(revokeUrl)。`kind` 目前只有 `assertion`。 */
|
|
237
|
+
export interface GatewayMcpAssignmentAuth {
|
|
238
|
+
kind: string;
|
|
239
|
+
mintUrl: string;
|
|
240
|
+
revokeUrl: string;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* GET /api/me/mcp-assignments 响应。
|
|
244
|
+
* `revision` 是清单级序号(任何会影响清单的写入都 +1),客户端先比它、不同再逐条深比。
|
|
245
|
+
*/
|
|
246
|
+
export interface GatewayMcpAssignmentsView {
|
|
247
|
+
revision: number;
|
|
248
|
+
assignments: GatewayMcpAssignment[];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* GET /api/me/office-config 响应:本部署 OnlyOffice 集成的地址与共享 JWT 密钥。
|
|
252
|
+
*
|
|
253
|
+
* `jwtSecret` 是**共享的**一把密钥(Document Server 社区版只有一把),终端拿到它只为
|
|
254
|
+
* 签编辑器 config / 验保存回调;它只应在 host 侧使用,绝不进浏览器或日志。
|
|
255
|
+
*/
|
|
256
|
+
export interface GatewayOfficeConfig {
|
|
257
|
+
documentServerUrl: string;
|
|
258
|
+
jwtSecret: string;
|
|
259
|
+
/**
|
|
260
|
+
* 是否启用文档中继(管理员在网关侧的中央开关)。true 时在线编辑走
|
|
261
|
+
* 「上传 → DS 从签名 URL 拉 → 回调落版本 → 桌面端轮询取回」,不再要求
|
|
262
|
+
* DS 能回连用户电脑。旧版网关缺省该字段 → 视同 false(自动回落本机模式)。
|
|
263
|
+
*/
|
|
264
|
+
relayEnabled?: boolean;
|
|
265
|
+
}
|
|
266
|
+
/** POST /api/me/office-relay/documents 响应:DS 侧两条签名 URL。 */
|
|
267
|
+
export interface GatewayRelayDocument {
|
|
268
|
+
id: string;
|
|
269
|
+
documentKey: string;
|
|
270
|
+
readUrl: string;
|
|
271
|
+
callbackUrl: string;
|
|
272
|
+
expiresAt: string;
|
|
273
|
+
}
|
|
274
|
+
/** GET /api/me/office-relay/documents/:id 响应。 */
|
|
275
|
+
export interface GatewayRelayStatus {
|
|
276
|
+
state: string;
|
|
277
|
+
revision: number;
|
|
278
|
+
contentSha256: string;
|
|
279
|
+
sizeBytes: number;
|
|
280
|
+
expiresAt: string;
|
|
281
|
+
savedAt: string;
|
|
282
|
+
}
|
|
283
|
+
/** GET …/content 的结果:204 表示客户端手里的版本已是最新。 */
|
|
284
|
+
export interface GatewayRelayContent {
|
|
285
|
+
notModified: boolean;
|
|
286
|
+
revision: number;
|
|
287
|
+
bytes?: Uint8Array;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* POST /api/me/mcp-assertions 响应:一张 **120 秒**的身份断言(RS256 JWT)+
|
|
291
|
+
* 去哪换令牌。断言本身不是凭据(换不成令牌就是废纸),所以它可以出现在这里;
|
|
292
|
+
* 真正的 per-user 令牌由 MCP 服务在 exchange 时一次性返回,网关与核心都拿不到。
|
|
293
|
+
*/
|
|
294
|
+
export interface GatewayMcpAssertionTicket {
|
|
295
|
+
assertion: string;
|
|
296
|
+
mintUrl: string;
|
|
297
|
+
revokeUrl: string;
|
|
298
|
+
expiresAt: string;
|
|
299
|
+
serverName: string;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* GET /api/me/skill-assignments 的一条分配(对齐 ai-hub spec §4.1 的冻结契约)。
|
|
303
|
+
*
|
|
304
|
+
* `skillName` 是**服务端权威**下发的装载名,也是受管目录名与模型侧 `/<名>` 的唯一来源;
|
|
305
|
+
* `contentSha256` 是「要不要重装」的唯一依据(`version` 只是给人看的标签,客户端**不得**
|
|
306
|
+
* 拿它比大小——它甚至是服务端派生值、库里没有这一列)。
|
|
307
|
+
*/
|
|
308
|
+
export interface GatewaySkillAssignment {
|
|
309
|
+
assignmentId: string;
|
|
310
|
+
/** 单条版本号:管理员每次推送 +1。 */
|
|
311
|
+
revision: number;
|
|
312
|
+
/** 管理端唯一名(只服务 URL 与展示,不是装载名)。 */
|
|
313
|
+
slug: string;
|
|
314
|
+
displayName: string;
|
|
315
|
+
skillName: string;
|
|
316
|
+
description: string;
|
|
317
|
+
contentSha256: string;
|
|
318
|
+
sizeBytes: number;
|
|
319
|
+
/** 展示用标签;服务端可缺省,缺省时为空串。 */
|
|
320
|
+
version: string;
|
|
321
|
+
updatedAt: string;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* GET /api/me/skill-assignments 响应。**响应体里没有包体**(清单与包体分开两条端点)。
|
|
325
|
+
* `revision` 是清单级序号(任何影响清单的写入都 +1),客户端先比它、不同再逐条深比。
|
|
326
|
+
*/
|
|
327
|
+
export interface GatewaySkillAssignmentsView {
|
|
328
|
+
revision: number;
|
|
329
|
+
assignments: GatewaySkillAssignment[];
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* GET /api/me/skill-packages/:assignmentId 的结果:ZIP 原始字节 + 服务端声明的身份。
|
|
333
|
+
*
|
|
334
|
+
* `contentSha256` 取自响应头 `X-HiWork-Content-Sha256`,是**入库时算定**的那份哈希
|
|
335
|
+
* (spec §4.2:服务端只存取、不在响应时重新打包,否则 Go 版本一升级哈希就会漂,
|
|
336
|
+
* 客户端的内容校验会变成误报);客户端必须拿它与收到的字节比对后才落盘(spec §6)。
|
|
337
|
+
*/
|
|
338
|
+
export interface GatewaySkillPackage {
|
|
339
|
+
skillName: string;
|
|
340
|
+
contentSha256: string;
|
|
341
|
+
bytes: Uint8Array;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* GET /api/me/workbench-apps 里的一条应用(spec §4.1 的字段,`icon` 是网关侧的线名)。
|
|
345
|
+
*
|
|
346
|
+
* 这份声明刻意**宽松**(除 `code` 外全部可选,`executor` / `inputs` / `outputs` 用开放记录):
|
|
347
|
+
* 清单是**服务端演进**的,spec §4.1 的演进纪律要求「未知字段忽略、未知 `executor.kind` /
|
|
348
|
+
* `inputs[].type` 原样透传」。形状判据与逐字段规范化在消费端 `/hiwork-workbench` 的 Host 半边
|
|
349
|
+
* (`src/workbench-bridge.ts`)——本层只做传输与状态码映射,把 JSON 直接断言成契约类型。
|
|
350
|
+
*/
|
|
351
|
+
export interface GatewayWorkbenchApp {
|
|
352
|
+
/** 应用唯一标识(管理端唯一名,也是卡片与服务端路径的坐标)。 */
|
|
353
|
+
code: string;
|
|
354
|
+
/** 单条版本号:管理员每次改动 +1。 */
|
|
355
|
+
revision?: number;
|
|
356
|
+
displayName?: string;
|
|
357
|
+
summary?: string;
|
|
358
|
+
/** 图标地址(网关线名就是 `icon`,不是 `iconUrl`;契约面的规范名见 `WorkbenchAppView`)。 */
|
|
359
|
+
icon?: string;
|
|
360
|
+
category?: {
|
|
361
|
+
code?: string;
|
|
362
|
+
name?: string;
|
|
363
|
+
};
|
|
364
|
+
tags?: string[];
|
|
365
|
+
featured?: boolean;
|
|
366
|
+
/**
|
|
367
|
+
* 执行器声明。`kind` 是**开放集合**(一期 `link` / `http-task`,二期 `session-task` /
|
|
368
|
+
* `embedded`):客户端遇到不认识的 kind 必须置灰 + 提示升级,绝不崩,所以这里不收敛成联合类型。
|
|
369
|
+
*/
|
|
370
|
+
executor?: {
|
|
371
|
+
kind?: string;
|
|
372
|
+
mode?: string;
|
|
373
|
+
} & Record<string, unknown>;
|
|
374
|
+
/** 输入项(形状随 `type` 演进:`accept` / `options` / `default` 只对某些 type 有意义)。 */
|
|
375
|
+
inputs?: Array<Record<string, unknown>>;
|
|
376
|
+
outputs?: Array<Record<string, unknown>>;
|
|
377
|
+
/** `[]` | `['external']` | `['metered']`(只影响确认策略与风险提示,不改变契约形状)。 */
|
|
378
|
+
effects?: string[];
|
|
379
|
+
}
|
|
380
|
+
/** GET /api/me/workbench-apps 响应(spec §4.1)。 */
|
|
381
|
+
export interface GatewayWorkbenchAppsView {
|
|
382
|
+
/**
|
|
383
|
+
* 清单**整体**的契约版本(与 `revision` 不是一回事:前者是契约演进,后者是内容变更)。
|
|
384
|
+
* 本轮不据此拒收任何响应,原样透传给消费方自行判断。
|
|
385
|
+
*/
|
|
386
|
+
schemaVersion?: number;
|
|
387
|
+
/** 清单级序号:服务端任何影响清单的写入都 +1(客户端先比它,不同再逐条深比)。 */
|
|
388
|
+
revision: number;
|
|
389
|
+
apps: GatewayWorkbenchApp[];
|
|
390
|
+
}
|
|
212
391
|
/** 网关客户端端口:orchestration 只依赖此接口。 */
|
|
213
392
|
export interface GatewayClient {
|
|
214
393
|
/**
|
|
@@ -248,6 +427,26 @@ export interface GatewayClient {
|
|
|
248
427
|
getMyKeyRecharges(sessionCookie: string, keyId?: string, modelName?: string): Promise<GatewayRecharge[]>;
|
|
249
428
|
/** 网关权威计价:只回金额与生效单价,客户端不做任何算式。 */
|
|
250
429
|
estimateCost(sessionCookie: string, items: GatewayCostEstimateItem[]): Promise<GatewayCostEstimateResponse>;
|
|
430
|
+
/** 管理员下发给当前账号的 MCP 清单(响应里没有令牌)。 */
|
|
431
|
+
getMyMcpAssignments(sessionCookie: string): Promise<GatewayMcpAssignmentsView>;
|
|
432
|
+
/** 本部署 OnlyOffice 集成配置(地址 + 共享 JWT 密钥 + 中继开关;部署未配置时 503)。 */
|
|
433
|
+
getMyOfficeConfig(sessionCookie: string): Promise<GatewayOfficeConfig>;
|
|
434
|
+
/** 上传文档到中继(原始字节 + 文件名头),拿回 DS 侧签名 URL。 */
|
|
435
|
+
createOfficeRelayDocument(sessionCookie: string, name: string, bytes: Uint8Array, mode?: 'view' | 'edit'): Promise<GatewayRelayDocument>;
|
|
436
|
+
/** 轮询中继会话状态(revision 单调递增即"有新版本")。 */
|
|
437
|
+
getOfficeRelayStatus(sessionCookie: string, id: string): Promise<GatewayRelayStatus>;
|
|
438
|
+
/** 取回当前版本字节;`revision` 用于"我手里已是这版"的短路(204)。 */
|
|
439
|
+
getOfficeRelayContent(sessionCookie: string, id: string, revision?: number): Promise<GatewayRelayContent>;
|
|
440
|
+
/** 关闭中继会话(立即清理字节)。 */
|
|
441
|
+
closeOfficeRelayDocument(sessionCookie: string, id: string): Promise<void>;
|
|
442
|
+
/** 为某条分配签一张 120 秒的身份断言(拿它去 MCP 服务换 per-user 令牌)。 */
|
|
443
|
+
issueMyMcpAssertion(sessionCookie: string, assignmentId: string): Promise<GatewayMcpAssertionTicket>;
|
|
444
|
+
/** 管理员下发给当前账号的技能清单(响应里没有包体——包体走 {@link getMySkillPackage})。 */
|
|
445
|
+
getMySkillAssignments(sessionCookie: string): Promise<GatewaySkillAssignmentsView>;
|
|
446
|
+
/** 取某条分配的技能包体(ZIP 字节 + 服务端声明的 sha256);分配不存在 404、未启用 503。 */
|
|
447
|
+
getMySkillPackage(sessionCookie: string, assignmentId: string): Promise<GatewaySkillPackage>;
|
|
448
|
+
/** 管理员登记并分配给当前账号的工作台应用清单(spec §4.1 的冻结契约;响应里没有令牌)。 */
|
|
449
|
+
getMyWorkbenchApps(sessionCookie: string): Promise<GatewayWorkbenchAppsView>;
|
|
251
450
|
logout(sessionCookie: string): Promise<void>;
|
|
252
451
|
}
|
|
253
452
|
/**
|
|
@@ -313,6 +512,121 @@ export declare class AiRouterClient implements GatewayClient {
|
|
|
313
512
|
* `input_tokens` 不含 cache;任何折算与单价计算都在网关侧完成。
|
|
314
513
|
*/
|
|
315
514
|
estimateCost(sessionCookie: string, items: GatewayCostEstimateItem[]): Promise<GatewayCostEstimateResponse>;
|
|
515
|
+
/**
|
|
516
|
+
* GET /api/me/mcp-assignments:管理员下发给当前账号的 MCP 清单。
|
|
517
|
+
*
|
|
518
|
+
* 不复用 {@link getMeJson}:那条路径把 403 映射成 `NO_KEY_ASSIGNED`(「尚未被分配
|
|
519
|
+
* 虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key 的账号照样应该有
|
|
520
|
+
* 清单(分 MCP 与分 Key 是两件事)。沿用它的 403 文案会把「没有 Key」错报成原因。
|
|
521
|
+
*/
|
|
522
|
+
getMyMcpAssignments(sessionCookie: string): Promise<GatewayMcpAssignmentsView>;
|
|
523
|
+
/**
|
|
524
|
+
* POST /api/me/office-relay/documents:上传文档字节到中继。
|
|
525
|
+
*
|
|
526
|
+
* 文件名走 `X-Office-Document-Name` 头(HTTP 头只能带 ASCII,用 encodeURIComponent
|
|
527
|
+
* 编码;网关侧 PathUnescape 解码)。状态码映射成稳定 code,供插件层给用户文案。
|
|
528
|
+
*/
|
|
529
|
+
createOfficeRelayDocument(sessionCookie: string, name: string, bytes: Uint8Array, mode?: 'view' | 'edit'): Promise<GatewayRelayDocument>;
|
|
530
|
+
/** GET /api/me/office-relay/documents/:id:轮询状态。 */
|
|
531
|
+
getOfficeRelayStatus(sessionCookie: string, id: string): Promise<GatewayRelayStatus>;
|
|
532
|
+
/**
|
|
533
|
+
* GET /api/me/office-relay/documents/:id/content[?v=N]:取回当前版本字节。
|
|
534
|
+
* 204 表示"你手里的版本已是最新"(不是错误)。
|
|
535
|
+
*/
|
|
536
|
+
getOfficeRelayContent(sessionCookie: string, id: string, revision?: number): Promise<GatewayRelayContent>;
|
|
537
|
+
/** DELETE /api/me/office-relay/documents/:id:关闭会话(幂等)。 */
|
|
538
|
+
closeOfficeRelayDocument(sessionCookie: string, id: string): Promise<void>;
|
|
539
|
+
/**
|
|
540
|
+
* GET /api/me/office-config:本部署 OnlyOffice 集成(Document Server 地址 + 共享 JWT 密钥)。
|
|
541
|
+
*
|
|
542
|
+
* 与 {@link getMyMcpAssignments} 同样的理由不复用 `getMeJson`:那条路径把 403 映射成
|
|
543
|
+
* `NO_KEY_ASSIGNED`,而本端点只看会话(分集成配置与分 Key 是两件事)。503 是
|
|
544
|
+
* 「部署未配置」的稳定信号,单独映射——消费方(hiwork-office)据此提示联系管理员
|
|
545
|
+
* 并回退本地手工配置,而不是报一句含糊的「网关不可达」。
|
|
546
|
+
*/
|
|
547
|
+
getMyOfficeConfig(sessionCookie: string): Promise<GatewayOfficeConfig>;
|
|
548
|
+
/**
|
|
549
|
+
* POST /api/me/mcp-assertions:为某条分配签一张 120 秒断言。
|
|
550
|
+
*
|
|
551
|
+
* 三种拒绝各有确定含义(服务端 handler 的 404/409/503),必须分开映射——否则
|
|
552
|
+
* 客户端只会看到一句「取断言失败」,既分不清「被撤销了」(该卸连接)还是
|
|
553
|
+
* 「服务停用了」(该重试)还是「部署没配私钥」(找管理员)。
|
|
554
|
+
*/
|
|
555
|
+
issueMyMcpAssertion(sessionCookie: string, assignmentId: string): Promise<GatewayMcpAssertionTicket>;
|
|
556
|
+
/**
|
|
557
|
+
* GET /api/me/skill-assignments:管理员下发给当前账号的技能清单(spec §4.1)。
|
|
558
|
+
*
|
|
559
|
+
* 与 {@link getMyMcpAssignments} 同样的理由不复用 `getMeJson`:那条路径把 403 映射成
|
|
560
|
+
* `NO_KEY_ASSIGNED`(「尚未被分配虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key
|
|
561
|
+
* 的账号照样应该有技能清单(分技能与分 Key 是两件事)。
|
|
562
|
+
*
|
|
563
|
+
* **这条路径上只有 401 有独立语义,其余全部收敛成 `GATEWAY_UNREACHABLE`**,理由逐条:
|
|
564
|
+
* - 401 = 会话过期/缺失(`middleware` 的会话鉴权)——唯一一条服务端**真的会为这条路由**发出的
|
|
565
|
+
* 拒绝,也是唯一一条能让人做点什么(重新登录)的;
|
|
566
|
+
* - 403 不会出现:这条路由的鉴权只有会话,没有「有没有被分配虚拟 KEY」那一层(正因为如此,
|
|
567
|
+
* 这里才不能复用 `getMeJson`,那个函数会把 403 读成「没分配 KEY」);
|
|
568
|
+
* - 404 不是这条路由的响应形状:清单**永远**是 200 + 一份数组,某条技能被停用 / 被撤销是
|
|
569
|
+
* 「不在数组里」,不是 404(`MyAssignments` 逐条过滤,不会为单条抛错)。真收到 404 只可能是
|
|
570
|
+
* 「路径压根没被路由」——老网关没有这条路由的时候就是这个样子,而它与反代配错无法区分,
|
|
571
|
+
* 所以诚实读法是网关不可达,不是「管理员把技能全撤了」;
|
|
572
|
+
* - 503 不会出现:员工端这条路由**无条件注册**,服务端没有任何「技能下发未启用」的闸
|
|
573
|
+
* (`getMySkillPackage` 那条 503 也一样没有生产者,见那里的注释),整份响应由 service
|
|
574
|
+
* 直接产出;
|
|
575
|
+
* - 其余(5xx / 反代错误页 / 维护页)同理:没有可解释的拒绝语义,收敛成网关不可达。
|
|
576
|
+
*
|
|
577
|
+
* 形状校验(`assignments` 必须是数组)**不在这里**:本方法只做传输与状态码映射,把 JSON 直接
|
|
578
|
+
* 断言成契约类型。真正的形状判据在消费端 `/hiwork-managed-skills` 的 Host 半边
|
|
579
|
+
* (`src/managed-skills.ts` 的 `fetchOnce`)——那里才决定「这一份算不算成功拉到的清单」。
|
|
580
|
+
*/
|
|
581
|
+
getMySkillAssignments(sessionCookie: string): Promise<GatewaySkillAssignmentsView>;
|
|
582
|
+
/**
|
|
583
|
+
* GET /api/me/skill-packages/:assignmentId:取一条分配的包体字节(spec §4.2)。
|
|
584
|
+
*
|
|
585
|
+
* **这条路径是「两个端口」里的另一条**,它才有独立的拒绝语义(与服务端 handler 一一对应):
|
|
586
|
+
* - 404 = 这条分配不在这个账号名下(已被撤销 / 从未属于它,`ErrSkillAssignmentNotForUser`
|
|
587
|
+
* ——技能被停用 / 分配被停用也走这一条,服务端刻意不区分),
|
|
588
|
+
* - 503 = 部署未启用技能下发(**保留的防御性映射:当前没有生产者**,见下)。
|
|
589
|
+
* 两者必须分开映射,否则客户端只会看到一句「拉包失败」。用途是**别把原因咽下去**:404 让人
|
|
590
|
+
* 停止重试、把原因显示出来(「这条分配已失效,稍后同步会重新核对」),503 让人知道该找管理员。
|
|
591
|
+
* ⚠️ 事实核对(spec §4.5):`SKILL_DISTRIBUTION_DISABLED` 全仓**只有本方法这一个生产方**
|
|
592
|
+
* (本方法里 503 那一支),而 ai-hub 目前**没有任何**会产生这个 503 的代码——员工端两条路由在
|
|
593
|
+
* `cmd/server/main.go` 里无条件注册,技能 / 分配被停用在员工端走 404
|
|
594
|
+
* (`ErrSkillAssignmentNotForUser`)。映射保留是**防御性 + 与 MCP 同形**(那里
|
|
595
|
+
* `MCP_ASSERTION_DISABLED` 真有生产者:`me_mcp_handler.go` 的 503),别拿它当「会发生的分支」
|
|
596
|
+
* 去设计行为。
|
|
597
|
+
* ⚠️ **这两条都只是「失败的原因」,不是删除本地受管技能的依据**:删除的唯一权柄是「成功拉到的
|
|
598
|
+
* 清单」(spec §6 的唯一安全不变量),由 Host 的清单端点产出。本文件里没有任何一处据 404 或
|
|
599
|
+
* `contentSha256` 去删本地技能(全仓 `SKILL_ASSIGNMENT_GONE` 只有生产方、类型联合、文案表与
|
|
600
|
+
* 测试,没有消费方)。
|
|
601
|
+
* 404 而不泄露「这条分配是否存在」是服务端的选择(§4.2 明确要 404 不要 403)。
|
|
602
|
+
*
|
|
603
|
+
* 本调用方从不发 `If-None-Match`(§4.2 说 304 那条是给管理端与 curl 排障用的),所以 304
|
|
604
|
+
* 不在这条路径上;真收到(反代插话)会落进下面那条 → 网关不可达。
|
|
605
|
+
*
|
|
606
|
+
* 包体**只在本函数里短暂存在**:交给 Host 侧频道(src/managed-skills.ts)后立即 base64
|
|
607
|
+
* 出去,网关会话 cookie 全程不出 Host,浏览器侧永远拿不到字节。
|
|
608
|
+
*/
|
|
609
|
+
getMySkillPackage(sessionCookie: string, assignmentId: string): Promise<GatewaySkillPackage>;
|
|
610
|
+
/**
|
|
611
|
+
* GET /api/me/workbench-apps:管理员登记并分配给当前账号的工作台应用清单(spec §4.1)。
|
|
612
|
+
*
|
|
613
|
+
* 与技能清单同款**不复用 `getMeJson`**:那条路径把 403 映射成 `NO_KEY_ASSIGNED`(「尚未被分配
|
|
614
|
+
* 虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key 的账号照样应该有工作台。
|
|
615
|
+
*
|
|
616
|
+
* 状态码映射(**与技能清单有一处刻意的不同**,理由在下面):
|
|
617
|
+
* - 401 = 会话过期/缺失:唯一一条能让人做点什么(重新登录)的拒绝;
|
|
618
|
+
* - **404 = 本部署未启用工作台**:这条路由在网关侧是「有配置才有」的(spec §10 的降级矩阵:
|
|
619
|
+
* 「网关未配置 workbench → 回 404」),因此 404 不是「应用清单为空」也不是网络故障,
|
|
620
|
+
* 而是一个可展示的功能状态——客户端据此显示「本部署尚未启用工作台」而不是报错。
|
|
621
|
+
* ⚠️ 技能清单那半**刻意**没有这一档(那里 404 只能是路径没被路由,收敛成网关不可达);
|
|
622
|
+
* 两边的差别是服务端语义的差别,不是实现风格的差别,别顺手对齐;
|
|
623
|
+
* - 其余(403 / 5xx / 反代错误页 / 维护页)一律 `GATEWAY_UNREACHABLE`:没有可解释的拒绝语义。
|
|
624
|
+
*
|
|
625
|
+
* 形状校验(`apps` 必须是数组)**不在这里**:本方法只做传输与状态码映射,把 JSON 直接断言成
|
|
626
|
+
* 契约类型;真正的形状判据在消费端 `/hiwork-workbench` 的 Host 半边(`src/workbench-bridge.ts`),
|
|
627
|
+
* 那里才决定「这一份算不算成功拉到的清单」。
|
|
628
|
+
*/
|
|
629
|
+
getMyWorkbenchApps(sessionCookie: string): Promise<GatewayWorkbenchAppsView>;
|
|
316
630
|
/**
|
|
317
631
|
* /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
|
|
318
632
|
* 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* 即可(见 `hiwork-automation` 的 `contracts.ts`),运行期只按服务名读取。
|
|
22
22
|
*/
|
|
23
23
|
import type { FeatureRegistry } from './feature-registry.ts';
|
|
24
|
+
import type { ManagedMcpAssertion, ManagedMcpSnapshot } from './managed-mcp.ts';
|
|
25
|
+
import type { ManagedSkillsFace } from './managed-skills.ts';
|
|
26
|
+
import type { ManagedWorkbenchFace } from './managed-workbench.ts';
|
|
24
27
|
/** cordis 服务名:`hiwork-core` 的 client 半边提供,业务插件按名注入。 */
|
|
25
28
|
export declare const HIWORK_FEATURE_CENTER_SERVICE = "hiworkFeatureCenter";
|
|
26
29
|
/**
|
|
@@ -38,4 +41,64 @@ export interface HiWorkCoreClientFace {
|
|
|
38
41
|
readonly openNativeSession: (sessionId: string) => void;
|
|
39
42
|
/** 回到原生「新会话」首页:先退出 takeover,再触发原生新会话动作。 */
|
|
40
43
|
readonly openNativeHome: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* 管理员下发的 MCP(连接器域消费,见 `src/client/managed-mcp.ts`)。
|
|
46
|
+
*
|
|
47
|
+
* **可选成员**:老版本 hiwork-core 没有这个面、新版本 hiwork-capabilities 遇到它
|
|
48
|
+
* 时只会看到 `undefined`(能力中心照常工作,只是没有受管连接);反过来新 core 遇到
|
|
49
|
+
* 老 capabilities 也只是没人来取。两侧因此不必严格同发一个版本。
|
|
50
|
+
*
|
|
51
|
+
* 这一面**只交出清单与短期断言**:换 per-user 令牌是连接器域自己拿断言去 MCP 服务
|
|
52
|
+
* 做的,核心既不换也不存。
|
|
53
|
+
*/
|
|
54
|
+
readonly managedMcp?: {
|
|
55
|
+
/** 本地镜像(同步读,可能是上一次成功的结果)。**不要拿它做删除决策**。 */
|
|
56
|
+
readonly snapshot: () => ManagedMcpSnapshot | undefined;
|
|
57
|
+
/** 本地镜像是否陈旧(或从未同步过)。 */
|
|
58
|
+
readonly isStale: (ttlMs?: number) => boolean;
|
|
59
|
+
/** 用 Host 的预热缓存拿一份清单(命中缓存时零网络请求)。 */
|
|
60
|
+
readonly warm: (signal?: AbortSignal) => Promise<ManagedMcpSnapshot>;
|
|
61
|
+
/** 强制拉一次,返回**刚拉到的**清单;失败抛错。只有它的返回值保证是当下的真相。 */
|
|
62
|
+
readonly refresh: (signal?: AbortSignal) => Promise<ManagedMcpSnapshot>;
|
|
63
|
+
/** 取一张 120 秒的身份断言 + 去哪换令牌。 */
|
|
64
|
+
readonly assertion: (assignmentId: string, signal?: AbortSignal) => Promise<ManagedMcpAssertion>;
|
|
65
|
+
/** 订阅镜像变化(登出 / 换账号时会被清空)。返回退订函数。 */
|
|
66
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 管理员下发的技能(技能域消费,见 `src/client/managed-skills.ts`;契约见 ai-hub 的
|
|
70
|
+
* spec `docs/superpowers/specs/2026-09-17-skill-distribution-design.md` §4.5)。
|
|
71
|
+
*
|
|
72
|
+
* **可选成员**(与 `managedMcp` 同款的可选解耦):新 core 遇到老 capabilities 只是
|
|
73
|
+
* **没人来取**(能力中心照常工作,只是没有受管技能那一块);老 core 遇到新 capabilities
|
|
74
|
+
* 只是**没有这个面**(`face.managedSkills` 为 `undefined`,消费方按「这个部署不支持受管
|
|
75
|
+
* 技能」降级)。两侧因此不必严格同发一个版本——发布顺序(spec §7)允许 core 先发。
|
|
76
|
+
*
|
|
77
|
+
* 这一面**只交出清单与包体字节**:技能链路没有服务端调用、没有凭证,所以既没有断言,
|
|
78
|
+
* 也没有任何票据;落盘与完整性校验在技能域,核心既不落盘也不解包。
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **`snapshot` 不参与同步**:它可能是上一次成功的结果(管理员刚撤掉的分配还在里面),
|
|
81
|
+
* 拿它做删除决策就会把「管理员撤销了」误做成「这次没拉到」——删除只允许走 `refresh`
|
|
82
|
+
* 成功返回的清单(spec §6 的唯一安全不变量,`src/client/managed-mcp.ts:9-12` 同款)。
|
|
83
|
+
*/
|
|
84
|
+
readonly managedSkills?: ManagedSkillsFace;
|
|
85
|
+
/**
|
|
86
|
+
* 管理员登记并分配给当前账号的**工作台应用清单**(工作台域消费,见 `src/client/managed-workbench.ts`;
|
|
87
|
+
* 契约见 hiwork-workbench 的 spec `docs/superpowers/specs/2026-09-17-hiwork-workbench-design.md`
|
|
88
|
+
* §4.1 / §4.5)。
|
|
89
|
+
*
|
|
90
|
+
* **可选成员**(与 `managedMcp` / `managedSkills` 同款的两级可选语义):新 core 遇到老
|
|
91
|
+
* workbench 只是**没人来取**;老 core 遇到新 workbench 只是**没有这个面**
|
|
92
|
+
* (`face.managedWorkbench` 为 `undefined`,工作台按「本部署尚未启用工作台」降级,spec §10)。
|
|
93
|
+
* 两侧因此不必严格同发一个版本——发布顺序(spec §11 的硬约束)允许 core 先发。
|
|
94
|
+
*
|
|
95
|
+
* **M0 只交出清单**:本面没有 `submit` / `poll` / `cancel` / `materialize`(那是 M1 的任务运行时,
|
|
96
|
+
* spec §4.5 的完整面在那时才补齐)。工作台消费方据此判断能力:这几个成员不存在,就是
|
|
97
|
+
* 「这个 core 只能看货架」。
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ **`snapshot` 不参与下架决策**:它可能是上一次成功的结果(管理员刚撤掉的条目还在里面),
|
|
100
|
+
* 拿它做删除决策就会把「管理员撤销了」误做成「这次没拉到」——「这个应用不在了」只允许走
|
|
101
|
+
* `refresh` 成功返回的清单(spec §4.5 的冻结纪律,`src/client/managed-mcp.ts:9-12` 同款)。
|
|
102
|
+
*/
|
|
103
|
+
readonly managedWorkbench?: ManagedWorkbenchFace;
|
|
41
104
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「管理员下发的 MCP」客户端取数层:经 `/hiwork-managed-mcp` loopback RPC 拿到
|
|
3
|
+
* 清单与短期断言,交给连接器域(hiwork-capabilities)去换令牌并落地。
|
|
4
|
+
*
|
|
5
|
+
* 铁律:
|
|
6
|
+
* - 客户端**不持有**会话 cookie,也**不直接 fetch 网关**——一切经 Host 频道;
|
|
7
|
+
* - 客户端**不做 exchange、不持有 MCP 令牌**:本层交出去的只有「清单 + 一张刚取的
|
|
8
|
+
* 断言 + mintUrl」,per-user 令牌只在连接器域内部存在;
|
|
9
|
+
* - **只有 {@link ManagedMcpStore.refresh} 成功返回的清单才允许用作删除本地受管连接的
|
|
10
|
+
* 依据**;{@link ManagedMcpStore.snapshot} 可能与界面展示共享,但它可能是上一次
|
|
11
|
+
* 成功的结果(服务端刚撤掉的分配还在里面),拿它做删除决策就会把「撤销」误做成
|
|
12
|
+
* 「没同步上」——反过来,拉取失败时它又是唯一的真相来源,所以两个入口必须分开。
|
|
13
|
+
*
|
|
14
|
+
* client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import
|
|
15
|
+
* Host 代码(类型按结构重声明,字段名与契约一致)。
|
|
16
|
+
*/
|
|
17
|
+
/** RPC 频道;必须与 Host `src/managed-mcp.ts` 的 `MANAGED_MCP_RPC_CHANNEL` 一致(由测试锁定)。 */
|
|
18
|
+
export declare const MANAGED_MCP_CHANNEL = "/hiwork-managed-mcp";
|
|
19
|
+
/** 端点白名单;必须与 Host `MANAGED_MCP_RPC_ENDPOINTS` 一致(由测试锁定)。 */
|
|
20
|
+
export declare const MANAGED_MCP_ENDPOINTS: readonly ["snapshot", "refresh", "assertion"];
|
|
21
|
+
export type ManagedMcpEndpoint = (typeof MANAGED_MCP_ENDPOINTS)[number];
|
|
22
|
+
/**
|
|
23
|
+
* 本地镜像的「陈旧」阈值:5 分钟。
|
|
24
|
+
* 触发时机里的「打开连接器页且 >5min stale 就同步一次」用它判定——短于它只会让
|
|
25
|
+
* 用户每次切页都打一遍网关,长于它则界面上的清单明显落后于管理员的推送。
|
|
26
|
+
*/
|
|
27
|
+
export declare const MANAGED_MCP_STALE_MS: number;
|
|
28
|
+
/** 一条分配(对齐 ai-hub spec §4.1 的冻结契约)。 */
|
|
29
|
+
export interface ManagedMcpAssignment {
|
|
30
|
+
assignmentId: string;
|
|
31
|
+
revision: number;
|
|
32
|
+
slug: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
/** 服务端权威下发的装载名:工具前缀 `mcp__<serverName>__` 跨版本稳定的锚点。 */
|
|
35
|
+
serverName: string;
|
|
36
|
+
/** `streamable-http | stdio`;认不认识的判断在连接器域(不认识的整条跳过并报冲突)。 */
|
|
37
|
+
transport: string;
|
|
38
|
+
url: string;
|
|
39
|
+
extraHeaders: Record<string, string>;
|
|
40
|
+
auth: {
|
|
41
|
+
kind: string;
|
|
42
|
+
mintUrl: string;
|
|
43
|
+
revokeUrl: string;
|
|
44
|
+
};
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
}
|
|
47
|
+
/** 一份清单快照。`stale` 为 true 表示最近一次拉取失败,展示的是上一次成功的结果。 */
|
|
48
|
+
export interface ManagedMcpSnapshot {
|
|
49
|
+
revision: number;
|
|
50
|
+
fetchedAt: string;
|
|
51
|
+
assignments: ManagedMcpAssignment[];
|
|
52
|
+
stale: boolean;
|
|
53
|
+
lastError?: {
|
|
54
|
+
code: string;
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** 取断言的结果:一张 120 秒的门票 + 去哪换令牌。 */
|
|
59
|
+
export interface ManagedMcpAssertion {
|
|
60
|
+
assertion: string;
|
|
61
|
+
mintUrl: string;
|
|
62
|
+
revokeUrl: string;
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
serverName: string;
|
|
65
|
+
}
|
|
66
|
+
/** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
|
|
67
|
+
export interface ManagedMcpRpcPort {
|
|
68
|
+
call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
69
|
+
}
|
|
70
|
+
/** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
|
|
71
|
+
export declare class ManagedMcpRpcError extends Error {
|
|
72
|
+
readonly code: string;
|
|
73
|
+
constructor(code: string, message: string);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 解开 DSH Connection 的结果信封。
|
|
77
|
+
* 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
|
|
78
|
+
* 通用失败,绝不把未知结构当数据用。
|
|
79
|
+
*/
|
|
80
|
+
export declare function unwrapManagedMcpResult<T>(value: unknown): T;
|
|
81
|
+
/** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
|
|
82
|
+
export declare function managedMcpErrorOf(error: unknown): {
|
|
83
|
+
code: string;
|
|
84
|
+
message: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* 受管 MCP 的取数仓库。
|
|
88
|
+
*
|
|
89
|
+
* 本地镜像(`snapshot()` 同步读)是有必要的:DSH 的 client 半边在不同插件之间只通过
|
|
90
|
+
* 这个面同步取状态,而清单本身是异步来的。镜像**只由成功响应覆写**,失败时不改动它。
|
|
91
|
+
*/
|
|
92
|
+
export declare class ManagedMcpStore {
|
|
93
|
+
private readonly rpc;
|
|
94
|
+
private current;
|
|
95
|
+
private readonly listeners;
|
|
96
|
+
private inFlight;
|
|
97
|
+
constructor(rpc: ManagedMcpRpcPort);
|
|
98
|
+
/** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做删除决策**(见文件头)。 */
|
|
99
|
+
snapshot(): ManagedMcpSnapshot | undefined;
|
|
100
|
+
/** 本地镜像是否已经陈旧(或从未同步过)。 */
|
|
101
|
+
isStale(ttlMs?: number, now?: number): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* 强制同步一次并返回**刚拉到的**清单。
|
|
104
|
+
*
|
|
105
|
+
* 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次
|
|
106
|
+
* 网络请求成功了」,用它做删除决策才是安全的;读镜像可能读到上一次的结果。
|
|
107
|
+
* 失败时抛错且**不动镜像**——调用方据此保持现有的连接一条不删。
|
|
108
|
+
*/
|
|
109
|
+
refresh(signal?: AbortSignal): Promise<ManagedMcpSnapshot>;
|
|
110
|
+
/**
|
|
111
|
+
* 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
|
|
112
|
+
*
|
|
113
|
+
* 与 {@link refresh} 的分工:这里是「启动时先把界面填上」的便宜入口——命中缓存时
|
|
114
|
+
* 一次网络请求都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功
|
|
115
|
+
* 拉取的结果」,但**可能比服务端现状旧**(最旧不超过 Host 的上一次成功拉取),
|
|
116
|
+
* 所以调用方按 `stale` / `isStale()` 判定它够不够权威,不够就再 `refresh()` 一次。
|
|
117
|
+
*/
|
|
118
|
+
warm(signal?: AbortSignal): Promise<ManagedMcpSnapshot>;
|
|
119
|
+
/** 取一张新鲜断言(120 秒)。失败抛错;本层不缓存、不换令牌。 */
|
|
120
|
+
assertion(assignmentId: string, signal?: AbortSignal): Promise<ManagedMcpAssertion>;
|
|
121
|
+
/** 订阅镜像变化(连接器域据此重渲染 / 重排同步)。返回退订函数。 */
|
|
122
|
+
subscribe(listener: () => void): () => void;
|
|
123
|
+
/** 清空镜像(登出 / 换账号:清单是按人的,留着会串号)。 */
|
|
124
|
+
clear(): void;
|
|
125
|
+
/**
|
|
126
|
+
* 并发去重:`warm` 与 `refresh` 撞在一起(启动同步与定时同步常常同拍)时共用一次
|
|
127
|
+
* 请求,且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
|
|
128
|
+
*/
|
|
129
|
+
private dedupe;
|
|
130
|
+
/** 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。 */
|
|
131
|
+
private adopt;
|
|
132
|
+
private emit;
|
|
133
|
+
}
|