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.
- package/lib/browser-login.js +42 -16
- package/lib/client.js +27 -5
- package/lib/index.js +96 -35
- package/lib/types/browser-login.d.ts +10 -1
- package/lib/types/client/daily-data.d.ts +31 -3
- package/lib/types/index.d.ts +19 -0
- package/package.json +1 -1
package/lib/browser-login.js
CHANGED
|
@@ -146,12 +146,23 @@ export async function startBrowserLogin(opts) {
|
|
|
146
146
|
}
|
|
147
147
|
return { authorizeUrl, state };
|
|
148
148
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
1965
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
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
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
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
|
-
|
|
19963
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20294
|
-
|
|
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
|
|
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<
|
|
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
|
|
106
|
-
*
|
|
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
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* 单次 today 拉取:成功(含池空)写缓存;任何失败进入节流并返回 null。
|
|
139
|
+
*
|
|
140
|
+
* 失败分两档记:{@link DAILY_LOCAL_FAILURE_CODES} 是宿主在打网之前判定的(无网关流量、登录后必然变化),
|
|
141
|
+
* 只记短节流;其余(网关透传失败、形状非法、未知错误)记满 {@link DAILY_FAILURE_TTL_MS}。
|
|
142
|
+
*/
|
|
115
143
|
private fetchToday;
|
|
116
144
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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 {
|