hiwork-core 0.0.38 → 0.0.40

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
@@ -8267,7 +8267,7 @@ defineMethod("transform", [
8267
8267
  ], ({ inner }, isInner) => inner.toString(isInner));
8268
8268
 
8269
8269
  // src/version.ts
8270
- var PKG_VERSION = false ? "dev" : "0.0.38";
8270
+ var PKG_VERSION = false ? "dev" : "0.0.40";
8271
8271
 
8272
8272
  // src/browser-login.ts
8273
8273
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -8278,6 +8278,7 @@ var API_KEY_REF = "AI_ROUTER_API_KEY";
8278
8278
  var SESSION_REF = "AI_ROUTER_SESSION";
8279
8279
  var PENDING_PASSWORD_REF = "AI_ROUTER_PENDING_PASSWORD";
8280
8280
  var REMEMBERED_PASSWORD_REF = "AI_ROUTER_REMEMBERED_PASSWORD";
8281
+ var REFRESH_TOKEN_REF = "AI_ROUTER_REFRESH_TOKEN";
8281
8282
  var PROVIDER_ROUTES = {
8282
8283
  "ai-router-chat": "openai-completions",
8283
8284
  "ai-router-anthropic": "anthropic-messages",
@@ -8435,6 +8436,7 @@ async function provisionSession(opts) {
8435
8436
  const displayName = opts.user.display_name?.trim() || void 0;
8436
8437
  const set2 = {
8437
8438
  ...opts.username === void 0 ? {} : { username: opts.username },
8439
+ ...opts.sessionExpiresAt === void 0 ? {} : { sessionExpiresAt: opts.sessionExpiresAt },
8438
8440
  ...displayName === void 0 ? {} : { displayName },
8439
8441
  lastLogin: {
8440
8442
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8600,12 +8602,15 @@ async function completeBrowserLogin(input, opts) {
8600
8602
  }
8601
8603
  const baseURL = opts.value.baseURL;
8602
8604
  if (baseURL === void 0 || baseURL === "") return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR, log);
8603
- const { sessionCookie, user } = await opts.gateway.exchangeCode({
8605
+ const { sessionCookie, user, sessionExpiresAt, refreshToken } = await opts.gateway.exchangeCode({
8604
8606
  code: input.code,
8605
8607
  codeVerifier: pending.codeVerifier,
8606
8608
  deviceId: pending.deviceId
8607
8609
  });
8608
8610
  await opts.credentials.set(SESSION_REF, sessionCookie);
8611
+ if (refreshToken === void 0) await opts.credentials.unset(REFRESH_TOKEN_REF).catch(() => {
8612
+ });
8613
+ else await opts.credentials.set(REFRESH_TOKEN_REF, refreshToken);
8609
8614
  const username = typeof user.username === "string" && user.username.trim() !== "" ? user.username.trim() : void 0;
8610
8615
  await provisionSession({
8611
8616
  gateway: opts.gateway,
@@ -8617,6 +8622,7 @@ async function completeBrowserLogin(input, opts) {
8617
8622
  sessionCookie,
8618
8623
  user,
8619
8624
  username,
8625
+ sessionExpiresAt,
8620
8626
  requestUnset: ["browserLoginRequest", "browserLogin"],
8621
8627
  ...opts.defaultModel === void 0 ? {} : { defaultModel: opts.defaultModel },
8622
8628
  ...opts.ensureWorkspace === void 0 ? {} : { ensureWorkspace: opts.ensureWorkspace }
@@ -8647,6 +8653,13 @@ function gatewayUserAgent(version2 = desktopAppVersion()) {
8647
8653
  }
8648
8654
 
8649
8655
  // src/ai-router-client.ts
8656
+ var sessionActivitySink;
8657
+ function setSessionActivitySink(sink) {
8658
+ sessionActivitySink = sink;
8659
+ }
8660
+ function clearSessionActivitySink(sink) {
8661
+ if (sessionActivitySink === sink) sessionActivitySink = void 0;
8662
+ }
8650
8663
  async function knowledgeErrorCodeFromBody(res) {
8651
8664
  try {
8652
8665
  const payload = await res.clone().json();
@@ -8658,6 +8671,7 @@ async function knowledgeErrorCodeFromBody(res) {
8658
8671
  var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
8659
8672
  var AUTH_TOKEN_PATH = "/api/auth/token";
8660
8673
  var AUTH_SESSIONS_PATH = "/api/auth/sessions";
8674
+ var AUTH_REFRESH_PATH = "/api/auth/refresh";
8661
8675
  var ME_ANALYTICS_SUMMARY_PATH = "/api/me/analytics/summary";
8662
8676
  var ME_ANALYTICS_TRENDS_PATH = "/api/me/analytics/trends";
8663
8677
  var ME_ANALYTICS_MODEL_TRENDS_PATH = "/api/me/analytics/model-trends";
@@ -8758,8 +8772,16 @@ var AiRouterClient = class {
8758
8772
  if (res.status === 401) throw new GatewayError("AUTH_FAILED", "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF", 401);
8759
8773
  if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
8760
8774
  if (sid === void 0) throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u767B\u5F55\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u4F1A\u8BDD Cookie", res.status);
8761
- const user = await res.json();
8762
- return { sessionCookie: `session_id=${sid}`, user };
8775
+ const { expires_at: rawExpiresAt, refresh_token: rawRefreshToken, ...user } = await res.json();
8776
+ const bodyExpiresAt = typeof rawExpiresAt === "string" && !Number.isNaN(Date.parse(rawExpiresAt)) ? rawExpiresAt : void 0;
8777
+ const sessionExpiresAt = bodyExpiresAt ?? parseSessionCookieExpiry(res.headers);
8778
+ const refreshToken = typeof rawRefreshToken === "string" && rawRefreshToken !== "" ? rawRefreshToken : void 0;
8779
+ return {
8780
+ sessionCookie: `session_id=${sid}`,
8781
+ user,
8782
+ ...sessionExpiresAt === void 0 ? {} : { sessionExpiresAt },
8783
+ ...refreshToken === void 0 ? {} : { refreshToken }
8784
+ };
8763
8785
  }
8764
8786
  /**
8765
8787
  * POST /api/auth/token:用一次性 code + PKCE verifier 换取会话。
@@ -8781,7 +8803,49 @@ var AiRouterClient = class {
8781
8803
  if (data === void 0 || data.session_id === void 0 || data.session_id === "") {
8782
8804
  throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u5151\u6362\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u4F1A\u8BDD\u6807\u8BC6", res.status);
8783
8805
  }
8784
- return { sessionCookie: `session_id=${data.session_id}`, user: data.user };
8806
+ const rawExpiresAt = typeof data.expires_at === "string" ? data.expires_at : void 0;
8807
+ const sessionExpiresAt = rawExpiresAt !== void 0 && !Number.isNaN(Date.parse(rawExpiresAt)) ? rawExpiresAt : void 0;
8808
+ const refreshToken = typeof data.refresh_token === "string" && data.refresh_token !== "" ? data.refresh_token : void 0;
8809
+ return {
8810
+ sessionCookie: `session_id=${data.session_id}`,
8811
+ user: data.user,
8812
+ ...sessionExpiresAt === void 0 ? {} : { sessionExpiresAt },
8813
+ ...refreshToken === void 0 ? {} : { refreshToken }
8814
+ };
8815
+ }
8816
+ /**
8817
+ * POST /api/auth/refresh:用 refresh grant 换一份新会话(M2 网关的静默重认证)。
8818
+ *
8819
+ * 拒绝语义必须分开,调用方据此决定「弃用 grant」还是「保留待重试」:
8820
+ * - 400 = grant 无效(过期 / 已被轮换 / 复用检测拒绝)→ `REFRESH_GRANT_INVALID`,本地作废;
8821
+ * - 404 = 老网关根本没有这条路由 → `REFRESH_GRANT_ABSENT`,本地作废且**静默降级**;
8822
+ * - 其余非 2xx(5xx / 反代错误页)与网络层异常 → `GATEWAY_UNREACHABLE`,grant 保留,
8823
+ * 下次 SESSION_EXPIRED 再试。
8824
+ *
8825
+ * 任何日志/异常信息都不得包含 refresh_token / session_id 明文。
8826
+ */
8827
+ async refreshSession(refreshToken) {
8828
+ const res = await this.requestOrUnreachable(AUTH_REFRESH_PATH, {
8829
+ method: "POST",
8830
+ body: { refresh_token: refreshToken }
8831
+ });
8832
+ if (res.status === 400) throw new GatewayError("REFRESH_GRANT_INVALID", "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", 400);
8833
+ if (res.status === 404) throw new GatewayError("REFRESH_GRANT_ABSENT", "\u7F51\u5173\u4E0D\u652F\u6301\u9759\u9ED8\u7EED\u671F", 404);
8834
+ if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
8835
+ const data = await readJson(res);
8836
+ const sid = typeof data?.session_id === "string" && data.session_id !== "" ? data.session_id : extractSessionId(res.headers);
8837
+ if (data === void 0 || data.user === void 0 || sid === void 0) {
8838
+ throw new GatewayError("GATEWAY_UNREACHABLE", "\u7F51\u5173\u7EED\u671F\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u4F1A\u8BDD\u6807\u8BC6", res.status);
8839
+ }
8840
+ const rawExpiresAt = typeof data.expires_at === "string" ? data.expires_at : void 0;
8841
+ const sessionExpiresAt = rawExpiresAt !== void 0 && !Number.isNaN(Date.parse(rawExpiresAt)) ? rawExpiresAt : void 0;
8842
+ const rotated = typeof data.refresh_token === "string" && data.refresh_token !== "" ? data.refresh_token : void 0;
8843
+ return {
8844
+ sessionCookie: `session_id=${sid}`,
8845
+ user: data.user,
8846
+ ...sessionExpiresAt === void 0 ? {} : { sessionExpiresAt },
8847
+ ...rotated === void 0 ? {} : { refreshToken: rotated }
8848
+ };
8785
8849
  }
8786
8850
  /** GET /api/auth/sessions:当前账号的登录设备列表。 */
8787
8851
  async listSessions(sessionCookie) {
@@ -9016,6 +9080,9 @@ var AiRouterClient = class {
9016
9080
  if (res.status === 503) {
9017
9081
  throw new GatewayError("MCP_ASSERTION_DISABLED", "\u672C\u90E8\u7F72\u672A\u542F\u7528 MCP \u65AD\u8A00\u7B7E\u53D1\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458", 503);
9018
9082
  }
9083
+ if (res.status === 429) {
9084
+ throw new GatewayError("MCP_ASSERTION_RATE_LIMITED", "\u65AD\u8A00\u7B7E\u53D1\u8FC7\u4E8E\u9891\u7E41\uFF0C\u7A0D\u540E\u81EA\u52A8\u91CD\u8BD5", 429);
9085
+ }
9019
9086
  if (!res.ok) throw new GatewayError("GATEWAY_UNREACHABLE", `\u7F51\u5173\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09`, res.status);
9020
9087
  return await parseMeJson(res);
9021
9088
  }
@@ -9389,7 +9456,19 @@ var AiRouterClient = class {
9389
9456
  opts.rawBody.byteOffset + opts.rawBody.byteLength
9390
9457
  );
9391
9458
  } else if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
9392
- return fetch(`${this.baseURL}${path}`, init);
9459
+ const res = await fetch(`${this.baseURL}${path}`, init);
9460
+ if (opts.sessionCookie !== void 0) {
9461
+ try {
9462
+ if (res.status === 401) sessionActivitySink?.({ kind: "unauthorized" });
9463
+ else if (res.ok) {
9464
+ const headerExpiresAt = res.headers.get("x-session-expires-at");
9465
+ const expiresAt = headerExpiresAt !== null && !Number.isNaN(Date.parse(headerExpiresAt)) ? headerExpiresAt : void 0;
9466
+ sessionActivitySink?.(expiresAt === void 0 ? { kind: "success" } : { kind: "success", expiresAt });
9467
+ }
9468
+ } catch {
9469
+ }
9470
+ }
9471
+ return res;
9393
9472
  }
9394
9473
  };
9395
9474
  function headerSafeValue(value) {
@@ -9427,6 +9506,27 @@ function extractSessionId(headers) {
9427
9506
  }
9428
9507
  return void 0;
9429
9508
  }
9509
+ function parseSessionCookieExpiry(headers, now = Date.now()) {
9510
+ const raws = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [headers.get("set-cookie")].filter((raw) => raw !== null);
9511
+ for (const raw of raws) {
9512
+ for (const section of raw.split(/,\s*(?=[!#$%&'*+\-.^_`|~0-9A-Za-z]+=)/)) {
9513
+ if (!/(?:^|;\s*)session_id=/.test(section)) continue;
9514
+ const expires = /;\s*Expires=([^;]+)/i.exec(section);
9515
+ const expiresRaw = expires?.[1];
9516
+ if (expiresRaw !== void 0) {
9517
+ const ms = Date.parse(expiresRaw);
9518
+ if (!Number.isNaN(ms)) return new Date(ms).toISOString();
9519
+ }
9520
+ const maxAge = /;\s*Max-Age=(-?\d+)/i.exec(section);
9521
+ if (maxAge !== null) {
9522
+ const seconds = Number(maxAge[1]);
9523
+ if (Number.isFinite(seconds)) return new Date(now + seconds * 1e3).toISOString();
9524
+ }
9525
+ return void 0;
9526
+ }
9527
+ }
9528
+ return void 0;
9529
+ }
9430
9530
  async function readJson(res) {
9431
9531
  try {
9432
9532
  return await res.json();
@@ -9577,6 +9677,58 @@ async function fetchClientPolicy(opts) {
9577
9677
  return void 0;
9578
9678
  }
9579
9679
  }
9680
+ var CLIENT_POLICY_POLL_INTERVAL_MS = 6 * 60 * 6e4;
9681
+ var CLIENT_POLICY_RETRY_DELAYS_MS = [6e4, 10 * 6e4, 60 * 6e4];
9682
+ function createClientPolicyPoller(deps) {
9683
+ let timer;
9684
+ let disposed = false;
9685
+ let running = false;
9686
+ let retryIndex = 0;
9687
+ const schedule = (delayMs) => {
9688
+ if (disposed) return;
9689
+ if (timer !== void 0) clearTimeout(timer);
9690
+ timer = setTimeout(() => {
9691
+ timer = void 0;
9692
+ void run();
9693
+ }, delayMs);
9694
+ timer.unref?.();
9695
+ };
9696
+ const run = async () => {
9697
+ if (disposed || running) return;
9698
+ running = true;
9699
+ let ok = false;
9700
+ try {
9701
+ ok = await deps.attempt();
9702
+ } catch {
9703
+ ok = false;
9704
+ } finally {
9705
+ running = false;
9706
+ }
9707
+ if (disposed) return;
9708
+ if (ok) {
9709
+ retryIndex = 0;
9710
+ schedule(CLIENT_POLICY_POLL_INTERVAL_MS);
9711
+ return;
9712
+ }
9713
+ const delay2 = CLIENT_POLICY_RETRY_DELAYS_MS[Math.min(retryIndex, CLIENT_POLICY_RETRY_DELAYS_MS.length - 1)];
9714
+ retryIndex += 1;
9715
+ deps.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C${Math.round(delay2 / 1e3)}s \u540E\u91CD\u8BD5`);
9716
+ schedule(delay2);
9717
+ };
9718
+ return {
9719
+ trigger: () => {
9720
+ if (disposed || running || timer !== void 0) return;
9721
+ void run();
9722
+ },
9723
+ dispose: () => {
9724
+ disposed = true;
9725
+ if (timer !== void 0) {
9726
+ clearTimeout(timer);
9727
+ timer = void 0;
9728
+ }
9729
+ }
9730
+ };
9731
+ }
9580
9732
 
9581
9733
  // src/default-model.ts
9582
9734
  var DEFAULT_MODEL_NS = "agent-default-model";
@@ -9783,14 +9935,18 @@ async function runLogin(opts) {
9783
9935
  const password = await opts.credentials.resolve(pending.passwordRef);
9784
9936
  if (password === void 0) throw new GatewayError("AUTH_FAILED", "\u767B\u5F55\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
9785
9937
  const device = await resolveDeviceInfo(opts.credentials);
9786
- const { sessionCookie, user } = await opts.gateway.login(pending.username, password.value, device);
9938
+ const { sessionCookie, user, sessionExpiresAt, refreshToken } = await opts.gateway.login(pending.username, password.value, device);
9787
9939
  await opts.credentials.set(SESSION_REF, sessionCookie);
9940
+ if (refreshToken === void 0) await opts.credentials.unset(REFRESH_TOKEN_REF).catch(() => {
9941
+ });
9942
+ else await opts.credentials.set(REFRESH_TOKEN_REF, refreshToken);
9788
9943
  await provisionSession({
9789
9944
  ...opts,
9790
9945
  baseURL,
9791
9946
  sessionCookie,
9792
9947
  user,
9793
9948
  username: pending.username,
9949
+ sessionExpiresAt,
9794
9950
  requestUnset: ["pendingLogin"],
9795
9951
  onApiKeyWritten: async () => {
9796
9952
  if (opts.value.rememberPassword === true) await opts.credentials.set(REMEMBERED_PASSWORD_REF, password.value);
@@ -9814,6 +9970,9 @@ async function runLogin(opts) {
9814
9970
  }
9815
9971
  }
9816
9972
  async function runRefresh(opts) {
9973
+ return refreshWith(opts, false);
9974
+ }
9975
+ async function refreshWith(opts, grantRetried) {
9817
9976
  const baseURL = opts.value.baseURL;
9818
9977
  if (baseURL === void 0) {
9819
9978
  await opts.state.patch({ unset: ["refreshRequest"] });
@@ -9850,6 +10009,28 @@ async function runRefresh(opts) {
9850
10009
  return null;
9851
10010
  } catch (error) {
9852
10011
  const refreshError = toLoginError(error, "\u65E0\u6CD5\u8FDE\u63A5\u7F51\u5173\uFF0C\u8BF7\u68C0\u67E5 baseURL \u6216\u7F51\u7EDC");
10012
+ if (refreshError.code === "SESSION_EXPIRED" && !grantRetried) {
10013
+ const grant = await opts.credentials.resolve(REFRESH_TOKEN_REF).catch(() => void 0);
10014
+ if (grant !== void 0 && grant.value !== "") {
10015
+ try {
10016
+ const renewed = await opts.gateway.refreshSession(grant.value);
10017
+ await opts.credentials.set(SESSION_REF, renewed.sessionCookie);
10018
+ if (renewed.refreshToken === void 0) await opts.credentials.unset(REFRESH_TOKEN_REF).catch(() => {
10019
+ });
10020
+ else await opts.credentials.set(REFRESH_TOKEN_REF, renewed.refreshToken);
10021
+ if (renewed.sessionExpiresAt !== void 0) {
10022
+ await opts.state.patch({ set: { sessionExpiresAt: renewed.sessionExpiresAt } });
10023
+ }
10024
+ return refreshWith(opts, true);
10025
+ } catch (grantError) {
10026
+ const code = grantError instanceof GatewayError ? grantError.code : void 0;
10027
+ if (code === "REFRESH_GRANT_INVALID" || code === "REFRESH_GRANT_ABSENT") {
10028
+ await opts.credentials.unset(REFRESH_TOKEN_REF).catch(() => {
10029
+ });
10030
+ }
10031
+ }
10032
+ }
10033
+ }
9853
10034
  if (refreshError.code === "SESSION_EXPIRED" && opts.value.rememberPassword === true && opts.value.username !== void 0) {
9854
10035
  const remembered = await opts.credentials.resolve(REMEMBERED_PASSWORD_REF).catch(() => void 0);
9855
10036
  if (remembered !== void 0) {
@@ -9874,6 +10055,8 @@ async function runLogout(opts) {
9874
10055
  });
9875
10056
  await opts.credentials.unset(SESSION_REF).catch(() => {
9876
10057
  });
10058
+ await opts.credentials.unset(REFRESH_TOKEN_REF).catch(() => {
10059
+ });
9877
10060
  await opts.credentials.unset(PENDING_PASSWORD_REF).catch(() => {
9878
10061
  });
9879
10062
  await opts.credentials.unset(REMEMBERED_PASSWORD_REF).catch(() => {
@@ -9892,7 +10075,9 @@ async function runLogout(opts) {
9892
10075
  "lastError",
9893
10076
  "keyId",
9894
10077
  "budgets",
9895
- "keys"
10078
+ "keys",
10079
+ // 会话寿命随会话一起清空:残留的旧 expiresAt 会让保活决策基于已失效的会话。
10080
+ "sessionExpiresAt"
9896
10081
  ]
9897
10082
  });
9898
10083
  }
@@ -18504,6 +18689,140 @@ async function readJson2(response) {
18504
18689
  }
18505
18690
  }
18506
18691
 
18692
+ // src/session-fanout.ts
18693
+ function buildSessionFanout(handlers) {
18694
+ const entries = [
18695
+ ["client policy", handlers.onPolicyRefresh],
18696
+ ["managed mcp", handlers.onManagedMcpRefresh],
18697
+ ["managed skills", handlers.onManagedSkillsRefresh],
18698
+ ["workbench", handlers.onWorkbenchRefresh]
18699
+ ];
18700
+ return () => {
18701
+ for (const [label, run] of entries) {
18702
+ try {
18703
+ run();
18704
+ } catch (error) {
18705
+ handlers.log?.(`hiwork-core: \u4F1A\u8BDD\u8054\u52A8\u5237\u65B0\u300C${label}\u300D\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
18706
+ }
18707
+ }
18708
+ };
18709
+ }
18710
+
18711
+ // src/session-supervisor.ts
18712
+ var SESSION_TTL_ASSUMPTION_MS = 24 * 60 * 6e4;
18713
+ var SESSION_CHECK_INTERVAL_MS = 60 * 6e4;
18714
+ var SESSION_RECOVERY_COOLDOWN_MS = 6e4;
18715
+ var SessionSupervisor = class {
18716
+ constructor(deps) {
18717
+ this.deps = deps;
18718
+ }
18719
+ timer;
18720
+ /** 上次「已知会话存活」的时刻;undefined = 本进程内还没确认过(重启后的首次检查照打)。 */
18721
+ lastAuthedSuccessAt;
18722
+ /**
18723
+ * 最近一次带会话成功响应里 `X-Session-Expires-At` 给出的**内存**寿命估计(原样时刻串)。
18724
+ * 只活在内存里——热路径绝不写 settings(Review Focus 5),只有心跳成功才落盘。
18725
+ */
18726
+ lastHeaderExpiresAt;
18727
+ /** 恢复请求是否在途(单飞去抖)。 */
18728
+ recoveryInFlight = false;
18729
+ /** 上次触发恢复的时刻(冷却窗口基准)。 */
18730
+ lastRecoveryAt;
18731
+ /** 开始兜底检查;重复调用不叠加定时器。 */
18732
+ start() {
18733
+ if (this.timer !== void 0) return;
18734
+ this.timer = setInterval(() => {
18735
+ void this.wake();
18736
+ }, SESSION_CHECK_INTERVAL_MS);
18737
+ this.timer.unref?.();
18738
+ }
18739
+ /** 停止兜底检查(调用后零活动)。 */
18740
+ stop() {
18741
+ if (this.timer === void 0) return;
18742
+ clearInterval(this.timer);
18743
+ this.timer = void 0;
18744
+ }
18745
+ /**
18746
+ * 带会话的请求成功 → 记活动时间(由 ai-router-client 的活动汇聚驱动)。
18747
+ * `expiresAt` 为响应头 `X-Session-Expires-At` 给出的会话寿命:**只更新内存估计**,
18748
+ * 绝不在此写 settings——热路径上每个成功请求都写盘会打爆设置面(Review Focus 5)。
18749
+ * 不可解析的值直接丢弃(当作没给),不覆盖既有估计。
18750
+ */
18751
+ noteAuthedSuccess(expiresAt) {
18752
+ this.lastAuthedSuccessAt = this.now();
18753
+ if (expiresAt !== void 0 && !Number.isNaN(Date.parse(expiresAt))) this.lastHeaderExpiresAt = expiresAt;
18754
+ }
18755
+ /**
18756
+ * 带会话的请求收到 401 → 去抖后触发一次会话恢复。
18757
+ * 去抖有两层:`recoveryInFlight`(单飞,覆盖「恢复请求还没落地,同刻又来了 5 条 401」)
18758
+ * 与 `SESSION_RECOVERY_COOLDOWN_MS` 冷却(覆盖恢复完成后短时间内的余波 401)。
18759
+ * 恢复失败只记日志——apply 后写入面需要几秒才就绪,那一侧的失败不该惊动宿主。
18760
+ */
18761
+ reportAuthed401(source) {
18762
+ if (this.recoveryInFlight) return;
18763
+ const now = this.now();
18764
+ if (this.lastRecoveryAt !== void 0 && now - this.lastRecoveryAt < SESSION_RECOVERY_COOLDOWN_MS) return;
18765
+ this.recoveryInFlight = true;
18766
+ this.lastRecoveryAt = now;
18767
+ void this.deps.requestRecovery().catch(() => {
18768
+ this.log(`hiwork-core: \u4F1A\u8BDD\u6062\u590D\u89E6\u53D1\u5931\u8D25\uFF08\u6765\u6E90 ${source}\uFF09\uFF0C\u7B49\u5F85\u4E0B\u4E2A 401 \u6216\u5FC3\u8DF3\u91CD\u8BD5`);
18769
+ }).finally(() => {
18770
+ this.recoveryInFlight = false;
18771
+ });
18772
+ }
18773
+ now() {
18774
+ return this.deps.now?.() ?? Date.now();
18775
+ }
18776
+ log(message) {
18777
+ this.deps.log?.(message);
18778
+ }
18779
+ /**
18780
+ * 一次兜底检查。判定顺序(早退即不发请求):
18781
+ * 无会话 / 无 baseURL → 返回;距上次已认证成功不足 TTL/2 → 返回;
18782
+ * 已知剩余寿命 > TTL/2 → 返回;否则 ping。
18783
+ * 寿命未知也 ping——重启后的首次检查即校验会话是否还活着。
18784
+ * 「已知剩余寿命」取 settings 值与内存估计(响应头)的**较大者**:头值只会更晚更准,
18785
+ * 用较大者才不会因为 settings 里的旧估算偏早而白打心跳。
18786
+ */
18787
+ async wake() {
18788
+ try {
18789
+ const session = await this.deps.getSession();
18790
+ if (session === void 0 || session === "") return;
18791
+ if (this.deps.getBaseURL() === void 0) return;
18792
+ const now = this.now();
18793
+ if (this.lastAuthedSuccessAt !== void 0 && now - this.lastAuthedSuccessAt < SESSION_TTL_ASSUMPTION_MS / 2) return;
18794
+ const knownExpiresAtMs = this.knownExpiresAtMs();
18795
+ if (knownExpiresAtMs !== void 0 && knownExpiresAtMs - now > SESSION_TTL_ASSUMPTION_MS / 2) return;
18796
+ let pinged;
18797
+ try {
18798
+ pinged = await this.deps.ping(session);
18799
+ } catch (error) {
18800
+ const expired = error instanceof GatewayError && error.code === "SESSION_EXPIRED";
18801
+ this.log(expired ? "hiwork-core: \u4F1A\u8BDD\u5FC3\u8DF3\u53D1\u73B0\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u7B49\u5F85 401 \u6C47\u805A\u89E6\u53D1\u6062\u590D" : "hiwork-core: \u4F1A\u8BDD\u5FC3\u8DF3\u8BF7\u6C42\u5931\u8D25\uFF0C\u5C06\u5728\u4E0B\u4E2A\u5468\u671F\u91CD\u8BD5");
18802
+ return;
18803
+ }
18804
+ const pingedExpiresAt = pinged?.expiresAt;
18805
+ const pingedMs = pingedExpiresAt === void 0 ? Number.NaN : Date.parse(pingedExpiresAt);
18806
+ const nextExpiresAt = pingedExpiresAt === void 0 || Number.isNaN(pingedMs) ? new Date(this.now() + SESSION_TTL_ASSUMPTION_MS).toISOString() : pingedExpiresAt;
18807
+ try {
18808
+ await this.deps.patchSessionExpiresAt(nextExpiresAt);
18809
+ } catch {
18810
+ this.log("hiwork-core: \u4F1A\u8BDD\u5FC3\u8DF3\u6210\u529F\u4F46\u5BFF\u547D\u56DE\u5199\u5931\u8D25\uFF0C\u5C06\u5728\u4E0B\u4E2A\u5468\u671F\u91CD\u8BD5");
18811
+ }
18812
+ } catch {
18813
+ this.log("hiwork-core: \u4F1A\u8BDD\u5FC3\u8DF3\u68C0\u67E5\u5931\u8D25\uFF0C\u5C06\u5728\u4E0B\u4E2A\u5468\u671F\u91CD\u8BD5");
18814
+ }
18815
+ }
18816
+ /**
18817
+ * 已知的会话到期时刻(epoch ms):settings 值与内存头值里**有效且较晚**的那个;
18818
+ * 两者都没有(或都不可解析)返回 undefined = 寿命未知。
18819
+ */
18820
+ knownExpiresAtMs() {
18821
+ const values = [this.deps.getSessionExpiresAt(), this.lastHeaderExpiresAt].map((iso) => iso === void 0 ? Number.NaN : Date.parse(iso)).filter((ms) => !Number.isNaN(ms));
18822
+ return values.length === 0 ? void 0 : Math.max(...values);
18823
+ }
18824
+ };
18825
+
18507
18826
  // src/pms-todo-bridge.ts
18508
18827
  var PMS_TODO_RPC_CHANNEL = "/hiwork-pms-todo";
18509
18828
  var PMS_TODO_RPC_ENDPOINTS = [
@@ -19168,6 +19487,34 @@ function registerIdentityContext(systemPrompt, deps) {
19168
19487
  };
19169
19488
  }
19170
19489
 
19490
+ // src/ttl-cache.ts
19491
+ var TtlCache = class {
19492
+ ttlMs;
19493
+ entry;
19494
+ constructor(opts) {
19495
+ this.ttlMs = opts.ttlMs;
19496
+ }
19497
+ /** 命中返回缓存值;会话键不同或已到期返回 undefined。`now` 可注入(缺省 Date.now)。 */
19498
+ get(sessionKey, now = Date.now()) {
19499
+ const hit = this.entry;
19500
+ if (hit === void 0 || hit.sessionKey !== sessionKey) return void 0;
19501
+ if (now - hit.at >= this.ttlMs) return void 0;
19502
+ return hit.value;
19503
+ }
19504
+ /** 写入/覆盖缓存(新会话键会顶掉旧会话那条)。 */
19505
+ put(sessionKey, value, now = Date.now()) {
19506
+ this.entry = { sessionKey, value, at: now };
19507
+ }
19508
+ /** 清掉指定会话键的条目(不影响别的键,虽然只会存在一条);不传参数则清空。 */
19509
+ invalidate(sessionKey) {
19510
+ if (sessionKey === void 0) {
19511
+ this.entry = void 0;
19512
+ return;
19513
+ }
19514
+ if (this.entry?.sessionKey === sessionKey) this.entry = void 0;
19515
+ }
19516
+ };
19517
+
19171
19518
  // src/knowledge-config.ts
19172
19519
  var HIWORK_KNOWLEDGE_SERVICE = "hiworkKnowledge";
19173
19520
  var ME_KNOWLEDGE_CONFIG_PATH = "/api/me/knowledge-config";
@@ -19256,8 +19603,9 @@ var KnowledgeConfigProvider = class {
19256
19603
  constructor(deps) {
19257
19604
  this.deps = deps;
19258
19605
  }
19259
- cached;
19260
- tokenCached;
19606
+ configCache = new TtlCache({ ttlMs: KNOWLEDGE_CONFIG_TTL_MS });
19607
+ // 令牌缓存比配置缓存短 30s(KNOWLEDGE_TOKEN_CACHE_TTL_MS 已扣掉余量,见其注释)。
19608
+ tokenCache = new TtlCache({ ttlMs: KNOWLEDGE_TOKEN_CACHE_TTL_MS });
19261
19609
  async token(opts) {
19262
19610
  const force = opts?.force === true;
19263
19611
  let sessionCookie;
@@ -19268,9 +19616,9 @@ var KnowledgeConfigProvider = class {
19268
19616
  return failure12("error");
19269
19617
  }
19270
19618
  if (!sessionCookie) return failure12("unauthenticated");
19271
- const hit = this.tokenCached;
19272
- if (!force && hit?.sessionCookie === sessionCookie && this.now() - hit.at < KNOWLEDGE_TOKEN_CACHE_TTL_MS) {
19273
- return hit.view;
19619
+ if (!force) {
19620
+ const hit = this.tokenCache.get(sessionCookie, this.now());
19621
+ if (hit !== void 0) return hit;
19274
19622
  }
19275
19623
  const gateway = this.deps.gateway();
19276
19624
  if (!gateway) return failure12("unreachable");
@@ -19295,7 +19643,7 @@ var KnowledgeConfigProvider = class {
19295
19643
  return id ? [{ id, name: textOrUndefined7(obj["name"]) ?? "" }] : [];
19296
19644
  }).filter((item) => knowledgeBaseIds.includes(item.id)) : [];
19297
19645
  const view = { ok: true, token, audience, knowledgeBaseIds, knowledgeBases };
19298
- this.tokenCached = { sessionCookie, view, at: this.now() };
19646
+ this.tokenCache.put(sessionCookie, view, this.now());
19299
19647
  return view;
19300
19648
  } catch (error) {
19301
19649
  const mapped = reasonOfErrorCode(error);
@@ -19315,10 +19663,8 @@ var KnowledgeConfigProvider = class {
19315
19663
  }
19316
19664
  if (sessionCookie === void 0 || sessionCookie === "") return failure12("unauthenticated");
19317
19665
  if (!force) {
19318
- const hit = this.cached;
19319
- if (hit !== void 0 && hit.sessionCookie === sessionCookie && this.now() - hit.at < KNOWLEDGE_CONFIG_TTL_MS) {
19320
- return hit.view;
19321
- }
19666
+ const hit = this.configCache.get(sessionCookie, this.now());
19667
+ if (hit !== void 0) return hit;
19322
19668
  }
19323
19669
  const gateway = this.deps.gateway();
19324
19670
  if (gateway === void 0) return failure12("unreachable");
@@ -19341,7 +19687,7 @@ var KnowledgeConfigProvider = class {
19341
19687
  this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 knowledge-config \u4E0D\u5B8C\u6574\uFF08\u7F3A base_url / api_key \u4E4B\u4E00\uFF09");
19342
19688
  return failure12("error");
19343
19689
  }
19344
- this.cached = { sessionCookie, view, at: this.now() };
19690
+ this.configCache.put(sessionCookie, view, this.now());
19345
19691
  return view;
19346
19692
  }
19347
19693
  now() {
@@ -19465,7 +19811,7 @@ var MemoryConfigProvider = class {
19465
19811
  constructor(deps) {
19466
19812
  this.deps = deps;
19467
19813
  }
19468
- cached;
19814
+ cache = new TtlCache({ ttlMs: MEMORY_CONFIG_TTL_MS });
19469
19815
  async config(opts) {
19470
19816
  const force = opts?.force === true;
19471
19817
  let sessionCookie;
@@ -19477,10 +19823,8 @@ var MemoryConfigProvider = class {
19477
19823
  }
19478
19824
  if (sessionCookie === void 0 || sessionCookie === "") return failure13("unauthenticated");
19479
19825
  if (!force) {
19480
- const hit = this.cached;
19481
- if (hit !== void 0 && hit.sessionCookie === sessionCookie && this.now() - hit.at < MEMORY_CONFIG_TTL_MS) {
19482
- return hit.view;
19483
- }
19826
+ const hit = this.cache.get(sessionCookie, this.now());
19827
+ if (hit !== void 0) return hit;
19484
19828
  }
19485
19829
  const gateway = this.deps.gateway();
19486
19830
  if (gateway === void 0) return failure13("unreachable", MESSAGE_NO_GATEWAY);
@@ -19503,7 +19847,7 @@ var MemoryConfigProvider = class {
19503
19847
  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");
19504
19848
  return failure13("error");
19505
19849
  }
19506
- this.cached = { sessionCookie, view, at: this.now() };
19850
+ this.cache.put(sessionCookie, view, this.now());
19507
19851
  return view;
19508
19852
  }
19509
19853
  now() {
@@ -19969,6 +20313,7 @@ var Config = Schema.object({
19969
20313
  keyId: Schema.string().volatile(),
19970
20314
  username: Schema.string().volatile(),
19971
20315
  displayName: Schema.string().volatile(),
20316
+ sessionExpiresAt: Schema.string().volatile(),
19972
20317
  lastLogin: Schema.object({ at: Schema.string().required(), modelCount: Schema.number().required(), gatewayName: Schema.string().required() }).default(void 0).volatile(),
19973
20318
  lastError: lastErrorSchema.volatile(),
19974
20319
  pendingLogin: Schema.object({ username: Schema.string().required(), passwordRef: Schema.string().required() }).default(void 0).volatile(),
@@ -20323,22 +20668,60 @@ function apply(ctx, config) {
20323
20668
  }
20324
20669
  };
20325
20670
  let inFlight;
20326
- let policyRequested = false;
20327
- const requestClientPolicy = () => {
20328
- if (policyRequested) return;
20329
- const baseURL = current().baseURL;
20330
- if (baseURL === void 0 || baseURL === "") return;
20331
- policyRequested = true;
20332
- void projectLoginMode({
20333
- baseURL,
20334
- credentials: ports.credentials,
20335
- state: ports.state,
20336
- log: (message) => {
20337
- ctx.logger.warn(message);
20338
- }
20339
- }).catch(() => {
20340
- });
20671
+ const sessionSupervisor = new SessionSupervisor({
20672
+ getSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
20673
+ getSessionExpiresAt: () => current().sessionExpiresAt,
20674
+ getBaseURL: () => current().baseURL,
20675
+ ping: async (sessionCookie) => {
20676
+ const baseURL = current().baseURL;
20677
+ if (baseURL === void 0 || baseURL === "") throw new Error("baseURL \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u4F1A\u8BDD\u5FC3\u8DF3");
20678
+ const sessions = await gatewayFor(baseURL).listSessions(sessionCookie);
20679
+ const expiresAt = sessions.find((item) => item.current === true && item.expires_at !== void 0)?.expires_at;
20680
+ return expiresAt === void 0 ? {} : { expiresAt };
20681
+ },
20682
+ patchSessionExpiresAt: (iso) => ports.state.patch({ set: { sessionExpiresAt: iso } }),
20683
+ requestRecovery: () => ports.state.patch({ set: { refreshRequest: { at: (/* @__PURE__ */ new Date()).toISOString() } } }),
20684
+ log: (message) => {
20685
+ ctx.logger.warn(message);
20686
+ }
20687
+ });
20688
+ const sessionActivitySink2 = (event) => {
20689
+ if (event.kind === "success") sessionSupervisor.noteAuthedSuccess(event.expiresAt);
20690
+ else sessionSupervisor.reportAuthed401("gateway");
20341
20691
  };
20692
+ ctx.effect(() => {
20693
+ setSessionActivitySink(sessionActivitySink2);
20694
+ sessionSupervisor.start();
20695
+ return () => {
20696
+ sessionSupervisor.stop();
20697
+ clearSessionActivitySink(sessionActivitySink2);
20698
+ };
20699
+ }, "hiwork-core: session supervisor");
20700
+ const policyPoller = createClientPolicyPoller({
20701
+ attempt: async () => {
20702
+ const baseURL = current().baseURL;
20703
+ if (baseURL === void 0 || baseURL === "") return false;
20704
+ let fetched = false;
20705
+ await projectLoginMode({
20706
+ baseURL,
20707
+ credentials: ports.credentials,
20708
+ state: ports.state,
20709
+ fetchPolicy: async (input) => {
20710
+ const policy = await fetchClientPolicy(input);
20711
+ fetched = policy !== void 0;
20712
+ return policy;
20713
+ },
20714
+ log: (message) => {
20715
+ ctx.logger.warn(message);
20716
+ }
20717
+ });
20718
+ return fetched;
20719
+ },
20720
+ log: (message) => {
20721
+ ctx.logger.warn(message);
20722
+ }
20723
+ });
20724
+ let notifySessionProvisioned;
20342
20725
  const dispatchSectionChange = () => {
20343
20726
  const value = current();
20344
20727
  const requested = value.pendingLogin ?? value.browserLoginRequest ?? value.refreshRequest ?? value.logoutRequest;
@@ -20355,6 +20738,7 @@ function apply(ctx, config) {
20355
20738
  ctx.logger.warn(message);
20356
20739
  }
20357
20740
  });
20741
+ if (value.pendingLogin !== void 0 || value.refreshRequest !== void 0) notifySessionProvisioned?.();
20358
20742
  } catch (error) {
20359
20743
  ctx.logger.error("hiwork-core: ai-router section change failed", error);
20360
20744
  if (isHmrNestedWriteError(error)) {
@@ -20377,7 +20761,7 @@ function apply(ctx, config) {
20377
20761
  };
20378
20762
  ctx.effect(() => {
20379
20763
  const consume = () => {
20380
- requestClientPolicy();
20764
+ policyPoller.trigger();
20381
20765
  dispatchSectionChange();
20382
20766
  };
20383
20767
  consume();
@@ -20385,6 +20769,7 @@ function apply(ctx, config) {
20385
20769
  timer.unref?.();
20386
20770
  return () => {
20387
20771
  clearInterval(timer);
20772
+ policyPoller.dispose();
20388
20773
  };
20389
20774
  }, "hiwork-core: login request poll");
20390
20775
  void ctx.root.loader.await().then(() => importLegacyMenuSection({
@@ -20394,7 +20779,7 @@ function apply(ctx, config) {
20394
20779
  })).catch((error) => {
20395
20780
  ctx.logger.warn(`hiwork-core: \u65E7\u7248\u83DC\u5355\u504F\u597D\u8FC1\u79FB\u672A\u6267\u884C\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
20396
20781
  });
20397
- requestClientPolicy();
20782
+ policyPoller.trigger();
20398
20783
  void (async () => {
20399
20784
  for (const delay2 of STALE_BROWSER_LOGIN_RECONCILE_DELAYS_MS) {
20400
20785
  if (delay2 > 0) await new Promise((resolve3) => {
@@ -20599,6 +20984,27 @@ function apply(ctx, config) {
20599
20984
  clearInterval(timer);
20600
20985
  };
20601
20986
  }, "hiwork-core: workbench warmup");
20987
+ notifySessionProvisioned = buildSessionFanout({
20988
+ onPolicyRefresh: () => policyPoller.trigger(),
20989
+ onManagedMcpRefresh: () => {
20990
+ void managedMcp.load().catch((error) => {
20991
+ ctx.logger.warn(`hiwork-core: \u4F1A\u8BDD\u4F9B\u7ED9\u540E\u5237\u65B0\u53D7\u7BA1 MCP \u6E05\u5355\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
20992
+ });
20993
+ },
20994
+ onManagedSkillsRefresh: () => {
20995
+ void managedSkills.load().catch((error) => {
20996
+ ctx.logger.warn(`hiwork-core: \u4F1A\u8BDD\u4F9B\u7ED9\u540E\u5237\u65B0\u53D7\u7BA1\u6280\u80FD\u6E05\u5355\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
20997
+ });
20998
+ },
20999
+ onWorkbenchRefresh: () => {
21000
+ void workbench.load().catch((error) => {
21001
+ ctx.logger.warn(`hiwork-core: \u4F1A\u8BDD\u4F9B\u7ED9\u540E\u5237\u65B0\u5DE5\u4F5C\u53F0\u6E05\u5355\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
21002
+ });
21003
+ },
21004
+ log: (message) => {
21005
+ ctx.logger.warn(message);
21006
+ }
21007
+ });
20602
21008
  const pmsTodoDeps = {
20603
21009
  resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
20604
21010
  gateway: () => {
@@ -20709,9 +21115,9 @@ function apply(ctx, config) {
20709
21115
  }), "hiwork-core: plugin updates rpc");
20710
21116
  ctx.effect(() => registerIpcMessageHandler({
20711
21117
  source: process,
20712
- complete: (input) => {
21118
+ complete: async (input) => {
20713
21119
  const value = current();
20714
- return completeBrowserLogin(input, {
21120
+ const result = await completeBrowserLogin(input, {
20715
21121
  ...ports,
20716
21122
  value,
20717
21123
  gateway: gatewayFor(value.baseURL ?? ""),
@@ -20719,6 +21125,8 @@ function apply(ctx, config) {
20719
21125
  ctx.logger.warn(message);
20720
21126
  }
20721
21127
  });
21128
+ if (result === null) notifySessionProvisioned?.();
21129
+ return result;
20722
21130
  },
20723
21131
  setDevice: (info) => {
20724
21132
  setDeviceInfo(info);