hiwork-core 0.0.35 → 0.0.37

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.
@@ -146,12 +146,23 @@ export async function startBrowserLogin(opts) {
146
146
  }
147
147
  return { authorizeUrl, state };
148
148
  }
149
- /** 把失败写进状态并结束等待态(清 browserLogin 与请求字段)。 */
150
- async function failBrowserLogin(state, loginError) {
151
- await state.patch({
152
- set: { lastError: loginError },
153
- unset: ['browserLoginRequest', 'browserLogin'],
154
- }).catch(() => { });
149
+ /**
150
+ * 把失败写进状态并结束等待态(清 browserLogin 与请求字段)。
151
+ *
152
+ * ⚠️ 写回可能失败(settings 尚未可写 / entry 被 HMR 摘掉):那样等待态不会结束,界面会停在
153
+ * 「等待完成授权」而没有任何线索。这里不静默吞掉——`state.patch` 已带退避重试,还能抛出来
154
+ * 说明重试窗口也用尽了。
155
+ */
156
+ async function failBrowserLogin(state, loginError, log) {
157
+ try {
158
+ await state.patch({
159
+ set: { lastError: loginError },
160
+ unset: ['browserLoginRequest', 'browserLogin'],
161
+ });
162
+ }
163
+ catch (error) {
164
+ log(`hiwork-core: 浏览器授权失败态写回被拒,界面可能停在「等待完成授权」(${error instanceof Error ? error.message : typeof error})`);
165
+ }
155
166
  return loginError;
156
167
  }
157
168
  /**
@@ -161,17 +172,32 @@ async function failBrowserLogin(state, loginError) {
161
172
  * `browserLogin` 是持久化的。宿主一旦重启(应用重开、DSH 崩溃重启、开发期 HMR),
162
173
  * 两者就永久错位:所有回调都被 `completeBrowserLogin` 的「没有待完成的授权请求」丢弃,
163
174
  * 界面却仍按持久化的 `browserLogin` 停在「等待完成授权」,用户只能靠「取消」脱困。
175
+ * 更糟的是登录门挂载时会拿这个**已经死掉的**授权 URL 自动打开浏览器,用户无论怎么
176
+ * 在浏览器里完成授权都回不来——这正是「浏览器打开了但授权回不到桌面端」的成因。
164
177
  *
165
178
  * 因此启动即把这种**不可能完成**的等待态收尾成失败:写一条 lastError 并清掉等待字段,
166
179
  * 让用户回到登录入口重新发起。没有等待态时不动任何状态。
180
+ *
181
+ * ⚠️ **返回值必须被检查,调用方必须重试**:`apply()` 跑在任何 settings 写入可用之前,
182
+ * 此时写回会被 `settings namespace "<ns>" is not registered` 拒绝(实测 1.0.8 桌面端:
183
+ * apply 时刻命名空间尚未注册,约 3.7s 后才可写)。捕获后吞掉这个拒绝,等于等待态永远
184
+ * 清不掉——本函数原先正是 `.catch(() => {})` + 单次调用,缺陷即由此而来。
185
+ *
186
+ * @returns `true` = 已收尾(原本就没有等待态,或已成功清掉);`false` = 写回被拒,需重试。
167
187
  */
168
188
  export async function reconcileStaleBrowserLogin(state, value) {
169
189
  if (value.browserLogin === undefined)
170
- return;
171
- await state.patch({
172
- set: { lastError: { code: 'AUTH_FAILED', message: '上次浏览器授权未完成,请重新登录' } },
173
- unset: ['browserLoginRequest', 'browserLogin'],
174
- }).catch(() => { });
190
+ return true;
191
+ try {
192
+ await state.patch({
193
+ set: { lastError: { code: 'AUTH_FAILED', message: '上次浏览器授权未完成,请重新登录' } },
194
+ unset: ['browserLoginRequest', 'browserLogin'],
195
+ });
196
+ return true;
197
+ }
198
+ catch {
199
+ return false;
200
+ }
175
201
  }
176
202
  /**
177
203
  * 处理 `hiwork://auth/callback`:校验 state → 兑换一次性 code → 写会话凭据 → 复用供给尾部。
@@ -201,20 +227,20 @@ export async function completeBrowserLogin(input, opts) {
201
227
  try {
202
228
  if (now() - pending.startedAt > BROWSER_LOGIN_TTL_MS) {
203
229
  log('hiwork-core: 浏览器授权回调已超时(超过 10 分钟),已拒绝');
204
- return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权已超时,请重新登录' });
230
+ return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权已超时,请重新登录' }, log);
205
231
  }
206
232
  if (typeof input.error === 'string' && input.error !== '') {
207
233
  // 授权页拒绝 / 网关下发 error:不兑换 code。error 截断后入日志,防日志注入。
208
234
  log(`hiwork-core: 浏览器授权被拒绝(${input.error.slice(0, 64)})`);
209
- return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权被取消或失败,请重新登录' });
235
+ return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权被取消或失败,请重新登录' }, log);
210
236
  }
211
237
  if (typeof input.code !== 'string' || input.code === '') {
212
238
  log('hiwork-core: 浏览器授权回调缺少 code,已丢弃');
213
- return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权回调不完整,请重新登录' });
239
+ return await failBrowserLogin(opts.state, { code: 'AUTH_FAILED', message: '授权回调不完整,请重新登录' }, log);
214
240
  }
215
241
  const baseURL = opts.value.baseURL;
216
242
  if (baseURL === undefined || baseURL === '')
217
- return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR);
243
+ return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR, log);
218
244
  const { sessionCookie, user } = await opts.gateway.exchangeCode({
219
245
  code: input.code,
220
246
  codeVerifier: pending.codeVerifier,
@@ -240,7 +266,7 @@ export async function completeBrowserLogin(input, opts) {
240
266
  }
241
267
  catch (error) {
242
268
  const loginError = toLoginError(error, '无法连接网关,请检查 baseURL 或网络');
243
- return await failBrowserLogin(opts.state, loginError);
269
+ return await failBrowserLogin(opts.state, loginError, log);
244
270
  }
245
271
  finally {
246
272
  pendingAuth = undefined;
package/lib/client.js CHANGED
@@ -1876,6 +1876,8 @@ function AboutSection({ appVersion }) {
1876
1876
  // src/client/daily-data.ts
1877
1877
  var DAILY_CHANNEL = "/hiwork-daily";
1878
1878
  var DAILY_FAILURE_TTL_MS = 5 * 6e4;
1879
+ var DAILY_LOCAL_RETRY_MS = 2e3;
1880
+ var DAILY_LOCAL_FAILURE_CODES = ["SESSION_EXPIRED", "GATEWAY_UNREACHABLE"];
1879
1881
  var DailyRpcError = class extends Error {
1880
1882
  constructor(code, message) {
1881
1883
  super(message);
@@ -1947,27 +1949,38 @@ var DailyDataStore = class {
1947
1949
  this.now = options.now ?? (() => Date.now());
1948
1950
  this.localDay = options.localDay ?? (() => localDayIndex());
1949
1951
  this.failureTtlMs = options.failureTtlMs ?? DAILY_FAILURE_TTL_MS;
1952
+ this.localRetryMs = options.localRetryMs ?? DAILY_LOCAL_RETRY_MS;
1950
1953
  }
1951
1954
  entry;
1952
1955
  failureAt;
1956
+ /** 上一次失败是否为宿主本地前置失败({@link DAILY_LOCAL_FAILURE_CODES})——决定节流用哪个 TTL。 */
1957
+ localFailure = false;
1953
1958
  pending;
1954
1959
  now;
1955
1960
  localDay;
1956
1961
  failureTtlMs;
1962
+ localRetryMs;
1957
1963
  /** 清空缓存与防抖状态(登出 / 换账号时调用)。 */
1958
1964
  clear() {
1959
1965
  this.entry = void 0;
1960
1966
  this.failureAt = void 0;
1967
+ this.localFailure = false;
1961
1968
  this.pending = void 0;
1962
1969
  }
1963
1970
  /**
1964
- * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);失败防抖期内返回 null 不
1965
- * 重复打网关;并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
1971
+ * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);节流期内返回 null 不重复打网关;
1972
+ * 并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
1973
+ *
1974
+ * 节流时长分两档:宿主本地前置失败(未登录 / 网关未配置)只等 {@link DAILY_LOCAL_RETRY_MS},
1975
+ * 其余失败等 {@link DAILY_FAILURE_TTL_MS}——理由见两个常量的说明(首次安装必先启动后登录)。
1966
1976
  */
1967
1977
  async getToday() {
1968
1978
  const day = this.localDay();
1969
1979
  if (this.entry !== void 0 && this.entry.day === day) return this.entry.view;
1970
- if (this.failureAt !== void 0 && this.now() - this.failureAt < this.failureTtlMs) return null;
1980
+ if (this.failureAt !== void 0) {
1981
+ const ttl = this.localFailure ? this.localRetryMs : this.failureTtlMs;
1982
+ if (this.now() - this.failureAt < ttl) return null;
1983
+ }
1971
1984
  if (this.pending === void 0) {
1972
1985
  this.pending = this.fetchToday(day).finally(() => {
1973
1986
  this.pending = void 0;
@@ -1989,17 +2002,25 @@ var DailyDataStore = class {
1989
2002
  return null;
1990
2003
  }
1991
2004
  }
1992
- /** 单次 today 拉取:成功(含池空)写缓存;任何失败进入 5 分钟防抖并返回 null。 */
2005
+ /**
2006
+ * 单次 today 拉取:成功(含池空)写缓存;任何失败进入节流并返回 null。
2007
+ *
2008
+ * 失败分两档记:{@link DAILY_LOCAL_FAILURE_CODES} 是宿主在打网之前判定的(无网关流量、登录后必然变化),
2009
+ * 只记短节流;其余(网关透传失败、形状非法、未知错误)记满 {@link DAILY_FAILURE_TTL_MS}。
2010
+ */
1993
2011
  async fetchToday(day) {
1994
2012
  try {
1995
2013
  const raw = await this.rpc.call(DAILY_CHANNEL, "today", {});
1996
2014
  const response = parseTodayResponse(unwrapDailyResult(raw));
1997
2015
  if (response === void 0) throw new DailyRpcError("internal", "\u6BCF\u65E5\u4E00\u53E5\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
1998
2016
  this.failureAt = void 0;
2017
+ this.localFailure = false;
1999
2018
  const view = response.item === null ? null : { ...response.item, date: response.date };
2000
2019
  this.entry = { view, day };
2001
2020
  return view;
2002
- } catch {
2021
+ } catch (error) {
2022
+ const code = error instanceof DailyRpcError ? error.code : "";
2023
+ this.localFailure = DAILY_LOCAL_FAILURE_CODES.includes(code);
2003
2024
  this.failureAt = this.now();
2004
2025
  return null;
2005
2026
  }
@@ -7195,6 +7216,7 @@ function apply(ctx) {
7195
7216
  ctx.effect(() => observeAccountChange(controller.inject().hooks.aiRouterCard, () => {
7196
7217
  usage.clear();
7197
7218
  sessionCost.clear();
7219
+ dailyQuotes.clear();
7198
7220
  managedMcp.clear();
7199
7221
  managedSkills.clear();
7200
7222
  managedWorkbench.clear();
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.35";
8270
+ var PKG_VERSION = false ? "dev" : "0.0.37";
8271
8271
 
8272
8272
  // src/browser-login.ts
8273
8273
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -8549,21 +8549,28 @@ async function startBrowserLogin(opts) {
8549
8549
  }
8550
8550
  return { authorizeUrl, state };
8551
8551
  }
8552
- async function failBrowserLogin(state, loginError) {
8553
- await state.patch({
8554
- set: { lastError: loginError },
8555
- unset: ["browserLoginRequest", "browserLogin"]
8556
- }).catch(() => {
8557
- });
8552
+ async function failBrowserLogin(state, loginError, log) {
8553
+ try {
8554
+ await state.patch({
8555
+ set: { lastError: loginError },
8556
+ unset: ["browserLoginRequest", "browserLogin"]
8557
+ });
8558
+ } catch (error) {
8559
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u5931\u8D25\u6001\u5199\u56DE\u88AB\u62D2\uFF0C\u754C\u9762\u53EF\u80FD\u505C\u5728\u300C\u7B49\u5F85\u5B8C\u6210\u6388\u6743\u300D\uFF08${error instanceof Error ? error.message : typeof error}\uFF09`);
8560
+ }
8558
8561
  return loginError;
8559
8562
  }
8560
8563
  async function reconcileStaleBrowserLogin(state, value) {
8561
- if (value.browserLogin === void 0) return;
8562
- await state.patch({
8563
- set: { lastError: { code: "AUTH_FAILED", message: "\u4E0A\u6B21\u6D4F\u89C8\u5668\u6388\u6743\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" } },
8564
- unset: ["browserLoginRequest", "browserLogin"]
8565
- }).catch(() => {
8566
- });
8564
+ if (value.browserLogin === void 0) return true;
8565
+ try {
8566
+ await state.patch({
8567
+ set: { lastError: { code: "AUTH_FAILED", message: "\u4E0A\u6B21\u6D4F\u89C8\u5668\u6388\u6743\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" } },
8568
+ unset: ["browserLoginRequest", "browserLogin"]
8569
+ });
8570
+ return true;
8571
+ } catch {
8572
+ return false;
8573
+ }
8567
8574
  }
8568
8575
  async function completeBrowserLogin(input, opts) {
8569
8576
  const now = opts.now ?? Date.now;
@@ -8581,18 +8588,18 @@ async function completeBrowserLogin(input, opts) {
8581
8588
  try {
8582
8589
  if (now() - pending.startedAt > BROWSER_LOGIN_TTL_MS) {
8583
8590
  log("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5DF2\u8D85\u65F6\uFF08\u8D85\u8FC7 10 \u5206\u949F\uFF09\uFF0C\u5DF2\u62D2\u7EDD");
8584
- return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
8591
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }, log);
8585
8592
  }
8586
8593
  if (typeof input.error === "string" && input.error !== "") {
8587
8594
  log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u88AB\u62D2\u7EDD\uFF08${input.error.slice(0, 64)}\uFF09`);
8588
- return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u88AB\u53D6\u6D88\u6216\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
8595
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u88AB\u53D6\u6D88\u6216\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }, log);
8589
8596
  }
8590
8597
  if (typeof input.code !== "string" || input.code === "") {
8591
8598
  log("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u7F3A\u5C11 code\uFF0C\u5DF2\u4E22\u5F03");
8592
- return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u56DE\u8C03\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
8599
+ return await failBrowserLogin(opts.state, { code: "AUTH_FAILED", message: "\u6388\u6743\u56DE\u8C03\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }, log);
8593
8600
  }
8594
8601
  const baseURL = opts.value.baseURL;
8595
- if (baseURL === void 0 || baseURL === "") return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR);
8602
+ if (baseURL === void 0 || baseURL === "") return await failBrowserLogin(opts.state, NO_BASE_URL_ERROR, log);
8596
8603
  const { sessionCookie, user } = await opts.gateway.exchangeCode({
8597
8604
  code: input.code,
8598
8605
  codeVerifier: pending.codeVerifier,
@@ -8617,7 +8624,7 @@ async function completeBrowserLogin(input, opts) {
8617
8624
  return null;
8618
8625
  } catch (error) {
8619
8626
  const loginError = toLoginError(error, "\u65E0\u6CD5\u8FDE\u63A5\u7F51\u5173\uFF0C\u8BF7\u68C0\u67E5 baseURL \u6216\u7F51\u7EDC");
8620
- return await failBrowserLogin(opts.state, loginError);
8627
+ return await failBrowserLogin(opts.state, loginError, log);
8621
8628
  } finally {
8622
8629
  pendingAuth = void 0;
8623
8630
  }
@@ -19959,19 +19966,45 @@ function pathOps(patch) {
19959
19966
  for (const key of patch.unset ?? []) ops.push({ op: "unset", path: [key] });
19960
19967
  return ops;
19961
19968
  }
19962
- async function writeNamespace(settings, ns, ops) {
19963
- for (let attempt = 0; attempt < 2; attempt++) {
19969
+ var HMR_NESTED_FRAGMENT = "HMR transactions cannot be nested";
19970
+ var ENTRY_UNAVAILABLE_FRAGMENTS = ["No configurable plugin entry", "is no longer configurable"];
19971
+ var WRITE_DEADLINE_MS = 15e3;
19972
+ var WRITE_BACKOFF_MIN_MS = 50;
19973
+ var WRITE_BACKOFF_MAX_MS = 1e3;
19974
+ function isHmrNestedWriteError(error) {
19975
+ return error instanceof Error && error.message.includes(HMR_NESTED_FRAGMENT);
19976
+ }
19977
+ function isRetriableWriteError(error) {
19978
+ if (!(error instanceof Error)) return false;
19979
+ return error.name === "SettingsConflictError" || isHmrNestedWriteError(error) || ENTRY_UNAVAILABLE_FRAGMENTS.some((fragment) => error.message.includes(fragment));
19980
+ }
19981
+ function delay(ms) {
19982
+ return new Promise((resolve3) => {
19983
+ setTimeout(resolve3, ms);
19984
+ });
19985
+ }
19986
+ async function writeNamespace(settings, ns, ops, opts = {}) {
19987
+ const deadline = Date.now() + (opts.deadlineMs ?? WRITE_DEADLINE_MS);
19988
+ const maxBackoff = opts.maxBackoffMs ?? WRITE_BACKOFF_MAX_MS;
19989
+ let backoff = opts.minBackoffMs ?? WRITE_BACKOFF_MIN_MS;
19990
+ for (; ; ) {
19964
19991
  const descriptor = settings.describe().find((d) => d.ns === ns);
19965
- if (descriptor === void 0) throw new Error(`settings namespace "${ns}" is not registered`);
19992
+ if (descriptor === void 0) {
19993
+ if (Date.now() >= deadline) throw new Error(`settings namespace "${ns}" is not registered`);
19994
+ await delay(backoff);
19995
+ backoff = Math.min(backoff * 2, maxBackoff);
19996
+ continue;
19997
+ }
19966
19998
  try {
19967
19999
  await settings.mutate(ns, ops, descriptor.revision);
19968
20000
  return;
19969
20001
  } catch (error) {
19970
- if (error instanceof Error && error.name === "SettingsConflictError") continue;
19971
- throw error;
20002
+ if (!isRetriableWriteError(error)) throw error;
20003
+ if (Date.now() >= deadline) throw error;
20004
+ await delay(backoff);
20005
+ backoff = Math.min(backoff * 2, maxBackoff);
19972
20006
  }
19973
20007
  }
19974
- throw new Error(`settings namespace "${ns}" write conflicted repeatedly`);
19975
20008
  }
19976
20009
  function readDefaultSelection(settings) {
19977
20010
  const value = settings.describe().find((d) => d.ns === DEFAULT_MODEL_NS)?.value;
@@ -20188,6 +20221,8 @@ async function importLegacyMenuSection(opts) {
20188
20221
  log(`hiwork-core: \u65E7\u7248\u83DC\u5355\u504F\u597D\u8FC1\u79FB\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
20189
20222
  }
20190
20223
  }
20224
+ var STALE_BROWSER_LOGIN_RECONCILE_DELAYS_MS = [0, 500, 1500, 4e3, 8e3];
20225
+ var LOGIN_REQUEST_POLL_MS = 200;
20191
20226
  function apply(ctx, config) {
20192
20227
  const current = () => plainValue(config);
20193
20228
  const settings = ctx.get("settings");
@@ -20266,9 +20301,7 @@ function apply(ctx, config) {
20266
20301
  }).catch(() => {
20267
20302
  });
20268
20303
  };
20269
- ctx.on("settings/document-updated", (ns) => {
20270
- if (ns !== NS) return;
20271
- requestClientPolicy();
20304
+ const dispatchSectionChange = () => {
20272
20305
  const value = current();
20273
20306
  const requested = value.pendingLogin ?? value.browserLoginRequest ?? value.refreshRequest ?? value.logoutRequest;
20274
20307
  if (requested === void 0) return;
@@ -20286,18 +20319,36 @@ function apply(ctx, config) {
20286
20319
  });
20287
20320
  } catch (error) {
20288
20321
  ctx.logger.error("hiwork-core: ai-router section change failed", error);
20322
+ if (isHmrNestedWriteError(error)) {
20323
+ ctx.logger.warn("hiwork-core: \u8F6E\u8BE2\u4E0A\u4E0B\u6587\u5DF2\u88AB HMR \u4E8B\u52A1\u6C61\u67D3\uFF0C\u672C\u5B9E\u4F8B\u540E\u7EED\u56DE\u5199\u5C06\u5168\u90E8\u88AB\u62D2\uFF08\u9700\u91CD\u6302\u63D2\u4EF6\u6216\u91CD\u542F\u5E94\u7528\uFF09");
20324
+ }
20289
20325
  await ports.credentials.unset(PENDING_PASSWORD_REF).catch(() => {
20290
20326
  });
20291
- await ports.state.patch({
20292
- set: { lastError: { code: "GATEWAY_UNREACHABLE", message: "\u7F51\u5173\u63D2\u4EF6\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u4E0E\u7F51\u7EDC" } },
20293
- unset: ["pendingLogin", "browserLoginRequest", "browserLogin", "refreshRequest", "logoutRequest"]
20294
- }).catch(() => {
20295
- });
20327
+ try {
20328
+ await ports.state.patch({
20329
+ set: { lastError: { code: "GATEWAY_UNREACHABLE", message: "\u7F51\u5173\u63D2\u4EF6\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u4E0E\u7F51\u7EDC" } },
20330
+ unset: ["pendingLogin", "browserLoginRequest", "browserLogin", "refreshRequest", "logoutRequest"]
20331
+ });
20332
+ } catch (patchError) {
20333
+ ctx.logger.error("hiwork-core: ai-router \u8BF7\u6C42\u5B57\u6BB5\u6E05\u7406\u5931\u8D25\uFF0C\u754C\u9762\u53EF\u80FD\u505C\u5728\u7B49\u5F85\u4E2D", patchError);
20334
+ }
20296
20335
  }
20297
20336
  })().finally(() => {
20298
20337
  inFlight = void 0;
20299
20338
  });
20300
- });
20339
+ };
20340
+ ctx.effect(() => {
20341
+ const consume = () => {
20342
+ requestClientPolicy();
20343
+ dispatchSectionChange();
20344
+ };
20345
+ consume();
20346
+ const timer = setInterval(consume, LOGIN_REQUEST_POLL_MS);
20347
+ timer.unref?.();
20348
+ return () => {
20349
+ clearInterval(timer);
20350
+ };
20351
+ }, "hiwork-core: login request poll");
20301
20352
  void ctx.root.loader.await().then(() => importLegacyMenuSection({
20302
20353
  ctx,
20303
20354
  settings,
@@ -20306,7 +20357,15 @@ function apply(ctx, config) {
20306
20357
  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`);
20307
20358
  });
20308
20359
  requestClientPolicy();
20309
- void reconcileStaleBrowserLogin(ports.state, current());
20360
+ void (async () => {
20361
+ for (const delay2 of STALE_BROWSER_LOGIN_RECONCILE_DELAYS_MS) {
20362
+ if (delay2 > 0) await new Promise((resolve3) => {
20363
+ setTimeout(resolve3, delay2);
20364
+ });
20365
+ if (await reconcileStaleBrowserLogin(ports.state, current())) return;
20366
+ }
20367
+ ctx.logger.warn("hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u7B49\u5F85\u6001\u5BF9\u8D26\u672A\u5199\u5165\uFF0C\u754C\u9762\u53EF\u80FD\u505C\u5728\u300C\u7B49\u5F85\u5B8C\u6210\u6388\u6743\u300D");
20368
+ })();
20310
20369
  const connection = ctx.get("connection");
20311
20370
  if (connection === void 0) throw new Error("connection service unavailable");
20312
20371
  ctx.effect(() => registerUsageRpc({ connection, logger: ctx.logger }, {
@@ -20638,8 +20697,10 @@ export {
20638
20697
  apply,
20639
20698
  createDefaultModelPort,
20640
20699
  inject,
20700
+ isHmrNestedWriteError,
20641
20701
  name,
20642
20702
  plainValue,
20643
20703
  projectLoginMode,
20644
- registerIpcMessageHandler
20704
+ registerIpcMessageHandler,
20705
+ writeNamespace
20645
20706
  };
@@ -80,11 +80,20 @@ export declare function startBrowserLogin(opts: BrowserLoginStartPorts): Promise
80
80
  * `browserLogin` 是持久化的。宿主一旦重启(应用重开、DSH 崩溃重启、开发期 HMR),
81
81
  * 两者就永久错位:所有回调都被 `completeBrowserLogin` 的「没有待完成的授权请求」丢弃,
82
82
  * 界面却仍按持久化的 `browserLogin` 停在「等待完成授权」,用户只能靠「取消」脱困。
83
+ * 更糟的是登录门挂载时会拿这个**已经死掉的**授权 URL 自动打开浏览器,用户无论怎么
84
+ * 在浏览器里完成授权都回不来——这正是「浏览器打开了但授权回不到桌面端」的成因。
83
85
  *
84
86
  * 因此启动即把这种**不可能完成**的等待态收尾成失败:写一条 lastError 并清掉等待字段,
85
87
  * 让用户回到登录入口重新发起。没有等待态时不动任何状态。
88
+ *
89
+ * ⚠️ **返回值必须被检查,调用方必须重试**:`apply()` 跑在任何 settings 写入可用之前,
90
+ * 此时写回会被 `settings namespace "<ns>" is not registered` 拒绝(实测 1.0.8 桌面端:
91
+ * apply 时刻命名空间尚未注册,约 3.7s 后才可写)。捕获后吞掉这个拒绝,等于等待态永远
92
+ * 清不掉——本函数原先正是 `.catch(() => {})` + 单次调用,缺陷即由此而来。
93
+ *
94
+ * @returns `true` = 已收尾(原本就没有等待态,或已成功清掉);`false` = 写回被拒,需重试。
86
95
  */
87
- export declare function reconcileStaleBrowserLogin(state: AiRouterStatePort, value: Pick<AiRouterNamespaceValue, 'browserLogin'>): Promise<void>;
96
+ export declare function reconcileStaleBrowserLogin(state: AiRouterStatePort, value: Pick<AiRouterNamespaceValue, 'browserLogin'>): Promise<boolean>;
88
97
  /**
89
98
  * 处理 `hiwork://auth/callback`:校验 state → 兑换一次性 code → 写会话凭据 → 复用供给尾部。
90
99
  *
@@ -20,6 +20,22 @@ export declare const DAILY_ENDPOINTS: readonly ["today", "random"];
20
20
  export type DailyEndpoint = (typeof DAILY_ENDPOINTS)[number];
21
21
  /** 失败防抖 TTL:5 分钟——观察循环逐帧触发,绝不逐帧重试网关。 */
22
22
  export declare const DAILY_FAILURE_TTL_MS: number;
23
+ /**
24
+ * 失败防抖 TTL 对「宿主本地前置失败」的替代值(毫秒)。
25
+ *
26
+ * {@link DAILY_LOCAL_FAILURE_CODES} 里的错误由 Host 在**打网之前**判定(未登录 / 网关地址未配置),
27
+ * 不产生任何网关流量;而它们在「用户完成登录」或「配置补上」之后必然变化。若也计入 5 分钟防抖,
28
+ * 就会把「登录后立刻能用」变成「最多再等 5 分钟」——**首次安装正是这个顺序**(先启动、后登录),
29
+ * 用户看到的是「首次启动没有每日一句,切几次页面 / 关掉重开才有」。
30
+ * 2 秒只为压住观察循环逐帧触发时的空转(并发已由 `pending` 合并为至多一次在途),不改变可见行为。
31
+ */
32
+ export declare const DAILY_LOCAL_RETRY_MS = 2000;
33
+ /**
34
+ * Host 在打网之前判定的失败 code(见 `src/daily-rpc.ts` 的 `handleDailyEndpoint`):
35
+ * `SESSION_EXPIRED`(未登录 / 会话失效)与 `GATEWAY_UNREACHABLE`(网关地址未配置)。
36
+ * 只豁免这两个明确「本地」的 code;网关透传的失败与未知错误一律按真实上游失败进 5 分钟防抖。
37
+ */
38
+ export declare const DAILY_LOCAL_FAILURE_CODES: readonly ["SESSION_EXPIRED", "GATEWAY_UNREACHABLE"];
23
39
  /**
24
40
  * 单条每日一句的视图形状(网关 JSON 按结构重声明后的归一结果):
25
41
  * 可选字段缺省一律归一为空串 / 空数组,视图据此隐藏对应区块。
@@ -90,20 +106,27 @@ export declare class DailyDataStore implements DailyQuoteLoader {
90
106
  private readonly rpc;
91
107
  private entry;
92
108
  private failureAt;
109
+ /** 上一次失败是否为宿主本地前置失败({@link DAILY_LOCAL_FAILURE_CODES})——决定节流用哪个 TTL。 */
110
+ private localFailure;
93
111
  private pending;
94
112
  private readonly now;
95
113
  private readonly localDay;
96
114
  private readonly failureTtlMs;
115
+ private readonly localRetryMs;
97
116
  constructor(rpc: DailyRpcPort, options?: {
98
117
  now?: (() => number) | undefined;
99
118
  localDay?: (() => number) | undefined;
100
119
  failureTtlMs?: number | undefined;
120
+ localRetryMs?: number | undefined;
101
121
  });
102
122
  /** 清空缓存与防抖状态(登出 / 换账号时调用)。 */
103
123
  clear(): void;
104
124
  /**
105
- * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);失败防抖期内返回 null 不
106
- * 重复打网关;并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
125
+ * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);节流期内返回 null 不重复打网关;
126
+ * 并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
127
+ *
128
+ * 节流时长分两档:宿主本地前置失败(未登录 / 网关未配置)只等 {@link DAILY_LOCAL_RETRY_MS},
129
+ * 其余失败等 {@link DAILY_FAILURE_TTL_MS}——理由见两个常量的说明(首次安装必先启动后登录)。
107
130
  */
108
131
  getToday(): Promise<DailyTodayView | null>;
109
132
  /**
@@ -111,6 +134,11 @@ export declare class DailyDataStore implements DailyQuoteLoader {
111
134
  * 失败 / 暂无随机条目返回 null(视图保持当前条目、无任何提示)。
112
135
  */
113
136
  getRandom(excludeId?: number): Promise<DailyQuoteView | null>;
114
- /** 单次 today 拉取:成功(含池空)写缓存;任何失败进入 5 分钟防抖并返回 null。 */
137
+ /**
138
+ * 单次 today 拉取:成功(含池空)写缓存;任何失败进入节流并返回 null。
139
+ *
140
+ * 失败分两档记:{@link DAILY_LOCAL_FAILURE_CODES} 是宿主在打网之前判定的(无网关流量、登录后必然变化),
141
+ * 只记短节流;其余(网关透传失败、形状非法、未知错误)记满 {@link DAILY_FAILURE_TTL_MS}。
142
+ */
115
143
  private fetchToday;
116
144
  }
@@ -133,6 +133,25 @@ export type SettingsPathOp = {
133
133
  op: 'unset';
134
134
  path: readonly string[];
135
135
  };
136
+ /** 失败是否是「事务内写」——调用方据此判断轮询上下文已被 HMR 事务污染(见 apply)。 */
137
+ export declare function isHmrNestedWriteError(error: unknown): boolean;
138
+ /**
139
+ * 写一个命名空间(= profile entry id)。
140
+ *
141
+ * 每次尝试都重读 `describe()` 取最新 revision;可重试错误在截止时间前带退避重试,超时后
142
+ * 抛出**真实**错误——不降级成「conflicted repeatedly」,免得把根因从日志里抹掉。
143
+ * 退避上限 1s:命名空间注册与 HMR 事务都在秒级内收敛,重试代价可忽略。
144
+ *
145
+ * @param settings - settings 服务。
146
+ * @param ns - 命名空间(profile entry id)。
147
+ * @param ops - 路径操作。
148
+ * @param opts - 重试窗口与退避(单测用来把窗口压到毫秒级)。
149
+ */
150
+ export declare function writeNamespace(settings: SettingsForms, ns: string, ops: SettingsPathOp[], opts?: {
151
+ deadlineMs?: number;
152
+ minBackoffMs?: number;
153
+ maxBackoffMs?: number;
154
+ }): Promise<void>;
136
155
  /** 校正默认模型要用到的 llm 注册表部分(便于单测注入假实现)。 */
137
156
  export interface LlmRouteRegistry {
138
157
  listProviders(): readonly {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiwork-core",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "type": "module",
5
5
  "description": "HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)",
6
6
  "license": "UNLICENSED",