hiwork-core 0.0.22 → 0.0.29

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/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.22";
3140
+ var PKG_VERSION = false ? "dev" : "0.0.29";
3141
3141
 
3142
3142
  // src/ai-router-client.ts
3143
3143
  var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
@@ -3157,6 +3157,19 @@ var ME_SKILL_PACKAGES_PATH = "/api/me/skill-packages";
3157
3157
  var ME_WORKBENCH_APPS_PATH = "/api/me/workbench-apps";
3158
3158
  var ME_OFFICE_CONFIG_PATH = "/api/me/office-config";
3159
3159
  var ME_OFFICE_RELAY_DOCUMENTS_PATH = "/api/me/office-relay/documents";
3160
+ var ME_VOICE_CONFIG_PATH = "/api/me/voice-config";
3161
+ var ME_VOICE_TRANSCRIBE_PATH = "/api/me/voice-transcribe";
3162
+ var VOICE_TRANSCRIBE_TIMEOUT_MS = 6e4;
3163
+ var VOICE_CONFIG_FALLBACK_MAX_SECONDS = 60;
3164
+ var VOICE_ERROR_FALLBACK_MESSAGE = {
3165
+ VOICE_DISABLED: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u8BED\u97F3\u8BC6\u522B\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
3166
+ VOICE_NOT_CONFIGURED: "\u672C\u90E8\u7F72\u5C1A\u672A\u914D\u7F6E\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
3167
+ VOICE_LIMIT_REACHED: "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
3168
+ VOICE_AUDIO_TOO_LARGE: "\u5F55\u97F3\u8FC7\u957F\u6216\u6587\u4EF6\u8FC7\u5927\uFF0C\u8BF7\u7F29\u77ED\u540E\u91CD\u8BD5",
3169
+ VOICE_BAD_REQUEST: "\u5F55\u97F3\u6570\u636E\u4E0D\u5408\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u5F55\u5236",
3170
+ VOICE_UPSTREAM: "\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
3171
+ };
3172
+ var VOICE_ERROR_DEFAULT_MESSAGE = "\u8BED\u97F3\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3160
3173
  var DEFAULT_CLIENT_ID = "hiwork-desktop";
3161
3174
  var DEFAULT_REDIRECT_URI = "hiwork://auth/callback";
3162
3175
  var GatewayError = class extends Error {
@@ -3548,6 +3561,81 @@ var AiRouterClient = class {
3548
3561
  if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3549
3562
  return await parseMeJson(res);
3550
3563
  }
3564
+ /**
3565
+ * GET /api/me/voice-config:当前账号的语音识别开关与录音时长上限。
3566
+ *
3567
+ * 与每日一句的 `getMyDailyQuote` 同款,**这两个方法不在 {@link GatewayClient} 接口上**:
3568
+ * 那个接口是编排层的端口,「语音识别」与它无关,加进去只会逼着每处编排层假网关
3569
+ * (tests/orchestration.spec.ts 里一堆)都补两个用不到的桩。频道侧改用结构子集
3570
+ * `VoiceGatewayPort`(见 `src/voice-rpc.ts`),与 usage / daily 三处一致。
3571
+ *
3572
+ * 与 {@link getMyMcpAssignments} 同样的理由**不复用 `getMeJson`**:那条路径把 403 映射成
3573
+ * `NO_KEY_ASSIGNED`(「尚未被分配虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配
3574
+ * Key 的账号照样应该有语音识别(分能力与分 Key 是两件事)。沿用它的文案会把「没有 Key」
3575
+ * 错报成原因。
3576
+ *
3577
+ * 本端点契约**恒 200**(开关开与关都是 200 + `enabled` 布尔),因此除 401 以外没有任何
3578
+ * 需要单独解释的拒绝语义:403 / 404 / 5xx / 反代错误页一律收敛成 `GATEWAY_UNREACHABLE`。
3579
+ *
3580
+ * 形状收敛(**本层负责**,消费方拿到的永远是一个合法的 {@link GatewayVoiceConfig}):
3581
+ * 网关线上是 snake_case `max_audio_seconds`,这里转 camelCase;字段缺失 / 类型不符
3582
+ * 按「禁用 + 60 秒」处理——配置读不出来时话筒本就该消失,用一个 undefined 让消费方
3583
+ * 去猜是把形状问题推给下游。真正的「读不到就回落」再往上一层(`src/voice-rpc.ts`)。
3584
+ */
3585
+ async getMyVoiceConfig(sessionCookie) {
3586
+ const res = await this.requestOrUnreachable(ME_VOICE_CONFIG_PATH, { method: "GET", sessionCookie });
3587
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3588
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
3589
+ const data = await parseMeJson(res);
3590
+ const seconds = data.max_audio_seconds;
3591
+ return {
3592
+ enabled: data.enabled === true,
3593
+ maxDurationSeconds: typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds : VOICE_CONFIG_FALLBACK_MAX_SECONDS
3594
+ };
3595
+ }
3596
+ /**
3597
+ * POST /api/me/voice-transcribe:把一段录音(base64)交给网关转发上游做识别。
3598
+ *
3599
+ * 状态码映射与服务端 handler 一一对应,**不许合并**:
3600
+ * - 401 = 会话过期(唯一一条能让人做点什么——重新登录——的拒绝);
3601
+ * - 503 = 网关自己发的「功能不可用」,再按响应体 `code` 细分:
3602
+ * `VOICE_NOT_CONFIGURED`(部署没配上游)/ `VOICE_DISABLED`(管理员把开关关了)。
3603
+ * 两者给的提示不同(该找管理员配服务 vs 该找管理员开开关)。
3604
+ * ⚠️ 响应体没有 `code`(非 JSON / 反代错误页)也归 `VOICE_DISABLED`——这条路由的
3605
+ * 503 语义就是「没开」,而**上游故障走 502**(见下),不会伪装成 503;
3606
+ * - 429 / 413 / 400 = 配额 / 超长 / 请求体不合法,三种不同的事,分开映射(合并后
3607
+ * 客户端只会看到一句「识别失败」,413 的人不知道该录短一点);
3608
+ * - 其余(含 502 = 上游出错)→ `VOICE_UPSTREAM`。
3609
+ *
3610
+ * message 一律取网关响应体里的 `message`(网关侧是冻结的中文文案,会直接展示给用户);
3611
+ * 响应体不是合法 JSON / 缺 message 时退回本地兜底文案。**绝不把上游 / 网关原文回显**:
3612
+ * 读取响应体走 {@link readVoiceError}(解析失败即空对象),与 `parseMeJson` 不回显原文同源。
3613
+ *
3614
+ * 超时**单独放宽到 60s**(见 {@link VOICE_TRANSCRIBE_TIMEOUT_MS}):这条要上传录音 +
3615
+ * 等上游识别,构造时的 10s 默认值在这里是误报源。
3616
+ */
3617
+ async transcribeVoice(sessionCookie, audioBase64) {
3618
+ const res = await this.requestOrUnreachable(ME_VOICE_TRANSCRIBE_PATH, {
3619
+ method: "POST",
3620
+ sessionCookie,
3621
+ body: { audio_base64: audioBase64 },
3622
+ timeoutMs: VOICE_TRANSCRIBE_TIMEOUT_MS
3623
+ });
3624
+ if (res.status === 401) throw new GatewayError("SESSION_EXPIRED", "\u7F51\u5173\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 401);
3625
+ if (!res.ok) {
3626
+ const body = await readVoiceError(res);
3627
+ if (res.status === 503) {
3628
+ const code = body.code === "VOICE_NOT_CONFIGURED" ? "VOICE_NOT_CONFIGURED" : "VOICE_DISABLED";
3629
+ throw new GatewayError(code, voiceMessage(code, body), 503);
3630
+ }
3631
+ if (res.status === 429) throw new GatewayError("VOICE_LIMIT_REACHED", voiceMessage("VOICE_LIMIT_REACHED", body), 429);
3632
+ if (res.status === 413) throw new GatewayError("VOICE_AUDIO_TOO_LARGE", voiceMessage("VOICE_AUDIO_TOO_LARGE", body), 413);
3633
+ if (res.status === 400) throw new GatewayError("VOICE_BAD_REQUEST", voiceMessage("VOICE_BAD_REQUEST", body), 400);
3634
+ throw new GatewayError("VOICE_UPSTREAM", voiceMessage("VOICE_UPSTREAM", body), res.status);
3635
+ }
3636
+ const data = await parseMeJson(res);
3637
+ return { text: typeof data.text === "string" ? data.text : "" };
3638
+ }
3551
3639
  /**
3552
3640
  * /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
3553
3641
  * 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
@@ -3584,7 +3672,7 @@ var AiRouterClient = class {
3584
3672
  const init = {
3585
3673
  method: opts.method,
3586
3674
  headers,
3587
- signal: AbortSignal.timeout(this.timeoutMs)
3675
+ signal: AbortSignal.timeout(opts.timeoutMs ?? this.timeoutMs)
3588
3676
  };
3589
3677
  if (opts.rawBody !== void 0) {
3590
3678
  init.body = opts.rawBody.buffer.slice(
@@ -3652,6 +3740,18 @@ async function parseMeJson(res) {
3652
3740
  }
3653
3741
  return data;
3654
3742
  }
3743
+ async function readVoiceError(res) {
3744
+ const data = await readJson(res);
3745
+ if (typeof data !== "object" || data === null) return {};
3746
+ const record = data;
3747
+ return {
3748
+ ...typeof record.code === "string" && record.code !== "" ? { code: record.code } : {},
3749
+ ...typeof record.message === "string" && record.message.trim() !== "" ? { message: record.message.trim() } : {}
3750
+ };
3751
+ }
3752
+ function voiceMessage(code, body) {
3753
+ return body.message ?? VOICE_ERROR_FALLBACK_MESSAGE[code] ?? VOICE_ERROR_DEFAULT_MESSAGE;
3754
+ }
3655
3755
  function isInvalidGrant(payload) {
3656
3756
  return typeof payload === "object" && payload !== null && payload.error === "invalid_grant";
3657
3757
  }
@@ -4218,8 +4318,8 @@ function parseAppVersionPayload(raw) {
4218
4318
  if (keys.length > 0) throw new AppVersionRequestError("bad-request", `\u684C\u9762\u7AEF\u7248\u672C\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${keys[0]}'\u3002`);
4219
4319
  }
4220
4320
  function normalizedVersion(raw) {
4221
- const version = typeof raw === "string" ? raw.trim() : "";
4222
- return version === "" ? null : version;
4321
+ const version2 = typeof raw === "string" ? raw.trim() : "";
4322
+ return version2 === "" ? null : version2;
4223
4323
  }
4224
4324
  async function handleAppVersionEndpoint(endpoint, rawPayload, deps, signal) {
4225
4325
  try {
@@ -4652,6 +4752,123 @@ function registerUsageRpc(ctx, deps) {
4652
4752
  };
4653
4753
  }
4654
4754
 
4755
+ // src/voice-rpc.ts
4756
+ var VOICE_RPC_CHANNEL = "/hiwork-voice";
4757
+ var VOICE_RPC_ENDPOINTS = ["status", "transcribe"];
4758
+ var VoiceRequestError = class extends Error {
4759
+ constructor(code, message) {
4760
+ super(message);
4761
+ this.code = code;
4762
+ this.name = "VoiceRequestError";
4763
+ }
4764
+ };
4765
+ var VOICE_MESSAGE_FALLBACK = {
4766
+ SESSION_EXPIRED: "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
4767
+ GATEWAY_UNREACHABLE: "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
4768
+ VOICE_DISABLED: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u8BED\u97F3\u8BC6\u522B\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
4769
+ VOICE_NOT_CONFIGURED: "\u672C\u90E8\u7F72\u5C1A\u672A\u914D\u7F6E\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
4770
+ VOICE_LIMIT_REACHED: "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
4771
+ VOICE_AUDIO_TOO_LARGE: "\u5F55\u97F3\u8FC7\u957F\uFF0C\u8BF7\u7F29\u77ED\u540E\u91CD\u8BD5",
4772
+ VOICE_BAD_REQUEST: "\u5F55\u97F3\u6570\u636E\u4E0D\u5408\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u5F55\u5236",
4773
+ VOICE_UPSTREAM: "\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
4774
+ RATE_LIMITED: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
4775
+ AUTH_FAILED: "\u767B\u5F55\u72B6\u6001\u5F02\u5E38\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
4776
+ };
4777
+ var VOICE_GENERIC_FAILURE_MESSAGE = "\u8BED\u97F3\u8BC6\u522B\u6682\u65F6\u65E0\u6CD5\u4F7F\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
4778
+ var VOICE_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
4779
+ var VOICE_FALLBACK_CONFIG = { enabled: false, maxDurationSeconds: 60 };
4780
+ function displayVoiceGatewayMessage(code, message) {
4781
+ const text = typeof message === "string" ? message.trim() : "";
4782
+ if (text === "") return VOICE_MESSAGE_FALLBACK[code] ?? VOICE_GENERIC_FAILURE_MESSAGE;
4783
+ const leaksImplementationDetail = /https?:\/\//i.test(text) || /\bHTTP\s*\d{3}\b/i.test(text) || /\bbaseURL\b/i.test(text);
4784
+ if (leaksImplementationDetail) return VOICE_MESSAGE_FALLBACK[code] ?? VOICE_GENERIC_FAILURE_MESSAGE;
4785
+ return text;
4786
+ }
4787
+ function parseVoicePayload(endpoint, raw) {
4788
+ if (raw === void 0 || raw === null) {
4789
+ if (endpoint === "transcribe") throw new VoiceRequestError("bad-request", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u7F3A\u5C11 audioBase64\u3002");
4790
+ return {};
4791
+ }
4792
+ if (typeof raw !== "object" || Array.isArray(raw)) {
4793
+ throw new VoiceRequestError("bad-request", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
4794
+ }
4795
+ const record = raw;
4796
+ const keys = Object.keys(record);
4797
+ if (endpoint === "status") {
4798
+ if (keys.length > 0) throw new VoiceRequestError("bad-request", `\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${keys[0]}'\u3002`);
4799
+ return {};
4800
+ }
4801
+ for (const key of keys) {
4802
+ if (key !== "audioBase64") throw new VoiceRequestError("bad-request", `\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${key}'\u3002`);
4803
+ }
4804
+ const rawAudio = record["audioBase64"];
4805
+ if (typeof rawAudio !== "string" || rawAudio.trim() === "") {
4806
+ throw new VoiceRequestError("bad-request", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u7684 audioBase64 \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");
4807
+ }
4808
+ return { audioBase64: rawAudio };
4809
+ }
4810
+ function isVoiceRpcEndpoint(endpoint) {
4811
+ return VOICE_RPC_ENDPOINTS.includes(endpoint);
4812
+ }
4813
+ function isAborted4(signal) {
4814
+ return signal?.aborted === true;
4815
+ }
4816
+ async function handleVoiceEndpoint(endpoint, rawPayload, deps, signal) {
4817
+ try {
4818
+ if (isAborted4(signal)) return failure4("cancelled", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4819
+ if (!isVoiceRpcEndpoint(endpoint)) {
4820
+ throw new VoiceRequestError("bad-request", `\u672A\u77E5\u7684\u8BED\u97F3\u8BC6\u522B\u7AEF\u70B9 '${endpoint}'`);
4821
+ }
4822
+ const payload = parseVoicePayload(endpoint, rawPayload);
4823
+ const sessionCookie = await deps.resolveSession();
4824
+ if (sessionCookie === void 0 || sessionCookie === "") {
4825
+ return failure4("SESSION_EXPIRED", VOICE_SESSION_EXPIRED_MESSAGE);
4826
+ }
4827
+ const gateway = deps.gateway();
4828
+ if (gateway === void 0) {
4829
+ 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");
4830
+ }
4831
+ if (isAborted4(signal)) return failure4("cancelled", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4832
+ if (endpoint === "status") return { ok: true, value: await readVoiceConfig(gateway, sessionCookie, deps.log) };
4833
+ return { ok: true, value: await transcribe(gateway, sessionCookie, payload) };
4834
+ } catch (error) {
4835
+ if (error instanceof VoiceRequestError) return failure4(error.code, error.message);
4836
+ if (error instanceof GatewayError) {
4837
+ return failure4(error.code, displayVoiceGatewayMessage(error.code, error.message));
4838
+ }
4839
+ deps.log?.(`hiwork-core: \u8BED\u97F3\u8BC6\u522B RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
4840
+ return failure4("internal", VOICE_GENERIC_FAILURE_MESSAGE);
4841
+ }
4842
+ }
4843
+ async function readVoiceConfig(gateway, sessionCookie, log) {
4844
+ try {
4845
+ const config = await gateway.getMyVoiceConfig(sessionCookie);
4846
+ return {
4847
+ enabled: config.enabled === true,
4848
+ maxDurationSeconds: config.maxDurationSeconds > 0 ? config.maxDurationSeconds : VOICE_FALLBACK_CONFIG.maxDurationSeconds
4849
+ };
4850
+ } catch (error) {
4851
+ log?.(`hiwork-core: \u8BED\u97F3\u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u6309\u7981\u7528\u56DE\u843D\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
4852
+ return { ...VOICE_FALLBACK_CONFIG };
4853
+ }
4854
+ }
4855
+ async function transcribe(gateway, sessionCookie, payload) {
4856
+ const audioBase64 = payload.audioBase64;
4857
+ if (audioBase64 === void 0) throw new VoiceRequestError("bad-request", "\u8BED\u97F3\u8BC6\u522B\u8BF7\u6C42\u7F3A\u5C11 audioBase64\u3002");
4858
+ const result = await gateway.transcribeVoice(sessionCookie, audioBase64);
4859
+ return { text: result.text };
4860
+ }
4861
+ function failure4(code, message) {
4862
+ return { ok: false, error: { code, message, details: {} } };
4863
+ }
4864
+ function registerVoiceRpc(ctx, deps) {
4865
+ const handler = (endpoint, payload, signal) => handleVoiceEndpoint(endpoint, payload, deps, signal);
4866
+ const remove = ctx.connection.rpc.handle(VOICE_RPC_CHANNEL, handler, { authority: "loopback" });
4867
+ return async () => {
4868
+ await remove();
4869
+ };
4870
+ }
4871
+
4655
4872
  // src/managed-mcp.ts
4656
4873
  var MANAGED_MCP_RPC_CHANNEL = "/hiwork-managed-mcp";
4657
4874
  var MANAGED_MCP_RPC_ENDPOINTS = ["snapshot", "refresh", "assertion"];
@@ -4816,12 +5033,12 @@ var ManagedMcpStore = class {
4816
5033
  }
4817
5034
  }
4818
5035
  };
4819
- function isAborted4(signal) {
5036
+ function isAborted5(signal) {
4820
5037
  return signal?.aborted === true;
4821
5038
  }
4822
5039
  async function handleManagedMcpEndpoint(endpoint, rawPayload, deps, store, signal) {
4823
5040
  try {
4824
- if (isAborted4(signal)) return failure4("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5041
+ if (isAborted5(signal)) return failure5("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4825
5042
  if (!isManagedMcpRpcEndpoint(endpoint)) {
4826
5043
  throw new ManagedMcpRequestError("bad-request", `\u672A\u77E5\u7684 MCP \u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
4827
5044
  }
@@ -4842,19 +5059,19 @@ async function handleManagedMcpEndpoint(endpoint, rawPayload, deps, store, signa
4842
5059
  async function handleAssertion(assignmentId, deps, signal) {
4843
5060
  const sessionCookie = await deps.resolveSession();
4844
5061
  if (sessionCookie === void 0 || sessionCookie === "") {
4845
- return failure4("SESSION_EXPIRED", MANAGED_MCP_SESSION_EXPIRED_MESSAGE);
5062
+ return failure5("SESSION_EXPIRED", MANAGED_MCP_SESSION_EXPIRED_MESSAGE);
4846
5063
  }
4847
5064
  const gateway = deps.gateway();
4848
5065
  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");
5066
+ 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");
4850
5067
  }
4851
- if (isAborted4(signal)) return failure4("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5068
+ if (isAborted5(signal)) return failure5("cancelled", "MCP \u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
4852
5069
  const ticket = await gateway.issueMyMcpAssertion(sessionCookie, assignmentId);
4853
5070
  const assertion = textOrUndefined(ticket?.assertion);
4854
5071
  const mintUrl = textOrUndefined(ticket?.mintUrl);
4855
5072
  if (assertion === void 0 || mintUrl === void 0) {
4856
5073
  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);
5074
+ return failure5("internal", MANAGED_MCP_GENERIC_FAILURE_MESSAGE);
4858
5075
  }
4859
5076
  return {
4860
5077
  ok: true,
@@ -4867,7 +5084,7 @@ async function handleAssertion(assignmentId, deps, signal) {
4867
5084
  }
4868
5085
  };
4869
5086
  }
4870
- function failure4(code, message) {
5087
+ function failure5(code, message) {
4871
5088
  return { ok: false, error: { code, message, details: {} } };
4872
5089
  }
4873
5090
  function registerManagedMcpRpc(ctx, deps, store) {
@@ -5098,12 +5315,12 @@ var ManagedSkillsStore = class {
5098
5315
  }
5099
5316
  }
5100
5317
  };
5101
- function isAborted5(signal) {
5318
+ function isAborted6(signal) {
5102
5319
  return signal?.aborted === true;
5103
5320
  }
5104
5321
  async function handleManagedSkillsEndpoint(endpoint, rawPayload, deps, store, signal) {
5105
5322
  try {
5106
- if (isAborted5(signal)) return failure5("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5323
+ if (isAborted6(signal)) return failure6("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5107
5324
  if (!isManagedSkillsRpcEndpoint(endpoint)) {
5108
5325
  throw new ManagedSkillsRequestError("bad-request", `\u672A\u77E5\u7684\u6280\u80FD\u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
5109
5326
  }
@@ -5124,24 +5341,24 @@ async function handleManagedSkillsEndpoint(endpoint, rawPayload, deps, store, si
5124
5341
  async function handlePackage(assignmentId, deps, signal) {
5125
5342
  const sessionCookie = await deps.resolveSession();
5126
5343
  if (sessionCookie === void 0 || sessionCookie === "") {
5127
- return failure5("SESSION_EXPIRED", MANAGED_SKILLS_SESSION_EXPIRED_MESSAGE);
5344
+ return failure6("SESSION_EXPIRED", MANAGED_SKILLS_SESSION_EXPIRED_MESSAGE);
5128
5345
  }
5129
5346
  const gateway = deps.gateway();
5130
5347
  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");
5348
+ return failure6("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
5349
  }
5133
- if (isAborted5(signal)) return failure5("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5350
+ if (isAborted6(signal)) return failure6("cancelled", "\u6280\u80FD\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5134
5351
  const pkg = await gateway.getMySkillPackage(sessionCookie, assignmentId);
5135
5352
  const bytes = pkg?.bytes;
5136
5353
  const skillName = textOrUndefined2(pkg?.skillName);
5137
5354
  const contentSha256 = textOrUndefined2(pkg?.contentSha256);
5138
5355
  if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0 || skillName === void 0 || contentSha256 === void 0) {
5139
5356
  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);
5357
+ return failure6("internal", MANAGED_SKILLS_GENERIC_FAILURE_MESSAGE);
5141
5358
  }
5142
5359
  if (bytes.byteLength > MAX_MANAGED_SKILL_ARCHIVE_BYTES) {
5143
5360
  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);
5361
+ return failure6(MANAGED_SKILLS_PACKAGE_TOO_LARGE_CODE, MANAGED_SKILLS_PACKAGE_TOO_LARGE_MESSAGE);
5145
5362
  }
5146
5363
  return {
5147
5364
  ok: true,
@@ -5155,7 +5372,7 @@ async function handlePackage(assignmentId, deps, signal) {
5155
5372
  }
5156
5373
  };
5157
5374
  }
5158
- function failure5(code, message) {
5375
+ function failure6(code, message) {
5159
5376
  return { ok: false, error: { code, message, details: {} } };
5160
5377
  }
5161
5378
  function registerManagedSkillsRpc(ctx, deps, store) {
@@ -5297,7 +5514,7 @@ function failureOf3(error) {
5297
5514
  function normalizeSession2(sessionCookie) {
5298
5515
  return sessionCookie === void 0 || sessionCookie === "" ? void 0 : sessionCookie;
5299
5516
  }
5300
- function isAborted6(signal) {
5517
+ function isAborted7(signal) {
5301
5518
  return signal?.aborted === true;
5302
5519
  }
5303
5520
  var WorkbenchStore = class {
@@ -5422,7 +5639,7 @@ var WorkbenchStore = class {
5422
5639
  };
5423
5640
  async function handleWorkbenchEndpoint(endpoint, rawPayload, deps, store, signal) {
5424
5641
  try {
5425
- if (isAborted6(signal)) return failure6("cancelled", "\u5DE5\u4F5C\u53F0\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5642
+ if (isAborted7(signal)) return failure7("cancelled", "\u5DE5\u4F5C\u53F0\u6E05\u5355\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
5426
5643
  if (!isWorkbenchRpcEndpoint(endpoint)) {
5427
5644
  throw new WorkbenchRequestError("bad-request", `\u672A\u77E5\u7684\u5DE5\u4F5C\u53F0\u6E05\u5355\u7AEF\u70B9 '${endpoint}'`);
5428
5645
  }
@@ -5435,7 +5652,7 @@ async function handleWorkbenchEndpoint(endpoint, rawPayload, deps, store, signal
5435
5652
  return { ok: false, error: failureOf3(error) };
5436
5653
  }
5437
5654
  }
5438
- function failure6(code, message) {
5655
+ function failure7(code, message) {
5439
5656
  return { ok: false, error: { code, message, details: {} } };
5440
5657
  }
5441
5658
  function registerWorkbenchRpc(ctx, deps, store) {
@@ -5448,8 +5665,6 @@ function registerWorkbenchRpc(ctx, deps, store) {
5448
5665
 
5449
5666
  // src/pms-client.ts
5450
5667
  var PMS_BASE_URL = "https://pms.hivery.cn";
5451
- var AUTH_LOGIN_PATH = "/api/auth/login";
5452
- var AUTH_REFRESH_PATH = "/api/auth/refresh";
5453
5668
  var TODO_TODAY_PATH = "/api/todo/today";
5454
5669
  var TODO_STATISTICS_PATH = "/api/todo/statistics";
5455
5670
  var TASK_LIST_PATH = "/api/task/tasks";
@@ -5501,26 +5716,60 @@ var PmsClient = class {
5501
5716
  this.log = options.log;
5502
5717
  }
5503
5718
  /**
5504
- * `POST /api/auth/login`:用账号密码换 JWT(access + refresh)。
5719
+ * `POST {mintUrl}`:用一张网关签发的 **120 秒身份断言**换一把 **10 小时 PMS JWT**
5720
+ * (spec `2026-09-21-pms-zero-input-login-design.md` §4.1b)。
5505
5721
  *
5506
- * 失败语义(由 {@link PmsClient.call} 统一给出):
5507
- * - 账号密码不对 → `UNAUTHORIZED`(HTTP 401 **或** 信封 `code:401`,两者同义);
5508
- * - 其余(网络 / 超时 / 5xx / 非业务信封)→ `UPSTREAM_UNAVAILABLE`。
5509
- * **本方法不重试**,调用方也不许替它重试(§7:避免用错密码连续触发 PMS 的失败锁定)。
5510
- */
5511
- async login(username, password) {
5512
- const envelope = await this.call(AUTH_LOGIN_PATH, { method: "POST", body: { username, password } });
5513
- return this.tokenSet(envelope.data);
5514
- }
5515
- /**
5516
- * `POST /api/auth/refresh`:用 refresh token 续一次 access token。
5722
+ * 与其余方法的两处形状差异都是契约给定的,不是本层偷懒:
5723
+ * - **`mintUrl` 是绝对地址**,来自网关的连接器分配(`auth.mintUrl`)——它**不拼** {@link PMS_BASE_URL}:
5724
+ * 那个常量是「PMS 部署地址」,而兑换端点由网关的分配清单说了算(本层不猜也不改写,见
5725
+ * {@link PmsClient.send});
5726
+ * - **不解 `Result` 信封**:成功体就是 `{token, expiresAt, userId, username}`,失败体是
5727
+ * `{error:"assertion_rejected"}` 这类——两者都不是业务信封。
5728
+ *
5729
+ * 失败语义:
5730
+ * - HTTP 401 / 403(断言过期、`aud` 不符、签名不过、工号在 PMS 不存在)→ `UNAUTHORIZED`:用户该做的
5731
+ * 是**重新登录 HiWork 客户端**(断言来自网关会话),不是再输一次 PMS 密码(spec D8);
5732
+ * - 其余(网络 / 超时 / 400 / 503 / 非 JSON / 2xx 却形状不符)→ `UPSTREAM_UNAVAILABLE`。
5517
5733
  *
5518
- * refresh token 失效(7 天到期 / 已被 PMS 撤销)→ `UNAUTHORIZED`,此时**只能**重新输入账号密码;
5519
- * 网络问题 → `UPSTREAM_UNAVAILABLE`(那是「稍后重试」,不是「要重新登录」——两者对用户是两件事)。
5734
+ * **本方法不重试**:断言是一次性的(PMS 侧拿 `jti` 做 2 分钟防重放),要重试必须由调用方**换一张新
5735
+ * 断言**再调——重发同一张只会换来第二个 403(spec §6)。
5736
+ *
5737
+ * @param mintUrl - 网关分配里的 `auth.mintUrl`(绝对 URL)。
5738
+ * @param assertion - 网关签发的短命断言(**不进日志、不落盘**,用完即弃)。
5739
+ * @param deviceRef - 设备标识串(可空;只进 PMS 侧审计,不参与鉴权)。
5740
+ * @returns 令牌与它的元数据(`token` 交给 Host 内存,见 `src/pms-todo-bridge.ts`)。
5520
5741
  */
5521
- async refresh(refreshToken) {
5522
- const envelope = await this.call(AUTH_REFRESH_PATH, { method: "POST", body: { refreshToken } });
5523
- return this.tokenSet(envelope.data);
5742
+ async exchangeAssertion(mintUrl, assertion, deviceRef) {
5743
+ const response = await this.send(mintUrl, { method: "POST", body: { assertion, deviceRef } });
5744
+ if (response.status === 401 || response.status === 403) {
5745
+ this.log?.(`hiwork-core: PMS \u6362\u4EE4\u724C\u88AB\u62D2\uFF08HTTP ${response.status}\uFF09`);
5746
+ throw new PmsError("UNAUTHORIZED", PMS_UNAUTHORIZED_MESSAGE, response.status);
5747
+ }
5748
+ if (response.status < 200 || response.status >= 300) {
5749
+ this.log?.(`hiwork-core: PMS \u6362\u4EE4\u724C\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
5750
+ throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE, response.status);
5751
+ }
5752
+ const payload = await readJson2(response);
5753
+ if (!isRecord3(payload)) {
5754
+ this.log?.("hiwork-core: PMS \u6362\u4EE4\u724C\u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\uFF08\u53EF\u80FD\u7684\u4E2D\u95F4\u5C42\uFF09");
5755
+ throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE, response.status);
5756
+ }
5757
+ const token = textOf(payload["token"]);
5758
+ if (token === void 0) {
5759
+ this.log?.("hiwork-core: PMS \u6362\u4EE4\u724C\u54CD\u5E94\u7F3A token\uFF08\u5951\u7EA6\u7834\u574F\uFF09\uFF0C\u6309\u4E0A\u6E38\u4E0D\u53EF\u7528\u5904\u7406");
5760
+ throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE, response.status);
5761
+ }
5762
+ if (!isInteger(payload["userId"])) {
5763
+ this.log?.("hiwork-core: PMS \u6362\u4EE4\u724C\u54CD\u5E94\u7F3A userId / \u5F62\u6001\u4E0D\u5BF9\uFF08\u5951\u7EA6\u7834\u574F\uFF09\uFF0C\u6309\u4E0A\u6E38\u4E0D\u53EF\u7528\u5904\u7406");
5764
+ throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE, response.status);
5765
+ }
5766
+ return {
5767
+ token,
5768
+ // 两个非关键字段按容错处理:令牌已经拿到了,不为一个坏字段让用户重来一次。
5769
+ expiresAt: expiresAtOr(payload["expiresAt"], this.now() + PMS_ACCESS_TOKEN_TTL_MS),
5770
+ userId: payload["userId"],
5771
+ username: textOf(payload["username"]) ?? ""
5772
+ };
5524
5773
  }
5525
5774
  /**
5526
5775
  * `GET /api/todo/today`:今日待办(⚠️ 口径比名字宽:**今天 + 明天 + 无截止日期**都算,spec §D4)。
@@ -5656,15 +5905,22 @@ var PmsClient = class {
5656
5905
  };
5657
5906
  }
5658
5907
  /**
5659
- * 发一次调用并解开 `Result` 信封。**所有错误都在这里收敛**,返回值只有 `data`。
5908
+ * 发一次请求:拼地址、塞请求头、按 {@link PmsClientOptions.timeoutMs} 超时。**只有传输**。
5660
5909
  *
5661
- * 顺序是刻意的:**先看 HTTP 401,再解信封**——PMS 未登录时回的是 Spring Security 形状
5662
- * (`{path, code:"AUTH_001", message}`),它的 `message` 是给浏览器看跳登录页的,既不是业务文案
5663
- * 也可能带 `path` 等实现细节。把它当业务信封解析,轻则文案变成一句英文,重则把「凭据失效」
5664
- * 误判成「上游故障」而让用户不知道要重新输入密码。
5910
+ * 与 {@link PmsClient.call} 的分工:`call` 负责 `Result` 信封与业务码,本方法不管响应体是什么形状
5911
+ * ——换令牌那条路({@link PmsClient.exchangeAssertion})既不解信封也不认业务码,但**超时口径与请求头
5912
+ * 必须与其余调用逐字相同**(spec §7:统一 10s、令牌只在有身份时发),所以两条路共用这一处,
5913
+ * 而不是各抄一份、将来各自漂移。
5914
+ *
5915
+ * **重定向自己跟**(`redirect: 'manual'` + {@link redirectTargetOf}):`fetch` 的默认 `follow` 会把
5916
+ * 重定向后的请求按 fetch 语义**把 POST 改成 GET**,于是请求落在一个只认 POST 的映射上、拿到一个
5917
+ * 「200 + 业务错误信封」,看上去像上游坏了。判据与修法见 {@link PMS_MAX_REDIRECTS}。
5918
+ *
5919
+ * @param url - 相对路径(拼 {@link PmsClient.baseURL})或**绝对 URL**(原样使用)。
5920
+ * @param opts - 方法 / 可选 JSON 体 / 可选 Bearer 令牌。
5921
+ * @returns 原始 `Response`:状态码由调用方判定,本方法不把任何状态码当成错误。
5665
5922
  */
5666
- async call(path, opts) {
5667
- let response;
5923
+ async send(url, opts) {
5668
5924
  try {
5669
5925
  const headers = {
5670
5926
  "user-agent": `hiwork-core/${PKG_VERSION}`,
@@ -5675,13 +5931,34 @@ var PmsClient = class {
5675
5931
  const init = {
5676
5932
  method: opts.method,
5677
5933
  headers,
5678
- signal: AbortSignal.timeout(this.timeoutMs)
5934
+ // 手动跟随:默认的 `follow` 会把重定向后的 POST 降级成 GET(见本方法的抬头)。
5935
+ redirect: "manual"
5679
5936
  };
5680
5937
  if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
5681
- response = await fetch(`${this.baseURL}${path}`, init);
5938
+ const deadline = Date.now() + this.timeoutMs;
5939
+ let target = isAbsoluteUrl(url) ? url : `${this.baseURL}${url}`;
5940
+ let response = await fetchWithinDeadline(target, init, deadline);
5941
+ for (let hop = 0; ; hop += 1) {
5942
+ const next = redirectTargetOf(target, response, hop);
5943
+ if (next === void 0) return response;
5944
+ target = next;
5945
+ response = await fetchWithinDeadline(target, init, deadline);
5946
+ }
5682
5947
  } catch {
5683
5948
  throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE);
5684
5949
  }
5950
+ }
5951
+ /**
5952
+ * 发一次调用并解开 `Result` 信封。**PMS 的业务错误都在这里收敛**,返回值只有 `data`
5953
+ * (传输层的失败已在 {@link PmsClient.send} 收敛)。
5954
+ *
5955
+ * 顺序是刻意的:**先看 HTTP 401,再解信封**——PMS 未登录时回的是 Spring Security 形状
5956
+ * (`{path, code:"AUTH_001", message}`),它的 `message` 是给浏览器看跳登录页的,既不是业务文案
5957
+ * 也可能带 `path` 等实现细节。把它当业务信封解析,轻则文案变成一句英文,重则把「凭据失效」
5958
+ * 误判成「上游故障」而让用户不知道要重新输入密码。
5959
+ */
5960
+ async call(path, opts) {
5961
+ const response = await this.send(path, opts);
5685
5962
  if (response.status === 401) {
5686
5963
  throw new PmsError("UNAUTHORIZED", PMS_UNAUTHORIZED_MESSAGE, 401);
5687
5964
  }
@@ -5713,32 +5990,41 @@ var PmsClient = class {
5713
5990
  }
5714
5991
  return { code, data: payload["data"] };
5715
5992
  }
5716
- /** 登录 / 刷新的 `data` → {@link PmsTokenSet}。200 却不给 token 视为契约破坏(上游不可用)。 */
5717
- tokenSet(data) {
5718
- if (!isRecord3(data)) {
5719
- this.log?.("hiwork-core: PMS \u8BA4\u8BC1\u54CD\u5E94\u7F3A data\uFF08\u5951\u7EA6\u7834\u574F\uFF09\uFF0C\u6309\u4E0A\u6E38\u4E0D\u53EF\u7528\u5904\u7406");
5720
- throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE);
5721
- }
5722
- const accessToken = textOf(data["token"]);
5723
- if (accessToken === void 0) {
5724
- this.log?.("hiwork-core: PMS \u8BA4\u8BC1\u54CD\u5E94\u7F3A token\uFF08\u5951\u7EA6\u7834\u574F\uFF09\uFF0C\u6309\u4E0A\u6E38\u4E0D\u53EF\u7528\u5904\u7406");
5725
- throw new PmsError("UPSTREAM_UNAVAILABLE", PMS_GENERIC_FAILURE_MESSAGE);
5726
- }
5727
- const refreshToken = textOf(data["refreshToken"]);
5728
- const expiresIn = data["expiresIn"];
5729
- const ttlMs = typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : PMS_ACCESS_TOKEN_TTL_MS;
5730
- const userId = isInteger(data["userId"]) ? data["userId"] : void 0;
5731
- return {
5732
- accessToken,
5733
- ...refreshToken === void 0 ? {} : { refreshToken },
5734
- ...userId === void 0 ? {} : { userId },
5735
- expiresAt: this.now() + ttlMs
5736
- };
5737
- }
5738
5993
  };
5739
5994
  function isRecord3(value) {
5740
5995
  return typeof value === "object" && value !== null && !Array.isArray(value);
5741
5996
  }
5997
+ function isAbsoluteUrl(url) {
5998
+ return /^https?:\/\//i.test(url);
5999
+ }
6000
+ var PMS_MAX_REDIRECTS = 2;
6001
+ var METHOD_PRESERVING_REDIRECTS = /* @__PURE__ */ new Set([301, 302, 307, 308]);
6002
+ function redirectTargetOf(current, response, hop) {
6003
+ if (!METHOD_PRESERVING_REDIRECTS.has(response.status)) return void 0;
6004
+ if (hop >= PMS_MAX_REDIRECTS) return void 0;
6005
+ const location = response.headers.get("location");
6006
+ if (location === null || location.trim() === "") return void 0;
6007
+ let from2;
6008
+ let next;
6009
+ try {
6010
+ from2 = new URL(current);
6011
+ next = new URL(location.trim(), current);
6012
+ } catch {
6013
+ return void 0;
6014
+ }
6015
+ if (next.protocol !== "https:" && next.protocol !== from2.protocol) return void 0;
6016
+ if (next.hostname !== from2.hostname) return void 0;
6017
+ return next.toString();
6018
+ }
6019
+ async function fetchWithinDeadline(target, init, deadline) {
6020
+ const remaining = deadline - Date.now();
6021
+ if (remaining <= 0) throw new Error("pms request deadline exceeded");
6022
+ return await fetch(target, { ...init, signal: AbortSignal.timeout(remaining) });
6023
+ }
6024
+ function expiresAtOr(value, fallbackMs) {
6025
+ if (typeof value === "string" && value !== "" && Number.isFinite(Date.parse(value))) return value;
6026
+ return new Date(fallbackMs).toISOString();
6027
+ }
5742
6028
  function textOf(value) {
5743
6029
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
5744
6030
  }
@@ -5831,18 +6117,18 @@ async function readJson2(response) {
5831
6117
  var PMS_TODO_RPC_CHANNEL = "/hiwork-pms-todo";
5832
6118
  var PMS_TODO_RPC_ENDPOINTS = [
5833
6119
  "status",
5834
- "login",
5835
- "logout",
5836
6120
  "today",
5837
6121
  "statistics",
5838
6122
  "setStatus",
5839
6123
  "myTasks",
5840
6124
  "completeTask"
5841
6125
  ];
6126
+ var PMS_ASSIGNMENT_SLUG = "pms";
5842
6127
  var PMS_CREDENTIALS_REF = "HIWORK_PMS_CREDENTIALS";
5843
6128
  var PMS_TODO_ACCESS_TOKEN_TTL_MS = 10 * 60 * 60 * 1e3;
5844
- var PMS_TODO_NOT_CONFIGURED_MESSAGE = "\u5C1A\u672A\u914D\u7F6E PMS \u8D26\u53F7\uFF0C\u8BF7\u5148\u8F93\u5165\u8D26\u53F7\u5BC6\u7801";
5845
- var PMS_TODO_UNAUTHORIZED_MESSAGE = PMS_UNAUTHORIZED_MESSAGE;
6129
+ var PMS_TODO_REUSE_MIN_REMAINING_RATIO = 0.5;
6130
+ var PMS_TODO_NOT_CONFIGURED_MESSAGE = "\u7BA1\u7406\u5458\u5C1A\u672A\u628A PMS \u63A5\u5165\u8FD9\u53F0\u5BA2\u6237\u7AEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458";
6131
+ var PMS_TODO_UNAUTHORIZED_MESSAGE = "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55 HiWork \u5BA2\u6237\u7AEF";
5846
6132
  var PMS_TODO_FORBIDDEN_MESSAGE = PMS_FORBIDDEN_MESSAGE;
5847
6133
  var PMS_TODO_GENERIC_FAILURE_MESSAGE = PMS_GENERIC_FAILURE_MESSAGE;
5848
6134
  var PMS_TODO_CANCELLED_MESSAGE = "PMS \u5F85\u529E\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002";
@@ -5853,9 +6139,19 @@ var PmsTodoRequestError = class extends Error {
5853
6139
  this.name = "PmsTodoRequestError";
5854
6140
  }
5855
6141
  };
6142
+ var PmsTodoError = class extends Error {
6143
+ constructor(code, message) {
6144
+ super(message);
6145
+ this.code = code;
6146
+ this.name = "PmsTodoError";
6147
+ }
6148
+ };
5856
6149
  function isPmsTodoRpcEndpoint(endpoint) {
5857
6150
  return PMS_TODO_RPC_ENDPOINTS.includes(endpoint);
5858
6151
  }
6152
+ function textOf2(value) {
6153
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
6154
+ }
5859
6155
  function asRecord(endpoint, raw) {
5860
6156
  if (raw === void 0 || raw === null) return {};
5861
6157
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -5872,24 +6168,10 @@ function rejectUnknownKeys(endpoint, record, allowed) {
5872
6168
  }
5873
6169
  function parsePmsTodoPayload(endpoint, raw) {
5874
6170
  const record = asRecord(endpoint, raw);
5875
- if (endpoint === "status" || endpoint === "logout" || endpoint === "today" || endpoint === "statistics" || endpoint === "myTasks") {
6171
+ if (endpoint === "status" || endpoint === "today" || endpoint === "statistics" || endpoint === "myTasks") {
5876
6172
  rejectUnknownKeys(endpoint, record, []);
5877
6173
  return { endpoint };
5878
6174
  }
5879
- if (endpoint === "login") {
5880
- rejectUnknownKeys(endpoint, record, ["username", "password", "remember"]);
5881
- const username = typeof record["username"] === "string" ? record["username"].trim() : "";
5882
- if (username === "") throw new PmsTodoRequestError("PMS \u767B\u5F55\u8BF7\u6C42\u7F3A\u5C11 username\u3002");
5883
- const password = record["password"];
5884
- if (typeof password !== "string" || password === "") {
5885
- throw new PmsTodoRequestError("PMS \u767B\u5F55\u8BF7\u6C42\u7F3A\u5C11 password\u3002");
5886
- }
5887
- const remember = record["remember"];
5888
- if (remember !== void 0 && typeof remember !== "boolean") {
5889
- throw new PmsTodoRequestError("PMS \u767B\u5F55\u8BF7\u6C42\u7684 remember \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
5890
- }
5891
- return { endpoint, username, password, remember: remember === true };
5892
- }
5893
6175
  if (endpoint === "completeTask") {
5894
6176
  rejectUnknownKeys(endpoint, record, ["id"]);
5895
6177
  const id2 = record["id"];
@@ -5937,139 +6219,95 @@ function displayPmsTodoMessage(code, message) {
5937
6219
  if (leaksImplementationDetail) return PMS_TODO_MESSAGE_FALLBACK[code] ?? PMS_TODO_GENERIC_FAILURE_MESSAGE;
5938
6220
  return text;
5939
6221
  }
5940
- var PmsTodoError = class extends Error {
5941
- constructor(code, message) {
5942
- super(message);
5943
- this.code = code;
5944
- this.name = "PmsTodoError";
5945
- }
5946
- };
5947
6222
  function errorName(error) {
5948
6223
  return error instanceof Error ? error.name : typeof error;
5949
6224
  }
5950
- function parseCredentialsRecord(raw) {
5951
- let parsed;
6225
+ function gatewayCodeOf(error) {
6226
+ return error instanceof GatewayError ? error.code : errorName(error);
6227
+ }
6228
+ async function clearLegacyCredentials(credentials, log) {
5952
6229
  try {
5953
- parsed = JSON.parse(raw);
5954
- } catch {
5955
- return void 0;
6230
+ const hit = await credentials.resolve(PMS_CREDENTIALS_REF);
6231
+ if (hit === void 0 || hit.value === "") return;
6232
+ await credentials.unset(PMS_CREDENTIALS_REF);
6233
+ log?.("hiwork-core: \u5DF2\u6E05\u9664 v1 \u9057\u7559\u7684 PMS \u51ED\u636E\u8BB0\u5F55");
6234
+ } catch (error) {
6235
+ log?.(`hiwork-core: PMS \u9057\u7559\u51ED\u636E\u6E05\u9664\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
5956
6236
  }
5957
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
5958
- const record = parsed;
5959
- const username = typeof record["username"] === "string" ? record["username"].trim() : "";
5960
- if (username === "") return void 0;
5961
- const password = typeof record["password"] === "string" && record["password"] !== "" ? record["password"] : void 0;
5962
- const refreshToken = typeof record["refreshToken"] === "string" && record["refreshToken"] !== "" ? record["refreshToken"] : void 0;
5963
- const expiresAt = typeof record["expiresAt"] === "number" && Number.isFinite(record["expiresAt"]) ? record["expiresAt"] : void 0;
5964
- const userId = typeof record["userId"] === "number" && Number.isSafeInteger(record["userId"]) ? record["userId"] : void 0;
6237
+ }
6238
+ function sessionOf(result, now) {
6239
+ const parsed = Date.parse(result.expiresAt);
5965
6240
  return {
5966
- username,
5967
- ...password === void 0 ? {} : { password },
5968
- ...refreshToken === void 0 ? {} : { refreshToken },
5969
- ...expiresAt === void 0 ? {} : { expiresAt },
5970
- ...userId === void 0 ? {} : { userId }
6241
+ token: result.token,
6242
+ // `PmsClient` 已保证 `expiresAt` 可解析(缺失时它自己兜底成 now + 10 小时);这里再兜一次
6243
+ // 是为了「上游回了一个解析不了的串」时不至于把过期时刻变成 NaN(那会让复用判据永远为假)。
6244
+ expiresAt: Number.isFinite(parsed) ? parsed : now + PMS_TODO_ACCESS_TOKEN_TTL_MS,
6245
+ issuedAt: now,
6246
+ userId: result.userId,
6247
+ username: result.username
5971
6248
  };
5972
6249
  }
5973
- function serializeCredentialsRecord(record) {
5974
- return JSON.stringify({
5975
- username: record.username,
5976
- ...record.password === void 0 ? {} : { password: record.password },
5977
- ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken },
5978
- ...record.expiresAt === void 0 ? {} : { expiresAt: record.expiresAt },
5979
- ...record.userId === void 0 ? {} : { userId: record.userId }
5980
- });
5981
- }
5982
6250
  var PmsTodoStore = class {
5983
6251
  constructor(deps) {
5984
6252
  this.deps = deps;
5985
6253
  }
5986
- /** 内存里的凭据记录(不 remember 时它是这份凭据唯一的家)。 */
5987
- record;
5988
- /** 是否已经从 credentials 服务读过一次(避免每次调用都读盘)。 */
5989
- loaded = false;
5990
- /**
5991
- * 当前会话是否**属于磁盘上的那条记录**(remember=true 的登录、或启动时从记录里恢复)。
5992
- * 只有它为真时,refresh 换来的新 token 才回写记录——否则一次「不记住」的登录会顺手把
5993
- * refresh token 写给下一个人(登出后磁盘上仍有可用凭据,这正是不 remember 的相反面)。
5994
- */
5995
- persisted = false;
5996
- /** access token(**只在内存**)。 */
5997
- access;
5998
- /** 世代号:`login` / `logout` 各 +1,用来作废它之前发起的在飞请求(换账号的语义同工作台)。 */
5999
- generation = 0;
6000
- /** 在飞请求**连同世代与端点**记账:同一世代 + 同一端点才允许合并。 */
6254
+ /** 内存里的会话(**唯一的家**,不落盘;重启后自然重换)。 */
6255
+ session;
6256
+ /** 网关分配清单里那条 `pms` 分配(只在内存里记住,免得每次取数都拉一遍清单)。 */
6257
+ assignment;
6258
+ /** 最近一次「为什么还没就绪」(进 `status` 的 `reason`;成功换到令牌时清空)。 */
6259
+ reason;
6260
+ /** 在飞请求按端点记账:同一端点才允许合并。 */
6001
6261
  inFlight;
6002
6262
  now() {
6003
6263
  return this.deps.now?.() ?? Date.now();
6004
6264
  }
6005
- /** 当前状态(同步语义的取数,异步实现:可能读一次 credentials)。**永不抛**。 */
6006
- async status() {
6007
- const record = await this.currentRecord();
6008
- return this.statusOf(record);
6009
- }
6010
6265
  /**
6011
- * 用账号密码登录并**换取** JWT。
6266
+ * 当前状态(spec §4.4)。**永不抛**:失败在快照里表现为 `configured=false` + `reason`。
6012
6267
  *
6013
- * ⚠️ **绝不自动重试**(spec §7 的硬要求):PMS 对连续失败登录有锁定策略,而「自动重试」正是
6014
- * 最容易不小心连打两次的地方——用户手一抖打错一位密码,第二次尝试就会把账号推向锁定。
6015
- * 所以本方法**只发一次**请求,失败即返回错误(UI 引导用户自己再输一次)。
6268
+ * 顺序是刻意的:
6269
+ * 1. 手上有一张未过期的 JWT → 直接报 `loggedIn`,**一次网络都不发**(面板每次挂载都读它);
6270
+ * 2. 没有 → 问一次网关「到底有没有 `pms` 分配」。`configured` 的判据只能是它:用户填没填过
6271
+ * 账号密码这件事已经不存在了(D3),而「有没有分配」只有网关知道。第二次起走内存里的那条
6272
+ * 分配,不再打网关。
6016
6273
  *
6017
- * `remember` 为真才落盘(§4.4)。落盘写的是**完整**记录(含密码),因为下次启动时要靠它
6018
- * 继续换 token——这不是「记住密码」的 UI 便利,而是 v1 认证方案本身(§3 D2)。
6274
+ * `reason` 只在「还没就绪」时出现:`no-assignment` / `gateway-signed-out` / `unavailable` 三种,
6275
+ * 分别对应「找管理员」/「重新登录 HiWork」/「稍后重试」(spec §3 D8 的 UI 动作各不相同)。
6019
6276
  */
6020
- async login(input) {
6021
- const tokens = await this.deps.pms.login(input.username, input.password);
6022
- const record = {
6023
- username: input.username,
6024
- password: input.password,
6025
- ...tokens.refreshToken === void 0 ? {} : { refreshToken: tokens.refreshToken },
6026
- expiresAt: tokens.expiresAt,
6027
- // §3 D7:登录响应里的 `userId` 必须**在这里**捕获——「我的任务」要用它当 `assigneeId`,
6028
- // 而除了登录/刷新没有别的地方能拿到它(不猜、不按用户名反查)。
6029
- ...tokens.userId === void 0 ? {} : { userId: tokens.userId }
6030
- };
6031
- this.generation += 1;
6032
- this.record = record;
6033
- this.loaded = true;
6034
- this.access = { token: tokens.accessToken, expiresAt: tokens.expiresAt };
6035
- this.persisted = input.remember;
6036
- if (input.remember) {
6037
- await this.persist(record);
6038
- } else {
6039
- await this.forgetStored();
6277
+ async status() {
6278
+ const session = this.session;
6279
+ if (session !== void 0 && this.now() < session.expiresAt) {
6280
+ return {
6281
+ configured: true,
6282
+ loggedIn: true,
6283
+ username: session.username,
6284
+ expiresAt: new Date(session.expiresAt).toISOString(),
6285
+ userId: session.userId,
6286
+ ...this.reason === void 0 ? {} : { reason: this.reason }
6287
+ };
6288
+ }
6289
+ try {
6290
+ const cookie = await this.sessionCookie();
6291
+ const gateway = this.gatewayPort();
6292
+ await this.ensureAssignment(gateway, cookie);
6293
+ return { configured: true, loggedIn: false, ...this.reason === void 0 ? {} : { reason: this.reason } };
6294
+ } catch {
6295
+ return { configured: false, loggedIn: false, reason: this.reason ?? "unavailable" };
6040
6296
  }
6041
- this.deps.log?.("hiwork-core: PMS \u8D26\u53F7\u5DF2\u767B\u5F55\uFF08\u51ED\u636E\u53EA\u5B58\u4E8E Host\uFF09");
6042
- return this.statusOf(record);
6043
- }
6044
- /**
6045
- * 登出:清内存 + 清凭据记录(spec §4.1 的 `logout`)。
6046
- *
6047
- * 不清 PMS 侧的服务端会话:PMS 的 JWT 是无状态签发的(没有「撤销单个 token」的接口),
6048
- * 我们能做且该做的是**不再持有**它——删掉本地凭据后,下一次取数一定要求重新输入。
6049
- */
6050
- async logout() {
6051
- this.generation += 1;
6052
- this.record = void 0;
6053
- this.loaded = true;
6054
- this.persisted = false;
6055
- this.access = void 0;
6056
- await this.forgetStored();
6057
- this.deps.log?.("hiwork-core: PMS \u51ED\u636E\u5DF2\u6E05\u9664");
6058
- return this.statusOf(void 0);
6059
6297
  }
6060
6298
  /** `GET /api/todo/today` + 本次拉取时刻。同一时刻的并发调用合并成一次 PMS 请求。 */
6061
6299
  async today() {
6062
6300
  return await this.dedupe("today", async () => {
6063
- const token = await this.authorize();
6064
- const items = await this.deps.pms.today(token);
6301
+ const session = await this.authorize();
6302
+ const items = await this.deps.pms.today(session.token);
6065
6303
  return { items, fetchedAt: new Date(this.now()).toISOString() };
6066
6304
  });
6067
6305
  }
6068
6306
  /** `GET /api/todo/statistics`。并发调用同样合并。 */
6069
6307
  async statistics() {
6070
6308
  return await this.dedupe("statistics", async () => {
6071
- const token = await this.authorize();
6072
- return await this.deps.pms.statistics(token);
6309
+ const session = await this.authorize();
6310
+ return await this.deps.pms.statistics(session.token);
6073
6311
  });
6074
6312
  }
6075
6313
  /**
@@ -6080,30 +6318,21 @@ var PmsTodoStore = class {
6080
6318
  * 的顺序处理」——那个语义 PMS 也不提供。
6081
6319
  */
6082
6320
  async setStatus(input) {
6083
- const token = await this.authorize();
6084
- return await this.deps.pms.setStatus(token, input.id, input.status);
6321
+ const session = await this.authorize();
6322
+ return await this.deps.pms.setStatus(session.token, input.id, input.status);
6085
6323
  }
6086
6324
  /**
6087
6325
  * `GET /api/task/tasks`:**我的未完成项目任务**(spec §3 D7)。
6088
6326
  *
6089
- * `userId` 只从凭据记录里取(客户端传不进来,见 `parsePmsTodoPayload` 的注释)——这是
6327
+ * `userId` 只从 mint 响应里取(客户端传不进来,见 `parsePmsTodoPayload` 的注释)——这是
6090
6328
  * 「只能查自己」唯一可靠的落点:端点白名单里也没有任何「按 id 查别人」的形状。
6091
6329
  *
6092
- * 记录里**没有** `userId`(2026-09-18 之前登录的旧记录)时收敛成 `UNAUTHORIZED`:用户重新登录
6093
- * 一次即可拿到它。这里刻意**不**回退成「按用户名去查用户列表」之类的动作——那需要额外的接口与
6094
- * 权限,而且会在 Host 里凭空多一条拿别人数据的路径。
6095
- *
6096
6330
  * 并发调用合并(同 `today`):这是**取数**,同一时刻问两遍没有意义。
6097
6331
  */
6098
6332
  async myTasks(opts = {}) {
6099
6333
  return await this.dedupe("myTasks", async () => {
6100
- const token = await this.authorize();
6101
- const userId = (await this.currentRecord())?.userId;
6102
- if (userId === void 0) {
6103
- this.deps.log?.("hiwork-core: PMS \u51ED\u636E\u8BB0\u5F55\u91CC\u6CA1\u6709 userId\uFF0C\u6211\u7684\u4EFB\u52A1\u9700\u8981\u91CD\u65B0\u767B\u5F55\u4E00\u6B21");
6104
- throw new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6105
- }
6106
- return await this.deps.pms.myTasks(token, userId, opts);
6334
+ const session = await this.authorize();
6335
+ return await this.deps.pms.myTasks(session.token, session.userId, opts);
6107
6336
  });
6108
6337
  }
6109
6338
  /**
@@ -6116,132 +6345,203 @@ var PmsTodoStore = class {
6116
6345
  * UI 按 §3 D7 提示「请到 PMS 操作」——**不是**重新登录。
6117
6346
  */
6118
6347
  async completeTask(input) {
6119
- const token = await this.authorize();
6120
- return await this.deps.pms.completeTask(token, input.id);
6348
+ const session = await this.authorize();
6349
+ return await this.deps.pms.completeTask(session.token, input.id);
6121
6350
  }
6122
6351
  /**
6123
- * 取一个能用的 access token(凭证获取的唯一入口,spec §3 D3 说 v2 只替换这里)。
6352
+ * 取一个能用的 PMS JWT(凭据获取的**唯一入口**,spec §3 D10)。
6124
6353
  *
6125
6354
  * 顺序是刻意的:
6126
- * 1. 内存里有未过期的 access token → 直接用(零网络请求);
6127
- * 2. 过期了 → 先试 `refresh(refreshToken)`(§D2:refresh 有效就完全不必打扰用户);
6128
- * refresh 成功会**顺手**把新的 access / refresh token 写回内存(原来是持久化的会话则一并落盘);
6129
- * 3. refresh 失败(401 / 记录里根本没有 refresh token)→ `UNAUTHORIZED`,UI 要求重新输入。
6355
+ * 1. 内存里有一张「剩余寿命 ≥ 50%」的令牌 → 直接用(零网络请求);
6356
+ * 2. 否则:解析网关会话 → 从分配清单里取 `slug='pms'` 那条(D9)→ 签一张 120 秒断言 →
6357
+ * 打分配的 mint 端点换一张 10 小时 JWT → 存回内存({@link PmsTodoStore.mint})。
6130
6358
  *
6131
- * ⚠️ 这里**没有**「拿存下来的密码重新登录」这一支,而且是刻意的:§7 要求凭据失效时
6132
- * **不自动重试**——用存下来的密码自动重登 = 每次 token 过期都自动替用户登一次,密码一旦改成
6133
- * 别的,就会变成一条稳定的「定时用错密码打 PMS」的流水,直到账号被锁。密码只在用户**明确**
6134
- * 提交一次 `login` 时才用(那时它也确实是用户刚输入的)。
6359
+ * ⚠️ **断言绝不缓存**:它只有 120 秒、且 PMS 侧拿 `jti` 做一次性防重放,缓存下来只会是一张废纸。
6360
+ * ⚠️ 这里**没有**「拿存下来的账号密码重新登一次」这一支,而且是结构上没有:账号密码通道已删
6361
+ * (spec §3 D3),本文件里也搜不到任何密码字段(§7 的验收项)。
6135
6362
  */
6136
6363
  async authorize() {
6137
- const access = this.access;
6138
- if (access !== void 0 && this.now() < access.expiresAt) return access.token;
6139
- const record = await this.currentRecord();
6140
- if (record === void 0) {
6141
- throw new PmsTodoError("NOT_CONFIGURED", PMS_TODO_NOT_CONFIGURED_MESSAGE);
6142
- }
6143
- const refreshToken = record.refreshToken;
6144
- if (refreshToken === void 0) {
6145
- throw new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6146
- }
6147
- const tokens = await this.deps.pms.refresh(refreshToken);
6148
- const next = {
6149
- ...record,
6150
- ...tokens.refreshToken === void 0 ? {} : { refreshToken: tokens.refreshToken },
6151
- expiresAt: tokens.expiresAt
6152
- };
6153
- this.record = next;
6154
- this.access = { token: tokens.accessToken, expiresAt: tokens.expiresAt };
6155
- if (this.persisted) await this.persist(next);
6156
- return tokens.accessToken;
6364
+ const cached = this.session;
6365
+ if (cached !== void 0 && this.reusable(cached)) return cached;
6366
+ const cookie = await this.sessionCookie();
6367
+ const gateway = this.gatewayPort();
6368
+ const assignment = await this.ensureAssignment(gateway, cookie);
6369
+ return await this.mint(gateway, cookie, assignment);
6157
6370
  }
6158
6371
  /**
6159
- * 状态投影(不读盘、不发网络)。
6372
+ * 这张令牌还能不能接着用(spec §4.5:剩余寿命 ≥ 50% 才复用)。
6160
6373
  *
6161
- * `expiresAt` **只报还没过期的那个时刻**:记录里那份可能是上一次会话留下的(进程重启后内存里
6162
- * 没有 access token,只有磁盘上的旧 `expiresAt`),把它照实报出去,界面就会显示一个已经过去的
6163
- * 「有效期至」,看着像还能用。过期了就不写这个键——refresh 会顺手把它换成新的。
6374
+ * 判据与 `hiwork-capabilities/src/connectors/assignments.ts` 的 `isCredentialReusable` 同口径:
6375
+ * 分母是**这张令牌自己的寿命**(`expiresAt - issuedAt`),不是某个写死的 10 小时——服务端将来
6376
+ * 改短有效期,复用口径跟着它走。
6164
6377
  */
6165
- statusOf(record) {
6378
+ reusable(session) {
6166
6379
  const now = this.now();
6167
- const access = this.access;
6168
- const hasAccess = access !== void 0 && now < access.expiresAt;
6169
- const hasRefresh = record?.refreshToken !== void 0 && record.refreshToken !== "";
6170
- const expiresAt = access?.expiresAt ?? record?.expiresAt;
6171
- const freshExpiry = expiresAt !== void 0 && expiresAt > now ? expiresAt : void 0;
6172
- return {
6173
- configured: record !== void 0,
6174
- loggedIn: hasAccess || hasRefresh,
6175
- ...record === void 0 ? {} : { username: record.username },
6176
- ...freshExpiry === void 0 ? {} : { expiresAt: new Date(freshExpiry).toISOString() }
6177
- };
6380
+ if (now >= session.expiresAt) return false;
6381
+ const lifetime = session.expiresAt - session.issuedAt;
6382
+ if (lifetime <= 0) return false;
6383
+ return session.expiresAt - now >= lifetime * PMS_TODO_REUSE_MIN_REMAINING_RATIO;
6384
+ }
6385
+ /** 网关会话 cookie;缺失即 `UNAUTHORIZED`(动作在 HiWork:重新登录客户端)。 */
6386
+ async sessionCookie() {
6387
+ const cookie = await this.deps.resolveSession();
6388
+ if (cookie === void 0 || cookie === "") {
6389
+ this.fail("gateway-signed-out");
6390
+ throw new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6391
+ }
6392
+ return cookie;
6393
+ }
6394
+ /** 当前 baseURL 对应的网关客户端;没配置 baseURL 时同样是「去 HiWork 登录」这一类动作。 */
6395
+ gatewayPort() {
6396
+ const gateway = this.deps.gateway();
6397
+ if (gateway === void 0) {
6398
+ this.fail("gateway-signed-out");
6399
+ throw new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6400
+ }
6401
+ return gateway;
6178
6402
  }
6179
6403
  /**
6180
- * 当前凭据记录:内存优先,首次调用时从 credentials 服务读一次。
6404
+ * 取出这条客户端**被分配**的 `pms` 服务(spec §3 D9)。
6181
6405
  *
6182
- * 「读一次就记住 `loaded`」是有意的:凭据的写入方只有本类(`login` / `logout` / refresh 回写),
6183
- * 因此在同一进程里,读盘只需要一次。跨进程变更(别的进程写了同一个 ref)本就不可观测,
6184
- * 硬要每次重新读只会让「不 remember 的登录」被磁盘上的旧记录当场覆盖。
6406
+ * 匹配依据只有 slug(逐字等于 {@link PMS_ASSIGNMENT_SLUG}):清单里还有 visio-mcp 之类的分配,
6407
+ * 「取第一条断言分配」在分配多起来之后会随机串台。**不看 `auth.kind`**:那是服务端演进的开放
6408
+ * 集合,我们只认自己写死的 slug(D9 的原文就是「唯一匹配依据」)。
6409
+ *
6410
+ * 没有这条分配 → `NOT_CONFIGURED`:UI 提示「找管理员」,**不给任何手工配置入口**(spec §6)。
6185
6411
  */
6186
- async currentRecord() {
6187
- if (this.loaded) return this.record;
6188
- this.loaded = true;
6412
+ async ensureAssignment(gateway, cookie) {
6413
+ const cached = this.assignment;
6414
+ if (cached !== void 0) return cached;
6415
+ let view;
6189
6416
  try {
6190
- const hit = await this.deps.credentials.resolve(PMS_CREDENTIALS_REF);
6191
- if (hit === void 0 || hit.value === "") return void 0;
6192
- const record = parseCredentialsRecord(hit.value);
6193
- this.record = record;
6194
- this.persisted = record !== void 0;
6195
- return record;
6417
+ view = await gateway.getMyMcpAssignments(cookie);
6196
6418
  } catch (error) {
6197
- this.deps.log?.(`hiwork-core: PMS \u51ED\u636E\u8BFB\u53D6\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
6198
- return void 0;
6419
+ throw this.gatewayFailure(error);
6199
6420
  }
6421
+ const list = Array.isArray(view?.assignments) ? view.assignments : [];
6422
+ for (const item of list) {
6423
+ if (item?.slug !== PMS_ASSIGNMENT_SLUG) continue;
6424
+ const assignmentId = textOf2(item.assignmentId);
6425
+ if (assignmentId === void 0) continue;
6426
+ const assignment = { assignmentId };
6427
+ this.assignment = assignment;
6428
+ this.reason = void 0;
6429
+ return assignment;
6430
+ }
6431
+ this.fail("no-assignment");
6432
+ throw new PmsTodoError("NOT_CONFIGURED", PMS_TODO_NOT_CONFIGURED_MESSAGE);
6200
6433
  }
6201
6434
  /**
6202
- * 落盘(**唯一**写 credentials 的地方,只在 remember 为真时被调用)。
6435
+ * 签一张断言(120 秒、一次性),并取出它的 `mintUrl`。
6203
6436
  *
6204
- * 失败**不**把一次成功的登录判为失败:会话已经拿到了,内存里照常可用;密码存不下的后果只是
6205
- * 「重启后要重新输入」。反过来把登录判为失败会让用户以为密码错了。
6437
+ * `mintUrl` 由**门票**给(`auth.mintUrl` 的权威副本在网关侧),本层不猜也不改写它。
6206
6438
  */
6207
- async persist(record) {
6439
+ async assertionTicket(gateway, cookie, assignment) {
6440
+ let ticket;
6208
6441
  try {
6209
- await this.deps.credentials.set(PMS_CREDENTIALS_REF, serializeCredentialsRecord(record));
6210
- this.persisted = true;
6442
+ ticket = await gateway.issueMyMcpAssertion(cookie, assignment.assignmentId);
6211
6443
  } catch (error) {
6212
- this.deps.log?.(`hiwork-core: PMS \u51ED\u636E\u843D\u76D8\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
6444
+ throw this.gatewayFailure(error);
6445
+ }
6446
+ const assertion = textOf2(ticket?.assertion);
6447
+ const mintUrl = textOf2(ticket?.mintUrl);
6448
+ if (assertion === void 0 || mintUrl === void 0) {
6449
+ this.deps.log?.("hiwork-core: PMS \u53D6\u65AD\u8A00\u6536\u5230\u65E0\u6548\u54CD\u5E94\uFF08\u7F3A assertion / mintUrl\uFF09");
6450
+ this.fail("unavailable");
6451
+ throw new PmsTodoError("INTERNAL", PMS_TODO_GENERIC_FAILURE_MESSAGE);
6213
6452
  }
6453
+ return { assertion, mintUrl };
6214
6454
  }
6215
6455
  /**
6216
- * 抹掉磁盘上的记录(logout / 不记住的登录)。
6456
+ * 断言 → 令牌(spec §4.1b 的客户端侧),失败语义按 §6 / plan 全局约束 2 分两类:
6217
6457
  *
6218
- * 先 `resolve` 一次再 `unset`:没有记录时一次写都不做(测试与磁盘都清清爽爽),有记录时
6219
- * **必须**清掉——否则一条旧的 account+refresh token 会在下次启动时被当成「这个人配置过凭据」,
6220
- * 用户明明刚选了「不记住」却在重启后仍是登录态。
6458
+ * - **被拒(401 / 403)→ 不重试**:断言过期 / `aud` 不符 / 签名不过 / 工号在 PMS 不存在,
6459
+ * 四种原因在 PMS 侧都是同一个 403(防账号探测),重发只会连打;动作是「重新登录 HiWork」。
6460
+ * - **网络 / 5xx → 换一张新断言重试一次**:断言可能是在途耗掉的(120 秒的门票),而同一张断言
6461
+ * 是一次性的(`jti` 防重放),所以重试必须重新签一张。仍失败 → `UPSTREAM_UNAVAILABLE`。
6462
+ *
6463
+ * 日志只记 assignmentId 与事件(**不记断言、不记令牌**,spec §7)。
6221
6464
  */
6222
- async forgetStored() {
6465
+ async mint(gateway, cookie, assignment) {
6466
+ const deviceRef = await this.deviceRef();
6467
+ let attempt = 0;
6468
+ while (attempt < 2) {
6469
+ attempt += 1;
6470
+ const ticket = await this.assertionTicket(gateway, cookie, assignment);
6471
+ try {
6472
+ const session = sessionOf(
6473
+ await this.deps.pms.exchangeAssertion(ticket.mintUrl, ticket.assertion, deviceRef),
6474
+ this.now()
6475
+ );
6476
+ this.session = session;
6477
+ this.reason = void 0;
6478
+ return session;
6479
+ } catch (error) {
6480
+ if (error instanceof PmsError && error.code === "UNAUTHORIZED") {
6481
+ this.deps.log?.(`hiwork-core: PMS \u6362\u4EE4\u724C\u88AB\u62D2\uFF08\u5206\u914D ${assignment.assignmentId}\uFF09`);
6482
+ this.fail("gateway-signed-out");
6483
+ throw new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6484
+ }
6485
+ if (error instanceof PmsError && error.code === "UPSTREAM_UNAVAILABLE" && attempt === 1) {
6486
+ this.deps.log?.(`hiwork-core: PMS \u6362\u4EE4\u724C\u5931\u8D25\uFF08\u5206\u914D ${assignment.assignmentId}\uFF09\uFF0C\u6362\u4E00\u5F20\u65B0\u65AD\u8A00\u91CD\u8BD5\u4E00\u6B21`);
6487
+ continue;
6488
+ }
6489
+ this.fail("unavailable");
6490
+ throw error instanceof PmsError ? error : new PmsTodoError("INTERNAL", PMS_TODO_GENERIC_FAILURE_MESSAGE);
6491
+ }
6492
+ }
6493
+ this.fail("unavailable");
6494
+ throw new PmsTodoError("UPSTREAM_UNAVAILABLE", PMS_TODO_GENERIC_FAILURE_MESSAGE);
6495
+ }
6496
+ /** 设备标识(只进 PMS 侧审计、不参与鉴权):解析失败留空,不拦登录。 */
6497
+ async deviceRef() {
6223
6498
  try {
6224
- const hit = await this.deps.credentials.resolve(PMS_CREDENTIALS_REF);
6225
- if (hit === void 0 || hit.value === "") return;
6226
- await this.deps.credentials.unset(PMS_CREDENTIALS_REF);
6499
+ return await this.deps.resolveDeviceId();
6227
6500
  } catch (error) {
6228
- this.deps.log?.(`hiwork-core: PMS \u51ED\u636E\u6E05\u9664\u5931\u8D25\uFF08${errorName(error)}\uFF09`);
6501
+ this.deps.log?.(`hiwork-core: \u8BBE\u5907\u6807\u8BC6\u89E3\u6790\u5931\u8D25\uFF08${errorName(error)}\uFF09\uFF0CdeviceRef \u7559\u7A7A`);
6502
+ return "";
6503
+ }
6504
+ }
6505
+ /**
6506
+ * 网关异常 → 本频道的稳定错误(spec §3 D8)。
6507
+ *
6508
+ * 关键的分法:**会话失效**与**分配被撤销/停用**要分开。前者是用户自己的动作(重新登录 HiWork),
6509
+ * 后者是管理员那边的动作(重新分配)——归到同一句话,用户就会对着一个自己修不了的问题反复重试。
6510
+ */
6511
+ gatewayFailure(error) {
6512
+ this.deps.log?.(`hiwork-core: PMS \u7F51\u5173\u8C03\u7528\u5931\u8D25\uFF08${gatewayCodeOf(error)}\uFF09`);
6513
+ if (error instanceof GatewayError) {
6514
+ if (error.code === "SESSION_EXPIRED") {
6515
+ this.fail("gateway-signed-out");
6516
+ return new PmsTodoError("UNAUTHORIZED", PMS_TODO_UNAUTHORIZED_MESSAGE);
6517
+ }
6518
+ if (error.code === "MCP_ASSIGNMENT_GONE" || error.code === "MCP_NOT_DELIVERABLE") {
6519
+ this.assignment = void 0;
6520
+ this.fail("no-assignment");
6521
+ return new PmsTodoError("NOT_CONFIGURED", PMS_TODO_NOT_CONFIGURED_MESSAGE);
6522
+ }
6229
6523
  }
6524
+ this.fail("unavailable");
6525
+ return new PmsTodoError("UPSTREAM_UNAVAILABLE", PMS_TODO_GENERIC_FAILURE_MESSAGE);
6526
+ }
6527
+ /** 记下「为什么还没就绪」(只影响 `status` 的 `reason`,不影响错误码)。 */
6528
+ fail(reason) {
6529
+ this.reason = reason;
6230
6530
  }
6231
6531
  /**
6232
- * 并发合并:同一世代 + 同一端点的在飞请求共用一次调用与同一个返回值。
6532
+ * 并发合并:同一端点的在飞请求共用一次调用与同一个返回值(面板挂载与手动刷新常常同拍)。
6233
6533
  *
6234
- * 世代判据与 `src/client/managed-workbench.ts` 的 `dedupe` 同形:`login` / `logout` 之后
6235
- * 账上那条属于**上一个人**,谁都不许复用(复用等于把上一个人的待办显示给新账号)。
6534
+ * v1 还带一个「世代号」(`login` / `logout` 之后账上那条属于上一个人);现在**身份只有一个来源**
6535
+ * (网关会话),进程内没有换人的入口,所以按端点名合并即可。换账号的清理由客户端半边的
6536
+ * `clear()`(镜像)与会话本身负责。
6236
6537
  */
6237
6538
  async dedupe(key, task) {
6238
6539
  const inFlight = this.inFlight;
6239
- if (inFlight !== void 0 && inFlight.generation === this.generation && inFlight.key === key) {
6540
+ if (inFlight !== void 0 && inFlight.key === key) {
6240
6541
  return await inFlight.task;
6241
6542
  }
6242
- const generation = this.generation;
6243
6543
  const promise = task();
6244
- this.inFlight = { generation, key, task: promise };
6544
+ this.inFlight = { key, task: promise };
6245
6545
  try {
6246
6546
  return await promise;
6247
6547
  } finally {
@@ -6251,7 +6551,7 @@ var PmsTodoStore = class {
6251
6551
  };
6252
6552
  async function handlePmsTodoEndpoint(endpoint, rawPayload, deps, store, signal) {
6253
6553
  try {
6254
- if (signal?.aborted === true) return failure7("BAD_REQUEST", PMS_TODO_CANCELLED_MESSAGE);
6554
+ if (signal?.aborted === true) return failure8("BAD_REQUEST", PMS_TODO_CANCELLED_MESSAGE);
6255
6555
  if (!isPmsTodoRpcEndpoint(endpoint)) {
6256
6556
  throw new PmsTodoRequestError(`\u672A\u77E5\u7684 PMS \u5F85\u529E\u7AEF\u70B9 '${endpoint}'`);
6257
6557
  }
@@ -6259,10 +6559,6 @@ async function handlePmsTodoEndpoint(endpoint, rawPayload, deps, store, signal)
6259
6559
  switch (payload.endpoint) {
6260
6560
  case "status":
6261
6561
  return { ok: true, value: await store.status() };
6262
- case "login":
6263
- return { ok: true, value: await store.login({ username: payload.username, password: payload.password, remember: payload.remember }) };
6264
- case "logout":
6265
- return { ok: true, value: await store.logout() };
6266
6562
  case "today":
6267
6563
  return { ok: true, value: await store.today() };
6268
6564
  case "statistics":
@@ -6279,7 +6575,7 @@ async function handlePmsTodoEndpoint(endpoint, rawPayload, deps, store, signal)
6279
6575
  return { ok: false, error: pmsTodoErrorOf(error) };
6280
6576
  }
6281
6577
  }
6282
- function failure7(code, message) {
6578
+ function failure8(code, message) {
6283
6579
  return { ok: false, error: { code, message, details: {} } };
6284
6580
  }
6285
6581
  function registerPmsTodoRpc(ctx, deps, store) {
@@ -6310,7 +6606,7 @@ function isRecord4(value) {
6310
6606
  function textOrUndefined4(value) {
6311
6607
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
6312
6608
  }
6313
- function isAborted7(signal) {
6609
+ function isAborted8(signal) {
6314
6610
  return signal?.aborted === true;
6315
6611
  }
6316
6612
  function parsePluginUpdatesPayload(endpoint, raw) {
@@ -6353,7 +6649,7 @@ function toSnapshot(raw) {
6353
6649
  }
6354
6650
  async function handlePluginUpdatesEndpoint(endpoint, rawPayload, deps, signal) {
6355
6651
  try {
6356
- if (isAborted7(signal)) return failure8("cancelled", "\u63D2\u4EF6\u66F4\u65B0\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
6652
+ if (isAborted8(signal)) return failure9("cancelled", "\u63D2\u4EF6\u66F4\u65B0\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
6357
6653
  if (!isPluginUpdatesRpcEndpoint(endpoint)) {
6358
6654
  throw new PluginUpdatesRequestError("bad-request", `\u672A\u77E5\u7684\u63D2\u4EF6\u66F4\u65B0\u7AEF\u70B9 '${endpoint}'`);
6359
6655
  }
@@ -6369,12 +6665,12 @@ async function handlePluginUpdatesEndpoint(endpoint, rawPayload, deps, signal) {
6369
6665
  await plugins.applyNow();
6370
6666
  return { ok: true, value: { requested: true } };
6371
6667
  } catch (error) {
6372
- if (error instanceof PluginUpdatesRequestError) return failure8(error.code, error.message);
6668
+ if (error instanceof PluginUpdatesRequestError) return failure9(error.code, error.message);
6373
6669
  deps.log?.(`hiwork-core: \u63D2\u4EF6\u66F4\u65B0 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
6374
- return failure8("internal", PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE);
6670
+ return failure9("internal", PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE);
6375
6671
  }
6376
6672
  }
6377
- function failure8(code, message) {
6673
+ function failure9(code, message) {
6378
6674
  return { ok: false, error: { code, message, details: {} } };
6379
6675
  }
6380
6676
  function registerPluginUpdatesRpc(ctx, deps) {
@@ -6445,16 +6741,15 @@ function provideIdentity(ctx, deps) {
6445
6741
  };
6446
6742
  }
6447
6743
 
6448
- // src/memory-config.ts
6449
- var HIWORK_MEMORY_SERVICE = "hiworkMemory";
6450
- var ME_MEMORY_CONFIG_PATH = "/api/me/memory-config";
6451
- var MEMORY_CONFIG_TTL_MS = 5 * 60 * 1e3;
6452
- var MEMORY_CONFIG_TIMEOUT_MS = 1e4;
6453
- var MESSAGE_NOT_CONFIGURED = "\u7BA1\u7406\u5458\u8FD8\u6CA1\u5F00\u542F\u56E2\u961F\u8BB0\u5FC6\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5F00\u901A";
6744
+ // src/knowledge-config.ts
6745
+ var HIWORK_KNOWLEDGE_SERVICE = "hiworkKnowledge";
6746
+ var ME_KNOWLEDGE_CONFIG_PATH = "/api/me/knowledge-config";
6747
+ var KNOWLEDGE_CONFIG_TTL_MS = 5 * 60 * 1e3;
6748
+ var KNOWLEDGE_CONFIG_TIMEOUT_MS = 1e4;
6749
+ var MESSAGE_NOT_CONFIGURED = "\u7BA1\u7406\u5458\u8FD8\u6CA1\u5F00\u542F\u77E5\u8BC6\u5E93\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5F00\u901A";
6454
6750
  var MESSAGE_UNAUTHENTICATED = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
6455
- var MESSAGE_UNREACHABLE = "\u8BB0\u5FC6\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6456
- var MESSAGE_NO_GATEWAY = "\u8FD8\u6CA1\u8FDE\u4E0A\u4F01\u4E1A\u7F51\u5173\uFF0C\u8BF7\u5148\u767B\u5F55\u540E\u518D\u8BD5";
6457
- var MESSAGE_GENERIC = "\u8BB0\u5FC6\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6751
+ var MESSAGE_UNREACHABLE = "\u77E5\u8BC6\u5E93\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6752
+ var MESSAGE_GENERIC = "\u77E5\u8BC6\u5E93\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6458
6753
  function messageFor(reason) {
6459
6754
  switch (reason) {
6460
6755
  case "not-configured":
@@ -6467,7 +6762,7 @@ function messageFor(reason) {
6467
6762
  return MESSAGE_GENERIC;
6468
6763
  }
6469
6764
  }
6470
- function failure9(reason, message) {
6765
+ function failure10(reason, message) {
6471
6766
  return { ok: false, reason, message: message ?? messageFor(reason) };
6472
6767
  }
6473
6768
  function textOrUndefined6(value) {
@@ -6475,26 +6770,17 @@ function textOrUndefined6(value) {
6475
6770
  const text = value.trim();
6476
6771
  return text === "" ? void 0 : text;
6477
6772
  }
6478
- function teamIdsOf(value) {
6479
- if (!Array.isArray(value)) return [];
6480
- const ids = [];
6481
- for (const item of value) {
6482
- const text = textOrUndefined6(item);
6483
- if (text !== void 0) ids.push(text);
6484
- }
6485
- return ids;
6486
- }
6487
- function projectMemoryConfig(body) {
6488
- const panelUrl = textOrUndefined6(body["panel_url"] ?? body["panelUrl"]);
6489
- const serviceId = textOrUndefined6(body["service_id"] ?? body["serviceId"]);
6490
- const apiKey = textOrUndefined6(body["api_key"] ?? body["apiKey"] ?? body["userKey"]);
6491
- if (panelUrl === void 0 || serviceId === void 0 || apiKey === void 0) return void 0;
6773
+ function projectKnowledgeConfig(body) {
6774
+ const baseUrl = textOrUndefined6(body["base_url"] ?? body["baseUrl"]);
6775
+ const apiKey = textOrUndefined6(body["api_key"] ?? body["apiKey"]);
6776
+ if (baseUrl === void 0 || apiKey === void 0) return void 0;
6492
6777
  return {
6493
6778
  ok: true,
6494
- panelUrl,
6495
- serviceId,
6496
- teamIds: teamIdsOf(body["team_ids"] ?? body["teamIds"]),
6497
- apiKey
6779
+ baseUrl,
6780
+ apiKey,
6781
+ agentId: textOrUndefined6(body["agent_id"] ?? body["agentId"]) ?? "",
6782
+ chatModelId: textOrUndefined6(body["chat_model_id"] ?? body["chatModelId"]) ?? "",
6783
+ tenantId: textOrUndefined6(body["tenant_id"] ?? body["tenantId"]) ?? ""
6498
6784
  };
6499
6785
  }
6500
6786
  function errorName2(error) {
@@ -6506,9 +6792,9 @@ function reasonOfErrorCode(error) {
6506
6792
  switch (code) {
6507
6793
  case "SESSION_EXPIRED":
6508
6794
  return "unauthenticated";
6509
- case "MEMORY_NOT_CONFIGURED":
6795
+ case "KNOWLEDGE_NOT_CONFIGURED":
6510
6796
  return "not-configured";
6511
- case "MEMORY_UNREACHABLE":
6797
+ case "KNOWLEDGE_UNREACHABLE":
6512
6798
  case "GATEWAY_UNREACHABLE":
6513
6799
  return "unreachable";
6514
6800
  default:
@@ -6521,6 +6807,168 @@ function reasonOfStatus(status) {
6521
6807
  if (status === 504) return "unreachable";
6522
6808
  return "error";
6523
6809
  }
6810
+ var KnowledgeConfigProvider = class {
6811
+ constructor(deps) {
6812
+ this.deps = deps;
6813
+ }
6814
+ cached;
6815
+ async config(opts) {
6816
+ const force = opts?.force === true;
6817
+ let sessionCookie;
6818
+ try {
6819
+ sessionCookie = await this.deps.resolveSession();
6820
+ } catch (error) {
6821
+ this.deps.log?.(`hiwork-core: \u77E5\u8BC6\u5E93\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6822
+ return failure10("error");
6823
+ }
6824
+ if (sessionCookie === void 0 || sessionCookie === "") return failure10("unauthenticated");
6825
+ if (!force) {
6826
+ const hit = this.cached;
6827
+ if (hit !== void 0 && hit.sessionCookie === sessionCookie && this.now() - hit.at < KNOWLEDGE_CONFIG_TTL_MS) {
6828
+ return hit.view;
6829
+ }
6830
+ }
6831
+ const gateway = this.deps.gateway();
6832
+ if (gateway === void 0) return failure10("unreachable");
6833
+ let outcome;
6834
+ try {
6835
+ outcome = await gateway.getMyKnowledgeConfig(sessionCookie);
6836
+ } catch (error) {
6837
+ const mapped = reasonOfErrorCode(error);
6838
+ if (mapped !== void 0) return failure10(mapped);
6839
+ this.deps.log?.(`hiwork-core: \u77E5\u8BC6\u5E93\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6840
+ return failure10("error");
6841
+ }
6842
+ if (!outcome.ok) {
6843
+ if (outcome.kind === "network") return failure10("unreachable");
6844
+ if (outcome.kind === "http") return failure10(reasonOfStatus(outcome.status));
6845
+ return failure10("error");
6846
+ }
6847
+ const view = projectKnowledgeConfig(outcome.body);
6848
+ if (view === void 0) {
6849
+ this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 knowledge-config \u4E0D\u5B8C\u6574\uFF08\u7F3A base_url / api_key \u4E4B\u4E00\uFF09");
6850
+ return failure10("error");
6851
+ }
6852
+ this.cached = { sessionCookie, view, at: this.now() };
6853
+ return view;
6854
+ }
6855
+ now() {
6856
+ return (this.deps.now ?? Date.now)();
6857
+ }
6858
+ };
6859
+ function provideKnowledgeConfig(ctx, deps) {
6860
+ const service = new KnowledgeConfigProvider(deps);
6861
+ const dispose = ctx.provide(HIWORK_KNOWLEDGE_SERVICE, service);
6862
+ return typeof dispose === "function" ? dispose : () => {
6863
+ };
6864
+ }
6865
+ function createKnowledgeConfigGateway(baseURL, opts = {}) {
6866
+ const root = baseURL.replace(/\/+$/, "");
6867
+ const timeoutMs = opts.timeoutMs ?? KNOWLEDGE_CONFIG_TIMEOUT_MS;
6868
+ const doFetch = opts.fetchImpl ?? fetch;
6869
+ return {
6870
+ async getMyKnowledgeConfig(sessionCookie) {
6871
+ try {
6872
+ const res = await doFetch(`${root}${ME_KNOWLEDGE_CONFIG_PATH}`, {
6873
+ method: "GET",
6874
+ headers: {
6875
+ "user-agent": `hiwork-core/${PKG_VERSION}`,
6876
+ cookie: sessionCookie
6877
+ },
6878
+ signal: AbortSignal.timeout(timeoutMs)
6879
+ });
6880
+ if (!res.ok) return { ok: false, kind: "http", status: res.status };
6881
+ let body;
6882
+ try {
6883
+ body = await res.json();
6884
+ } catch {
6885
+ return { ok: false, kind: "invalid" };
6886
+ }
6887
+ if (body === null || typeof body !== "object") return { ok: false, kind: "invalid" };
6888
+ return { ok: true, body };
6889
+ } catch {
6890
+ return { ok: false, kind: "network" };
6891
+ }
6892
+ }
6893
+ };
6894
+ }
6895
+
6896
+ // src/memory-config.ts
6897
+ var HIWORK_MEMORY_SERVICE = "hiworkMemory";
6898
+ var ME_MEMORY_CONFIG_PATH = "/api/me/memory-config";
6899
+ var MEMORY_CONFIG_TTL_MS = 5 * 60 * 1e3;
6900
+ var MEMORY_CONFIG_TIMEOUT_MS = 1e4;
6901
+ var MESSAGE_NOT_CONFIGURED2 = "\u7BA1\u7406\u5458\u8FD8\u6CA1\u5F00\u542F\u56E2\u961F\u8BB0\u5FC6\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5F00\u901A";
6902
+ var MESSAGE_UNAUTHENTICATED2 = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
6903
+ var MESSAGE_UNREACHABLE2 = "\u8BB0\u5FC6\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6904
+ var MESSAGE_NO_GATEWAY = "\u8FD8\u6CA1\u8FDE\u4E0A\u4F01\u4E1A\u7F51\u5173\uFF0C\u8BF7\u5148\u767B\u5F55\u540E\u518D\u8BD5";
6905
+ var MESSAGE_GENERIC2 = "\u8BB0\u5FC6\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6906
+ function messageFor2(reason) {
6907
+ switch (reason) {
6908
+ case "not-configured":
6909
+ return MESSAGE_NOT_CONFIGURED2;
6910
+ case "unauthenticated":
6911
+ return MESSAGE_UNAUTHENTICATED2;
6912
+ case "unreachable":
6913
+ return MESSAGE_UNREACHABLE2;
6914
+ case "error":
6915
+ return MESSAGE_GENERIC2;
6916
+ }
6917
+ }
6918
+ function failure11(reason, message) {
6919
+ return { ok: false, reason, message: message ?? messageFor2(reason) };
6920
+ }
6921
+ function textOrUndefined7(value) {
6922
+ if (typeof value !== "string") return void 0;
6923
+ const text = value.trim();
6924
+ return text === "" ? void 0 : text;
6925
+ }
6926
+ function teamIdsOf(value) {
6927
+ if (!Array.isArray(value)) return [];
6928
+ const ids = [];
6929
+ for (const item of value) {
6930
+ const text = textOrUndefined7(item);
6931
+ if (text !== void 0) ids.push(text);
6932
+ }
6933
+ return ids;
6934
+ }
6935
+ function projectMemoryConfig(body) {
6936
+ const panelUrl = textOrUndefined7(body["panel_url"] ?? body["panelUrl"]);
6937
+ const serviceId = textOrUndefined7(body["service_id"] ?? body["serviceId"]);
6938
+ const apiKey = textOrUndefined7(body["api_key"] ?? body["apiKey"] ?? body["userKey"]);
6939
+ if (panelUrl === void 0 || serviceId === void 0 || apiKey === void 0) return void 0;
6940
+ return {
6941
+ ok: true,
6942
+ panelUrl,
6943
+ serviceId,
6944
+ teamIds: teamIdsOf(body["team_ids"] ?? body["teamIds"]),
6945
+ apiKey
6946
+ };
6947
+ }
6948
+ function errorName3(error) {
6949
+ return error instanceof Error ? error.name : typeof error;
6950
+ }
6951
+ function reasonOfErrorCode2(error) {
6952
+ const code = error?.code;
6953
+ if (typeof code !== "string") return void 0;
6954
+ switch (code) {
6955
+ case "SESSION_EXPIRED":
6956
+ return "unauthenticated";
6957
+ case "MEMORY_NOT_CONFIGURED":
6958
+ return "not-configured";
6959
+ case "MEMORY_UNREACHABLE":
6960
+ case "GATEWAY_UNREACHABLE":
6961
+ return "unreachable";
6962
+ default:
6963
+ return void 0;
6964
+ }
6965
+ }
6966
+ function reasonOfStatus2(status) {
6967
+ if (status === 401) return "unauthenticated";
6968
+ if (status === 503) return "not-configured";
6969
+ if (status === 504) return "unreachable";
6970
+ return "error";
6971
+ }
6524
6972
  var MemoryConfigProvider = class {
6525
6973
  constructor(deps) {
6526
6974
  this.deps = deps;
@@ -6532,10 +6980,10 @@ var MemoryConfigProvider = class {
6532
6980
  try {
6533
6981
  sessionCookie = await this.deps.resolveSession();
6534
6982
  } catch (error) {
6535
- this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6536
- return failure9("error");
6983
+ this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
6984
+ return failure11("error");
6537
6985
  }
6538
- if (sessionCookie === void 0 || sessionCookie === "") return failure9("unauthenticated");
6986
+ if (sessionCookie === void 0 || sessionCookie === "") return failure11("unauthenticated");
6539
6987
  if (!force) {
6540
6988
  const hit = this.cached;
6541
6989
  if (hit !== void 0 && hit.sessionCookie === sessionCookie && this.now() - hit.at < MEMORY_CONFIG_TTL_MS) {
@@ -6543,25 +6991,25 @@ var MemoryConfigProvider = class {
6543
6991
  }
6544
6992
  }
6545
6993
  const gateway = this.deps.gateway();
6546
- if (gateway === void 0) return failure9("unreachable", MESSAGE_NO_GATEWAY);
6994
+ if (gateway === void 0) return failure11("unreachable", MESSAGE_NO_GATEWAY);
6547
6995
  let outcome;
6548
6996
  try {
6549
6997
  outcome = await gateway.getMyMemoryConfig(sessionCookie);
6550
6998
  } catch (error) {
6551
- const mapped = reasonOfErrorCode(error);
6552
- if (mapped !== void 0) return failure9(mapped);
6553
- this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6554
- return failure9("error");
6999
+ const mapped = reasonOfErrorCode2(error);
7000
+ if (mapped !== void 0) return failure11(mapped);
7001
+ this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
7002
+ return failure11("error");
6555
7003
  }
6556
7004
  if (!outcome.ok) {
6557
- if (outcome.kind === "network") return failure9("unreachable");
6558
- if (outcome.kind === "http") return failure9(reasonOfStatus(outcome.status));
6559
- return failure9("error");
7005
+ if (outcome.kind === "network") return failure11("unreachable");
7006
+ if (outcome.kind === "http") return failure11(reasonOfStatus2(outcome.status));
7007
+ return failure11("error");
6560
7008
  }
6561
7009
  const view = projectMemoryConfig(outcome.body);
6562
7010
  if (view === void 0) {
6563
7011
  this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 memory-config \u4E0D\u5B8C\u6574\uFF08\u7F3A panel_url / service_id / api_key \u4E4B\u4E00\uFF09");
6564
- return failure9("error");
7012
+ return failure11("error");
6565
7013
  }
6566
7014
  this.cached = { sessionCookie, view, at: this.now() };
6567
7015
  return view;
@@ -6754,9 +7202,1352 @@ function provideOfficeRelay(ctx, deps) {
6754
7202
  };
6755
7203
  }
6756
7204
 
7205
+ // node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
7206
+ import { createRequire } from "node:module";
7207
+
7208
+ // node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
7209
+ var RemoteError = class extends Error {
7210
+ code;
7211
+ details;
7212
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
7213
+ isDSHRemoteError = true;
7214
+ /**
7215
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
7216
+ * @param message - human diagnostic carried across the wire.
7217
+ * @param details - structured payload typed by the code.
7218
+ * @param options - standard Error options (`cause` survives in-process only).
7219
+ */
7220
+ constructor(code, message, details, options) {
7221
+ super(message, options);
7222
+ this.code = code;
7223
+ this.details = details;
7224
+ this.name = "RemoteError";
7225
+ }
7226
+ };
7227
+ var TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
7228
+ function isTypertRemoteSegment(value) {
7229
+ return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
7230
+ }
7231
+ var REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
7232
+ function bindTypertRemote(service, serviceKey, options = {}) {
7233
+ validateName("service key", serviceKey);
7234
+ const namespace = options.namespace ?? serviceKey;
7235
+ validateName("namespace", namespace);
7236
+ return Object.freeze({
7237
+ service,
7238
+ serviceKey,
7239
+ namespace
7240
+ });
7241
+ }
7242
+ var TypertRemoteService = class extends Service {
7243
+ /** Visible binding consumed by the Gateway's source-mode discovery. */
7244
+ typertRemote;
7245
+ /**
7246
+ * Register the Service and bind the same key to Typert Gateway.
7247
+ * @param ctx - owning Cordis Context.
7248
+ * @param serviceKey - exact Cordis service key and default wire namespace.
7249
+ * @param options - optional distinct wire namespace.
7250
+ */
7251
+ constructor(ctx, serviceKey, options = {}) {
7252
+ super(ctx, serviceKey);
7253
+ this.typertRemote = bindTypertRemote(this, this.name, options);
7254
+ }
7255
+ };
7256
+ function Remote(methodExportOrOptions, context) {
7257
+ if (typeof methodExportOrOptions === "string") {
7258
+ validateName("Remote export name", methodExportOrOptions);
7259
+ return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
7260
+ }
7261
+ if (typeof methodExportOrOptions === "object") {
7262
+ if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError('typert-protocol: Remote options must contain exactly mode: "stream"');
7263
+ return remoteDecorator({ kind: "direct" }, "stream");
7264
+ }
7265
+ if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
7266
+ addMarkerInitializer(context, { kind: "direct" });
7267
+ }
7268
+ function remoteOptionMode(options) {
7269
+ return Reflect.get(options, "mode");
7270
+ }
7271
+ function remoteDecorator(invocation, mode, exportName) {
7272
+ return function(_method, context) {
7273
+ addMarkerInitializer(context, invocation, mode, exportName);
7274
+ };
7275
+ }
7276
+ function readRemoteMethodDescriptor(prototype) {
7277
+ const property2 = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
7278
+ if (property2 === void 0) return void 0;
7279
+ const descriptor = property2.value;
7280
+ if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
7281
+ const version2 = Reflect.get(descriptor, "version");
7282
+ if (version2 !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version2)}`);
7283
+ const methods = Reflect.get(descriptor, "methods");
7284
+ if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
7285
+ return descriptor;
7286
+ }
7287
+ function addMarkerInitializer(context, invocation, mode, exportName) {
7288
+ if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
7289
+ const method = context.name;
7290
+ context.addInitializer(function() {
7291
+ const prototype = Object.getPrototypeOf(this);
7292
+ if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
7293
+ mark(prototype, method, invocation, mode, exportName);
7294
+ });
7295
+ }
7296
+ function mark(prototype, method, invocation, mode, exportName) {
7297
+ const descriptor = readRemoteMethodDescriptor(prototype);
7298
+ const marker = Object.freeze({
7299
+ method,
7300
+ ...exportName === void 0 || exportName === method ? {} : { exportName },
7301
+ ...mode === void 0 ? {} : { mode },
7302
+ invocation: Object.freeze(invocation)
7303
+ });
7304
+ const current = descriptor?.methods.find((candidate) => candidate.method === method);
7305
+ if (current !== void 0) {
7306
+ if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
7307
+ throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
7308
+ }
7309
+ Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
7310
+ configurable: true,
7311
+ value: Object.freeze({
7312
+ version: 1,
7313
+ methods: Object.freeze([...descriptor?.methods ?? [], marker])
7314
+ })
7315
+ });
7316
+ }
7317
+ function sameInvocation(left, right) {
7318
+ if (left.kind === "direct") return right.kind === "direct";
7319
+ if (right.kind === "direct") return false;
7320
+ return left.context === right.context;
7321
+ }
7322
+ function validateName(subject, value) {
7323
+ if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
7324
+ }
7325
+
7326
+ // node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
7327
+ var MAX_TIMER_DELAY_MS = 2147483647;
7328
+
7329
+ // node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
7330
+ function freezeMessage(message) {
7331
+ return deepFreeze(structuredClone(message));
7332
+ }
7333
+ var HarnessError = class extends Error {
7334
+ /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
7335
+ code;
7336
+ constructor(message, code, options) {
7337
+ super(message, options);
7338
+ this.code = code;
7339
+ this.name = new.target.name;
7340
+ }
7341
+ };
7342
+ var EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
7343
+ var STRUCTURED_CONTEXT_OVERFLOW = new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
7344
+ var TOO_LARGE_FOR_CONTEXT = new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
7345
+ var EXCEEDS_MODEL_CONTEXT = new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
7346
+ var DEFAULT_MAX_RETRIES = 5;
7347
+ var DEFAULT_INITIAL_DELAY_MS = 500;
7348
+ var DEFAULT_MAX_DELAY_MS = 1e4;
7349
+ var DEFAULT_JITTER_RATIO = 0.1;
7350
+ var DEFAULT_RETRYABLE_CODES = Object.freeze([
7351
+ EMPTY_RESPONSE_CODE,
7352
+ "RATE_LIMIT",
7353
+ "SERVER",
7354
+ "TIMEOUT",
7355
+ "TRANSPORT"
7356
+ ]);
7357
+ var backoffSchema = Schema.object({
7358
+ initialDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
7359
+ maxDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
7360
+ jitterRatio: Schema.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
7361
+ });
7362
+ var normalPolicySchema = Schema.object({
7363
+ mode: Schema.const("normal").required(),
7364
+ maxRetries: Schema.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
7365
+ retryableCodes: Schema.array(Schema.string()).default([...DEFAULT_RETRYABLE_CODES]),
7366
+ backoff: backoffSchema
7367
+ });
7368
+ var alwaysPolicySchema = Schema.object({
7369
+ mode: Schema.const("always").required(),
7370
+ backoff: backoffSchema
7371
+ });
7372
+ var RetryPolicySchema = Schema.union([normalPolicySchema, alwaysPolicySchema]);
7373
+ var NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
7374
+ "mode",
7375
+ "maxRetries",
7376
+ "retryableCodes",
7377
+ "backoff"
7378
+ ]);
7379
+ var ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
7380
+ "mode",
7381
+ "maxRetries",
7382
+ "retryableCodes",
7383
+ "backoff"
7384
+ ]);
7385
+ var BACKOFF_KEYS = /* @__PURE__ */ new Set([
7386
+ "initialDelayMs",
7387
+ "maxDelayMs",
7388
+ "jitterRatio"
7389
+ ]);
7390
+ function validateKeys(value, allowed, path) {
7391
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
7392
+ }
7393
+ function resolveBackoff(config, path) {
7394
+ if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
7395
+ const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
7396
+ const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
7397
+ const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
7398
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
7399
+ if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
7400
+ if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
7401
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
7402
+ return Object.freeze({
7403
+ initialDelayMs,
7404
+ maxDelayMs,
7405
+ jitterRatio
7406
+ });
7407
+ }
7408
+ function resolveRetryPolicy(config, path) {
7409
+ if (config === void 0) return Object.freeze({
7410
+ mode: "normal",
7411
+ maxRetries: DEFAULT_MAX_RETRIES,
7412
+ retryableCodes: DEFAULT_RETRYABLE_CODES,
7413
+ ...resolveBackoff(void 0, `${path}.backoff`)
7414
+ });
7415
+ switch (config.mode) {
7416
+ case "normal": {
7417
+ validateKeys(config, NORMAL_POLICY_KEYS, path);
7418
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
7419
+ const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
7420
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
7421
+ if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
7422
+ if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
7423
+ if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
7424
+ return Object.freeze({
7425
+ mode: "normal",
7426
+ maxRetries,
7427
+ retryableCodes: Object.freeze([...retryableCodes]),
7428
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
7429
+ });
7430
+ }
7431
+ case "always":
7432
+ validateKeys(config, ALWAYS_POLICY_KEYS, path);
7433
+ return Object.freeze({
7434
+ mode: "always",
7435
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
7436
+ });
7437
+ default:
7438
+ throw new Error(`${path}.mode must be "normal" or "always"`);
7439
+ }
7440
+ }
7441
+ function callConfigEquals(a, b) {
7442
+ if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
7443
+ if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
7444
+ return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
7445
+ }
7446
+ function normalizeLlmFailure(value) {
7447
+ const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
7448
+ const carried = ownFailureSnapshot(error);
7449
+ if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
7450
+ return Object.freeze({
7451
+ message: errorMessage(error),
7452
+ code: harnessErrorCode(error)
7453
+ });
7454
+ }
7455
+ function thrownMessage(value) {
7456
+ try {
7457
+ const message = String(value);
7458
+ return message.length > 0 ? message : "LLM adapter failed";
7459
+ } catch (_hostileThrownValue) {
7460
+ return "LLM adapter failed";
7461
+ }
7462
+ }
7463
+ function ownErrorCode(error) {
7464
+ try {
7465
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
7466
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
7467
+ } catch (_sdkPropertyTrap) {
7468
+ return;
7469
+ }
7470
+ }
7471
+ function ownFailureSnapshot(error) {
7472
+ try {
7473
+ const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
7474
+ return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
7475
+ } catch (_sdkPropertyTrap) {
7476
+ return;
7477
+ }
7478
+ }
7479
+ function failureSnapshot(value) {
7480
+ if (typeof value !== "object" || value === null) return void 0;
7481
+ try {
7482
+ const candidate = value;
7483
+ const message = candidate.message;
7484
+ const code = candidate.code;
7485
+ const status = candidate.status;
7486
+ const providerRetryAfterMs = candidate.providerRetryAfterMs;
7487
+ const requestId = candidate.requestId;
7488
+ if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0)) return void 0;
7489
+ return Object.freeze({
7490
+ message,
7491
+ code,
7492
+ ...status === void 0 ? {} : { status },
7493
+ ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
7494
+ ...requestId === void 0 ? {} : { requestId }
7495
+ });
7496
+ } catch (_sdkFailureGetter) {
7497
+ return;
7498
+ }
7499
+ }
7500
+ function errorMessage(error) {
7501
+ try {
7502
+ const message = error.message;
7503
+ if (typeof message === "string" && message.length > 0) return message;
7504
+ } catch (_sdkMessageGetter) {
7505
+ }
7506
+ return "LLM adapter failed";
7507
+ }
7508
+ function harnessErrorCode(error) {
7509
+ return error instanceof HarnessError ? error.code : "UNKNOWN";
7510
+ }
7511
+ function quoted(value) {
7512
+ return JSON.stringify(value);
7513
+ }
7514
+ function textOnlyImageText(ref) {
7515
+ return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
7516
+ }
7517
+ function contentHasImage(content) {
7518
+ return content.some((block) => block.type === "image" || block.type === "tool-result" && contentHasImage(block.content));
7519
+ }
7520
+ function contentHasFile(content) {
7521
+ for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
7522
+ return false;
7523
+ }
7524
+ function fileHandleText(ref, readonlyPath) {
7525
+ const digest = String(ref.attachmentId).slice(7, 15);
7526
+ const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
7527
+ if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
7528
+ return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
7529
+ }
7530
+ function replaceFilesWithHandles(blocks, resolvePath) {
7531
+ let next;
7532
+ for (const [index, block] of blocks.entries()) {
7533
+ if (block.type === "file") {
7534
+ next ??= blocks.slice(0, index);
7535
+ next.push({
7536
+ type: "text",
7537
+ text: fileHandleText(block.attachment, resolvePath(block.attachment))
7538
+ });
7539
+ continue;
7540
+ }
7541
+ if (block.type === "tool-result") {
7542
+ const content = replaceFilesWithHandles(block.content, resolvePath);
7543
+ if (content !== block.content) {
7544
+ next ??= blocks.slice(0, index);
7545
+ next.push({
7546
+ ...block,
7547
+ content
7548
+ });
7549
+ continue;
7550
+ }
7551
+ }
7552
+ next?.push(block);
7553
+ }
7554
+ return next ?? blocks;
7555
+ }
7556
+ function projectFilesToText(messages, resolvePath) {
7557
+ if (!messages.some((message) => contentHasFile(message.content))) return messages;
7558
+ return messages.map((message) => {
7559
+ const content = replaceFilesWithHandles(message.content, resolvePath);
7560
+ return content === message.content ? message : {
7561
+ ...message,
7562
+ content
7563
+ };
7564
+ });
7565
+ }
7566
+ function replaceImagesForTextModel(blocks) {
7567
+ let next;
7568
+ for (const [index, block] of blocks.entries()) {
7569
+ if (block.type === "image") {
7570
+ next ??= blocks.slice(0, index);
7571
+ next.push({
7572
+ type: "text",
7573
+ text: textOnlyImageText(block.attachment)
7574
+ });
7575
+ continue;
7576
+ }
7577
+ if (block.type === "tool-result") {
7578
+ const content = replaceImagesForTextModel(block.content);
7579
+ if (content !== block.content) {
7580
+ next ??= blocks.slice(0, index);
7581
+ next.push({
7582
+ ...block,
7583
+ content
7584
+ });
7585
+ continue;
7586
+ }
7587
+ }
7588
+ next?.push(block);
7589
+ }
7590
+ return next ?? blocks;
7591
+ }
7592
+ function projectImagesForTextModel(messages) {
7593
+ if (!messages.some((message) => contentHasImage(message.content))) return messages;
7594
+ return messages.map((message) => {
7595
+ const content = replaceImagesForTextModel(message.content);
7596
+ return content === message.content ? message : {
7597
+ ...message,
7598
+ content
7599
+ };
7600
+ });
7601
+ }
7602
+ var { version } = createRequire(import.meta.url)("../package.json");
7603
+ var __runInitializers = function(thisArg, initializers, value) {
7604
+ var useValue = arguments.length > 2;
7605
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
7606
+ return useValue ? value : void 0;
7607
+ };
7608
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
7609
+ function accept(f) {
7610
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
7611
+ return f;
7612
+ }
7613
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
7614
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
7615
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
7616
+ var _, done = false;
7617
+ for (var i = decorators.length - 1; i >= 0; i--) {
7618
+ var context = {};
7619
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
7620
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
7621
+ context.addInitializer = function(f) {
7622
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
7623
+ extraInitializers.push(accept(f || null));
7624
+ };
7625
+ var result = (0, decorators[i])(kind === "accessor" ? {
7626
+ get: descriptor.get,
7627
+ set: descriptor.set
7628
+ } : descriptor[key], context);
7629
+ if (kind === "accessor") {
7630
+ if (result === void 0) continue;
7631
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
7632
+ if (_ = accept(result.get)) descriptor.get = _;
7633
+ if (_ = accept(result.set)) descriptor.set = _;
7634
+ if (_ = accept(result.init)) initializers.unshift(_);
7635
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
7636
+ else descriptor[key] = _;
7637
+ }
7638
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
7639
+ done = true;
7640
+ };
7641
+ var LlmError = class extends HarnessError {
7642
+ /** Serializable facts retained beside this live Error. */
7643
+ failure;
7644
+ /**
7645
+ * @param message - non-empty human-readable failure summary.
7646
+ * @param code - non-empty stable provider-neutral machine code.
7647
+ * @param options - optional cause and validated serializable provider facts.
7648
+ */
7649
+ constructor(message, code, options) {
7650
+ if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
7651
+ if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
7652
+ if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
7653
+ if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
7654
+ if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
7655
+ super(message, code, options);
7656
+ this.name = "LlmError";
7657
+ this.failure = Object.freeze({
7658
+ message,
7659
+ code,
7660
+ ...options?.status === void 0 ? {} : { status: options.status },
7661
+ ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
7662
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId }
7663
+ });
7664
+ }
7665
+ };
7666
+ var LlmRuntime = (() => {
7667
+ let _classSuper = TypertRemoteService;
7668
+ let _instanceExtraInitializers = [];
7669
+ let _listProviders_decorators;
7670
+ let _listConfigurableProviders_decorators;
7671
+ let _remoteDiscoverModels_decorators;
7672
+ return class LlmRuntime extends _classSuper {
7673
+ static {
7674
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
7675
+ _listProviders_decorators = [Remote];
7676
+ _listConfigurableProviders_decorators = [Remote];
7677
+ _remoteDiscoverModels_decorators = [Remote("discoverModels")];
7678
+ __esDecorate(this, null, _listProviders_decorators, {
7679
+ kind: "method",
7680
+ name: "listProviders",
7681
+ static: false,
7682
+ private: false,
7683
+ access: {
7684
+ has: (obj) => "listProviders" in obj,
7685
+ get: (obj) => obj.listProviders
7686
+ },
7687
+ metadata: _metadata
7688
+ }, null, _instanceExtraInitializers);
7689
+ __esDecorate(this, null, _listConfigurableProviders_decorators, {
7690
+ kind: "method",
7691
+ name: "listConfigurableProviders",
7692
+ static: false,
7693
+ private: false,
7694
+ access: {
7695
+ has: (obj) => "listConfigurableProviders" in obj,
7696
+ get: (obj) => obj.listConfigurableProviders
7697
+ },
7698
+ metadata: _metadata
7699
+ }, null, _instanceExtraInitializers);
7700
+ __esDecorate(this, null, _remoteDiscoverModels_decorators, {
7701
+ kind: "method",
7702
+ name: "remoteDiscoverModels",
7703
+ static: false,
7704
+ private: false,
7705
+ access: {
7706
+ has: (obj) => "remoteDiscoverModels" in obj,
7707
+ get: (obj) => obj.remoteDiscoverModels
7708
+ },
7709
+ metadata: _metadata
7710
+ }, null, _instanceExtraInitializers);
7711
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
7712
+ enumerable: true,
7713
+ configurable: true,
7714
+ writable: true,
7715
+ value: _metadata
7716
+ });
7717
+ }
7718
+ adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
7719
+ directory = /* @__PURE__ */ new Map();
7720
+ discoveries = /* @__PURE__ */ new Map();
7721
+ constructor(ctx) {
7722
+ super(ctx, "llm");
7723
+ }
7724
+ /** Notify topology observers without letting one broken listener veto the commit. */
7725
+ emitAdaptersUpdated() {
7726
+ let invariantFailure;
7727
+ for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
7728
+ const returned = listener();
7729
+ if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
7730
+ this.warnAdaptersListenerFailure(error);
7731
+ });
7732
+ } catch (error) {
7733
+ if (error?.code === "INVARIANT") {
7734
+ invariantFailure ??= error;
7735
+ continue;
7736
+ }
7737
+ this.warnAdaptersListenerFailure(error);
7738
+ }
7739
+ if (invariantFailure !== void 0) throw invariantFailure;
7740
+ }
7741
+ /** Contained-listener diagnostic shared by the sync and async failure paths. */
7742
+ warnAdaptersListenerFailure(error) {
7743
+ this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
7744
+ this.ctx.logger.warn(error);
7745
+ }
7746
+ /**
7747
+ * Register an adapter for the given provider routes. Throws `LlmError` with code
7748
+ * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
7749
+ * Disposed with the fiber.
7750
+ * @param providers - every provider route this adapter should serve.
7751
+ * @param adapter - the adapter that streams calls for those providers.
7752
+ * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
7753
+ */
7754
+ registerAdapter(providers, adapter) {
7755
+ const owned = /* @__PURE__ */ new Set();
7756
+ let released = false;
7757
+ const dispose = this.ctx.effect(function* () {
7758
+ if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
7759
+ this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
7760
+ yield () => {
7761
+ released = true;
7762
+ for (const provider of owned) this.adapters.delete(provider);
7763
+ owned.clear();
7764
+ this.emitAdaptersUpdated();
7765
+ };
7766
+ }.bind(this), "llm.registerAdapter()");
7767
+ const handle = () => void dispose();
7768
+ handle.replace = (next) => {
7769
+ if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
7770
+ this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
7771
+ };
7772
+ return handle;
7773
+ }
7774
+ /**
7775
+ * Validate one candidate route set for `adapter`, treating routes this
7776
+ * registration already holds as available. Nothing is mutated: a rejected
7777
+ * candidate leaves the registry exactly as it was.
7778
+ */
7779
+ prepareRoutes(providers, adapter, owned) {
7780
+ const unique = /* @__PURE__ */ new Set();
7781
+ const registrations = [];
7782
+ for (const provider of providers) {
7783
+ if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
7784
+ if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
7785
+ const info = adapter.providerInfo(provider);
7786
+ if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
7787
+ unique.add(provider);
7788
+ const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
7789
+ registrations.push({
7790
+ adapter,
7791
+ provider: {
7792
+ id: info.id,
7793
+ name: info.name
7794
+ },
7795
+ retryPolicy
7796
+ });
7797
+ }
7798
+ return registrations;
7799
+ }
7800
+ /**
7801
+ * Swap this registration's routes for the prepared ones in one synchronous
7802
+ * section, so no observer can see the registry between the release and the
7803
+ * re-registration. The route set's one mutation point is also where
7804
+ * `llm/adapters-updated` is published, so a `replace` announces itself
7805
+ * exactly like a first registration.
7806
+ */
7807
+ commitRoutes(owned, registrations) {
7808
+ for (const provider of owned) this.adapters.delete(provider);
7809
+ owned.clear();
7810
+ for (const registration of registrations) {
7811
+ this.adapters.set(registration.provider.id, registration);
7812
+ owned.add(registration.provider.id);
7813
+ }
7814
+ this.emitAdaptersUpdated();
7815
+ }
7816
+ /**
7817
+ * Describe provider routes with a registered adapter.
7818
+ * @returns detached provider metadata in registration order.
7819
+ */
7820
+ listProviders() {
7821
+ return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
7822
+ }
7823
+ /**
7824
+ * Declare provider routes an adapter plugin can activate through
7825
+ * configuration. Registration is all-or-nothing: an empty list, invalid
7826
+ * entry, or a provider already declared by any registration throws
7827
+ * `LlmError` without registering the rest. Disposed with the fiber.
7828
+ * @param entries - every configurable provider this plugin owns.
7829
+ * @returns a handle that withdraws all of them, and can atomically replace them.
7830
+ */
7831
+ registerConfigurableProviders(entries) {
7832
+ let held = [];
7833
+ let disposed = false;
7834
+ const commit = (candidates) => {
7835
+ const detached = [];
7836
+ const own = new Set(held.map((entry) => entry.provider));
7837
+ for (const entry of candidates) {
7838
+ if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
7839
+ if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
7840
+ if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
7841
+ detached.push({
7842
+ ...entry,
7843
+ settingsPath: [...entry.settingsPath]
7844
+ });
7845
+ }
7846
+ for (const entry of held) this.directory.delete(entry.provider);
7847
+ for (const entry of detached) this.directory.set(entry.provider, entry);
7848
+ held = detached;
7849
+ this.emitAdaptersUpdated();
7850
+ };
7851
+ const dispose = this.ctx.effect(function* () {
7852
+ if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
7853
+ commit(entries);
7854
+ yield () => {
7855
+ disposed = true;
7856
+ for (const entry of held) this.directory.delete(entry.provider);
7857
+ held = [];
7858
+ this.emitAdaptersUpdated();
7859
+ };
7860
+ }.bind(this), "llm.registerConfigurableProviders()");
7861
+ const handle = () => void dispose();
7862
+ handle.replace = (next) => {
7863
+ if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
7864
+ commit(next);
7865
+ };
7866
+ return handle;
7867
+ }
7868
+ /**
7869
+ * List every declared configurable provider, registered or dormant.
7870
+ * @returns detached directory entries in declaration order.
7871
+ */
7872
+ listConfigurableProviders() {
7873
+ return [...this.directory.values()].map((entry) => ({
7874
+ ...entry,
7875
+ settingsPath: [...entry.settingsPath]
7876
+ }));
7877
+ }
7878
+ /**
7879
+ * Offer to interrogate provider endpoints on behalf of the settings
7880
+ * namespace this plugin owns. The namespace is the key because that is what
7881
+ * a configuration surface already holds from the configurable-provider
7882
+ * directory, and because a provider being *added* has no route to name yet.
7883
+ * Disposed with the fiber.
7884
+ * @param settingsNs - the namespace whose profiles this discovery serves.
7885
+ * @param discover - interrogates one endpoint and must honor the supplied signal.
7886
+ * @returns the disposer that withdraws the offer.
7887
+ */
7888
+ registerModelDiscovery(settingsNs, discover) {
7889
+ const dispose = this.ctx.effect(function* () {
7890
+ if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
7891
+ if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
7892
+ this.discoveries.set(settingsNs, discover);
7893
+ yield () => {
7894
+ this.discoveries.delete(settingsNs);
7895
+ };
7896
+ }.bind(this), "llm.registerModelDiscovery()");
7897
+ return () => void dispose();
7898
+ }
7899
+ /**
7900
+ * Interrogate one provider endpoint for the models it advertises. The
7901
+ * request describes a draft, not a stored route, so nothing here reads or
7902
+ * writes settings or credentials — the caller owns both, and the reply is
7903
+ * candidate metadata a surface may offer for adoption.
7904
+ * @param settingsNs - namespace whose registered discovery serves this draft.
7905
+ * @param request - the endpoint, protocol, and one-shot credential to use.
7906
+ * @param signal - caller cancellation.
7907
+ * @returns the advertised models, deduplicated in endpoint order.
7908
+ */
7909
+ async discoverModels(settingsNs, request, signal) {
7910
+ const discover = this.discoveries.get(settingsNs);
7911
+ if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
7912
+ if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
7913
+ const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
7914
+ const seen = /* @__PURE__ */ new Set();
7915
+ const models = [];
7916
+ for (const model of discovered) {
7917
+ if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
7918
+ seen.add(model.id);
7919
+ models.push({
7920
+ id: model.id,
7921
+ ...model.name === void 0 ? {} : { name: model.name },
7922
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
7923
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
7924
+ });
7925
+ }
7926
+ return models;
7927
+ }
7928
+ /**
7929
+ * Remote adapter for one draft provider interrogation.
7930
+ * @param settingsNs - namespace whose registered discovery serves this draft.
7931
+ * @param request - endpoint, protocol, and one-shot credential to use.
7932
+ * @param signal - caller cancellation supplied by the Remote carrier.
7933
+ * @returns advertised models in endpoint order.
7934
+ * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
7935
+ */
7936
+ async remoteDiscoverModels(settingsNs, request, signal) {
7937
+ try {
7938
+ return await this.discoverModels(settingsNs, request, signal);
7939
+ } catch (error) {
7940
+ throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
7941
+ settingsNs,
7942
+ ...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
7943
+ }, { cause: error });
7944
+ }
7945
+ }
7946
+ /**
7947
+ * Resolve the retry policy captured when one provider route was registered.
7948
+ * @param provider - registered provider route to inspect.
7949
+ * @returns the provider-owned policy, with normal defaults already resolved.
7950
+ */
7951
+ providerRetryPolicy(provider) {
7952
+ return this.registration(provider).retryPolicy;
7953
+ }
7954
+ /**
7955
+ * Resolve provider-side request-image pricing for one exact route, or
7956
+ * `undefined` when the provider is unregistered or declares none. Unknown
7957
+ * providers degrade to `undefined` rather than throwing because callers
7958
+ * price durable history whose route may no longer be mounted.
7959
+ * @param provider - provider route named by a request header.
7960
+ * @param model - exact model id named by the same header.
7961
+ * @returns the owning adapter's image pricing for the route, when declared.
7962
+ */
7963
+ imageRequestPricing(provider, model) {
7964
+ return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
7965
+ }
7966
+ /**
7967
+ * Resolve the exact text one durable file occurrence contributes to every
7968
+ * provider request in the current execution environment.
7969
+ * @param ref - durable verbatim file reference from model history.
7970
+ * @returns the same deterministic handle text used at adapter dispatch.
7971
+ */
7972
+ fileRequestText(ref) {
7973
+ return fileHandleText(ref, this.fileReadPath(ref));
7974
+ }
7975
+ /** Detach typed adapter-owned modality metadata. */
7976
+ detachedModalities(modalities) {
7977
+ return modalities === void 0 ? void 0 : [...modalities];
7978
+ }
7979
+ /**
7980
+ * Discover models advertised by one registered provider. Catalog membership
7981
+ * is advisory and never changes routing or request validation.
7982
+ * @param provider - registered provider route to inspect.
7983
+ * @returns detached model metadata in adapter-preferred order.
7984
+ */
7985
+ async listModels(provider) {
7986
+ const models = await this.registration(provider).adapter.listModels(provider);
7987
+ const seen = /* @__PURE__ */ new Set();
7988
+ return models.map((model) => {
7989
+ if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
7990
+ seen.add(model.id);
7991
+ const inputModalities = this.detachedModalities(model.inputModalities);
7992
+ return {
7993
+ provider: model.provider,
7994
+ id: model.id,
7995
+ name: model.name,
7996
+ ...model.description === void 0 ? {} : { description: model.description },
7997
+ ...inputModalities === void 0 ? {} : { inputModalities }
7998
+ };
7999
+ });
8000
+ }
8001
+ /**
8002
+ * Resolve and validate all metadata from the adapter that owns one exact
8003
+ * route. The result is detached from adapter-owned objects; catalog
8004
+ * membership remains advisory and does not control request routing.
8005
+ * @param provider - registered provider route to inspect.
8006
+ * @param model - exact model id passed to the adapter.
8007
+ * @param signal - optional cancellation for adapter-owned asynchronous lookup.
8008
+ * @returns exact model identity plus available context and reasoning metadata.
8009
+ */
8010
+ async resolveModelInfo(provider, model, signal) {
8011
+ return this.resolveModelInfoFor(this.registration(provider), model, signal);
8012
+ }
8013
+ async resolveModelInfoFor(registration, model, signal) {
8014
+ const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
8015
+ return this.normalizeModelInfo(registration, model, resolved);
8016
+ }
8017
+ /** Validate and detach one adapter-returned exact model result. */
8018
+ normalizeModelInfo(registration, model, resolved) {
8019
+ const provider = registration.provider.id;
8020
+ if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
8021
+ const context = resolved.context;
8022
+ if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
8023
+ const inputModalities = this.detachedModalities(resolved.inputModalities);
8024
+ const systemPromptUpdate = resolved.systemPromptUpdate;
8025
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
8026
+ const defaultMaxTokens = resolved.defaultMaxTokens;
8027
+ if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
8028
+ const info = {
8029
+ provider,
8030
+ id: model,
8031
+ name: resolved.name,
8032
+ ...resolved.description === void 0 ? {} : { description: resolved.description },
8033
+ ...inputModalities === void 0 ? {} : { inputModalities },
8034
+ ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
8035
+ ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
8036
+ ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
8037
+ };
8038
+ const reasoning = resolved.reasoning;
8039
+ if (reasoning === void 0) return info;
8040
+ if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
8041
+ const seen = /* @__PURE__ */ new Set();
8042
+ const efforts = reasoning.efforts.map((effort) => {
8043
+ if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
8044
+ seen.add(effort.id);
8045
+ return {
8046
+ id: effort.id,
8047
+ name: effort.name,
8048
+ ...effort.description === void 0 ? {} : { description: effort.description }
8049
+ };
8050
+ });
8051
+ if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
8052
+ return {
8053
+ ...info,
8054
+ reasoning: {
8055
+ efforts,
8056
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
8057
+ }
8058
+ };
8059
+ }
8060
+ /**
8061
+ * Validate a conversation call config against its exact model capability and
8062
+ * materialize adapter-configured defaults. Unsupported explicit efforts
8063
+ * reject before provider I/O; no clamping or aliasing is performed. This
8064
+ * standalone query does not bind a later dispatch; use {@link prepareCall}
8065
+ * when logging and streaming must share one adapter registration.
8066
+ * @param config - provider/model route and optional request controls.
8067
+ * @param signal - optional cancellation for adapter-owned capability lookup.
8068
+ * @returns a detached config only when a default must be materialized.
8069
+ */
8070
+ async resolveCallConfig(config, signal) {
8071
+ return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
8072
+ }
8073
+ async resolveCallFor(registration, config, signal) {
8074
+ const info = await this.resolveModelInfoFor(registration, config.model, signal);
8075
+ return this.resolveCallWithInfo(config, info);
8076
+ }
8077
+ /** Validate request controls against one already-bound exact model result. */
8078
+ resolveCallWithInfo(config, info) {
8079
+ const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
8080
+ ...config,
8081
+ maxTokens: info.defaultMaxTokens
8082
+ } : config;
8083
+ const reasoning = info.reasoning;
8084
+ const requested = defaulted.reasoningEffort;
8085
+ let resolvedConfig = defaulted;
8086
+ if (reasoning === void 0) {
8087
+ if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
8088
+ } else {
8089
+ const effective = requested ?? reasoning.defaultEffort;
8090
+ if (effective !== void 0) {
8091
+ if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
8092
+ if (requested !== effective) resolvedConfig = {
8093
+ ...defaulted,
8094
+ reasoningEffort: effective
8095
+ };
8096
+ }
8097
+ }
8098
+ return {
8099
+ config: resolvedConfig,
8100
+ ...info.context === void 0 ? {} : { context: info.context },
8101
+ modelInfo: info
8102
+ };
8103
+ }
8104
+ /**
8105
+ * Resolve one call under its current adapter registration. The returned
8106
+ * one-shot handle keeps that registration across header logging and dispatch,
8107
+ * so HMR cannot combine one adapter's capability result with another adapter.
8108
+ * @param config - provider/model route and optional request controls.
8109
+ * @param signal - optional cancellation for adapter-owned capability lookup.
8110
+ * @returns a prepared config and its registration-bound stream entry point.
8111
+ */
8112
+ async prepareCall(config, signal) {
8113
+ const registration = this.registration(config.provider);
8114
+ const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
8115
+ const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
8116
+ const resolved = this.resolveCallWithInfo(config, modelInfo);
8117
+ const resolvedConfig = deepFreeze(structuredClone(resolved.config));
8118
+ const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
8119
+ const adapterDefaults = deepFreeze({
8120
+ ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
8121
+ ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
8122
+ });
8123
+ let dispatched = false;
8124
+ return Object.freeze({
8125
+ config: resolvedConfig,
8126
+ retryPolicy: registration.retryPolicy,
8127
+ adapterDefaults,
8128
+ ...context === void 0 ? {} : { context },
8129
+ ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
8130
+ ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
8131
+ stream: (options) => {
8132
+ if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
8133
+ if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
8134
+ dispatched = true;
8135
+ return this.streamWithRegistration(options, {
8136
+ registration,
8137
+ config: resolvedConfig,
8138
+ modelInfo,
8139
+ dispatch: (options2) => adapterCall.stream(options2)
8140
+ });
8141
+ }
8142
+ });
8143
+ }
8144
+ registration(provider) {
8145
+ const registration = this.adapters.get(provider);
8146
+ if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
8147
+ return registration;
8148
+ }
8149
+ /** Remove replay state whose historical route is owned by another adapter. */
8150
+ forAdapter(options, adapter) {
8151
+ const messages = options.messages.map((message) => {
8152
+ const source = message.source;
8153
+ if (message.role !== "assistant" || source.kind !== "model" || source.replayState === void 0) return message;
8154
+ if (this.adapters.get(source.provider)?.adapter === adapter) return message;
8155
+ return freezeMessage({
8156
+ ...message,
8157
+ source: {
8158
+ kind: "model",
8159
+ provider: source.provider,
8160
+ model: source.model
8161
+ }
8162
+ });
8163
+ });
8164
+ if (messages.every((message, index) => message === options.messages[index])) return options;
8165
+ const filtered = {
8166
+ ...options,
8167
+ messages
8168
+ };
8169
+ return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
8170
+ }
8171
+ /**
8172
+ * Resolve the current execution-world read path of one durable file
8173
+ * reference through the mounted attachment and filesystem providers.
8174
+ */
8175
+ fileReadPath(ref) {
8176
+ let hostPath;
8177
+ try {
8178
+ hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
8179
+ } catch {
8180
+ return;
8181
+ }
8182
+ if (hostPath === void 0) return void 0;
8183
+ return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
8184
+ }
8185
+ /**
8186
+ * Final adapter boundary. Adapter selection, dispatch, iterator construction,
8187
+ * and iteration failures become one terminal failure chunk. Middleware and
8188
+ * downstream consumer failures remain thrown plugin or consumer errors.
8189
+ */
8190
+ async *adapterStream(options, prepared) {
8191
+ let iterator;
8192
+ try {
8193
+ const registration = prepared?.registration ?? this.registration(options.provider);
8194
+ const adapter = registration.adapter;
8195
+ let modelInfo;
8196
+ let resolvedConfig;
8197
+ let dispatch;
8198
+ if (prepared === void 0) {
8199
+ const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
8200
+ modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
8201
+ resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
8202
+ dispatch = (options2) => adapterCall.stream(options2);
8203
+ } else {
8204
+ modelInfo = prepared.modelInfo;
8205
+ resolvedConfig = prepared.config;
8206
+ dispatch = prepared.dispatch;
8207
+ }
8208
+ if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
8209
+ const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
8210
+ ...options,
8211
+ ...resolvedConfig
8212
+ }) : {
8213
+ ...options,
8214
+ ...resolvedConfig
8215
+ };
8216
+ let projectedMessages = resolvedOptions.messages;
8217
+ if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
8218
+ if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
8219
+ const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
8220
+ ...resolvedOptions,
8221
+ messages: projectedMessages
8222
+ }) : {
8223
+ ...resolvedOptions,
8224
+ messages: projectedMessages
8225
+ };
8226
+ iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
8227
+ } catch (error) {
8228
+ yield adapterFailureChunk(error, options.signal);
8229
+ return;
8230
+ }
8231
+ let completed = false;
8232
+ try {
8233
+ while (true) {
8234
+ let item;
8235
+ try {
8236
+ const next = await iterator.next();
8237
+ item = next.done ? { done: true } : {
8238
+ done: false,
8239
+ value: next.value
8240
+ };
8241
+ } catch (error) {
8242
+ completed = true;
8243
+ yield adapterFailureChunk(error, options.signal);
8244
+ return;
8245
+ }
8246
+ if (item.done) {
8247
+ completed = true;
8248
+ return;
8249
+ }
8250
+ yield item.value;
8251
+ }
8252
+ } finally {
8253
+ if (!completed) {
8254
+ const close = iterator.return?.bind(iterator);
8255
+ if (close) await close();
8256
+ }
8257
+ }
8258
+ }
8259
+ /**
8260
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
8261
+ * retained only when the same adapter instance owns its historical provider
8262
+ * and the target provider. Final adapter selection remains fixed through
8263
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
8264
+ * dispatch, and iteration failures become terminal `error` or `aborted`
8265
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
8266
+ * remain thrown.
8267
+ * @param options - the full request; `options.provider` selects the adapter.
8268
+ * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
8269
+ */
8270
+ stream(options) {
8271
+ return this.streamWithRegistration(options);
8272
+ }
8273
+ streamWithRegistration(options, prepared) {
8274
+ return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
8275
+ }
8276
+ };
8277
+ })();
8278
+ function adapterFailureChunk(error, signal) {
8279
+ const failure12 = normalizeLlmFailure(error);
8280
+ return {
8281
+ type: "finish",
8282
+ reason: signal?.aborted || failure12.code === "ABORTED" ? {
8283
+ kind: "aborted",
8284
+ failure: failure12
8285
+ } : {
8286
+ kind: "error",
8287
+ failure: failure12
8288
+ }
8289
+ };
8290
+ }
8291
+
8292
+ // node_modules/.pnpm/@deepseek-ai+dsh-web@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-llm@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2_/node_modules/@deepseek-ai/dsh-web/lib/index.js
8293
+ var WebError = class extends HarnessError {
8294
+ };
8295
+ var WebRuntime = class extends Service {
8296
+ /**
8297
+ * Provider selection config. Operational env overrides feed the SAME fields:
8298
+ * `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
8299
+ * `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
8300
+ */
8301
+ static Config = Schema.object({
8302
+ searchProvider: Schema.string(),
8303
+ fetchProvider: Schema.string()
8304
+ });
8305
+ searchProviders = /* @__PURE__ */ new Map();
8306
+ fetchProviders = /* @__PURE__ */ new Map();
8307
+ searchProviderId;
8308
+ fetchProviderId;
8309
+ constructor(ctx, config = {}) {
8310
+ super(ctx, "web");
8311
+ this.searchProviderId = config.searchProvider ?? process.env.DSH_WEB_SEARCH_PROVIDER;
8312
+ this.fetchProviderId = config.fetchProvider ?? process.env.DSH_WEB_FETCH_PROVIDER;
8313
+ }
8314
+ /**
8315
+ * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
8316
+ * if its id is already registered for search. Returns a disposer; disposed
8317
+ * with the calling fiber.
8318
+ * @param provider - the provider; its `id` is the registry key.
8319
+ * @returns the disposer that unregisters the provider.
8320
+ */
8321
+ registerSearchProvider(provider) {
8322
+ return this.registerProvider(this.searchProviders, provider);
8323
+ }
8324
+ /**
8325
+ * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
8326
+ * if its id is already registered for fetch. Returns a disposer; disposed
8327
+ * with the calling fiber.
8328
+ * @param provider - the provider; its `id` is the registry key.
8329
+ * @returns the disposer that unregisters the provider.
8330
+ */
8331
+ registerFetchProvider(provider) {
8332
+ return this.registerProvider(this.fetchProviders, provider);
8333
+ }
8334
+ registerProvider(store, provider) {
8335
+ if (store.has(provider.id)) throw new WebError(`a web provider with id "${provider.id}" is already registered`, "WEB_DUPLICATE_PROVIDER");
8336
+ const dispose = this.ctx.effect(function* () {
8337
+ store.set(provider.id, provider);
8338
+ yield () => store.delete(provider.id);
8339
+ }, "web.registerProvider()");
8340
+ return () => void dispose();
8341
+ }
8342
+ /**
8343
+ * Run one search through the selected provider. Resolves the provider at call
8344
+ * time with the selection rules above; throws {@link WebError} when the
8345
+ * capability cannot run. The seam enforces `request.maxResults` on the result:
8346
+ * if the provider over-returns, `sources[]` is truncated and `truncated` set.
8347
+ * @param request - the query and optional result limit.
8348
+ * @param signal - optional cancellation signal forwarded to the provider.
8349
+ * @returns the provider's results, capped to `request.maxResults`.
8350
+ */
8351
+ async search(request, signal) {
8352
+ return capSources(await resolveProvider({
8353
+ providers: this.searchProviders,
8354
+ ...this.searchProviderId !== void 0 ? { configuredId: this.searchProviderId } : {}
8355
+ }).search(request, signal), request.maxResults);
8356
+ }
8357
+ /**
8358
+ * Retrieve one URL through the selected provider. Resolves the provider at
8359
+ * call time with the selection rules above; throws {@link WebError} when the
8360
+ * capability cannot run. A non-2xx response is a result, not a throw.
8361
+ * @param request - the URL plus retrieval options.
8362
+ * @param signal - optional cancellation signal forwarded to the provider.
8363
+ * @returns the retrieval outcome; non-2xx responses resolve descriptively.
8364
+ */
8365
+ async fetch(request, signal) {
8366
+ return resolveProvider({
8367
+ providers: this.fetchProviders,
8368
+ ...this.fetchProviderId !== void 0 ? { configuredId: this.fetchProviderId } : {}
8369
+ }).fetch(request, signal);
8370
+ }
8371
+ };
8372
+ function resolveProvider(selection) {
8373
+ const { configuredId, providers } = selection;
8374
+ if (configuredId !== void 0) {
8375
+ const provider = providers.get(configuredId);
8376
+ if (!provider) throw new WebError(`configured web provider "${configuredId}" is not registered`, "WEB_PROVIDER_CONFIGURED_MISSING");
8377
+ if (!provider.available()) throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, "WEB_PROVIDER_CONFIGURED_UNAVAILABLE");
8378
+ return provider;
8379
+ }
8380
+ const usable = [...providers.values()].filter((provider) => provider.available());
8381
+ const [single] = usable;
8382
+ if (single === void 0) throw new WebError("no usable web provider is registered", "WEB_PROVIDER_UNAVAILABLE");
8383
+ if (usable.length > 1) throw new WebError(`multiple usable web providers are registered (${usable.map((provider) => provider.id).join(", ")}); configure one explicitly`, "WEB_PROVIDER_AMBIGUOUS");
8384
+ return single;
8385
+ }
8386
+ function capSources(result, maxResults) {
8387
+ if (maxResults === void 0 || result.sources.length <= maxResults) return result;
8388
+ return {
8389
+ ...result,
8390
+ sources: result.sources.slice(0, maxResults),
8391
+ truncated: true
8392
+ };
8393
+ }
8394
+
8395
+ // src/web-search.ts
8396
+ var HIWORK_SEARCH_PROVIDER_ID = "hiwork-managed-search";
8397
+ var ME_WEB_SEARCH_PATH = "/api/me/web-search";
8398
+ var WEB_SEARCH_TIMEOUT_MS = 12e3;
8399
+ var HiworkSearchProvider = class {
8400
+ constructor(deps) {
8401
+ this.deps = deps;
8402
+ }
8403
+ id = HIWORK_SEARCH_PROVIDER_ID;
8404
+ available() {
8405
+ return true;
8406
+ }
8407
+ async search(request, signal) {
8408
+ if (signal?.aborted) throw aborted();
8409
+ let sessionCookie;
8410
+ try {
8411
+ sessionCookie = await this.deps.resolveSession();
8412
+ } catch (error) {
8413
+ this.deps.log?.(`hiwork-core: \u8054\u7F51\u641C\u7D22\u4F1A\u8BDD\u89E3\u6790\u5931\u8D25\uFF08${errorName4(error)}\uFF09`);
8414
+ throw new WebError("\u8054\u7F51\u641C\u7D22\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", HIWORK_SEARCH_UNREACHABLE, { cause: error });
8415
+ }
8416
+ if (sessionCookie === void 0 || sessionCookie === "") {
8417
+ throw new WebError("\u767B\u5F55\u540E\u624D\u80FD\u4F7F\u7528\u8054\u7F51\u641C\u7D22", HIWORK_SEARCH_UNAUTHENTICATED);
8418
+ }
8419
+ const gateway = this.deps.gateway();
8420
+ if (gateway === void 0) {
8421
+ throw new WebError("\u8054\u7F51\u641C\u7D22\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", HIWORK_SEARCH_UNREACHABLE);
8422
+ }
8423
+ const outcome = await gateway.searchWeb(
8424
+ sessionCookie,
8425
+ // maxResults 是 tool-web 传进来的上限,透传给网关少拉数据(费用按次不按条,
8426
+ // 但少拉条数能压响应体)。seam 仍会在结果侧强制截断,这里只是配合。
8427
+ { query: request.query, ...request.maxResults !== void 0 ? { max_results: request.maxResults } : {} },
8428
+ signal
8429
+ );
8430
+ if (!outcome.ok) {
8431
+ throw outcomeToError(outcome);
8432
+ }
8433
+ const view = projectWebSearch(outcome.body);
8434
+ if (view === void 0) {
8435
+ this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 web-search \u54CD\u5E94\u4E0D\u7B26\u5408\u5951\u7EA6\uFF08results \u7F3A\u5931\u6216\u5F62\u72B6\u4E0D\u5BF9\uFF09");
8436
+ throw new WebError("\u8054\u7F51\u641C\u7D22\u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C", "WEB_PROVIDER_ERROR");
8437
+ }
8438
+ return view;
8439
+ }
8440
+ };
8441
+ var HIWORK_SEARCH_UNAUTHENTICATED = "HIWORK_SEARCH_UNAUTHENTICATED";
8442
+ var HIWORK_SEARCH_DISABLED = "HIWORK_SEARCH_DISABLED";
8443
+ var HIWORK_SEARCH_NOT_CONFIGURED = "HIWORK_SEARCH_NOT_CONFIGURED";
8444
+ var HIWORK_SEARCH_LIMIT = "HIWORK_SEARCH_LIMIT";
8445
+ var HIWORK_SEARCH_UNREACHABLE = "HIWORK_SEARCH_UNREACHABLE";
8446
+ var HIWORK_SEARCH_UPSTREAM = "HIWORK_SEARCH_UPSTREAM";
8447
+ function aborted() {
8448
+ return new WebError("\u8054\u7F51\u641C\u7D22\u5DF2\u53D6\u6D88", "WEB_ABORTED");
8449
+ }
8450
+ function outcomeToError(outcome) {
8451
+ switch (outcome.kind) {
8452
+ case "aborted":
8453
+ return aborted();
8454
+ case "network":
8455
+ return new WebError("\u8054\u7F51\u641C\u7D22\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", HIWORK_SEARCH_UNREACHABLE);
8456
+ case "invalid":
8457
+ return new WebError("\u8054\u7F51\u641C\u7D22\u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u7ED3\u679C", "WEB_PROVIDER_ERROR");
8458
+ case "http":
8459
+ return httpOutcomeToError(outcome.status, outcome.code);
8460
+ }
8461
+ }
8462
+ function httpOutcomeToError(status, code) {
8463
+ if (status === 401) return new WebError("\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", HIWORK_SEARCH_UNAUTHENTICATED);
8464
+ if (code === "SEARCH_DISABLED") return new WebError("\u7BA1\u7406\u5458\u8FD8\u6CA1\u5F00\u542F\u8054\u7F51\u641C\u7D22", HIWORK_SEARCH_DISABLED);
8465
+ if (code === "SEARCH_NOT_CONFIGURED") return new WebError("\u8054\u7F51\u641C\u7D22\u8FD8\u6CA1\u6709\u914D\u7F6E\u597D\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", HIWORK_SEARCH_NOT_CONFIGURED);
8466
+ if (status === 429 || code === "SEARCH_LIMIT_EXCEEDED") return new WebError("\u4ECA\u5929\u7684\u8054\u7F51\u641C\u7D22\u6B21\u6570\u5DF2\u7528\u5B8C\uFF0C\u660E\u5929\u518D\u6765", HIWORK_SEARCH_LIMIT);
8467
+ if (status === 502 || code === "SEARCH_UPSTREAM_ERROR") return new WebError("\u641C\u7D22\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", HIWORK_SEARCH_UPSTREAM);
8468
+ return new WebError("\u8054\u7F51\u641C\u7D22\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", "WEB_PROVIDER_ERROR");
8469
+ }
8470
+ function textOrUndefined8(value) {
8471
+ if (typeof value !== "string") return void 0;
8472
+ const text = value.trim();
8473
+ return text === "" ? void 0 : text;
8474
+ }
8475
+ function projectWebSearch(body) {
8476
+ const raw = body["results"];
8477
+ if (!Array.isArray(raw)) return void 0;
8478
+ const sources = [];
8479
+ for (const item of raw) {
8480
+ if (item === null || typeof item !== "object") continue;
8481
+ const record = item;
8482
+ const url = textOrUndefined8(record["url"]);
8483
+ if (url === void 0) continue;
8484
+ const title = textOrUndefined8(record["title"]);
8485
+ const snippet = textOrUndefined8(record["snippet"]) ?? textOrUndefined8(record["content"]);
8486
+ const publishedAt = textOrUndefined8(record["published_at"]);
8487
+ sources.push({
8488
+ url,
8489
+ ...title !== void 0 ? { title } : {},
8490
+ ...snippet !== void 0 ? { snippet } : {},
8491
+ ...publishedAt !== void 0 ? { publishedAt } : {}
8492
+ });
8493
+ }
8494
+ return { sources, truncated: false };
8495
+ }
8496
+ function errorName4(error) {
8497
+ return error instanceof Error ? error.name : typeof error;
8498
+ }
8499
+ function createWebSearchGateway(baseURL, opts = {}) {
8500
+ const root = baseURL.replace(/\/+$/, "");
8501
+ const timeoutMs = opts.timeoutMs ?? WEB_SEARCH_TIMEOUT_MS;
8502
+ const doFetch = opts.fetchImpl ?? fetch;
8503
+ return {
8504
+ async searchWeb(sessionCookie, body, signal) {
8505
+ const signals = [AbortSignal.timeout(timeoutMs)];
8506
+ if (signal !== void 0) signals.push(signal);
8507
+ const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
8508
+ let res;
8509
+ try {
8510
+ res = await doFetch(`${root}${ME_WEB_SEARCH_PATH}`, {
8511
+ method: "POST",
8512
+ headers: {
8513
+ "user-agent": `hiwork-core/${PKG_VERSION}`,
8514
+ cookie: sessionCookie,
8515
+ "content-type": "application/json"
8516
+ },
8517
+ body: JSON.stringify(body),
8518
+ signal: merged
8519
+ });
8520
+ } catch (error) {
8521
+ if (signal?.aborted) return { ok: false, kind: "aborted" };
8522
+ return { ok: false, kind: "network" };
8523
+ }
8524
+ if (!res.ok) {
8525
+ let code;
8526
+ try {
8527
+ const parsed2 = await res.json();
8528
+ if (typeof parsed2?.code === "string" && parsed2.code !== "") code = parsed2.code;
8529
+ } catch {
8530
+ }
8531
+ return { ok: false, kind: "http", status: res.status, ...code !== void 0 ? { code } : {} };
8532
+ }
8533
+ let parsed;
8534
+ try {
8535
+ parsed = await res.json();
8536
+ } catch {
8537
+ return { ok: false, kind: "invalid" };
8538
+ }
8539
+ if (parsed === null || typeof parsed !== "object") return { ok: false, kind: "invalid" };
8540
+ return { ok: true, body: parsed };
8541
+ }
8542
+ };
8543
+ }
8544
+ function registerManagedSearchProvider(web, deps) {
8545
+ return web.registerSearchProvider(new HiworkSearchProvider(deps));
8546
+ }
8547
+
6757
8548
  // src/index.ts
6758
8549
  var name = "hiwork-core";
6759
- var inject = ["settings", "credentials", "workspaceRegistry", "connection"];
8550
+ var inject = ["settings", "credentials", "workspaceRegistry", "connection", "web"];
6760
8551
  var NS = "ai-router";
6761
8552
  var PI_AI_NS = "llm-pi-ai";
6762
8553
  var lastErrorSchema = Schema.object({ code: Schema.string().required(), message: Schema.string().required() }).default(void 0);
@@ -6879,14 +8670,14 @@ function parseDeviceInfo(message) {
6879
8670
  const id = deviceId.value?.trim();
6880
8671
  if (id === void 0 || id === "") return void 0;
6881
8672
  const name2 = deviceName.value?.trim();
6882
- const version = appVersion.value?.trim();
8673
+ const version2 = appVersion.value?.trim();
6883
8674
  return {
6884
8675
  deviceId: id,
6885
8676
  ...name2 === void 0 || name2 === "" ? {} : { deviceName: name2 },
6886
- ...version === void 0 || version === "" ? {} : { appVersion: version }
8677
+ ...version2 === void 0 || version2 === "" ? {} : { appVersion: version2 }
6887
8678
  };
6888
8679
  }
6889
- function errorName3(error) {
8680
+ function errorName5(error) {
6890
8681
  return error instanceof Error ? error.name : typeof error;
6891
8682
  }
6892
8683
  function registerIpcMessageHandler(ports) {
@@ -6913,12 +8704,12 @@ function registerIpcMessageHandler(ports) {
6913
8704
  authInFlight = true;
6914
8705
  try {
6915
8706
  void Promise.resolve(ports.complete(input)).catch((error) => {
6916
- log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5904\u7406\u5F02\u5E38\uFF08${errorName3(error)}\uFF09`);
8707
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5904\u7406\u5F02\u5E38\uFF08${errorName5(error)}\uFF09`);
6917
8708
  }).finally(() => {
6918
8709
  authInFlight = false;
6919
8710
  });
6920
8711
  } catch (error) {
6921
- log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5206\u53D1\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
8712
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5206\u53D1\u5931\u8D25\uFF08${errorName5(error)}\uFF09`);
6922
8713
  authInFlight = false;
6923
8714
  }
6924
8715
  return;
@@ -6966,12 +8757,12 @@ async function projectLoginMode(opts) {
6966
8757
  }
6967
8758
  }
6968
8759
  } catch (error) {
6969
- opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C\u767B\u5F55\u65B9\u5F0F\u56DE\u9000 both\uFF08${errorName3(error)}\uFF09`);
8760
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C\u767B\u5F55\u65B9\u5F0F\u56DE\u9000 both\uFF08${errorName5(error)}\uFF09`);
6970
8761
  }
6971
8762
  try {
6972
8763
  await opts.state.patch({ set: { loginMode, passwordLogin } });
6973
8764
  } catch (error) {
6974
- opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u5199\u5165\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
8765
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u5199\u5165\u5931\u8D25\uFF08${errorName5(error)}\uFF09`);
6975
8766
  }
6976
8767
  return loginMode;
6977
8768
  }
@@ -7109,6 +8900,16 @@ function apply(ctx, config) {
7109
8900
  ctx.logger.warn(message);
7110
8901
  }
7111
8902
  }), "hiwork-core: daily rpc");
8903
+ ctx.effect(() => registerVoiceRpc({ connection, logger: ctx.logger }, {
8904
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
8905
+ gateway: () => {
8906
+ const baseURL = current().baseURL;
8907
+ return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
8908
+ },
8909
+ log: (message) => {
8910
+ ctx.logger.warn(message);
8911
+ }
8912
+ }), "hiwork-core: voice rpc");
7112
8913
  const managedMcpDeps = {
7113
8914
  resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
7114
8915
  gateway: () => {
@@ -7194,7 +8995,12 @@ function apply(ctx, config) {
7194
8995
  };
7195
8996
  }, "hiwork-core: workbench warmup");
7196
8997
  const pmsTodoDeps = {
7197
- credentials: ports.credentials,
8998
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
8999
+ gateway: () => {
9000
+ const baseURL = current().baseURL;
9001
+ return baseURL === void 0 || baseURL === "" ? void 0 : gatewayFor(baseURL);
9002
+ },
9003
+ resolveDeviceId: async () => (await resolveDeviceInfo(ports.credentials)).deviceId,
7198
9004
  pms: new PmsClient({ log: (message) => {
7199
9005
  ctx.logger.warn(message);
7200
9006
  } }),
@@ -7207,6 +9013,9 @@ function apply(ctx, config) {
7207
9013
  () => registerPmsTodoRpc({ connection, logger: ctx.logger }, pmsTodoDeps, pmsTodo),
7208
9014
  "hiwork-core: pms todo rpc"
7209
9015
  );
9016
+ void clearLegacyCredentials(ports.credentials, (message) => {
9017
+ ctx.logger.warn(message);
9018
+ });
7210
9019
  ctx.effect(() => provideOfficeCredentials(ctx, {
7211
9020
  resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
7212
9021
  gateway: () => {
@@ -7233,6 +9042,16 @@ function apply(ctx, config) {
7233
9042
  ctx.logger.warn(message);
7234
9043
  }
7235
9044
  }), "hiwork-core: memory config service");
9045
+ ctx.effect(() => provideKnowledgeConfig(ctx, {
9046
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
9047
+ gateway: () => {
9048
+ const baseURL = current().baseURL;
9049
+ return baseURL === void 0 || baseURL === "" ? void 0 : createKnowledgeConfigGateway(baseURL);
9050
+ },
9051
+ log: (message) => {
9052
+ ctx.logger.warn(message);
9053
+ }
9054
+ }), "hiwork-core: knowledge config service");
7236
9055
  ctx.effect(() => provideOfficeRelay(ctx, {
7237
9056
  resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
7238
9057
  gateway: () => {
@@ -7243,6 +9062,19 @@ function apply(ctx, config) {
7243
9062
  ctx.logger.warn(message);
7244
9063
  }
7245
9064
  }), "hiwork-core: office relay");
9065
+ const web = ctx.get("web");
9066
+ if (web !== void 0) {
9067
+ registerManagedSearchProvider(web, {
9068
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
9069
+ gateway: () => {
9070
+ const baseURL = current().baseURL;
9071
+ return baseURL === void 0 || baseURL === "" ? void 0 : createWebSearchGateway(baseURL);
9072
+ },
9073
+ log: (message) => {
9074
+ ctx.logger.warn(message);
9075
+ }
9076
+ });
9077
+ }
7246
9078
  ctx.effect(() => registerAppVersionRpc({ connection, logger: ctx.logger }, {
7247
9079
  appVersion: () => pushedAppVersion(),
7248
9080
  log: (message) => {