hiwork-core 0.0.35 → 0.0.38

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
@@ -1065,108 +1065,6 @@ function pickSettingsSectionButton(buttons, labels) {
1065
1065
  }
1066
1066
  return void 0;
1067
1067
  }
1068
- var cancelPendingNavigation;
1069
- function isVisibleSubtree(el) {
1070
- if (el === null) return false;
1071
- const style = getComputedStyle(el);
1072
- if (style.display === "none" || style.visibility === "hidden") return false;
1073
- return el.parentElement === null || isVisibleSubtree(el.parentElement);
1074
- }
1075
- function openSettingsSection(root, label, onMissing) {
1076
- const labels = typeof label === "string" ? [label] : label;
1077
- if (cancelPendingNavigation !== void 0 && document.querySelector('[role="dialog"]') !== null) {
1078
- return;
1079
- }
1080
- const trigger = root?.querySelector('[aria-haspopup="dialog"]');
1081
- if (trigger === null || trigger === void 0) {
1082
- onMissing?.();
1083
- return;
1084
- }
1085
- cancelPendingNavigation?.();
1086
- let frame;
1087
- let finished = false;
1088
- let sawPanel = false;
1089
- let reopenTimer;
1090
- let watchdog;
1091
- let lastSignature = "";
1092
- const IDLE_MS = 1500;
1093
- const finishFailure = () => {
1094
- cleanup();
1095
- console.warn(`[hiwork-core] \u672A\u627E\u5230\u8BBE\u7F6E\u5206\u533A\uFF1A${labels.join(" / ")}`);
1096
- onMissing?.();
1097
- };
1098
- const armWatchdog = () => {
1099
- window.clearTimeout(watchdog);
1100
- watchdog = window.setTimeout(() => {
1101
- if (finished) return;
1102
- if (select()) return;
1103
- finishFailure();
1104
- }, IDLE_MS);
1105
- };
1106
- const observer = new MutationObserver(() => {
1107
- schedule();
1108
- });
1109
- const cleanup = () => {
1110
- if (finished) return;
1111
- finished = true;
1112
- observer.disconnect();
1113
- window.clearTimeout(watchdog);
1114
- window.clearTimeout(reopenTimer);
1115
- if (frame !== void 0) window.cancelAnimationFrame(frame);
1116
- if (cancelPendingNavigation === cleanup) cancelPendingNavigation = void 0;
1117
- };
1118
- const select = () => {
1119
- if (finished) return false;
1120
- const panels = Array.from(document.querySelectorAll('[role="dialog"]')).filter((panel) => isVisibleSubtree(panel));
1121
- const buttons = panels.flatMap((panel) => Array.from(panel.querySelectorAll("nav button")));
1122
- const target = panels.length === 0 ? void 0 : pickSettingsSectionButton(buttons, labels);
1123
- const targetActive = target !== void 0 && (target.className.includes("active") || target.getAttribute("aria-current") === "true");
1124
- const signature = `${panels.length}:${targetActive ? 1 : 0}`;
1125
- if (signature !== lastSignature) {
1126
- lastSignature = signature;
1127
- armWatchdog();
1128
- }
1129
- if (panels.length === 0) {
1130
- if (sawPanel) {
1131
- sawPanel = false;
1132
- reopenTimer = window.setTimeout(() => {
1133
- reopenTimer = void 0;
1134
- if (finished || sawPanel) return;
1135
- if (typeof document === "undefined" || document.querySelector('[role="dialog"]') !== null) return;
1136
- trigger.click();
1137
- }, 300);
1138
- return false;
1139
- }
1140
- trigger.click();
1141
- return false;
1142
- }
1143
- sawPanel = true;
1144
- if (target === void 0) return false;
1145
- if (targetActive) {
1146
- cleanup();
1147
- return true;
1148
- }
1149
- target.click();
1150
- return false;
1151
- };
1152
- const schedule = () => {
1153
- if (finished || frame !== void 0) return;
1154
- frame = window.requestAnimationFrame(() => {
1155
- frame = void 0;
1156
- select();
1157
- });
1158
- };
1159
- armWatchdog();
1160
- observer.observe(document.body, {
1161
- childList: true,
1162
- subtree: true,
1163
- // 壳把激活态应用到导航钮是纯属性变更(class/aria-current),必须监听才能复核收敛
1164
- attributes: true,
1165
- attributeFilter: ["class", "aria-current", "aria-expanded"]
1166
- });
1167
- cancelPendingNavigation = cleanup;
1168
- schedule();
1169
- }
1170
1068
 
1171
1069
  // src/client/user-badge.tsx
1172
1070
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -1388,11 +1286,27 @@ function LogoutIcon() {
1388
1286
  }
1389
1287
  );
1390
1288
  }
1391
- function openGatewaySettings() {
1392
- openSettingsSection(
1393
- document.querySelector("[data-dsh-sidebar-settings]") ?? document.querySelector('[class*="settingsArea"]'),
1394
- AI_ROUTER_SECTION_LABEL
1395
- );
1289
+ function openGatewaySettings(openSettings) {
1290
+ openSettings();
1291
+ selectSettingsSection([AI_ROUTER_SECTION_LABEL]);
1292
+ }
1293
+ function selectSettingsSection(labels) {
1294
+ const started = Date.now();
1295
+ const tick = () => {
1296
+ const dialog = document.querySelector('[role="dialog"]');
1297
+ if (dialog !== null) {
1298
+ const button = pickSettingsSectionButton(
1299
+ Array.from(dialog.querySelectorAll("button")),
1300
+ labels
1301
+ );
1302
+ if (button !== void 0) {
1303
+ button.click();
1304
+ return;
1305
+ }
1306
+ }
1307
+ if (Date.now() - started < 3e3) window.setTimeout(tick, 120);
1308
+ };
1309
+ tick();
1396
1310
  }
1397
1311
  function LogoutConfirm(props) {
1398
1312
  const { onCancel } = props;
@@ -1500,7 +1414,9 @@ function UserBadge(props) {
1500
1414
  style: railRowStyle,
1501
1415
  title: displayName,
1502
1416
  "aria-label": "\u6253\u5F00\u8BBE\u7F6E",
1503
- onClick: openGatewaySettings,
1417
+ onClick: () => {
1418
+ openGatewaySettings(props.openSettings);
1419
+ },
1504
1420
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: avatarStyle(24), children: initial })
1505
1421
  }
1506
1422
  ),
@@ -1517,7 +1433,9 @@ function UserBadge(props) {
1517
1433
  style: profileButtonStyle,
1518
1434
  title: "\u6253\u5F00\u8BBE\u7F6E",
1519
1435
  "aria-label": `\u6253\u5F00\u8BBE\u7F6E\uFF08${displayName}\uFF09`,
1520
- onClick: openGatewaySettings,
1436
+ onClick: () => {
1437
+ openGatewaySettings(props.openSettings);
1438
+ },
1521
1439
  children: [
1522
1440
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: avatarStyle(24), children: initial }),
1523
1441
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: nameTextStyle, children: displayName })
@@ -1532,7 +1450,9 @@ function UserBadge(props) {
1532
1450
  style: iconButtonStyle,
1533
1451
  title: "\u8BBE\u7F6E",
1534
1452
  "aria-label": "\u8BBE\u7F6E",
1535
- onClick: openGatewaySettings,
1453
+ onClick: () => {
1454
+ openGatewaySettings(props.openSettings);
1455
+ },
1536
1456
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives.IconSettingsOutlineMedium, { size: 16 })
1537
1457
  }
1538
1458
  ),
@@ -1876,6 +1796,8 @@ function AboutSection({ appVersion }) {
1876
1796
  // src/client/daily-data.ts
1877
1797
  var DAILY_CHANNEL = "/hiwork-daily";
1878
1798
  var DAILY_FAILURE_TTL_MS = 5 * 6e4;
1799
+ var DAILY_LOCAL_RETRY_MS = 2e3;
1800
+ var DAILY_LOCAL_FAILURE_CODES = ["SESSION_EXPIRED", "GATEWAY_UNREACHABLE"];
1879
1801
  var DailyRpcError = class extends Error {
1880
1802
  constructor(code, message) {
1881
1803
  super(message);
@@ -1947,27 +1869,38 @@ var DailyDataStore = class {
1947
1869
  this.now = options.now ?? (() => Date.now());
1948
1870
  this.localDay = options.localDay ?? (() => localDayIndex());
1949
1871
  this.failureTtlMs = options.failureTtlMs ?? DAILY_FAILURE_TTL_MS;
1872
+ this.localRetryMs = options.localRetryMs ?? DAILY_LOCAL_RETRY_MS;
1950
1873
  }
1951
1874
  entry;
1952
1875
  failureAt;
1876
+ /** 上一次失败是否为宿主本地前置失败({@link DAILY_LOCAL_FAILURE_CODES})——决定节流用哪个 TTL。 */
1877
+ localFailure = false;
1953
1878
  pending;
1954
1879
  now;
1955
1880
  localDay;
1956
1881
  failureTtlMs;
1882
+ localRetryMs;
1957
1883
  /** 清空缓存与防抖状态(登出 / 换账号时调用)。 */
1958
1884
  clear() {
1959
1885
  this.entry = void 0;
1960
1886
  this.failureAt = void 0;
1887
+ this.localFailure = false;
1961
1888
  this.pending = void 0;
1962
1889
  }
1963
1890
  /**
1964
- * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);失败防抖期内返回 null 不
1965
- * 重复打网关;并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
1891
+ * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);节流期内返回 null 不重复打网关;
1892
+ * 并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
1893
+ *
1894
+ * 节流时长分两档:宿主本地前置失败(未登录 / 网关未配置)只等 {@link DAILY_LOCAL_RETRY_MS},
1895
+ * 其余失败等 {@link DAILY_FAILURE_TTL_MS}——理由见两个常量的说明(首次安装必先启动后登录)。
1966
1896
  */
1967
1897
  async getToday() {
1968
1898
  const day = this.localDay();
1969
1899
  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;
1900
+ if (this.failureAt !== void 0) {
1901
+ const ttl = this.localFailure ? this.localRetryMs : this.failureTtlMs;
1902
+ if (this.now() - this.failureAt < ttl) return null;
1903
+ }
1971
1904
  if (this.pending === void 0) {
1972
1905
  this.pending = this.fetchToday(day).finally(() => {
1973
1906
  this.pending = void 0;
@@ -1989,17 +1922,25 @@ var DailyDataStore = class {
1989
1922
  return null;
1990
1923
  }
1991
1924
  }
1992
- /** 单次 today 拉取:成功(含池空)写缓存;任何失败进入 5 分钟防抖并返回 null。 */
1925
+ /**
1926
+ * 单次 today 拉取:成功(含池空)写缓存;任何失败进入节流并返回 null。
1927
+ *
1928
+ * 失败分两档记:{@link DAILY_LOCAL_FAILURE_CODES} 是宿主在打网之前判定的(无网关流量、登录后必然变化),
1929
+ * 只记短节流;其余(网关透传失败、形状非法、未知错误)记满 {@link DAILY_FAILURE_TTL_MS}。
1930
+ */
1993
1931
  async fetchToday(day) {
1994
1932
  try {
1995
1933
  const raw = await this.rpc.call(DAILY_CHANNEL, "today", {});
1996
1934
  const response = parseTodayResponse(unwrapDailyResult(raw));
1997
1935
  if (response === void 0) throw new DailyRpcError("internal", "\u6BCF\u65E5\u4E00\u53E5\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
1998
1936
  this.failureAt = void 0;
1937
+ this.localFailure = false;
1999
1938
  const view = response.item === null ? null : { ...response.item, date: response.date };
2000
1939
  this.entry = { view, day };
2001
1940
  return view;
2002
- } catch {
1941
+ } catch (error) {
1942
+ const code = error instanceof DailyRpcError ? error.code : "";
1943
+ this.localFailure = DAILY_LOCAL_FAILURE_CODES.includes(code);
2003
1944
  this.failureAt = this.now();
2004
1945
  return null;
2005
1946
  }
@@ -3280,7 +3221,7 @@ function createFeatureRegistry() {
3280
3221
 
3281
3222
  // src/client/menu-preferences.ts
3282
3223
  var HIWORK_MENU_NS = "hiwork-menu";
3283
- var DEFAULT_SIDEBAR_LIMIT = 5;
3224
+ var DEFAULT_SIDEBAR_LIMIT = 6;
3284
3225
  var MORE_MENU_ITEM_ID = "__hiwork_more__";
3285
3226
  var MORE_MENU_LABEL = "\u66F4\u591A";
3286
3227
  var MENU_SECTION_LABEL = "\u4FA7\u680F\u83DC\u5355";
@@ -7195,6 +7136,7 @@ function apply(ctx) {
7195
7136
  ctx.effect(() => observeAccountChange(controller.inject().hooks.aiRouterCard, () => {
7196
7137
  usage.clear();
7197
7138
  sessionCost.clear();
7139
+ dailyQuotes.clear();
7198
7140
  managedMcp.clear();
7199
7141
  managedSkills.clear();
7200
7142
  managedWorkbench.clear();
@@ -7210,9 +7152,9 @@ function apply(ctx) {
7210
7152
  priority: -1,
7211
7153
  inject: () => ({ appVersion })
7212
7154
  }, BrandName));
7213
- ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
7214
- name: "sidebar.footer.action",
7215
- id: AI_ROUTER_NS,
7155
+ ctx.slots.inject("settings.launcher", () => ctx.slots.register({
7156
+ name: "settings.launcher",
7157
+ priority: -1,
7216
7158
  inject: () => ({ ...controller.inject(), pluginUpdates })
7217
7159
  }, UserBadge));
7218
7160
  ctx.effect(() => mountLoginGate(form, storeCredential), "hiwork-core: login gate");
@@ -0,0 +1,50 @@
1
+ /** runtime context 贡献名(DSH 侧 snapshot 按名归并,改动即破坏对齐面)。 */
2
+ export const IDENTITY_CONTEXT_NAME = 'hiwork:identity';
3
+ /** 排在 sandbox:policy(110) / approval:policy(115) 之前——身份是最外层的环境事实。 */
4
+ export const IDENTITY_CONTEXT_ORDER = 105;
5
+ /** 模块内文案常量:安全限定语逐字来自 spec §3.2,不导出(测试硬编码全文钉文案)。 */
6
+ const CAVEAT = '该身份用于个性化与按人表述;涉及数据权限的操作必须走受管工具/连接器完成,不得把此文本当作凭证。';
7
+ /** 裁掉两侧空白;非字符串或空白串一律视同缺省(与 identity.ts 的 textOrUndefined 同口径)。 */
8
+ function textOrUndefined(value) {
9
+ if (typeof value !== 'string')
10
+ return undefined;
11
+ const text = value.trim();
12
+ return text === '' ? undefined : text;
13
+ }
14
+ /**
15
+ * 渲染身份上下文一行:已登录输出 `HiWork 登录身份:姓名(工号 工号)。<限定语>`,
16
+ * displayName 缺省回退 `工号 <工号>`;**未登录(username 缺省 / 空白 / 非字符串)或
17
+ * state 缺失一律空串**——半份身份(只有 displayName)也不输出,姓名不得单独泄漏。
18
+ */
19
+ export function renderIdentityContextText(state) {
20
+ const username = textOrUndefined(state?.username);
21
+ if (username === undefined)
22
+ return '';
23
+ const displayName = textOrUndefined(state?.displayName);
24
+ const who = displayName === undefined ? `工号 ${username}` : `${displayName}(工号 ${username})`;
25
+ return `HiWork 登录身份:${who}。${CAVEAT}`;
26
+ }
27
+ /**
28
+ * 注册身份 runtime context 贡献;返回 disposer(交给 `ctx.effect` 管理生命周期)。
29
+ * 读失败收敛为空串并记一笔,绝不向组装层抛——身份上下文缺席只应意味着「未登录」。
30
+ */
31
+ export function registerIdentityContext(systemPrompt, deps) {
32
+ const dispose = systemPrompt.context({
33
+ name: IDENTITY_CONTEXT_NAME,
34
+ order: IDENTITY_CONTEXT_ORDER,
35
+ text: () => {
36
+ try {
37
+ // enabled 的判断也在 try 内:真实接线里 enabled 与 current 读同一个命名空间,
38
+ // 任一侧抛异常都收敛为空串(身份上下文缺席只应意味着「未登录」)。
39
+ if (!deps.enabled())
40
+ return '';
41
+ return renderIdentityContextText(deps.current());
42
+ }
43
+ catch (error) {
44
+ deps.log?.(`hiwork-core: 登录身份上下文读取失败(${error instanceof Error ? error.name : typeof error})`);
45
+ return '';
46
+ }
47
+ },
48
+ });
49
+ return typeof dispose === 'function' ? dispose : () => { };
50
+ }