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
package/lib/index.js
CHANGED
|
@@ -3137,7 +3137,7 @@ function isUnloading(ctx) {
|
|
|
3137
3137
|
}
|
|
3138
3138
|
|
|
3139
3139
|
// src/version.ts
|
|
3140
|
-
var PKG_VERSION = false ? "dev" : "0.0.
|
|
3140
|
+
var PKG_VERSION = false ? "dev" : "0.0.16";
|
|
3141
3141
|
|
|
3142
3142
|
// src/ai-router-client.ts
|
|
3143
3143
|
var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
|
|
@@ -3150,6 +3150,13 @@ var ME_KEY_RECHARGES_PATH = "/api/me/key/recharges";
|
|
|
3150
3150
|
var ME_COST_ESTIMATE_PATH = "/api/me/cost-estimate";
|
|
3151
3151
|
var ME_DAILY_QUOTE_PATH = "/api/me/daily-quote";
|
|
3152
3152
|
var ME_DAILY_QUOTE_RANDOM_PATH = "/api/me/daily-quote/random";
|
|
3153
|
+
var ME_MCP_ASSIGNMENTS_PATH = "/api/me/mcp-assignments";
|
|
3154
|
+
var ME_MCP_ASSERTIONS_PATH = "/api/me/mcp-assertions";
|
|
3155
|
+
var ME_SKILL_ASSIGNMENTS_PATH = "/api/me/skill-assignments";
|
|
3156
|
+
var ME_SKILL_PACKAGES_PATH = "/api/me/skill-packages";
|
|
3157
|
+
var ME_WORKBENCH_APPS_PATH = "/api/me/workbench-apps";
|
|
3158
|
+
var ME_OFFICE_CONFIG_PATH = "/api/me/office-config";
|
|
3159
|
+
var ME_OFFICE_RELAY_DOCUMENTS_PATH = "/api/me/office-relay/documents";
|
|
3153
3160
|
var DEFAULT_CLIENT_ID = "hiwork-desktop";
|
|
3154
3161
|
var DEFAULT_REDIRECT_URI = "hiwork://auth/callback";
|
|
3155
3162
|
var GatewayError = class extends Error {
|
|
@@ -3308,6 +3315,239 @@ var AiRouterClient = class {
|
|
|
3308
3315
|
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3309
3316
|
return await parseMeJson(res);
|
|
3310
3317
|
}
|
|
3318
|
+
/**
|
|
3319
|
+
* GET /api/me/mcp-assignments:管理员下发给当前账号的 MCP 清单。
|
|
3320
|
+
*
|
|
3321
|
+
* 不复用 {@link getMeJson}:那条路径把 403 映射成 `NO_KEY_ASSIGNED`(「尚未被分配
|
|
3322
|
+
* 虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key 的账号照样应该有
|
|
3323
|
+
* 清单(分 MCP 与分 Key 是两件事)。沿用它的 403 文案会把「没有 Key」错报成原因。
|
|
3324
|
+
*/
|
|
3325
|
+
async getMyMcpAssignments(sessionCookie) {
|
|
3326
|
+
const res = await this.requestOrUnreachable(ME_MCP_ASSIGNMENTS_PATH, { method: "GET", sessionCookie });
|
|
3327
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3328
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3329
|
+
return await parseMeJson(res);
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* POST /api/me/office-relay/documents:上传文档字节到中继。
|
|
3333
|
+
*
|
|
3334
|
+
* 文件名走 `X-Office-Document-Name` 头(HTTP 头只能带 ASCII,用 encodeURIComponent
|
|
3335
|
+
* 编码;网关侧 PathUnescape 解码)。状态码映射成稳定 code,供插件层给用户文案。
|
|
3336
|
+
*/
|
|
3337
|
+
async createOfficeRelayDocument(sessionCookie, name2, bytes, mode) {
|
|
3338
|
+
const res = await this.requestOrUnreachable(ME_OFFICE_RELAY_DOCUMENTS_PATH, {
|
|
3339
|
+
method: "POST",
|
|
3340
|
+
sessionCookie,
|
|
3341
|
+
rawBody: bytes,
|
|
3342
|
+
headers: {
|
|
3343
|
+
"content-type": "application/octet-stream",
|
|
3344
|
+
"x-office-document-name": encodeURIComponent(name2),
|
|
3345
|
+
// 打开模式只影响网关侧的 document key 变体(view / edit 不能共用 DS 会话)
|
|
3346
|
+
...mode === void 0 ? {} : { "x-office-document-mode": mode }
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3350
|
+
if (res.status === 404) {
|
|
3351
|
+
throw new GatewayError("RELAY_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6587\u6863\u4E2D\u7EE7\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 404);
|
|
3352
|
+
}
|
|
3353
|
+
if (res.status === 413) throw new GatewayError("RELAY_TOO_LARGE", "\u6587\u6863\u8D85\u8FC7\u5728\u7EBF\u7F16\u8F91\u7684\u5927\u5C0F\u4E0A\u9650", 413);
|
|
3354
|
+
if (res.status === 415) throw new GatewayError("RELAY_EXTENSION", "\u8BE5\u6587\u6863\u7C7B\u578B\u4E0D\u652F\u6301\u5728\u7EBF\u7F16\u8F91", 415);
|
|
3355
|
+
if (res.status === 429) throw new GatewayError("RELAY_QUOTA", "\u540C\u65F6\u6253\u5F00\u7684\u7F16\u8F91\u4F1A\u8BDD\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5", 429);
|
|
3356
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3357
|
+
return await parseMeJson(res);
|
|
3358
|
+
}
|
|
3359
|
+
/** GET /api/me/office-relay/documents/:id:轮询状态。 */
|
|
3360
|
+
async getOfficeRelayStatus(sessionCookie, id) {
|
|
3361
|
+
const res = await this.requestOrUnreachable(`${ME_OFFICE_RELAY_DOCUMENTS_PATH}/${encodeURIComponent(id)}`, {
|
|
3362
|
+
method: "GET",
|
|
3363
|
+
sessionCookie
|
|
3364
|
+
});
|
|
3365
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3366
|
+
if (res.status === 404) throw new GatewayError("RELAY_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6587\u6863\u4E2D\u7EE7\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 404);
|
|
3367
|
+
if (res.status === 410) throw new GatewayError("RELAY_EXPIRED", "\u7F16\u8F91\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u6587\u6863", 410);
|
|
3368
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3369
|
+
return await parseMeJson(res);
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* GET /api/me/office-relay/documents/:id/content[?v=N]:取回当前版本字节。
|
|
3373
|
+
* 204 表示"你手里的版本已是最新"(不是错误)。
|
|
3374
|
+
*/
|
|
3375
|
+
async getOfficeRelayContent(sessionCookie, id, revision) {
|
|
3376
|
+
const query = revision === void 0 ? "" : `?v=${revision}`;
|
|
3377
|
+
const res = await this.requestOrUnreachable(
|
|
3378
|
+
`${ME_OFFICE_RELAY_DOCUMENTS_PATH}/${encodeURIComponent(id)}/content${query}`,
|
|
3379
|
+
{ method: "GET", sessionCookie }
|
|
3380
|
+
);
|
|
3381
|
+
if (res.status === 204) {
|
|
3382
|
+
return { notModified: true, revision: revision ?? 0 };
|
|
3383
|
+
}
|
|
3384
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3385
|
+
if (res.status === 404) throw new GatewayError("RELAY_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6587\u6863\u4E2D\u7EE7\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 404);
|
|
3386
|
+
if (res.status === 410) throw new GatewayError("RELAY_EXPIRED", "\u7F16\u8F91\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u6587\u6863", 410);
|
|
3387
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3388
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
3389
|
+
return { notModified: false, revision: -1, bytes };
|
|
3390
|
+
}
|
|
3391
|
+
/** DELETE /api/me/office-relay/documents/:id:关闭会话(幂等)。 */
|
|
3392
|
+
async closeOfficeRelayDocument(sessionCookie, id) {
|
|
3393
|
+
const res = await this.requestOrUnreachable(`${ME_OFFICE_RELAY_DOCUMENTS_PATH}/${encodeURIComponent(id)}`, {
|
|
3394
|
+
method: "DELETE",
|
|
3395
|
+
sessionCookie
|
|
3396
|
+
});
|
|
3397
|
+
if (res.status === 204 || res.status === 404 || res.status === 410) return;
|
|
3398
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3399
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* GET /api/me/office-config:本部署 OnlyOffice 集成(Document Server 地址 + 共享 JWT 密钥)。
|
|
3403
|
+
*
|
|
3404
|
+
* 与 {@link getMyMcpAssignments} 同样的理由不复用 `getMeJson`:那条路径把 403 映射成
|
|
3405
|
+
* `NO_KEY_ASSIGNED`,而本端点只看会话(分集成配置与分 Key 是两件事)。503 是
|
|
3406
|
+
* 「部署未配置」的稳定信号,单独映射——消费方(hiwork-office)据此提示联系管理员
|
|
3407
|
+
* 并回退本地手工配置,而不是报一句含糊的「网关不可达」。
|
|
3408
|
+
*/
|
|
3409
|
+
async getMyOfficeConfig(sessionCookie) {
|
|
3410
|
+
const res = await this.requestOrUnreachable(ME_OFFICE_CONFIG_PATH, { method: "GET", sessionCookie });
|
|
3411
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3412
|
+
if (res.status === 503) {
|
|
3413
|
+
throw new GatewayError(
|
|
3414
|
+
"OFFICE_NOT_CONFIGURED",
|
|
3415
|
+
"\u672C\u90E8\u7F72\u5C1A\u672A\u914D\u7F6E HiWork Office \u6587\u6863\u529E\u516C\u96C6\u6210\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
3416
|
+
503
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3420
|
+
return await parseMeJson(res);
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* POST /api/me/mcp-assertions:为某条分配签一张 120 秒断言。
|
|
3424
|
+
*
|
|
3425
|
+
* 三种拒绝各有确定含义(服务端 handler 的 404/409/503),必须分开映射——否则
|
|
3426
|
+
* 客户端只会看到一句「取断言失败」,既分不清「被撤销了」(该卸连接)还是
|
|
3427
|
+
* 「服务停用了」(该重试)还是「部署没配私钥」(找管理员)。
|
|
3428
|
+
*/
|
|
3429
|
+
async issueMyMcpAssertion(sessionCookie, assignmentId) {
|
|
3430
|
+
const res = await this.requestOrUnreachable(ME_MCP_ASSERTIONS_PATH, {
|
|
3431
|
+
method: "POST",
|
|
3432
|
+
sessionCookie,
|
|
3433
|
+
body: { assignmentId }
|
|
3434
|
+
});
|
|
3435
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3436
|
+
if (res.status === 400 || res.status === 404) {
|
|
3437
|
+
throw new GatewayError("MCP_ASSIGNMENT_GONE", "\u8FD9\u6761\u5206\u914D\u5DF2\u5931\u6548\uFF0C\u7A0D\u540E\u540C\u6B65\u4F1A\u91CD\u65B0\u6838\u5BF9", res.status);
|
|
3438
|
+
}
|
|
3439
|
+
if (res.status === 409) {
|
|
3440
|
+
throw new GatewayError("MCP_NOT_DELIVERABLE", "\u8BE5 MCP \u670D\u52A1\u5F53\u524D\u4E0D\u53EF\u4E0B\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 409);
|
|
3441
|
+
}
|
|
3442
|
+
if (res.status === 503) {
|
|
3443
|
+
throw new GatewayError("MCP_ASSERTION_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528 MCP \u65AD\u8A00\u7B7E\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 503);
|
|
3444
|
+
}
|
|
3445
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3446
|
+
return await parseMeJson(res);
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* GET /api/me/skill-assignments:管理员下发给当前账号的技能清单(spec §4.1)。
|
|
3450
|
+
*
|
|
3451
|
+
* 与 {@link getMyMcpAssignments} 同样的理由不复用 `getMeJson`:那条路径把 403 映射成
|
|
3452
|
+
* `NO_KEY_ASSIGNED`(「尚未被分配虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key
|
|
3453
|
+
* 的账号照样应该有技能清单(分技能与分 Key 是两件事)。
|
|
3454
|
+
*
|
|
3455
|
+
* **这条路径上只有 401 有独立语义,其余全部收敛成 `GATEWAY_UNREACHABLE`**,理由逐条:
|
|
3456
|
+
* - 401 = 会话过期/缺失(`middleware` 的会话鉴权)——唯一一条服务端**真的会为这条路由**发出的
|
|
3457
|
+
* 拒绝,也是唯一一条能让人做点什么(重新登录)的;
|
|
3458
|
+
* - 403 不会出现:这条路由的鉴权只有会话,没有「有没有被分配虚拟 KEY」那一层(正因为如此,
|
|
3459
|
+
* 这里才不能复用 `getMeJson`,那个函数会把 403 读成「没分配 KEY」);
|
|
3460
|
+
* - 404 不是这条路由的响应形状:清单**永远**是 200 + 一份数组,某条技能被停用 / 被撤销是
|
|
3461
|
+
* 「不在数组里」,不是 404(`MyAssignments` 逐条过滤,不会为单条抛错)。真收到 404 只可能是
|
|
3462
|
+
* 「路径压根没被路由」——老网关没有这条路由的时候就是这个样子,而它与反代配错无法区分,
|
|
3463
|
+
* 所以诚实读法是网关不可达,不是「管理员把技能全撤了」;
|
|
3464
|
+
* - 503 不会出现:员工端这条路由**无条件注册**,服务端没有任何「技能下发未启用」的闸
|
|
3465
|
+
* (`getMySkillPackage` 那条 503 也一样没有生产者,见那里的注释),整份响应由 service
|
|
3466
|
+
* 直接产出;
|
|
3467
|
+
* - 其余(5xx / 反代错误页 / 维护页)同理:没有可解释的拒绝语义,收敛成网关不可达。
|
|
3468
|
+
*
|
|
3469
|
+
* 形状校验(`assignments` 必须是数组)**不在这里**:本方法只做传输与状态码映射,把 JSON 直接
|
|
3470
|
+
* 断言成契约类型。真正的形状判据在消费端 `/hiwork-managed-skills` 的 Host 半边
|
|
3471
|
+
* (`src/managed-skills.ts` 的 `fetchOnce`)——那里才决定「这一份算不算成功拉到的清单」。
|
|
3472
|
+
*/
|
|
3473
|
+
async getMySkillAssignments(sessionCookie) {
|
|
3474
|
+
const res = await this.requestOrUnreachable(ME_SKILL_ASSIGNMENTS_PATH, { method: "GET", sessionCookie });
|
|
3475
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3476
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3477
|
+
return await parseMeJson(res);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* GET /api/me/skill-packages/:assignmentId:取一条分配的包体字节(spec §4.2)。
|
|
3481
|
+
*
|
|
3482
|
+
* **这条路径是「两个端口」里的另一条**,它才有独立的拒绝语义(与服务端 handler 一一对应):
|
|
3483
|
+
* - 404 = 这条分配不在这个账号名下(已被撤销 / 从未属于它,`ErrSkillAssignmentNotForUser`
|
|
3484
|
+
* ——技能被停用 / 分配被停用也走这一条,服务端刻意不区分),
|
|
3485
|
+
* - 503 = 部署未启用技能下发(**保留的防御性映射:当前没有生产者**,见下)。
|
|
3486
|
+
* 两者必须分开映射,否则客户端只会看到一句「拉包失败」。用途是**别把原因咽下去**:404 让人
|
|
3487
|
+
* 停止重试、把原因显示出来(「这条分配已失效,稍后同步会重新核对」),503 让人知道该找管理员。
|
|
3488
|
+
* ⚠️ 事实核对(spec §4.5):`SKILL_DISTRIBUTION_DISABLED` 全仓**只有本方法这一个生产方**
|
|
3489
|
+
* (本方法里 503 那一支),而 ai-hub 目前**没有任何**会产生这个 503 的代码——员工端两条路由在
|
|
3490
|
+
* `cmd/server/main.go` 里无条件注册,技能 / 分配被停用在员工端走 404
|
|
3491
|
+
* (`ErrSkillAssignmentNotForUser`)。映射保留是**防御性 + 与 MCP 同形**(那里
|
|
3492
|
+
* `MCP_ASSERTION_DISABLED` 真有生产者:`me_mcp_handler.go` 的 503),别拿它当「会发生的分支」
|
|
3493
|
+
* 去设计行为。
|
|
3494
|
+
* ⚠️ **这两条都只是「失败的原因」,不是删除本地受管技能的依据**:删除的唯一权柄是「成功拉到的
|
|
3495
|
+
* 清单」(spec §6 的唯一安全不变量),由 Host 的清单端点产出。本文件里没有任何一处据 404 或
|
|
3496
|
+
* `contentSha256` 去删本地技能(全仓 `SKILL_ASSIGNMENT_GONE` 只有生产方、类型联合、文案表与
|
|
3497
|
+
* 测试,没有消费方)。
|
|
3498
|
+
* 404 而不泄露「这条分配是否存在」是服务端的选择(§4.2 明确要 404 不要 403)。
|
|
3499
|
+
*
|
|
3500
|
+
* 本调用方从不发 `If-None-Match`(§4.2 说 304 那条是给管理端与 curl 排障用的),所以 304
|
|
3501
|
+
* 不在这条路径上;真收到(反代插话)会落进下面那条 → 网关不可达。
|
|
3502
|
+
*
|
|
3503
|
+
* 包体**只在本函数里短暂存在**:交给 Host 侧频道(src/managed-skills.ts)后立即 base64
|
|
3504
|
+
* 出去,网关会话 cookie 全程不出 Host,浏览器侧永远拿不到字节。
|
|
3505
|
+
*/
|
|
3506
|
+
async getMySkillPackage(sessionCookie, assignmentId) {
|
|
3507
|
+
const res = await this.requestOrUnreachable(
|
|
3508
|
+
`${ME_SKILL_PACKAGES_PATH}/${encodeURIComponent(assignmentId)}`,
|
|
3509
|
+
{ method: "GET", sessionCookie }
|
|
3510
|
+
);
|
|
3511
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3512
|
+
if (res.status === 404) {
|
|
3513
|
+
throw new GatewayError("SKILL_ASSIGNMENT_GONE", "\u8FD9\u6761\u5206\u914D\u5DF2\u5931\u6548\uFF0C\u7A0D\u540E\u540C\u6B65\u4F1A\u91CD\u65B0\u6838\u5BF9", 404);
|
|
3514
|
+
}
|
|
3515
|
+
if (res.status === 503) {
|
|
3516
|
+
throw new GatewayError("SKILL_DISTRIBUTION_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6280\u80FD\u4E0B\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 503);
|
|
3517
|
+
}
|
|
3518
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3519
|
+
return {
|
|
3520
|
+
skillName: res.headers.get("x-hiwork-skill-name") ?? "",
|
|
3521
|
+
contentSha256: res.headers.get("x-hiwork-content-sha256") ?? "",
|
|
3522
|
+
bytes: new Uint8Array(await res.arrayBuffer())
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* GET /api/me/workbench-apps:管理员登记并分配给当前账号的工作台应用清单(spec §4.1)。
|
|
3527
|
+
*
|
|
3528
|
+
* 与技能清单同款**不复用 `getMeJson`**:那条路径把 403 映射成 `NO_KEY_ASSIGNED`(「尚未被分配
|
|
3529
|
+
* 虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key 的账号照样应该有工作台。
|
|
3530
|
+
*
|
|
3531
|
+
* 状态码映射(**与技能清单有一处刻意的不同**,理由在下面):
|
|
3532
|
+
* - 401 = 会话过期/缺失:唯一一条能让人做点什么(重新登录)的拒绝;
|
|
3533
|
+
* - **404 = 本部署未启用工作台**:这条路由在网关侧是「有配置才有」的(spec §10 的降级矩阵:
|
|
3534
|
+
* 「网关未配置 workbench → 回 404」),因此 404 不是「应用清单为空」也不是网络故障,
|
|
3535
|
+
* 而是一个可展示的功能状态——客户端据此显示「本部署尚未启用工作台」而不是报错。
|
|
3536
|
+
* ⚠️ 技能清单那半**刻意**没有这一档(那里 404 只能是路径没被路由,收敛成网关不可达);
|
|
3537
|
+
* 两边的差别是服务端语义的差别,不是实现风格的差别,别顺手对齐;
|
|
3538
|
+
* - 其余(403 / 5xx / 反代错误页 / 维护页)一律 `GATEWAY_UNREACHABLE`:没有可解释的拒绝语义。
|
|
3539
|
+
*
|
|
3540
|
+
* 形状校验(`apps` 必须是数组)**不在这里**:本方法只做传输与状态码映射,把 JSON 直接断言成
|
|
3541
|
+
* 契约类型;真正的形状判据在消费端 `/hiwork-workbench` 的 Host 半边(`src/workbench-bridge.ts`),
|
|
3542
|
+
* 那里才决定「这一份算不算成功拉到的清单」。
|
|
3543
|
+
*/
|
|
3544
|
+
async getMyWorkbenchApps(sessionCookie) {
|
|
3545
|
+
const res = await this.requestOrUnreachable(ME_WORKBENCH_APPS_PATH, { method: "GET", sessionCookie });
|
|
3546
|
+
if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
|
|
3547
|
+
if (res.status === 404) throw new GatewayError("WORKBENCH_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528\u5DE5\u4F5C\u53F0\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 404);
|
|
3548
|
+
if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
|
|
3549
|
+
return await parseMeJson(res);
|
|
3550
|
+
}
|
|
3311
3551
|
/**
|
|
3312
3552
|
* /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
|
|
3313
3553
|
* 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
|
|
@@ -3346,7 +3586,12 @@ var AiRouterClient = class {
|
|
|
3346
3586
|
headers,
|
|
3347
3587
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
3348
3588
|
};
|
|
3349
|
-
if (opts.
|
|
3589
|
+
if (opts.rawBody !== void 0) {
|
|
3590
|
+
init.body = opts.rawBody.buffer.slice(
|
|
3591
|
+
opts.rawBody.byteOffset,
|
|
3592
|
+
opts.rawBody.byteOffset + opts.rawBody.byteLength
|
|
3593
|
+
);
|
|
3594
|
+
} else if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
|
|
3350
3595
|
return fetch(`${this.baseURL}${path}`, init);
|
|
3351
3596
|
}
|
|
3352
3597
|
};
|
|
@@ -4407,6 +4652,947 @@ function registerUsageRpc(ctx, deps) {
|
|
|
4407
4652
|
};
|
|
4408
4653
|
}
|
|
4409
4654
|
|
|
4655
|
+
// src/managed-mcp.ts
|
|
4656
|
+
var MANAGED_MCP_RPC_CHANNEL = "/hiwork-managed-mcp";
|
|
4657
|
+
var MANAGED_MCP_RPC_ENDPOINTS = ["snapshot", "refresh", "assertion"];
|
|
4658
|
+
var MANAGED_MCP_REFRESH_INTERVAL_MS = 30 * 6e4;
|
|
4659
|
+
var ManagedMcpRequestError = class extends Error {
|
|
4660
|
+
constructor(code, message) {
|
|
4661
|
+
super(message);
|
|
4662
|
+
this.code = code;
|
|
4663
|
+
this.name = "ManagedMcpRequestError";
|
|
4664
|
+
}
|
|
4665
|
+
};
|
|
4666
|
+
var MANAGED_MCP_MESSAGE_FALLBACK = {
|
|
4667
|
+
SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
4668
|
+
GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
4669
|
+
MCP_ASSIGNMENT_GONE: "\u8FD9\u6761\u5206\u914D\u5DF2\u5931\u6548\uFF0C\u7A0D\u540E\u540C\u6B65\u4F1A\u91CD\u65B0\u6838\u5BF9",
|
|
4670
|
+
MCP_NOT_DELIVERABLE: "\u8BE5 MCP \u670D\u52A1\u5F53\u524D\u4E0D\u53EF\u4E0B\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
4671
|
+
MCP_ASSERTION_DISABLED: "\u672C\u90E8\u7F72\u672A\u542F\u7528 MCP \u65AD\u8A00\u7B7E\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
4672
|
+
NO_KEY_ASSIGNED: "\u5F53\u524D\u8D26\u53F7\u5C1A\u672A\u88AB\u5206\u914D\u865A\u62DF KEY\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458"
|
|
4673
|
+
};
|
|
4674
|
+
var MANAGED_MCP_GENERIC_FAILURE_MESSAGE = "MCP \u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
4675
|
+
var MANAGED_MCP_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
4676
|
+
function displayManagedMcpMessage(code, message) {
|
|
4677
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
4678
|
+
if (text === "") return MANAGED_MCP_MESSAGE_FALLBACK[code] ?? MANAGED_MCP_GENERIC_FAILURE_MESSAGE;
|
|
4679
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
|
|
4680
|
+
if (leaksImplementationDetail) return MANAGED_MCP_MESSAGE_FALLBACK[code] ?? MANAGED_MCP_GENERIC_FAILURE_MESSAGE;
|
|
4681
|
+
return text;
|
|
4682
|
+
}
|
|
4683
|
+
var ASSIGNMENT_ID_PATTERN = /^[0-9a-fA-F-]{1,64}$/;
|
|
4684
|
+
function parseManagedMcpPayload(endpoint, raw) {
|
|
4685
|
+
if (raw === void 0 || raw === null) {
|
|
4686
|
+
if (endpoint === "assertion") throw new ManagedMcpRequestError("bad-request", "\u53D6\u65AD\u8A00\u8BF7\u6C42\u7F3A\u5C11 assignmentId\u3002");
|
|
4687
|
+
return {};
|
|
4688
|
+
}
|
|
4689
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
4690
|
+
throw new ManagedMcpRequestError("bad-request", "MCP \u6E05\u5355\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
|
|
4691
|
+
}
|
|
4692
|
+
const record = raw;
|
|
4693
|
+
const keys = Object.keys(record);
|
|
4694
|
+
if (endpoint !== "assertion") {
|
|
4695
|
+
if (keys.length > 0) throw new ManagedMcpRequestError("bad-request", `MCP \u6E05\u5355\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${keys[0]}'\u3002`);
|
|
4696
|
+
return {};
|
|
4697
|
+
}
|
|
4698
|
+
for (const key of keys) {
|
|
4699
|
+
if (key !== "assignmentId") throw new ManagedMcpRequestError("bad-request", `\u53D6\u65AD\u8A00\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
|
|
4700
|
+
}
|
|
4701
|
+
const rawId = record["assignmentId"];
|
|
4702
|
+
if (typeof rawId !== "string" || !ASSIGNMENT_ID_PATTERN.test(rawId.trim())) {
|
|
4703
|
+
throw new ManagedMcpRequestError("bad-request", "\u53D6\u65AD\u8A00\u8BF7\u6C42\u7684 assignmentId \u4E0D\u5408\u6CD5\u3002");
|
|
4704
|
+
}
|
|
4705
|
+
return { assignmentId: rawId.trim() };
|
|
4706
|
+
}
|
|
4707
|
+
function isManagedMcpRpcEndpoint(endpoint) {
|
|
4708
|
+
return MANAGED_MCP_RPC_ENDPOINTS.includes(endpoint);
|
|
4709
|
+
}
|
|
4710
|
+
function textOrUndefined(value) {
|
|
4711
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
4712
|
+
}
|
|
4713
|
+
function normalizeAssignment(raw, log) {
|
|
4714
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
4715
|
+
const record = raw;
|
|
4716
|
+
const assignmentId = textOrUndefined(record["assignmentId"]);
|
|
4717
|
+
const serverName = textOrUndefined(record["serverName"]);
|
|
4718
|
+
const url = textOrUndefined(record["url"]);
|
|
4719
|
+
const transport = textOrUndefined(record["transport"]);
|
|
4720
|
+
const authRaw = record["auth"];
|
|
4721
|
+
const auth = typeof authRaw === "object" && authRaw !== null && !Array.isArray(authRaw) ? authRaw : void 0;
|
|
4722
|
+
const kind = textOrUndefined(auth?.["kind"]);
|
|
4723
|
+
const mintUrl = textOrUndefined(auth?.["mintUrl"]);
|
|
4724
|
+
if (assignmentId === void 0 || serverName === void 0 || url === void 0 || transport === void 0 || kind === void 0 || mintUrl === void 0) {
|
|
4725
|
+
log?.("hiwork-core: MCP \u6E05\u5355\u91CC\u6709\u4E00\u6761\u7ED3\u6784\u4E0D\u5B8C\u6574\u7684\u5206\u914D\uFF0C\u5DF2\u8DF3\u8FC7");
|
|
4726
|
+
return void 0;
|
|
4727
|
+
}
|
|
4728
|
+
const revision = record["revision"];
|
|
4729
|
+
return {
|
|
4730
|
+
assignmentId,
|
|
4731
|
+
revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
|
|
4732
|
+
slug: textOrUndefined(record["slug"]) ?? serverName,
|
|
4733
|
+
displayName: textOrUndefined(record["displayName"]) ?? serverName,
|
|
4734
|
+
serverName,
|
|
4735
|
+
transport,
|
|
4736
|
+
url,
|
|
4737
|
+
extraHeaders: stringMap(record["extraHeaders"]),
|
|
4738
|
+
auth: { kind, mintUrl, revokeUrl: textOrUndefined(auth?.["revokeUrl"]) ?? "" },
|
|
4739
|
+
updatedAt: textOrUndefined(record["updatedAt"]) ?? ""
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
function stringMap(raw) {
|
|
4743
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
4744
|
+
const out = {};
|
|
4745
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
4746
|
+
if (typeof value === "string") out[key] = value;
|
|
4747
|
+
}
|
|
4748
|
+
return out;
|
|
4749
|
+
}
|
|
4750
|
+
function failureOf(error) {
|
|
4751
|
+
if (error instanceof ManagedMcpRequestError) return { code: error.code, message: error.message, details: {} };
|
|
4752
|
+
if (error instanceof GatewayError) {
|
|
4753
|
+
return { code: error.code, message: displayManagedMcpMessage(error.code, error.message), details: {} };
|
|
4754
|
+
}
|
|
4755
|
+
return { code: "internal", message: MANAGED_MCP_GENERIC_FAILURE_MESSAGE, details: {} };
|
|
4756
|
+
}
|
|
4757
|
+
var ManagedMcpStore = class {
|
|
4758
|
+
constructor(deps, now = () => Date.now()) {
|
|
4759
|
+
this.deps = deps;
|
|
4760
|
+
this.now = now;
|
|
4761
|
+
}
|
|
4762
|
+
cached;
|
|
4763
|
+
lastError;
|
|
4764
|
+
inFlight;
|
|
4765
|
+
/**
|
|
4766
|
+
* 当前缓存(同步读,不发请求)。从没成功过返回 undefined;
|
|
4767
|
+
* 最近一次失败会以 `stale + lastError` 标注在上一次成功的结果上。
|
|
4768
|
+
*/
|
|
4769
|
+
peek() {
|
|
4770
|
+
if (this.cached === void 0) return void 0;
|
|
4771
|
+
return this.lastError === void 0 ? this.cached : { ...this.cached, stale: true, lastError: this.lastError };
|
|
4772
|
+
}
|
|
4773
|
+
/**
|
|
4774
|
+
* 去网关拉一次清单并更新缓存。失败时抛异常,且**缓存原样保留**。
|
|
4775
|
+
*
|
|
4776
|
+
* 并发调用合并成一次请求:后台预热与客户端手动同步撞在一起时不必打两遍网关。
|
|
4777
|
+
*/
|
|
4778
|
+
async load() {
|
|
4779
|
+
if (this.inFlight !== void 0) return await this.inFlight;
|
|
4780
|
+
const task = this.fetchOnce();
|
|
4781
|
+
this.inFlight = task;
|
|
4782
|
+
try {
|
|
4783
|
+
return await task;
|
|
4784
|
+
} finally {
|
|
4785
|
+
this.inFlight = void 0;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
async fetchOnce() {
|
|
4789
|
+
try {
|
|
4790
|
+
const sessionCookie = await this.deps.resolveSession();
|
|
4791
|
+
if (sessionCookie === void 0 || sessionCookie === "") {
|
|
4792
|
+
throw new GatewayError("SESSION_EXPIRED", MANAGED_MCP_SESSION_EXPIRED_MESSAGE);
|
|
4793
|
+
}
|
|
4794
|
+
const gateway = this.deps.gateway();
|
|
4795
|
+
if (gateway === void 0) {
|
|
4796
|
+
throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
|
|
4797
|
+
}
|
|
4798
|
+
const view = await gateway.getMyMcpAssignments(sessionCookie);
|
|
4799
|
+
const assignments = [];
|
|
4800
|
+
for (const item of Array.isArray(view?.assignments) ? view.assignments : []) {
|
|
4801
|
+
const normalized = normalizeAssignment(item, this.deps.log);
|
|
4802
|
+
if (normalized !== void 0) assignments.push(normalized);
|
|
4803
|
+
}
|
|
4804
|
+
const snapshot = {
|
|
4805
|
+
revision: typeof view?.revision === "number" && Number.isFinite(view.revision) ? view.revision : 0,
|
|
4806
|
+
fetchedAt: new Date(this.now()).toISOString(),
|
|
4807
|
+
assignments,
|
|
4808
|
+
stale: false
|
|
4809
|
+
};
|
|
4810
|
+
this.cached = snapshot;
|
|
4811
|
+
this.lastError = void 0;
|
|
4812
|
+
return snapshot;
|
|
4813
|
+
} catch (error) {
|
|
4814
|
+
this.lastError = failureOf(error);
|
|
4815
|
+
throw error;
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
};
|
|
4819
|
+
function isAborted4(signal) {
|
|
4820
|
+
return signal?.aborted === true;
|
|
4821
|
+
}
|
|
4822
|
+
async function handleManagedMcpEndpoint(endpoint, rawPayload, deps, store, signal) {
|
|
4823
|
+
try {
|
|
4824
|
+
if (isAborted4(signal)) return failure4("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
4825
|
+
if (!isManagedMcpRpcEndpoint(endpoint)) {
|
|
4826
|
+
throw new ManagedMcpRequestError("bad-request", `\u672A\u77E5\u7684 MCP \u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
|
|
4827
|
+
}
|
|
4828
|
+
const payload = parseManagedMcpPayload(endpoint, rawPayload);
|
|
4829
|
+
if (endpoint === "assertion") {
|
|
4830
|
+
const assignmentId = payload.assignmentId;
|
|
4831
|
+
if (assignmentId === void 0) throw new ManagedMcpRequestError("bad-request", "\u53D6\u65AD\u8A00\u8BF7\u6C42\u7F3A\u5C11 assignmentId\u3002");
|
|
4832
|
+
return await handleAssertion(assignmentId, deps, signal);
|
|
4833
|
+
}
|
|
4834
|
+
const cached = store.peek();
|
|
4835
|
+
if (endpoint === "snapshot" && cached !== void 0) return { ok: true, value: cached };
|
|
4836
|
+
return { ok: true, value: await store.load() };
|
|
4837
|
+
} catch (error) {
|
|
4838
|
+
deps.log?.(`hiwork-core: MCP \u6E05\u5355 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
4839
|
+
return { ok: false, error: failureOf(error) };
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
async function handleAssertion(assignmentId, deps, signal) {
|
|
4843
|
+
const sessionCookie = await deps.resolveSession();
|
|
4844
|
+
if (sessionCookie === void 0 || sessionCookie === "") {
|
|
4845
|
+
return failure4("SESSION_EXPIRED", MANAGED_MCP_SESSION_EXPIRED_MESSAGE);
|
|
4846
|
+
}
|
|
4847
|
+
const gateway = deps.gateway();
|
|
4848
|
+
if (gateway === void 0) {
|
|
4849
|
+
return failure4("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
|
|
4850
|
+
}
|
|
4851
|
+
if (isAborted4(signal)) return failure4("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
4852
|
+
const ticket = await gateway.issueMyMcpAssertion(sessionCookie, assignmentId);
|
|
4853
|
+
const assertion = textOrUndefined(ticket?.assertion);
|
|
4854
|
+
const mintUrl = textOrUndefined(ticket?.mintUrl);
|
|
4855
|
+
if (assertion === void 0 || mintUrl === void 0) {
|
|
4856
|
+
deps.log?.("hiwork-core: \u53D6\u65AD\u8A00\u6536\u5230\u65E0\u6548\u54CD\u5E94\uFF08\u7F3A assertion / mintUrl\uFF09");
|
|
4857
|
+
return failure4("internal", MANAGED_MCP_GENERIC_FAILURE_MESSAGE);
|
|
4858
|
+
}
|
|
4859
|
+
return {
|
|
4860
|
+
ok: true,
|
|
4861
|
+
value: {
|
|
4862
|
+
assertion,
|
|
4863
|
+
mintUrl,
|
|
4864
|
+
revokeUrl: textOrUndefined(ticket?.revokeUrl) ?? "",
|
|
4865
|
+
expiresAt: textOrUndefined(ticket?.expiresAt) ?? "",
|
|
4866
|
+
serverName: textOrUndefined(ticket?.serverName) ?? ""
|
|
4867
|
+
}
|
|
4868
|
+
};
|
|
4869
|
+
}
|
|
4870
|
+
function failure4(code, message) {
|
|
4871
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
4872
|
+
}
|
|
4873
|
+
function registerManagedMcpRpc(ctx, deps, store) {
|
|
4874
|
+
const handler = (endpoint, payload, signal) => handleManagedMcpEndpoint(endpoint, payload, deps, store, signal);
|
|
4875
|
+
const remove = ctx.connection.rpc.handle(MANAGED_MCP_RPC_CHANNEL, handler, { authority: "loopback" });
|
|
4876
|
+
return async () => {
|
|
4877
|
+
await remove();
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
// src/managed-skills.ts
|
|
4882
|
+
var MANAGED_SKILLS_RPC_CHANNEL = "/hiwork-managed-skills";
|
|
4883
|
+
var MANAGED_SKILLS_RPC_ENDPOINTS = ["snapshot", "refresh", "package"];
|
|
4884
|
+
var MANAGED_SKILLS_REFRESH_INTERVAL_MS = 30 * 6e4;
|
|
4885
|
+
var MAX_MANAGED_SKILL_ARCHIVE_BYTES = 4 * 1024 * 1024;
|
|
4886
|
+
var ManagedSkillsRequestError = class extends Error {
|
|
4887
|
+
constructor(code, message) {
|
|
4888
|
+
super(message);
|
|
4889
|
+
this.code = code;
|
|
4890
|
+
this.name = "ManagedSkillsRequestError";
|
|
4891
|
+
}
|
|
4892
|
+
};
|
|
4893
|
+
var MANAGED_SKILLS_MESSAGE_FALLBACK = {
|
|
4894
|
+
SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
4895
|
+
GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
4896
|
+
SKILL_ASSIGNMENT_GONE: "\u8FD9\u6761\u5206\u914D\u5DF2\u5931\u6548\uFF0C\u7A0D\u540E\u540C\u6B65\u4F1A\u91CD\u65B0\u6838\u5BF9",
|
|
4897
|
+
SKILL_DISTRIBUTION_DISABLED: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6280\u80FD\u4E0B\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458"
|
|
4898
|
+
};
|
|
4899
|
+
var MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE = "\u6280\u80FD\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
4900
|
+
var MANAGED_SKILLS_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
4901
|
+
var MANAGED_SKILLS_PACKAGE_TOO_LARGE_CODE = "SKILL_PACKAGE_TOO_LARGE";
|
|
4902
|
+
var MANAGED_SKILLS_PACKAGE_TOO_LARGE_MESSAGE = "\u8FD9\u4E2A\u6280\u80FD\u5305\u8D85\u8FC7\u4E86\u5927\u5C0F\u4E0A\u9650\uFF0C\u6682\u65F6\u88C5\u4E0D\u4E0A\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458";
|
|
4903
|
+
function displayManagedSkillsMessage(code, message) {
|
|
4904
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
4905
|
+
if (text === "") return MANAGED_SKILLS_MESSAGE_FALLBACK[code] ?? MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE;
|
|
4906
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
|
|
4907
|
+
if (leaksImplementationDetail) return MANAGED_SKILLS_MESSAGE_FALLBACK[code] ?? MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE;
|
|
4908
|
+
return text;
|
|
4909
|
+
}
|
|
4910
|
+
var ASSIGNMENT_ID_PATTERN2 = /^[0-9a-fA-F-]{1,64}$/;
|
|
4911
|
+
function parseManagedSkillsPayload(endpoint, raw) {
|
|
4912
|
+
if (raw === void 0 || raw === null) {
|
|
4913
|
+
if (endpoint === "package") throw new ManagedSkillsRequestError("bad-request", "\u53D6\u6280\u80FD\u5305\u8BF7\u6C42\u7F3A\u5C11 assignmentId\u3002");
|
|
4914
|
+
return {};
|
|
4915
|
+
}
|
|
4916
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
4917
|
+
throw new ManagedSkillsRequestError("bad-request", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
|
|
4918
|
+
}
|
|
4919
|
+
const record = raw;
|
|
4920
|
+
const keys = Object.keys(record);
|
|
4921
|
+
if (endpoint !== "package") {
|
|
4922
|
+
if (keys.length > 0) throw new ManagedSkillsRequestError("bad-request", `\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${keys[0]}'\u3002`);
|
|
4923
|
+
return {};
|
|
4924
|
+
}
|
|
4925
|
+
for (const key of keys) {
|
|
4926
|
+
if (key !== "assignmentId") throw new ManagedSkillsRequestError("bad-request", `\u53D6\u6280\u80FD\u5305\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
|
|
4927
|
+
}
|
|
4928
|
+
const rawId = record["assignmentId"];
|
|
4929
|
+
if (typeof rawId !== "string" || !ASSIGNMENT_ID_PATTERN2.test(rawId.trim())) {
|
|
4930
|
+
throw new ManagedSkillsRequestError("bad-request", "\u53D6\u6280\u80FD\u5305\u8BF7\u6C42\u7684 assignmentId \u4E0D\u5408\u6CD5\u3002");
|
|
4931
|
+
}
|
|
4932
|
+
return { assignmentId: rawId.trim() };
|
|
4933
|
+
}
|
|
4934
|
+
function isManagedSkillsRpcEndpoint(endpoint) {
|
|
4935
|
+
return MANAGED_SKILLS_RPC_ENDPOINTS.includes(endpoint);
|
|
4936
|
+
}
|
|
4937
|
+
function textOrUndefined2(value) {
|
|
4938
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
4939
|
+
}
|
|
4940
|
+
function normalizeAssignment2(raw, log) {
|
|
4941
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
4942
|
+
const record = raw;
|
|
4943
|
+
const assignmentId = textOrUndefined2(record["assignmentId"]);
|
|
4944
|
+
const skillName = textOrUndefined2(record["skillName"]);
|
|
4945
|
+
const contentSha256 = textOrUndefined2(record["contentSha256"]);
|
|
4946
|
+
if (assignmentId === void 0 || skillName === void 0 || contentSha256 === void 0) {
|
|
4947
|
+
log?.("hiwork-core: \u6280\u80FD\u6E05\u5355\u91CC\u6709\u4E00\u6761\u7ED3\u6784\u4E0D\u5B8C\u6574\u7684\u5206\u914D\uFF0C\u5DF2\u8DF3\u8FC7");
|
|
4948
|
+
return void 0;
|
|
4949
|
+
}
|
|
4950
|
+
const revision = record["revision"];
|
|
4951
|
+
const sizeBytes = record["sizeBytes"];
|
|
4952
|
+
return {
|
|
4953
|
+
assignmentId,
|
|
4954
|
+
revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
|
|
4955
|
+
slug: textOrUndefined2(record["slug"]) ?? skillName,
|
|
4956
|
+
displayName: textOrUndefined2(record["displayName"]) ?? skillName,
|
|
4957
|
+
skillName,
|
|
4958
|
+
description: textOrUndefined2(record["description"]) ?? "",
|
|
4959
|
+
contentSha256,
|
|
4960
|
+
sizeBytes: typeof sizeBytes === "number" && Number.isFinite(sizeBytes) ? sizeBytes : 0,
|
|
4961
|
+
version: textOrUndefined2(record["version"]) ?? "",
|
|
4962
|
+
updatedAt: textOrUndefined2(record["updatedAt"]) ?? ""
|
|
4963
|
+
};
|
|
4964
|
+
}
|
|
4965
|
+
function failureOf2(error) {
|
|
4966
|
+
if (error instanceof ManagedSkillsRequestError) return { code: error.code, message: error.message, details: {} };
|
|
4967
|
+
if (error instanceof GatewayError) {
|
|
4968
|
+
return { code: error.code, message: displayManagedSkillsMessage(error.code, error.message), details: {} };
|
|
4969
|
+
}
|
|
4970
|
+
return { code: "internal", message: MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE, details: {} };
|
|
4971
|
+
}
|
|
4972
|
+
function normalizeSession(sessionCookie) {
|
|
4973
|
+
return sessionCookie === void 0 || sessionCookie === "" ? void 0 : sessionCookie;
|
|
4974
|
+
}
|
|
4975
|
+
var ManagedSkillsStore = class {
|
|
4976
|
+
constructor(deps, now = () => Date.now()) {
|
|
4977
|
+
this.deps = deps;
|
|
4978
|
+
this.now = now;
|
|
4979
|
+
}
|
|
4980
|
+
cached;
|
|
4981
|
+
lastError;
|
|
4982
|
+
/** 产生 `cached` 的那个会话 cookie(只在内存里)。会话绑定的全部依据就是它。 */
|
|
4983
|
+
cachedSession;
|
|
4984
|
+
/**
|
|
4985
|
+
* 在飞请求连同它所用的会话一起记账:并发合并**只允许发生在同一会话内**。
|
|
4986
|
+
* 合并跨会话等于把 A 那次拉取的结果交给 B——`refresh` 是客户端唯一被允许用来删本地技能的那条
|
|
4987
|
+
* 路(spec §6 的唯一安全不变量),这条缝不能留。
|
|
4988
|
+
*/
|
|
4989
|
+
inFlight;
|
|
4990
|
+
/**
|
|
4991
|
+
* 当前缓存(同步读,不发请求)。从没成功过返回 undefined;
|
|
4992
|
+
* 最近一次失败会以 `stale + lastError` 标注在上一次成功的结果上。
|
|
4993
|
+
*
|
|
4994
|
+
* ⚠️ **本方法不做会话绑定**——它是 spec §4.5 冻结的同步读,而当前会话只能异步解析(见
|
|
4995
|
+
* {@link peekForCurrentSession}),同步函数里根本判不了「这份缓存还算不算我的」。
|
|
4996
|
+
* 所以模块内唯一的正确用法是:先 `await store.peekForCurrentSession()`,再(或由它内部)`peek()`。
|
|
4997
|
+
* 直接调 `peek()` 只适合诊断与测试断言内存状态。
|
|
4998
|
+
*/
|
|
4999
|
+
peek() {
|
|
5000
|
+
if (this.cached === void 0) return void 0;
|
|
5001
|
+
return this.lastError === void 0 ? this.cached : { ...this.cached, stale: true, lastError: this.lastError };
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* 「当前会话下的缓存」= 解析当前会话 → 剪枝 → {@link peek}。
|
|
5005
|
+
*
|
|
5006
|
+
* 为什么比对写在**这里**而不是 `peek()` 里:`peek()` 是 spec §4.5 冻结的**同步**接口,而
|
|
5007
|
+
* 「当前是谁」只有 `deps.resolveSession()` 异步拿得到。两者凑不到一个函数里,于是分工只能是
|
|
5008
|
+
* 「async 的一侧先绑会话,sync 的 `peek()` 再读」。这是本文件里唯一一处 async 前置步骤,
|
|
5009
|
+
* 端点实现(`handleManagedSkillsEndpoint`)必须走它、不许绕过。
|
|
5010
|
+
*/
|
|
5011
|
+
async peekForCurrentSession() {
|
|
5012
|
+
this.bindSession(normalizeSession(await this.deps.resolveSession()));
|
|
5013
|
+
return this.peek();
|
|
5014
|
+
}
|
|
5015
|
+
/**
|
|
5016
|
+
* 会话绑定:缓存只属于产生它的那个会话;当前会话不是它(含**当前没有会话**)→ 立刻丢弃。
|
|
5017
|
+
*
|
|
5018
|
+
* 这是刻意与 `src/managed-mcp.ts` **不同**的一处,**不许**为了「两半对齐」改回去:
|
|
5019
|
+
* - 技能这半的 {@link clear} 是 spec §4.5 冻结要求提供的接口,却**没有任何调用方**——端点白名单
|
|
5020
|
+
* 只有 `snapshot`/`refresh`/`package`,客户端够不到 Host 的内存,Host 自己也没有登出钩子;
|
|
5021
|
+
* 而「靠一个没人调的钩子保证安全」正是**已经失败过一次**的做法(MCP 那条链路里同样的
|
|
5022
|
+
* `clear()` 也悬着),不能把安全不变量挂在它上面。
|
|
5023
|
+
* - 绑会话**自带**:不依赖任何调用方、不依赖谁记得清缓存,且方向更严——登出后(当前会话为空)
|
|
5024
|
+
* 与换账号后都不会再吐出上一个人的清单。更严在这里恒等于更安全。
|
|
5025
|
+
*
|
|
5026
|
+
* 它也**不**触碰「失败不覆写缓存」那条铁律:剪枝的触发条件是**会话变了**,不是「这一次拉取
|
|
5027
|
+
* 失败了」。失败路径仍然原样保留本会话上一次成功的清单(`stale + lastError`)。
|
|
5028
|
+
*/
|
|
5029
|
+
bindSession(current) {
|
|
5030
|
+
if (current === this.cachedSession) return;
|
|
5031
|
+
this.cached = void 0;
|
|
5032
|
+
this.cachedSession = void 0;
|
|
5033
|
+
this.lastError = void 0;
|
|
5034
|
+
}
|
|
5035
|
+
/**
|
|
5036
|
+
* 去网关拉一次清单并更新缓存。失败时抛异常,且**本会话的缓存原样保留**。
|
|
5037
|
+
*
|
|
5038
|
+
* 并发调用合并成一次请求:后台预热与客户端手动同步撞在一起时不必打两遍网关。
|
|
5039
|
+
* 合并**只在同一会话内**——两个会话合并到一起,就等于把 A 的结果发给 B。
|
|
5040
|
+
*/
|
|
5041
|
+
async load() {
|
|
5042
|
+
const session = normalizeSession(await this.deps.resolveSession());
|
|
5043
|
+
const inFlight = this.inFlight;
|
|
5044
|
+
if (inFlight !== void 0 && inFlight.session === session) return await inFlight.task;
|
|
5045
|
+
const task = this.fetchOnce(session);
|
|
5046
|
+
this.inFlight = { session, task };
|
|
5047
|
+
try {
|
|
5048
|
+
return await task;
|
|
5049
|
+
} finally {
|
|
5050
|
+
if (this.inFlight?.task === task) this.inFlight = void 0;
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
/**
|
|
5054
|
+
* 清空内存清单(登出 / 换账号调用)。
|
|
5055
|
+
*
|
|
5056
|
+
* ⚠️ **当前没有任何调用方**:这是 spec §4.5 冻结要求提供的接口(客户端半边的同名方法有调用方,
|
|
5057
|
+
* Host 这半没有),而它的用途已经被**会话绑定**覆盖——缓存按会话剪枝,登出 / 换账号后再也吐不出
|
|
5058
|
+
* 上一个人的清单(见 {@link bindSession})。保留它是为了不改冻结接口;真要删得先改 spec §4.5。
|
|
5059
|
+
* 冻结接口里的成员不要悄悄删掉,哪怕它暂时是死的。
|
|
5060
|
+
*/
|
|
5061
|
+
clear() {
|
|
5062
|
+
this.cached = void 0;
|
|
5063
|
+
this.lastError = void 0;
|
|
5064
|
+
this.cachedSession = void 0;
|
|
5065
|
+
}
|
|
5066
|
+
async fetchOnce(sessionCookie) {
|
|
5067
|
+
try {
|
|
5068
|
+
if (sessionCookie === void 0) {
|
|
5069
|
+
throw new GatewayError("SESSION_EXPIRED", MANAGED_SKILLS_SESSION_EXPIRED_MESSAGE);
|
|
5070
|
+
}
|
|
5071
|
+
const gateway = this.deps.gateway();
|
|
5072
|
+
if (gateway === void 0) {
|
|
5073
|
+
throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
|
|
5074
|
+
}
|
|
5075
|
+
const view = await gateway.getMySkillAssignments(sessionCookie);
|
|
5076
|
+
if (typeof view !== "object" || view === null || !Array.isArray(view.assignments)) {
|
|
5077
|
+
this.deps.log?.("hiwork-core: \u6280\u80FD\u6E05\u5355\u54CD\u5E94\u4E0D\u662F\u5E26 assignments \u6570\u7EC4\u7684\u5BF9\u8C61\uFF08\u53EF\u80FD\u7684\u4E2D\u95F4\u5C42\uFF09\uFF0C\u6309\u53D6\u6570\u5931\u8D25\u5904\u7406");
|
|
5078
|
+
throw new GatewayError("GATEWAY_UNREACHABLE", MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE);
|
|
5079
|
+
}
|
|
5080
|
+
const assignments = [];
|
|
5081
|
+
for (const item of view.assignments) {
|
|
5082
|
+
const normalized = normalizeAssignment2(item, this.deps.log);
|
|
5083
|
+
if (normalized !== void 0) assignments.push(normalized);
|
|
5084
|
+
}
|
|
5085
|
+
const snapshot = {
|
|
5086
|
+
revision: typeof view.revision === "number" && Number.isFinite(view.revision) ? view.revision : 0,
|
|
5087
|
+
fetchedAt: new Date(this.now()).toISOString(),
|
|
5088
|
+
assignments,
|
|
5089
|
+
stale: false
|
|
5090
|
+
};
|
|
5091
|
+
this.cached = snapshot;
|
|
5092
|
+
this.cachedSession = sessionCookie;
|
|
5093
|
+
this.lastError = void 0;
|
|
5094
|
+
return snapshot;
|
|
5095
|
+
} catch (error) {
|
|
5096
|
+
this.lastError = failureOf2(error);
|
|
5097
|
+
throw error;
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
};
|
|
5101
|
+
function isAborted5(signal) {
|
|
5102
|
+
return signal?.aborted === true;
|
|
5103
|
+
}
|
|
5104
|
+
async function handleManagedSkillsEndpoint(endpoint, rawPayload, deps, store, signal) {
|
|
5105
|
+
try {
|
|
5106
|
+
if (isAborted5(signal)) return failure5("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
5107
|
+
if (!isManagedSkillsRpcEndpoint(endpoint)) {
|
|
5108
|
+
throw new ManagedSkillsRequestError("bad-request", `\u672A\u77E5\u7684\u6280\u80FD\u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
|
|
5109
|
+
}
|
|
5110
|
+
const payload = parseManagedSkillsPayload(endpoint, rawPayload);
|
|
5111
|
+
if (endpoint === "package") {
|
|
5112
|
+
const assignmentId = payload.assignmentId;
|
|
5113
|
+
if (assignmentId === void 0) throw new ManagedSkillsRequestError("bad-request", "\u53D6\u6280\u80FD\u5305\u8BF7\u6C42\u7F3A\u5C11 assignmentId\u3002");
|
|
5114
|
+
return await handlePackage(assignmentId, deps, signal);
|
|
5115
|
+
}
|
|
5116
|
+
const cached = await store.peekForCurrentSession();
|
|
5117
|
+
if (endpoint === "snapshot" && cached !== void 0) return { ok: true, value: cached };
|
|
5118
|
+
return { ok: true, value: await store.load() };
|
|
5119
|
+
} catch (error) {
|
|
5120
|
+
deps.log?.(`hiwork-core: \u6280\u80FD\u6E05\u5355 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5121
|
+
return { ok: false, error: failureOf2(error) };
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
async function handlePackage(assignmentId, deps, signal) {
|
|
5125
|
+
const sessionCookie = await deps.resolveSession();
|
|
5126
|
+
if (sessionCookie === void 0 || sessionCookie === "") {
|
|
5127
|
+
return failure5("SESSION_EXPIRED", MANAGED_SKILLS_SESSION_EXPIRED_MESSAGE);
|
|
5128
|
+
}
|
|
5129
|
+
const gateway = deps.gateway();
|
|
5130
|
+
if (gateway === void 0) {
|
|
5131
|
+
return failure5("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
|
|
5132
|
+
}
|
|
5133
|
+
if (isAborted5(signal)) return failure5("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
5134
|
+
const pkg = await gateway.getMySkillPackage(sessionCookie, assignmentId);
|
|
5135
|
+
const bytes = pkg?.bytes;
|
|
5136
|
+
const skillName = textOrUndefined2(pkg?.skillName);
|
|
5137
|
+
const contentSha256 = textOrUndefined2(pkg?.contentSha256);
|
|
5138
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0 || skillName === void 0 || contentSha256 === void 0) {
|
|
5139
|
+
deps.log?.("hiwork-core: \u53D6\u6280\u80FD\u5305\u6536\u5230\u65E0\u6548\u54CD\u5E94\uFF08\u7F3A skillName / contentSha256 \u6216\u7A7A\u5305\u4F53\uFF09");
|
|
5140
|
+
return failure5("internal", MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE);
|
|
5141
|
+
}
|
|
5142
|
+
if (bytes.byteLength > MAX_MANAGED_SKILL_ARCHIVE_BYTES) {
|
|
5143
|
+
deps.log?.("hiwork-core: \u6280\u80FD\u5305\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF0C\u5DF2\u62D2\u7EDD\u642C\u8FD0");
|
|
5144
|
+
return failure5(MANAGED_SKILLS_PACKAGE_TOO_LARGE_CODE, MANAGED_SKILLS_PACKAGE_TOO_LARGE_MESSAGE);
|
|
5145
|
+
}
|
|
5146
|
+
return {
|
|
5147
|
+
ok: true,
|
|
5148
|
+
value: {
|
|
5149
|
+
assignmentId,
|
|
5150
|
+
skillName,
|
|
5151
|
+
contentSha256,
|
|
5152
|
+
// Host 半边只跑在 Node 侧(`tsconfig.build.json` 排除 `src/client/**`,client bundle
|
|
5153
|
+
// 不 import 本文件),所以直接用 Buffer;客户端那半边的解码在 Task 11 的文件里。
|
|
5154
|
+
archiveBase64: Buffer.from(bytes).toString("base64")
|
|
5155
|
+
}
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
function failure5(code, message) {
|
|
5159
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
5160
|
+
}
|
|
5161
|
+
function registerManagedSkillsRpc(ctx, deps, store) {
|
|
5162
|
+
const handler = (endpoint, payload, signal) => handleManagedSkillsEndpoint(endpoint, payload, deps, store, signal);
|
|
5163
|
+
const remove = ctx.connection.rpc.handle(MANAGED_SKILLS_RPC_CHANNEL, handler, { authority: "loopback" });
|
|
5164
|
+
return async () => {
|
|
5165
|
+
await remove();
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
// src/workbench-bridge.ts
|
|
5170
|
+
var WORKBENCH_RPC_CHANNEL = "/hiwork-workbench";
|
|
5171
|
+
var WORKBENCH_RPC_ENDPOINTS = ["snapshot", "refresh"];
|
|
5172
|
+
var WORKBENCH_REFRESH_INTERVAL_MS = 30 * 6e4;
|
|
5173
|
+
var WorkbenchRequestError = class extends Error {
|
|
5174
|
+
constructor(code, message) {
|
|
5175
|
+
super(message);
|
|
5176
|
+
this.code = code;
|
|
5177
|
+
this.name = "WorkbenchRequestError";
|
|
5178
|
+
}
|
|
5179
|
+
};
|
|
5180
|
+
var WORKBENCH_GENERIC_FAILURE_MESSAGE = "\u5DE5\u4F5C\u53F0\u5E94\u7528\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
5181
|
+
var WORKBENCH_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
5182
|
+
var WORKBENCH_DISABLED_MESSAGE = "\u672C\u90E8\u7F72\u672A\u542F\u7528\u5DE5\u4F5C\u53F0\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458";
|
|
5183
|
+
var WORKBENCH_DISABLED_CODE = "WORKBENCH_DISABLED";
|
|
5184
|
+
var WORKBENCH_MESSAGE_FALLBACK = {
|
|
5185
|
+
SESSION_EXPIRED: WORKBENCH_SESSION_EXPIRED_MESSAGE,
|
|
5186
|
+
GATEWAY_UNREACHABLE: WORKBENCH_GENERIC_FAILURE_MESSAGE,
|
|
5187
|
+
[WORKBENCH_DISABLED_CODE]: WORKBENCH_DISABLED_MESSAGE
|
|
5188
|
+
};
|
|
5189
|
+
function displayWorkbenchMessage(code, message) {
|
|
5190
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
5191
|
+
if (text === "") return WORKBENCH_MESSAGE_FALLBACK[code] ?? WORKBENCH_GENERIC_FAILURE_MESSAGE;
|
|
5192
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
|
|
5193
|
+
if (leaksImplementationDetail) return WORKBENCH_MESSAGE_FALLBACK[code] ?? WORKBENCH_GENERIC_FAILURE_MESSAGE;
|
|
5194
|
+
return text;
|
|
5195
|
+
}
|
|
5196
|
+
function parseWorkbenchPayload(endpoint, raw) {
|
|
5197
|
+
if (raw === void 0 || raw === null) return {};
|
|
5198
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
5199
|
+
throw new WorkbenchRequestError("bad-request", "\u5DE5\u4F5C\u53F0\u6E05\u5355\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
|
|
5200
|
+
}
|
|
5201
|
+
const record = raw;
|
|
5202
|
+
const keys = Object.keys(record);
|
|
5203
|
+
const first = keys[0];
|
|
5204
|
+
if (first !== void 0) {
|
|
5205
|
+
throw new WorkbenchRequestError("bad-request", `\u5DE5\u4F5C\u53F0\u6E05\u5355\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${first}'\u3002`);
|
|
5206
|
+
}
|
|
5207
|
+
return {};
|
|
5208
|
+
}
|
|
5209
|
+
function isWorkbenchRpcEndpoint(endpoint) {
|
|
5210
|
+
return WORKBENCH_RPC_ENDPOINTS.includes(endpoint);
|
|
5211
|
+
}
|
|
5212
|
+
function textOrUndefined3(value) {
|
|
5213
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
5214
|
+
}
|
|
5215
|
+
function isRecord2(value) {
|
|
5216
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5217
|
+
}
|
|
5218
|
+
function stringArray(value) {
|
|
5219
|
+
if (!Array.isArray(value)) return [];
|
|
5220
|
+
return value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
5221
|
+
}
|
|
5222
|
+
function normalizeExecutor(raw) {
|
|
5223
|
+
if (!isRecord2(raw)) return void 0;
|
|
5224
|
+
const kind = textOrUndefined3(raw["kind"]);
|
|
5225
|
+
if (kind === void 0) return void 0;
|
|
5226
|
+
const executor = { ...raw, kind };
|
|
5227
|
+
const mode = textOrUndefined3(raw["mode"]);
|
|
5228
|
+
if (mode === void 0) delete executor.mode;
|
|
5229
|
+
else executor.mode = mode;
|
|
5230
|
+
return executor;
|
|
5231
|
+
}
|
|
5232
|
+
function normalizeInput(raw) {
|
|
5233
|
+
if (!isRecord2(raw)) return void 0;
|
|
5234
|
+
const key = textOrUndefined3(raw["key"]);
|
|
5235
|
+
if (key === void 0) return void 0;
|
|
5236
|
+
return {
|
|
5237
|
+
...raw,
|
|
5238
|
+
key,
|
|
5239
|
+
type: textOrUndefined3(raw["type"]) ?? "",
|
|
5240
|
+
label: textOrUndefined3(raw["label"]) ?? key,
|
|
5241
|
+
required: raw["required"] === true
|
|
5242
|
+
};
|
|
5243
|
+
}
|
|
5244
|
+
function normalizeOutput(raw) {
|
|
5245
|
+
if (!isRecord2(raw)) return void 0;
|
|
5246
|
+
const key = textOrUndefined3(raw["key"]);
|
|
5247
|
+
if (key === void 0) return void 0;
|
|
5248
|
+
return { ...raw, key, type: textOrUndefined3(raw["type"]) ?? "" };
|
|
5249
|
+
}
|
|
5250
|
+
function normalizeApp(raw, log) {
|
|
5251
|
+
if (!isRecord2(raw)) return void 0;
|
|
5252
|
+
const code = textOrUndefined3(raw["code"]);
|
|
5253
|
+
const executor = normalizeExecutor(raw["executor"]);
|
|
5254
|
+
if (code === void 0 || executor === void 0) {
|
|
5255
|
+
log?.("hiwork-core: \u5DE5\u4F5C\u53F0\u6E05\u5355\u91CC\u6709\u4E00\u6761\u7ED3\u6784\u4E0D\u5B8C\u6574\u7684\u5E94\u7528\uFF08\u7F3A code \u6216 executor.kind\uFF09\uFF0C\u5DF2\u8DF3\u8FC7");
|
|
5256
|
+
return void 0;
|
|
5257
|
+
}
|
|
5258
|
+
const iconUrl = textOrUndefined3(raw["icon"]);
|
|
5259
|
+
const category = isRecord2(raw["category"]) ? raw["category"] : void 0;
|
|
5260
|
+
const inputs = [];
|
|
5261
|
+
for (const item of Array.isArray(raw["inputs"]) ? raw["inputs"] : []) {
|
|
5262
|
+
const normalized = normalizeInput(item);
|
|
5263
|
+
if (normalized !== void 0) inputs.push(normalized);
|
|
5264
|
+
}
|
|
5265
|
+
const outputs = [];
|
|
5266
|
+
for (const item of Array.isArray(raw["outputs"]) ? raw["outputs"] : []) {
|
|
5267
|
+
const normalized = normalizeOutput(item);
|
|
5268
|
+
if (normalized !== void 0) outputs.push(normalized);
|
|
5269
|
+
}
|
|
5270
|
+
const revision = raw["revision"];
|
|
5271
|
+
return {
|
|
5272
|
+
code,
|
|
5273
|
+
displayName: textOrUndefined3(raw["displayName"]) ?? code,
|
|
5274
|
+
summary: textOrUndefined3(raw["summary"]) ?? "",
|
|
5275
|
+
// `exactOptionalPropertyTypes` 下不许显式写 undefined,用条件展开。
|
|
5276
|
+
...iconUrl === void 0 ? {} : { iconUrl },
|
|
5277
|
+
category: {
|
|
5278
|
+
code: textOrUndefined3(category?.["code"]) ?? "",
|
|
5279
|
+
name: textOrUndefined3(category?.["name"]) ?? ""
|
|
5280
|
+
},
|
|
5281
|
+
tags: stringArray(raw["tags"]),
|
|
5282
|
+
featured: raw["featured"] === true,
|
|
5283
|
+
revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
|
|
5284
|
+
executor,
|
|
5285
|
+
inputs,
|
|
5286
|
+
outputs,
|
|
5287
|
+
effects: stringArray(raw["effects"])
|
|
5288
|
+
};
|
|
5289
|
+
}
|
|
5290
|
+
function failureOf3(error) {
|
|
5291
|
+
if (error instanceof WorkbenchRequestError) return { code: error.code, message: error.message, details: {} };
|
|
5292
|
+
if (error instanceof GatewayError) {
|
|
5293
|
+
return { code: error.code, message: displayWorkbenchMessage(error.code, error.message), details: {} };
|
|
5294
|
+
}
|
|
5295
|
+
return { code: "internal", message: WORKBENCH_GENERIC_FAILURE_MESSAGE, details: {} };
|
|
5296
|
+
}
|
|
5297
|
+
function normalizeSession2(sessionCookie) {
|
|
5298
|
+
return sessionCookie === void 0 || sessionCookie === "" ? void 0 : sessionCookie;
|
|
5299
|
+
}
|
|
5300
|
+
function isAborted6(signal) {
|
|
5301
|
+
return signal?.aborted === true;
|
|
5302
|
+
}
|
|
5303
|
+
var WorkbenchStore = class {
|
|
5304
|
+
constructor(deps, now = () => Date.now()) {
|
|
5305
|
+
this.deps = deps;
|
|
5306
|
+
this.now = now;
|
|
5307
|
+
}
|
|
5308
|
+
cached;
|
|
5309
|
+
lastError;
|
|
5310
|
+
/** 产生 `cached` 的那个会话 cookie(只在内存里)。会话绑定的全部依据就是它。 */
|
|
5311
|
+
cachedSession;
|
|
5312
|
+
/**
|
|
5313
|
+
* 在飞请求连同它所用的会话一起记账:并发合并**只允许发生在同一会话内**。
|
|
5314
|
+
* 合并跨会话等于把 A 那次拉取的结果交给 B(登出 / 换账号后货架会显示上一个人的应用)。
|
|
5315
|
+
*/
|
|
5316
|
+
inFlight;
|
|
5317
|
+
/**
|
|
5318
|
+
* 当前缓存(同步读,不发请求)。从没成功过返回 undefined;
|
|
5319
|
+
* 最近一次失败会以 `stale + lastError` 标注在上一次成功的结果上。
|
|
5320
|
+
*
|
|
5321
|
+
* ⚠️ **本方法不做会话绑定**——它是 spec §4.5 冻结的同步读,而当前会话只能异步解析(见
|
|
5322
|
+
* {@link peekForCurrentSession}),同步函数里根本判不了「这份缓存还算不算我的」。
|
|
5323
|
+
* 所以模块内唯一的正确用法是:先 `await store.peekForCurrentSession()`,再(或由它内部)`peek()`。
|
|
5324
|
+
* 直接调 `peek()` 只适合诊断与测试断言内存状态。
|
|
5325
|
+
*/
|
|
5326
|
+
peek() {
|
|
5327
|
+
if (this.cached === void 0) return void 0;
|
|
5328
|
+
return this.lastError === void 0 ? this.cached : { ...this.cached, stale: true, lastError: this.lastError };
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* 「当前会话下的缓存」= 解析当前会话 → 剪枝 → {@link peek}。
|
|
5332
|
+
*
|
|
5333
|
+
* 与 `src/managed-skills.ts` 的同名方法逐字同款:`peek()` 是 spec §4.5 冻结的**同步**接口,而
|
|
5334
|
+
* 「当前是谁」只有 `deps.resolveSession()` 异步拿得到,于是分工只能是「async 的一侧先绑会话,
|
|
5335
|
+
* sync 的 `peek()` 再读」。端点实现必须走它、不许绕过。
|
|
5336
|
+
*/
|
|
5337
|
+
async peekForCurrentSession() {
|
|
5338
|
+
this.bindSession(normalizeSession2(await this.deps.resolveSession()));
|
|
5339
|
+
return this.peek();
|
|
5340
|
+
}
|
|
5341
|
+
/**
|
|
5342
|
+
* 会话绑定:缓存只属于产生它的那个会话;当前会话不是它(含**当前没有会话**)→ 立刻丢弃。
|
|
5343
|
+
*
|
|
5344
|
+
* 与技能清单那半逐字同款(那里写了为什么**不许**为了「两半对齐」改回 MCP 的写法):
|
|
5345
|
+
* 绑会话**自带**——不依赖任何调用方、不依赖谁记得清缓存,登出后(当前会话为空)与换账号后
|
|
5346
|
+
* 都不会再吐出上一个人的清单。`clear()` 也没有任何调用方(端点白名单够不到 Host 内存),
|
|
5347
|
+
* 把安全不变量挂在一个没人调的钩子上正是已经失败过一次的做法。
|
|
5348
|
+
*
|
|
5349
|
+
* 它也**不**触碰「失败不覆写缓存」那条铁律:剪枝的触发条件是**会话变了**,不是「这一次拉取
|
|
5350
|
+
* 失败了」。失败路径仍然原样保留本会话上一次成功的清单(`stale + lastError`)。
|
|
5351
|
+
*/
|
|
5352
|
+
bindSession(current) {
|
|
5353
|
+
if (current === this.cachedSession) return;
|
|
5354
|
+
this.cached = void 0;
|
|
5355
|
+
this.cachedSession = void 0;
|
|
5356
|
+
this.lastError = void 0;
|
|
5357
|
+
}
|
|
5358
|
+
/**
|
|
5359
|
+
* 去网关拉一次清单并更新缓存。失败时抛异常,且**本会话的缓存原样保留**。
|
|
5360
|
+
*
|
|
5361
|
+
* 并发调用合并成一次请求:后台预热与消费方的手动刷新撞在一起时不必打两遍网关。
|
|
5362
|
+
* 合并**只在同一会话内**——两个会话合并到一起,就等于把 A 的结果发给 B。
|
|
5363
|
+
*/
|
|
5364
|
+
async load() {
|
|
5365
|
+
const session = normalizeSession2(await this.deps.resolveSession());
|
|
5366
|
+
const inFlight = this.inFlight;
|
|
5367
|
+
if (inFlight !== void 0 && inFlight.session === session) return await inFlight.task;
|
|
5368
|
+
const task = this.fetchOnce(session);
|
|
5369
|
+
this.inFlight = { session, task };
|
|
5370
|
+
try {
|
|
5371
|
+
return await task;
|
|
5372
|
+
} finally {
|
|
5373
|
+
if (this.inFlight?.task === task) this.inFlight = void 0;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
/**
|
|
5377
|
+
* 清空内存清单(登出 / 换账号调用)。
|
|
5378
|
+
*
|
|
5379
|
+
* ⚠️ **当前没有任何调用方**:这是 spec §4.5 冻结要求提供的接口(与技能清单那半同款),而它的
|
|
5380
|
+
* 用途已经被**会话绑定**覆盖——缓存按会话剪枝,登出 / 换账号后再也吐不出上一个人的清单。
|
|
5381
|
+
* 保留它是为了不改冻结接口;真要删得先改 spec §4.5。冻结接口里的成员不要悄悄删掉。
|
|
5382
|
+
*/
|
|
5383
|
+
clear() {
|
|
5384
|
+
this.cached = void 0;
|
|
5385
|
+
this.lastError = void 0;
|
|
5386
|
+
this.cachedSession = void 0;
|
|
5387
|
+
}
|
|
5388
|
+
async fetchOnce(sessionCookie) {
|
|
5389
|
+
try {
|
|
5390
|
+
if (sessionCookie === void 0) {
|
|
5391
|
+
throw new GatewayError("SESSION_EXPIRED", WORKBENCH_SESSION_EXPIRED_MESSAGE);
|
|
5392
|
+
}
|
|
5393
|
+
const gateway = this.deps.gateway();
|
|
5394
|
+
if (gateway === void 0) {
|
|
5395
|
+
throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55");
|
|
5396
|
+
}
|
|
5397
|
+
const view = await gateway.getMyWorkbenchApps(sessionCookie);
|
|
5398
|
+
if (!isRecord2(view) || !Array.isArray(view.apps)) {
|
|
5399
|
+
this.deps.log?.("hiwork-core: \u5DE5\u4F5C\u53F0\u6E05\u5355\u54CD\u5E94\u4E0D\u662F\u5E26 apps \u6570\u7EC4\u7684\u5BF9\u8C61\uFF08\u53EF\u80FD\u7684\u4E2D\u95F4\u5C42\uFF09\uFF0C\u6309\u53D6\u6570\u5931\u8D25\u5904\u7406");
|
|
5400
|
+
throw new GatewayError("GATEWAY_UNREACHABLE", WORKBENCH_GENERIC_FAILURE_MESSAGE);
|
|
5401
|
+
}
|
|
5402
|
+
const apps = [];
|
|
5403
|
+
for (const item of view.apps) {
|
|
5404
|
+
const normalized = normalizeApp(item, this.deps.log);
|
|
5405
|
+
if (normalized !== void 0) apps.push(normalized);
|
|
5406
|
+
}
|
|
5407
|
+
const snapshot = {
|
|
5408
|
+
revision: typeof view.revision === "number" && Number.isFinite(view.revision) ? view.revision : 0,
|
|
5409
|
+
fetchedAt: new Date(this.now()).toISOString(),
|
|
5410
|
+
apps,
|
|
5411
|
+
stale: false
|
|
5412
|
+
};
|
|
5413
|
+
this.cached = snapshot;
|
|
5414
|
+
this.cachedSession = sessionCookie;
|
|
5415
|
+
this.lastError = void 0;
|
|
5416
|
+
return snapshot;
|
|
5417
|
+
} catch (error) {
|
|
5418
|
+
this.lastError = failureOf3(error);
|
|
5419
|
+
throw error;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
};
|
|
5423
|
+
async function handleWorkbenchEndpoint(endpoint, rawPayload, deps, store, signal) {
|
|
5424
|
+
try {
|
|
5425
|
+
if (isAborted6(signal)) return failure6("cancelled", "\u5DE5\u4F5C\u53F0\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
5426
|
+
if (!isWorkbenchRpcEndpoint(endpoint)) {
|
|
5427
|
+
throw new WorkbenchRequestError("bad-request", `\u672A\u77E5\u7684\u5DE5\u4F5C\u53F0\u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
|
|
5428
|
+
}
|
|
5429
|
+
parseWorkbenchPayload(endpoint, rawPayload);
|
|
5430
|
+
const cached = await store.peekForCurrentSession();
|
|
5431
|
+
if (endpoint === "snapshot" && cached !== void 0) return { ok: true, value: cached };
|
|
5432
|
+
return { ok: true, value: await store.load() };
|
|
5433
|
+
} catch (error) {
|
|
5434
|
+
deps.log?.(`hiwork-core: \u5DE5\u4F5C\u53F0\u6E05\u5355 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5435
|
+
return { ok: false, error: failureOf3(error) };
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
function failure6(code, message) {
|
|
5439
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
5440
|
+
}
|
|
5441
|
+
function registerWorkbenchRpc(ctx, deps, store) {
|
|
5442
|
+
const handler = (endpoint, payload, signal) => handleWorkbenchEndpoint(endpoint, payload, deps, store, signal);
|
|
5443
|
+
const remove = ctx.connection.rpc.handle(WORKBENCH_RPC_CHANNEL, handler, { authority: "loopback" });
|
|
5444
|
+
return async () => {
|
|
5445
|
+
await remove();
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
// src/office-credentials.ts
|
|
5450
|
+
var OFFICE_CREDENTIALS_SERVICE = "hiworkOfficeCredentials";
|
|
5451
|
+
var OFFICE_MESSAGE_FALLBACK = {
|
|
5452
|
+
SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
5453
|
+
GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
5454
|
+
OFFICE_NOT_CONFIGURED: "\u672C\u90E8\u7F72\u5C1A\u672A\u914D\u7F6E HiWork Office \u6587\u6863\u529E\u516C\u96C6\u6210\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458"
|
|
5455
|
+
};
|
|
5456
|
+
var OFFICE_CREDENTIALS_GENERIC_FAILURE_MESSAGE = "HiWork Office \u96C6\u6210\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
5457
|
+
var OFFICE_CREDENTIALS_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
5458
|
+
function displayOfficeGatewayMessage(code, message) {
|
|
5459
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
5460
|
+
if (text === "") return OFFICE_MESSAGE_FALLBACK[code] ?? OFFICE_CREDENTIALS_GENERIC_FAILURE_MESSAGE;
|
|
5461
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
|
|
5462
|
+
if (leaksImplementationDetail) return OFFICE_MESSAGE_FALLBACK[code] ?? OFFICE_CREDENTIALS_GENERIC_FAILURE_MESSAGE;
|
|
5463
|
+
return text;
|
|
5464
|
+
}
|
|
5465
|
+
var OfficeCredentialsBroker = class {
|
|
5466
|
+
constructor(deps) {
|
|
5467
|
+
this.deps = deps;
|
|
5468
|
+
}
|
|
5469
|
+
async resolve() {
|
|
5470
|
+
try {
|
|
5471
|
+
const sessionCookie = await this.deps.resolveSession();
|
|
5472
|
+
if (sessionCookie === void 0 || sessionCookie === "") {
|
|
5473
|
+
return { ok: false, code: "SESSION_EXPIRED", message: OFFICE_CREDENTIALS_SESSION_EXPIRED_MESSAGE };
|
|
5474
|
+
}
|
|
5475
|
+
const gateway = this.deps.gateway();
|
|
5476
|
+
if (gateway === void 0) {
|
|
5477
|
+
return {
|
|
5478
|
+
ok: false,
|
|
5479
|
+
code: "GATEWAY_UNREACHABLE",
|
|
5480
|
+
message: "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55"
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
const view = await gateway.getMyOfficeConfig(sessionCookie);
|
|
5484
|
+
const documentServerUrl = typeof view?.documentServerUrl === "string" ? view.documentServerUrl.trim() : "";
|
|
5485
|
+
const jwtSecret = typeof view?.jwtSecret === "string" ? view.jwtSecret.trim() : "";
|
|
5486
|
+
if (documentServerUrl === "" || jwtSecret === "") {
|
|
5487
|
+
this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 office-config \u4E0D\u5B8C\u6574\uFF08\u7F3A documentServerUrl \u6216 jwtSecret\uFF09");
|
|
5488
|
+
return { ok: false, code: "GATEWAY_UNREACHABLE", message: OFFICE_CREDENTIALS_GENERIC_FAILURE_MESSAGE };
|
|
5489
|
+
}
|
|
5490
|
+
return {
|
|
5491
|
+
ok: true,
|
|
5492
|
+
value: {
|
|
5493
|
+
documentServerUrl,
|
|
5494
|
+
jwtSecret,
|
|
5495
|
+
fetchedAt: (this.deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
5496
|
+
relayEnabled: view?.relayEnabled === true
|
|
5497
|
+
}
|
|
5498
|
+
};
|
|
5499
|
+
} catch (error) {
|
|
5500
|
+
if (error instanceof GatewayError) {
|
|
5501
|
+
return { ok: false, code: error.code, message: displayOfficeGatewayMessage(error.code, error.message) };
|
|
5502
|
+
}
|
|
5503
|
+
this.deps.log?.(
|
|
5504
|
+
`hiwork-core: HiWork Office \u51ED\u636E\u83B7\u53D6\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`
|
|
5505
|
+
);
|
|
5506
|
+
return { ok: false, code: "internal", message: OFFICE_CREDENTIALS_GENERIC_FAILURE_MESSAGE };
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
};
|
|
5510
|
+
function provideOfficeCredentials(ctx, deps) {
|
|
5511
|
+
const service = new OfficeCredentialsBroker(deps);
|
|
5512
|
+
const dispose = ctx.provide(OFFICE_CREDENTIALS_SERVICE, service);
|
|
5513
|
+
return typeof dispose === "function" ? dispose : () => {
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
// src/office-relay.ts
|
|
5518
|
+
var OFFICE_RELAY_SERVICE = "hiworkOfficeRelay";
|
|
5519
|
+
var OFFICE_RELAY_MESSAGE_FALLBACK = {
|
|
5520
|
+
SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
5521
|
+
GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
5522
|
+
RELAY_DISABLED: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u6587\u6863\u4E2D\u7EE7\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
5523
|
+
RELAY_EXPIRED: "\u7F16\u8F91\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u6587\u6863",
|
|
5524
|
+
RELAY_TOO_LARGE: "\u6587\u6863\u8D85\u8FC7\u5728\u7EBF\u7F16\u8F91\u7684\u5927\u5C0F\u4E0A\u9650",
|
|
5525
|
+
RELAY_EXTENSION: "\u8BE5\u6587\u6863\u7C7B\u578B\u4E0D\u652F\u6301\u5728\u7EBF\u7F16\u8F91",
|
|
5526
|
+
RELAY_QUOTA: "\u540C\u65F6\u6253\u5F00\u7684\u7F16\u8F91\u4F1A\u8BDD\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5"
|
|
5527
|
+
};
|
|
5528
|
+
var OFFICE_RELAY_GENERIC_FAILURE_MESSAGE = "\u6587\u6863\u4E2D\u7EE7\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
5529
|
+
var OFFICE_RELAY_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
5530
|
+
function displayOfficeRelayMessage(code, message) {
|
|
5531
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
5532
|
+
if (text === "") return OFFICE_RELAY_MESSAGE_FALLBACK[code] ?? OFFICE_RELAY_GENERIC_FAILURE_MESSAGE;
|
|
5533
|
+
const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
|
|
5534
|
+
if (leaksImplementationDetail) return OFFICE_RELAY_MESSAGE_FALLBACK[code] ?? OFFICE_RELAY_GENERIC_FAILURE_MESSAGE;
|
|
5535
|
+
return text;
|
|
5536
|
+
}
|
|
5537
|
+
var OfficeRelayBroker = class {
|
|
5538
|
+
constructor(deps) {
|
|
5539
|
+
this.deps = deps;
|
|
5540
|
+
}
|
|
5541
|
+
async withSession(run) {
|
|
5542
|
+
try {
|
|
5543
|
+
const sessionCookie = await this.deps.resolveSession();
|
|
5544
|
+
if (sessionCookie === void 0 || sessionCookie === "") {
|
|
5545
|
+
return { ok: false, code: "SESSION_EXPIRED", message: OFFICE_RELAY_SESSION_EXPIRED_MESSAGE };
|
|
5546
|
+
}
|
|
5547
|
+
const gateway = this.deps.gateway();
|
|
5548
|
+
if (gateway === void 0) {
|
|
5549
|
+
return { ok: false, code: "GATEWAY_UNREACHABLE", message: "\u7F51\u5173\u5730\u5740\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u7F51\u5173\u6A21\u578B\u300D\u91CC\u5B8C\u6210\u767B\u5F55" };
|
|
5550
|
+
}
|
|
5551
|
+
return { ok: true, value: await run(sessionCookie, gateway) };
|
|
5552
|
+
} catch (error) {
|
|
5553
|
+
if (error instanceof GatewayError) {
|
|
5554
|
+
return { ok: false, code: error.code, message: displayOfficeRelayMessage(error.code, error.message) };
|
|
5555
|
+
}
|
|
5556
|
+
this.deps.log?.(`hiwork-core: \u6587\u6863\u4E2D\u7EE7\u8C03\u7528\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5557
|
+
return { ok: false, code: "internal", message: OFFICE_RELAY_GENERIC_FAILURE_MESSAGE };
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
async upload(input, signal) {
|
|
5561
|
+
if (signal?.aborted === true) return { ok: false, code: "cancelled", message: "\u4E0A\u4F20\u5DF2\u53D6\u6D88\u3002" };
|
|
5562
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
5563
|
+
const created = await gateway.createOfficeRelayDocument(cookie, input.name, input.bytes, input.mode);
|
|
5564
|
+
return {
|
|
5565
|
+
id: created.id,
|
|
5566
|
+
documentKey: created.documentKey,
|
|
5567
|
+
readUrl: created.readUrl,
|
|
5568
|
+
callbackUrl: created.callbackUrl,
|
|
5569
|
+
expiresAt: created.expiresAt
|
|
5570
|
+
};
|
|
5571
|
+
});
|
|
5572
|
+
}
|
|
5573
|
+
async status(id, _signal) {
|
|
5574
|
+
return await this.withSession(async (cookie, gateway) => await gateway.getOfficeRelayStatus(cookie, id));
|
|
5575
|
+
}
|
|
5576
|
+
async download(id, revision, _signal) {
|
|
5577
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
5578
|
+
const content = await gateway.getOfficeRelayContent(cookie, id, revision);
|
|
5579
|
+
return content.notModified ? { notModified: true } : { notModified: false, ...content.bytes === void 0 ? {} : { bytes: content.bytes } };
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
async close(id, _signal) {
|
|
5583
|
+
return await this.withSession(async (cookie, gateway) => {
|
|
5584
|
+
await gateway.closeOfficeRelayDocument(cookie, id);
|
|
5585
|
+
return {};
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
};
|
|
5589
|
+
function provideOfficeRelay(ctx, deps) {
|
|
5590
|
+
const service = new OfficeRelayBroker(deps);
|
|
5591
|
+
const dispose = ctx.provide(OFFICE_RELAY_SERVICE, service);
|
|
5592
|
+
return typeof dispose === "function" ? dispose : () => {
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
|
|
4410
5596
|
// src/index.ts
|
|
4411
5597
|
var name = "hiwork-core";
|
|
4412
5598
|
var inject = ["settings", "credentials", "workspaceRegistry", "connection"];
|
|
@@ -4762,6 +5948,110 @@ function apply(ctx, config) {
|
|
|
4762
5948
|
ctx.logger.warn(message);
|
|
4763
5949
|
}
|
|
4764
5950
|
}), "hiwork-core: daily rpc");
|
|
5951
|
+
const managedMcpDeps = {
|
|
5952
|
+
resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
|
|
5953
|
+
gateway: () => {
|
|
5954
|
+
const baseURL = current().baseURL;
|
|
5955
|
+
return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
|
|
5956
|
+
},
|
|
5957
|
+
log: (message) => {
|
|
5958
|
+
ctx.logger.warn(message);
|
|
5959
|
+
}
|
|
5960
|
+
};
|
|
5961
|
+
const managedMcp = new ManagedMcpStore(managedMcpDeps);
|
|
5962
|
+
ctx.effect(
|
|
5963
|
+
() => registerManagedMcpRpc({ connection, logger: ctx.logger }, managedMcpDeps, managedMcp),
|
|
5964
|
+
"hiwork-core: managed mcp rpc"
|
|
5965
|
+
);
|
|
5966
|
+
ctx.effect(() => {
|
|
5967
|
+
const warm = () => {
|
|
5968
|
+
void managedMcp.load().catch((error) => {
|
|
5969
|
+
ctx.logger.warn(`hiwork-core: \u53D7\u7BA1 MCP \u6E05\u5355\u9884\u70ED\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5970
|
+
});
|
|
5971
|
+
};
|
|
5972
|
+
warm();
|
|
5973
|
+
const timer = setInterval(warm, MANAGED_MCP_REFRESH_INTERVAL_MS);
|
|
5974
|
+
timer.unref?.();
|
|
5975
|
+
return () => {
|
|
5976
|
+
clearInterval(timer);
|
|
5977
|
+
};
|
|
5978
|
+
}, "hiwork-core: managed mcp warmup");
|
|
5979
|
+
const managedSkillsDeps = {
|
|
5980
|
+
resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
|
|
5981
|
+
gateway: () => {
|
|
5982
|
+
const baseURL = current().baseURL;
|
|
5983
|
+
return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
|
|
5984
|
+
},
|
|
5985
|
+
log: (message) => {
|
|
5986
|
+
ctx.logger.warn(message);
|
|
5987
|
+
}
|
|
5988
|
+
};
|
|
5989
|
+
const managedSkills = new ManagedSkillsStore(managedSkillsDeps);
|
|
5990
|
+
ctx.effect(
|
|
5991
|
+
() => registerManagedSkillsRpc({ connection, logger: ctx.logger }, managedSkillsDeps, managedSkills),
|
|
5992
|
+
"hiwork-core: managed skills rpc"
|
|
5993
|
+
);
|
|
5994
|
+
ctx.effect(() => {
|
|
5995
|
+
const warm = () => {
|
|
5996
|
+
void managedSkills.load().catch((error) => {
|
|
5997
|
+
ctx.logger.warn(`hiwork-core: \u53D7\u7BA1\u6280\u80FD\u6E05\u5355\u9884\u70ED\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5998
|
+
});
|
|
5999
|
+
};
|
|
6000
|
+
warm();
|
|
6001
|
+
const timer = setInterval(warm, MANAGED_SKILLS_REFRESH_INTERVAL_MS);
|
|
6002
|
+
timer.unref?.();
|
|
6003
|
+
return () => {
|
|
6004
|
+
clearInterval(timer);
|
|
6005
|
+
};
|
|
6006
|
+
}, "hiwork-core: managed skills warmup");
|
|
6007
|
+
const workbenchDeps = {
|
|
6008
|
+
resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
|
|
6009
|
+
gateway: () => {
|
|
6010
|
+
const baseURL = current().baseURL;
|
|
6011
|
+
return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
|
|
6012
|
+
},
|
|
6013
|
+
log: (message) => {
|
|
6014
|
+
ctx.logger.warn(message);
|
|
6015
|
+
}
|
|
6016
|
+
};
|
|
6017
|
+
const workbench = new WorkbenchStore(workbenchDeps);
|
|
6018
|
+
ctx.effect(
|
|
6019
|
+
() => registerWorkbenchRpc({ connection, logger: ctx.logger }, workbenchDeps, workbench),
|
|
6020
|
+
"hiwork-core: workbench rpc"
|
|
6021
|
+
);
|
|
6022
|
+
ctx.effect(() => {
|
|
6023
|
+
const warm = () => {
|
|
6024
|
+
void workbench.load().catch((error) => {
|
|
6025
|
+
ctx.logger.warn(`hiwork-core: \u5DE5\u4F5C\u53F0\u5E94\u7528\u6E05\u5355\u9884\u70ED\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
6026
|
+
});
|
|
6027
|
+
};
|
|
6028
|
+
warm();
|
|
6029
|
+
const timer = setInterval(warm, WORKBENCH_REFRESH_INTERVAL_MS);
|
|
6030
|
+
timer.unref?.();
|
|
6031
|
+
return () => {
|
|
6032
|
+
clearInterval(timer);
|
|
6033
|
+
};
|
|
6034
|
+
}, "hiwork-core: workbench warmup");
|
|
6035
|
+
ctx.effect(() => provideOfficeCredentials(ctx, {
|
|
6036
|
+
resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
|
|
6037
|
+
gateway: () => {
|
|
6038
|
+
const baseURL = current().baseURL;
|
|
6039
|
+
return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
|
|
6040
|
+
},
|
|
6041
|
+
log: (message) => {
|
|
6042
|
+
ctx.logger.warn(message);
|
|
6043
|
+
}
|
|
6044
|
+
}), "hiwork-core: office credentials");
|
|
6045
|
+
ctx.effect(() => provideOfficeRelay(ctx, {
|
|
6046
|
+
resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
|
|
6047
|
+
gateway: () => {
|
|
6048
|
+
const baseURL = current().baseURL;
|
|
6049
|
+
return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
|
|
6050
|
+
},
|
|
6051
|
+
log: (message) => {
|
|
6052
|
+
ctx.logger.warn(message);
|
|
6053
|
+
}
|
|
6054
|
+
}), "hiwork-core: office relay");
|
|
4765
6055
|
ctx.effect(() => registerAppVersionRpc({ connection, logger: ctx.logger }, {
|
|
4766
6056
|
appVersion: () => pushedAppVersion(),
|
|
4767
6057
|
log: (message) => {
|