hiwork-core 0.0.19 → 0.0.22

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/README.md CHANGED
@@ -136,6 +136,14 @@ ctx.inject(['hiworkFeatureCenter'], (scope) => {
136
136
  - [src/office-relay.ts](src/office-relay.ts):文档中继的 host 侧服务(`hiworkOfficeRelay`)——
137
137
  跨 NAT 的在线编辑:插件把文档暂存到 DS 能访问的服务器侧(上传/轮询/取回/关闭),
138
138
  用户侧不再需要入向可达性;同样只在 Host 持 cookie、失败用稳定信封。
139
+ - [src/identity.ts](src/identity.ts):当前登录身份的只读 host 服务(`hiworkIdentity`)——
140
+ 业务插件(hiwork-memory 的记忆隔离键等)经 `ctx.get('hiworkIdentity')` 取
141
+ `{ loggedIn, subject, username, displayName, employeeName, teamName }`;`subject` 是
142
+ 账号名的 sha256 前 16 位(`u_` 前缀,**不含账号原文**),未登录回 `{ loggedIn: false }`。
143
+ - [src/memory-config.ts](src/memory-config.ts):记忆系统配置下发的只读 host 服务(`hiworkMemory`)——
144
+ hiwork-memory 经 `ctx.get('hiworkMemory').config()` 取网关下发的 `panelUrl` / `serviceId` /
145
+ `teamIds` / `apiKey`(网关 `GET /api/me/memory-config`;snake_case → camelCase 的转换只在 Host 做),
146
+ 成功结果 5 分钟内存缓存、`force` 可绕过;凭据只在内存里活、不落盘、不进日志。
139
147
  - [src/default-model.ts](src/default-model.ts):供给成功后校正 `agent-default-model`(默认模型落点)。
140
148
  - [src/client/login-gate.tsx](src/client/login-gate.tsx):全屏登录门禁页。
141
149
  - [src/client/section.tsx](src/client/section.tsx):设置页「AI Router 网关」卡片。
package/lib/client.js CHANGED
@@ -142,7 +142,7 @@ function browserLoginViewOf(value) {
142
142
  if (loginMode === "form") return { loginMode, browserLoginUrl: void 0, browserLoginPending: false };
143
143
  return { loginMode, browserLoginUrl: authorizeUrlOf(value), browserLoginPending: browserLoginPendingOf(value) };
144
144
  }
145
- function openAuthorizeUrl(url) {
145
+ function openExternalUrl(url) {
146
146
  if (url === void 0 || url === "") return;
147
147
  if (typeof window === "undefined") return;
148
148
  let protocol;
@@ -154,6 +154,9 @@ function openAuthorizeUrl(url) {
154
154
  if (protocol !== "https:" && protocol !== "http:") return;
155
155
  window.open(url, "_blank", "noopener,noreferrer");
156
156
  }
157
+ function openAuthorizeUrl(url) {
158
+ openExternalUrl(url);
159
+ }
157
160
  async function requestBrowserLogin(scope) {
158
161
  await scope.set("browserLoginRequest", { at: (/* @__PURE__ */ new Date()).toISOString() });
159
162
  }
@@ -1318,6 +1321,64 @@ function removeStaleHeroGreeting(doc) {
1318
1321
  for (const stale of Array.from(doc.querySelectorAll("[data-hiwork-hero-greeting]"))) stale.remove();
1319
1322
  }
1320
1323
 
1324
+ // src/client/manual-link.ts
1325
+ var MANUAL_URL = "https://hiworkapp.hivery.cn/manual.html";
1326
+ var MANUAL_STYLE_ID = "hiwork-core-manual-style";
1327
+ var MANUAL_STYLE = `
1328
+ [data-hiwork-core-manual-entry]{box-sizing:border-box;flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:auto;padding:0;border:0;border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary,#71717a);font-family:inherit;font-size:14px;line-height:1;font-weight:600;cursor:pointer;user-select:none;transition:background .16s ease,color .16s ease}
1329
+ [data-hiwork-core-manual-entry]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(24,24,27,.06));color:var(--dsw-alias-label-secondary,#3f3f46)}
1330
+ [data-hiwork-core-manual-entry]:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#4c8dff);outline-offset:2px}
1331
+ body[data-ds-dark-theme] [data-hiwork-core-manual-entry]{color:var(--dsw-alias-label-tertiary,#9ba3b4)}
1332
+ body[data-ds-dark-theme] [data-hiwork-core-manual-entry]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));color:var(--dsw-alias-label-secondary,#a6adb8)}
1333
+ @media (prefers-reduced-motion: reduce){[data-hiwork-core-manual-entry]{transition:none}}
1334
+ `;
1335
+ function ensureManualStyle(doc) {
1336
+ if (doc.getElementById(MANUAL_STYLE_ID) !== null) return;
1337
+ if (doc.head === null) return;
1338
+ const style = doc.createElement("style");
1339
+ style.id = MANUAL_STYLE_ID;
1340
+ style.textContent = MANUAL_STYLE;
1341
+ doc.head.append(style);
1342
+ }
1343
+ function openManual() {
1344
+ openExternalUrl(MANUAL_URL);
1345
+ }
1346
+ function findManualMount(doc) {
1347
+ const hero = doc.querySelector('[data-phase="hero"]');
1348
+ if (hero === null) return null;
1349
+ return hero.querySelector('[class*="_heroWorkspaceRow"]');
1350
+ }
1351
+ function buildButton(doc) {
1352
+ const button = doc.createElement("button");
1353
+ button.dataset.hiworkCoreManualEntry = "";
1354
+ button.type = "button";
1355
+ button.title = "\u4F7F\u7528\u624B\u518C";
1356
+ button.setAttribute("aria-label", "\u4F7F\u7528\u624B\u518C");
1357
+ button.append(doc.createTextNode("?"));
1358
+ button.addEventListener("click", (event) => {
1359
+ event.preventDefault();
1360
+ event.stopPropagation();
1361
+ openManual();
1362
+ });
1363
+ return button;
1364
+ }
1365
+ function removeStaleManualEntry(doc) {
1366
+ for (const stale of Array.from(doc.querySelectorAll("[data-hiwork-core-manual-entry]"))) {
1367
+ stale.remove();
1368
+ }
1369
+ }
1370
+ function decorateManualEntry(doc) {
1371
+ const row = findManualMount(doc);
1372
+ if (row === null) {
1373
+ removeStaleManualEntry(doc);
1374
+ return;
1375
+ }
1376
+ const existing = row.querySelector(":scope > [data-hiwork-core-manual-entry]");
1377
+ if (existing !== null) return;
1378
+ removeStaleManualEntry(doc);
1379
+ row.append(buildButton(doc));
1380
+ }
1381
+
1321
1382
  // src/client/daily-card.ts
1322
1383
  var DAILY_CARD_STYLE_ID = "dcu-daily-card-style";
1323
1384
  var DAILY_MAIN_FONT_SIZE_MAX = 64;
@@ -1608,6 +1669,7 @@ function observeHeroDecorations(doc, quotes) {
1608
1669
  };
1609
1670
  ensureDailyCardStyle(doc);
1610
1671
  ensureHeroStyle(doc);
1672
+ ensureManualStyle(doc);
1611
1673
  let applying = false;
1612
1674
  let frame;
1613
1675
  let lastDailyDay = -1;
@@ -1620,6 +1682,7 @@ function observeHeroDecorations(doc, quotes) {
1620
1682
  if (today !== lastDailyDay) lastDailyDay = today;
1621
1683
  decorateDailyCard(doc, quotes);
1622
1684
  decorateHeroGreeting(doc);
1685
+ decorateManualEntry(doc);
1623
1686
  } finally {
1624
1687
  applying = false;
1625
1688
  }
@@ -1752,6 +1815,8 @@ var footStyle = { margin: "2px 0 0", fontSize: 11, color: INK_CAPTION, textAlign
1752
1815
  var browserHintStyle = { margin: 0, fontSize: 12, lineHeight: 1.6, color: INK_CAPTION, textAlign: "center" };
1753
1816
  var browserActionsStyle = { display: "flex", alignItems: "center", justifyContent: "center", gap: 18 };
1754
1817
  var toggleStyle = { ...linkButton, alignSelf: "center", fontSize: 12.5 };
1818
+ var footBlockStyle = { display: "flex", flexDirection: "column", alignItems: "center", gap: 4 };
1819
+ var manualLinkStyle = { ...linkButton, fontWeight: 500 };
1755
1820
  function describeLoginError(error) {
1756
1821
  if (error === void 0) return void 0;
1757
1822
  if (error.code === "GATEWAY_UNREACHABLE") return "\u7F51\u5173\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u6539\u7528\u8D26\u53F7\u5BC6\u7801\u767B\u5F55";
@@ -1887,7 +1952,10 @@ function LoginGateModal(props) {
1887
1952
  browserLogin,
1888
1953
  form,
1889
1954
  showBrowserLogin && formVisible ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-secondary": true, "data-hiwork-core-form-collapse": true, type: "button", style: toggleStyle, onClick: () => setFormOpen(false), children: "\u6536\u8D77\u8D26\u53F7\u5BC6\u7801\u767B\u5F55" }) : null,
1890
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: footStyle, children: "\u4F01\u4E1A\u7EDF\u4E00\u8EAB\u4EFD\u8BA4\u8BC1 \xB7 LDAP" })
1955
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: footBlockStyle, children: [
1956
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: footStyle, children: "\u4F01\u4E1A\u7EDF\u4E00\u8EAB\u4EFD\u8BA4\u8BC1 \xB7 LDAP" }),
1957
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-secondary": true, "data-hiwork-core-manual-login": true, type: "button", style: manualLinkStyle, onClick: openManual, children: "\u4F7F\u7528\u624B\u518C" })
1958
+ ] })
1891
1959
  ] })
1892
1960
  ] })
1893
1961
  ] });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 当前登录身份的只读 host 服务(`hiworkIdentity`)。
3
+ *
4
+ * 背景:hiwork-core 已经把「谁登录了」完整地记在 `ai-router` 设置命名空间里
5
+ * (`username` / `displayName` / `keys`),但那是**本插件的私有状态段**——业务插件
6
+ * 要拿「当前登录员工」做数据隔离(hiwork-memory 的记忆隔离键、按人区分的本地
7
+ * 目录名等),既不该去解析别人的设置段,也不该把登录态复制一份自己维护。
8
+ * 本模块把那层读取收敛成一个稳定的小服务:消费方 `ctx.get('hiworkIdentity')`
9
+ * 拿到的是一份**只读视图**,登录态怎么变、什么时候变都由 Host 说了算。
10
+ *
11
+ * 设计要点(与宿主既有 RPC 服务同构,参照 `src/office-credentials.ts`):
12
+ * - **只读、同步、不抛异常**:`current()` 同步返回快照,未登录返回
13
+ * `{ loggedIn: false }`,已登录但还没拉到 keys 时 `employeeName` / `teamName`
14
+ * 缺省而不是报错(首次登录的中间态、老网关不带这两个字段都会走到这里);
15
+ * - **绝不外泄账号原文进隔离键**:`subject = 'u_' + sha256(username) 的前 16 位 hex`。
16
+ * subject 会进存储键 / 目录名,哈希保证它同一账号恒等(跨会话、跨设备稳定)、
17
+ * 不同账号几乎必然不同,同时不把企业账号明文写进磁盘或日志;
18
+ * `username` 字段仍在视图里(界面要显示账号),但**不承担隔离键职责**;
19
+ * - **不缓存、不订阅**:每次 `current()` 都读一次当前命名空间值,消费方不再需要
20
+ * 关心 Host 何时刷新过登录态;
21
+ * - 服务经 `ctx.provide` 提供给同进程的其他插件,消费方用 `ctx.get` 探测
22
+ * (老 core 上它是 undefined → 插件自行回退,不假设本服务存在)。
23
+ *
24
+ * 本模块不 import cordis 运行时:provide 走结构化的 {@link IdentityContext} 端口,
25
+ * 调用方(index.ts)传真实 ctx;单测直接伪造。
26
+ */
27
+ import { createHash } from 'node:crypto';
28
+ /** cordis 服务名(hiwork-memory 等消费方按此名 `ctx.get` 探测)。 */
29
+ export const HIWORK_IDENTITY_SERVICE = 'hiworkIdentity';
30
+ /** 视图字段名的语义名(供消费方日志 / 文档引用,避免各处硬编码字符串)。 */
31
+ export const IDENTITY_SUBJECT_PREFIX = 'u_';
32
+ /** 裁掉两侧空白;非字符串或空白串一律视同缺省。 */
33
+ function textOrUndefined(value) {
34
+ if (typeof value !== 'string')
35
+ return undefined;
36
+ const text = value.trim();
37
+ return text === '' ? undefined : text;
38
+ }
39
+ /**
40
+ * 由登录账号派生稳定 subject:`u_` + `sha256(账号名).hex` 的前 16 位。
41
+ *
42
+ * 输入先裁掉两侧空白(与视图里的 `username` 同口径),因此 `' alice '` 与 `'alice'`
43
+ * 派生同一个 subject;大小写不折叠——账号名在网关侧就是区分大小写的。
44
+ * **本函数绝不返回账号名原文**:subject 会落进存储键 / 目录名,哈希是隔离键
45
+ * 既能稳定标识同一员工、又不外泄账号的那条界线。
46
+ */
47
+ export function deriveSubject(username) {
48
+ const digest = createHash('sha256').update(username.trim(), 'utf8').digest('hex');
49
+ return `${IDENTITY_SUBJECT_PREFIX}${digest.slice(0, 16)}`;
50
+ }
51
+ /**
52
+ * 从 keys 里挑员工名 / 团队名:逐条取第一个非空值,两项**互不掩盖**
53
+ * (某把 Key 只带了 team_name,不会让另一把 Key 的 employee_name 被丢掉)。
54
+ * 同一账号的各把 Key 属于同一个人,命名相同的部分取值一致,因此不需要按 keyId 挑。
55
+ */
56
+ function pickKeyNames(keys) {
57
+ let employeeName;
58
+ let teamName;
59
+ for (const key of keys ?? []) {
60
+ if (employeeName === undefined)
61
+ employeeName = textOrUndefined(key?.employee_name);
62
+ if (teamName === undefined)
63
+ teamName = textOrUndefined(key?.team_name);
64
+ if (employeeName !== undefined && teamName !== undefined)
65
+ break;
66
+ }
67
+ return {
68
+ ...(employeeName === undefined ? {} : { employeeName }),
69
+ ...(teamName === undefined ? {} : { teamName }),
70
+ };
71
+ }
72
+ /** 视图投影(无状态;未登录 / 读失败一律收敛为 `{ loggedIn: false }`)。 */
73
+ class IdentityProvider {
74
+ deps;
75
+ constructor(deps) {
76
+ this.deps = deps;
77
+ }
78
+ current() {
79
+ try {
80
+ const state = this.deps.current();
81
+ const username = textOrUndefined(state?.username);
82
+ // 未登录:只给 loggedIn,绝不带半份身份出去。
83
+ if (username === undefined)
84
+ return { loggedIn: false };
85
+ const displayName = textOrUndefined(state?.displayName);
86
+ const { employeeName, teamName } = pickKeyNames(state?.keys);
87
+ return {
88
+ loggedIn: true,
89
+ subject: deriveSubject(username),
90
+ username,
91
+ ...(displayName === undefined ? {} : { displayName }),
92
+ ...(employeeName === undefined ? {} : { employeeName }),
93
+ ...(teamName === undefined ? {} : { teamName }),
94
+ };
95
+ }
96
+ catch (error) {
97
+ // 只读服务不该把宿主的读失败变成消费方的异常:收敛成「未登录」并记一笔。
98
+ this.deps.log?.(`hiwork-core: 登录身份读取失败(${error instanceof Error ? error.name : typeof error})`);
99
+ return { loggedIn: false };
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * 提供 host 侧服务 `hiworkIdentity`;返回 disposer(交给 `ctx.effect` 管理生命周期,
105
+ * 插件卸载时服务随 fiber 一并注销)。
106
+ */
107
+ export function provideIdentity(ctx, deps) {
108
+ const service = new IdentityProvider(deps);
109
+ const dispose = ctx.provide(HIWORK_IDENTITY_SERVICE, service);
110
+ return typeof dispose === 'function' ? dispose : () => { };
111
+ }
package/lib/index.js CHANGED
@@ -3137,7 +3137,7 @@ function isUnloading(ctx) {
3137
3137
  }
3138
3138
 
3139
3139
  // src/version.ts
3140
- var PKG_VERSION = false ? "dev" : "0.0.19";
3140
+ var PKG_VERSION = false ? "dev" : "0.0.22";
3141
3141
 
3142
3142
  // src/ai-router-client.ts
3143
3143
  var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
@@ -6385,6 +6385,228 @@ function registerPluginUpdatesRpc(ctx, deps) {
6385
6385
  };
6386
6386
  }
6387
6387
 
6388
+ // src/identity.ts
6389
+ import { createHash as createHash2 } from "node:crypto";
6390
+ var HIWORK_IDENTITY_SERVICE = "hiworkIdentity";
6391
+ var IDENTITY_SUBJECT_PREFIX = "u_";
6392
+ function textOrUndefined5(value) {
6393
+ if (typeof value !== "string") return void 0;
6394
+ const text = value.trim();
6395
+ return text === "" ? void 0 : text;
6396
+ }
6397
+ function deriveSubject(username) {
6398
+ const digest = createHash2("sha256").update(username.trim(), "utf8").digest("hex");
6399
+ return `${IDENTITY_SUBJECT_PREFIX}${digest.slice(0, 16)}`;
6400
+ }
6401
+ function pickKeyNames(keys) {
6402
+ let employeeName;
6403
+ let teamName;
6404
+ for (const key of keys ?? []) {
6405
+ if (employeeName === void 0) employeeName = textOrUndefined5(key?.employee_name);
6406
+ if (teamName === void 0) teamName = textOrUndefined5(key?.team_name);
6407
+ if (employeeName !== void 0 && teamName !== void 0) break;
6408
+ }
6409
+ return {
6410
+ ...employeeName === void 0 ? {} : { employeeName },
6411
+ ...teamName === void 0 ? {} : { teamName }
6412
+ };
6413
+ }
6414
+ var IdentityProvider = class {
6415
+ constructor(deps) {
6416
+ this.deps = deps;
6417
+ }
6418
+ current() {
6419
+ try {
6420
+ const state = this.deps.current();
6421
+ const username = textOrUndefined5(state?.username);
6422
+ if (username === void 0) return { loggedIn: false };
6423
+ const displayName = textOrUndefined5(state?.displayName);
6424
+ const { employeeName, teamName } = pickKeyNames(state?.keys);
6425
+ return {
6426
+ loggedIn: true,
6427
+ subject: deriveSubject(username),
6428
+ username,
6429
+ ...displayName === void 0 ? {} : { displayName },
6430
+ ...employeeName === void 0 ? {} : { employeeName },
6431
+ ...teamName === void 0 ? {} : { teamName }
6432
+ };
6433
+ } catch (error) {
6434
+ this.deps.log?.(
6435
+ `hiwork-core: \u767B\u5F55\u8EAB\u4EFD\u8BFB\u53D6\u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`
6436
+ );
6437
+ return { loggedIn: false };
6438
+ }
6439
+ }
6440
+ };
6441
+ function provideIdentity(ctx, deps) {
6442
+ const service = new IdentityProvider(deps);
6443
+ const dispose = ctx.provide(HIWORK_IDENTITY_SERVICE, service);
6444
+ return typeof dispose === "function" ? dispose : () => {
6445
+ };
6446
+ }
6447
+
6448
+ // src/memory-config.ts
6449
+ var HIWORK_MEMORY_SERVICE = "hiworkMemory";
6450
+ var ME_MEMORY_CONFIG_PATH = "/api/me/memory-config";
6451
+ var MEMORY_CONFIG_TTL_MS = 5 * 60 * 1e3;
6452
+ var MEMORY_CONFIG_TIMEOUT_MS = 1e4;
6453
+ var MESSAGE_NOT_CONFIGURED = "\u7BA1\u7406\u5458\u8FD8\u6CA1\u5F00\u542F\u56E2\u961F\u8BB0\u5FC6\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5F00\u901A";
6454
+ var MESSAGE_UNAUTHENTICATED = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
6455
+ var MESSAGE_UNREACHABLE = "\u8BB0\u5FC6\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6456
+ var MESSAGE_NO_GATEWAY = "\u8FD8\u6CA1\u8FDE\u4E0A\u4F01\u4E1A\u7F51\u5173\uFF0C\u8BF7\u5148\u767B\u5F55\u540E\u518D\u8BD5";
6457
+ var MESSAGE_GENERIC = "\u8BB0\u5FC6\u914D\u7F6E\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
6458
+ function messageFor(reason) {
6459
+ switch (reason) {
6460
+ case "not-configured":
6461
+ return MESSAGE_NOT_CONFIGURED;
6462
+ case "unauthenticated":
6463
+ return MESSAGE_UNAUTHENTICATED;
6464
+ case "unreachable":
6465
+ return MESSAGE_UNREACHABLE;
6466
+ case "error":
6467
+ return MESSAGE_GENERIC;
6468
+ }
6469
+ }
6470
+ function failure9(reason, message) {
6471
+ return { ok: false, reason, message: message ?? messageFor(reason) };
6472
+ }
6473
+ function textOrUndefined6(value) {
6474
+ if (typeof value !== "string") return void 0;
6475
+ const text = value.trim();
6476
+ return text === "" ? void 0 : text;
6477
+ }
6478
+ function teamIdsOf(value) {
6479
+ if (!Array.isArray(value)) return [];
6480
+ const ids = [];
6481
+ for (const item of value) {
6482
+ const text = textOrUndefined6(item);
6483
+ if (text !== void 0) ids.push(text);
6484
+ }
6485
+ return ids;
6486
+ }
6487
+ function projectMemoryConfig(body) {
6488
+ const panelUrl = textOrUndefined6(body["panel_url"] ?? body["panelUrl"]);
6489
+ const serviceId = textOrUndefined6(body["service_id"] ?? body["serviceId"]);
6490
+ const apiKey = textOrUndefined6(body["api_key"] ?? body["apiKey"] ?? body["userKey"]);
6491
+ if (panelUrl === void 0 || serviceId === void 0 || apiKey === void 0) return void 0;
6492
+ return {
6493
+ ok: true,
6494
+ panelUrl,
6495
+ serviceId,
6496
+ teamIds: teamIdsOf(body["team_ids"] ?? body["teamIds"]),
6497
+ apiKey
6498
+ };
6499
+ }
6500
+ function errorName2(error) {
6501
+ return error instanceof Error ? error.name : typeof error;
6502
+ }
6503
+ function reasonOfErrorCode(error) {
6504
+ const code = error?.code;
6505
+ if (typeof code !== "string") return void 0;
6506
+ switch (code) {
6507
+ case "SESSION_EXPIRED":
6508
+ return "unauthenticated";
6509
+ case "MEMORY_NOT_CONFIGURED":
6510
+ return "not-configured";
6511
+ case "MEMORY_UNREACHABLE":
6512
+ case "GATEWAY_UNREACHABLE":
6513
+ return "unreachable";
6514
+ default:
6515
+ return void 0;
6516
+ }
6517
+ }
6518
+ function reasonOfStatus(status) {
6519
+ if (status === 401) return "unauthenticated";
6520
+ if (status === 503) return "not-configured";
6521
+ if (status === 504) return "unreachable";
6522
+ return "error";
6523
+ }
6524
+ var MemoryConfigProvider = class {
6525
+ constructor(deps) {
6526
+ this.deps = deps;
6527
+ }
6528
+ cached;
6529
+ async config(opts) {
6530
+ const force = opts?.force === true;
6531
+ let sessionCookie;
6532
+ try {
6533
+ sessionCookie = await this.deps.resolveSession();
6534
+ } catch (error) {
6535
+ this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6536
+ return failure9("error");
6537
+ }
6538
+ if (sessionCookie === void 0 || sessionCookie === "") return failure9("unauthenticated");
6539
+ if (!force) {
6540
+ const hit = this.cached;
6541
+ if (hit !== void 0 && hit.sessionCookie === sessionCookie && this.now() - hit.at < MEMORY_CONFIG_TTL_MS) {
6542
+ return hit.view;
6543
+ }
6544
+ }
6545
+ const gateway = this.deps.gateway();
6546
+ if (gateway === void 0) return failure9("unreachable", MESSAGE_NO_GATEWAY);
6547
+ let outcome;
6548
+ try {
6549
+ outcome = await gateway.getMyMemoryConfig(sessionCookie);
6550
+ } catch (error) {
6551
+ const mapped = reasonOfErrorCode(error);
6552
+ if (mapped !== void 0) return failure9(mapped);
6553
+ this.deps.log?.(`hiwork-core: \u8BB0\u5FC6\u914D\u7F6E\u83B7\u53D6\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6554
+ return failure9("error");
6555
+ }
6556
+ if (!outcome.ok) {
6557
+ if (outcome.kind === "network") return failure9("unreachable");
6558
+ if (outcome.kind === "http") return failure9(reasonOfStatus(outcome.status));
6559
+ return failure9("error");
6560
+ }
6561
+ const view = projectMemoryConfig(outcome.body);
6562
+ if (view === void 0) {
6563
+ this.deps.log?.("hiwork-core: \u7F51\u5173\u8FD4\u56DE\u7684 memory-config \u4E0D\u5B8C\u6574\uFF08\u7F3A panel_url / service_id / api_key \u4E4B\u4E00\uFF09");
6564
+ return failure9("error");
6565
+ }
6566
+ this.cached = { sessionCookie, view, at: this.now() };
6567
+ return view;
6568
+ }
6569
+ now() {
6570
+ return (this.deps.now ?? Date.now)();
6571
+ }
6572
+ };
6573
+ function provideMemoryConfig(ctx, deps) {
6574
+ const service = new MemoryConfigProvider(deps);
6575
+ const dispose = ctx.provide(HIWORK_MEMORY_SERVICE, service);
6576
+ return typeof dispose === "function" ? dispose : () => {
6577
+ };
6578
+ }
6579
+ function createMemoryConfigGateway(baseURL, opts = {}) {
6580
+ const root = baseURL.replace(/\/+$/, "");
6581
+ const timeoutMs = opts.timeoutMs ?? MEMORY_CONFIG_TIMEOUT_MS;
6582
+ const doFetch = opts.fetchImpl ?? fetch;
6583
+ return {
6584
+ async getMyMemoryConfig(sessionCookie) {
6585
+ try {
6586
+ const res = await doFetch(`${root}${ME_MEMORY_CONFIG_PATH}`, {
6587
+ method: "GET",
6588
+ headers: {
6589
+ "user-agent": `hiwork-core/${PKG_VERSION}`,
6590
+ cookie: sessionCookie
6591
+ },
6592
+ signal: AbortSignal.timeout(timeoutMs)
6593
+ });
6594
+ if (!res.ok) return { ok: false, kind: "http", status: res.status };
6595
+ let body;
6596
+ try {
6597
+ body = await res.json();
6598
+ } catch {
6599
+ return { ok: false, kind: "invalid" };
6600
+ }
6601
+ if (body === null || typeof body !== "object") return { ok: false, kind: "invalid" };
6602
+ return { ok: true, body };
6603
+ } catch {
6604
+ return { ok: false, kind: "network" };
6605
+ }
6606
+ }
6607
+ };
6608
+ }
6609
+
6388
6610
  // src/office-credentials.ts
6389
6611
  var OFFICE_CREDENTIALS_SERVICE = "hiworkOfficeCredentials";
6390
6612
  var OFFICE_MESSAGE_FALLBACK = {
@@ -6664,7 +6886,7 @@ function parseDeviceInfo(message) {
6664
6886
  ...version === void 0 || version === "" ? {} : { appVersion: version }
6665
6887
  };
6666
6888
  }
6667
- function errorName2(error) {
6889
+ function errorName3(error) {
6668
6890
  return error instanceof Error ? error.name : typeof error;
6669
6891
  }
6670
6892
  function registerIpcMessageHandler(ports) {
@@ -6691,12 +6913,12 @@ function registerIpcMessageHandler(ports) {
6691
6913
  authInFlight = true;
6692
6914
  try {
6693
6915
  void Promise.resolve(ports.complete(input)).catch((error) => {
6694
- log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5904\u7406\u5F02\u5E38\uFF08${errorName2(error)}\uFF09`);
6916
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5904\u7406\u5F02\u5E38\uFF08${errorName3(error)}\uFF09`);
6695
6917
  }).finally(() => {
6696
6918
  authInFlight = false;
6697
6919
  });
6698
6920
  } catch (error) {
6699
- log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5206\u53D1\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6921
+ log(`hiwork-core: \u6D4F\u89C8\u5668\u6388\u6743\u56DE\u8C03\u5206\u53D1\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
6700
6922
  authInFlight = false;
6701
6923
  }
6702
6924
  return;
@@ -6744,12 +6966,12 @@ async function projectLoginMode(opts) {
6744
6966
  }
6745
6967
  }
6746
6968
  } catch (error) {
6747
- opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C\u767B\u5F55\u65B9\u5F0F\u56DE\u9000 both\uFF08${errorName2(error)}\uFF09`);
6969
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u62C9\u53D6\u5931\u8D25\uFF0C\u767B\u5F55\u65B9\u5F0F\u56DE\u9000 both\uFF08${errorName3(error)}\uFF09`);
6748
6970
  }
6749
6971
  try {
6750
6972
  await opts.state.patch({ set: { loginMode, passwordLogin } });
6751
6973
  } catch (error) {
6752
- opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u5199\u5165\u5931\u8D25\uFF08${errorName2(error)}\uFF09`);
6974
+ opts.log?.(`hiwork-core: \u5BA2\u6237\u7AEF\u7B56\u7565\u5199\u5165\u5931\u8D25\uFF08${errorName3(error)}\uFF09`);
6753
6975
  }
6754
6976
  return loginMode;
6755
6977
  }
@@ -6995,6 +7217,22 @@ function apply(ctx, config) {
6995
7217
  ctx.logger.warn(message);
6996
7218
  }
6997
7219
  }), "hiwork-core: office credentials");
7220
+ ctx.effect(() => provideIdentity(ctx, {
7221
+ current: () => current(),
7222
+ log: (message) => {
7223
+ ctx.logger.warn(message);
7224
+ }
7225
+ }), "hiwork-core: identity service");
7226
+ ctx.effect(() => provideMemoryConfig(ctx, {
7227
+ resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
7228
+ gateway: () => {
7229
+ const baseURL = current().baseURL;
7230
+ return baseURL === void 0 || baseURL === "" ? void 0 : createMemoryConfigGateway(baseURL);
7231
+ },
7232
+ log: (message) => {
7233
+ ctx.logger.warn(message);
7234
+ }
7235
+ }), "hiwork-core: memory config service");
6998
7236
  ctx.effect(() => provideOfficeRelay(ctx, {
6999
7237
  resolveSession: async () => (await ports.credentials.resolve(SESSION_REF))?.value,
7000
7238
  gateway: () => {
@@ -0,0 +1,266 @@
1
+ /**
2
+ * 记忆系统配置下发的只读 host 服务(`hiworkMemory`)。
3
+ *
4
+ * 背景:HiWork 给员工接了一套「记忆系统」(腾讯 TencentDB Agent Memory)。规范是
5
+ * **员工零操作**——记忆的服务地址、实例 ID、团队 ID、凭据**全部由网关下发**,插件
6
+ * 不得让员工填任何技术字段。本模块是这条链中间的那一环:
7
+ *
8
+ * ```
9
+ * hiwork-admin GET /api/me/memory-config ← 网关侧(provision + 下发)
10
+ * ↑ 本文件(hiwork-core,用登录会话调它)
11
+ * hiwork-memory ctx.get('hiworkMemory').config() ← 插件侧
12
+ * ```
13
+ *
14
+ * 设计要点(形状照 `src/identity.ts`,出网照 `src/ai-router-client.ts`):
15
+ * - **只读、失败不抛异常**:一律返回 `{ ok: false, reason, message }`,reason 四取值
16
+ * `not-configured` / `unauthenticated` / `unreachable` / `error`。失败是**数据**不是
17
+ * 异常:插件侧按 `ok` 判断,绝不会因为宿主的一次网络抖动把异常漏进会话。
18
+ * - **字段名分层只在本文件做**:网关那层是 **snake_case**(`panel_url` /
19
+ * `service_id` / `team_ids` / `api_key`,跨仓冻结契约),本文件把它转成 camelCase 的
20
+ * {@link HiworkMemoryConfigView} 再对外暴露——插件侧**永远不感知上游命名**。
21
+ * - **凭据只在内存里活**:`apiKey` 不写 settings、不写任何 storage、不写任何缓存文件
22
+ * (与 `src/office-credentials.ts` 同口径),也**不进任何日志**(含调试日志与错误
23
+ * 日志)——本模块只记事件与异常类型名,绝不记字段值。
24
+ * - **5 分钟内存缓存**:命中即不出网(省一次往返,也少一次网关侧的 provision 幂等压力);
25
+ * `force: true` 绕过缓存(「重新获取配置」与 401 自愈走这条)。缓存与**会话 cookie
26
+ * 绑定**:换了登录账号(登出再登入)时旧缓存直接失效,绝不把上一个员工的凭据发给
27
+ * 下一个员工(粘错 = 串号)。
28
+ * - **失败不进缓存**:拉不到时下一次调用重新出网——否则一次网关抖动会把员工锁在
29
+ * 「记忆不可用」里整整一个 TTL。
30
+ * - **message 是给非技术员工看的中文**:不含凭据、不含 URL 原文、不含 HTTP 状态码。
31
+ *
32
+ * 本模块不 import cordis 运行时:provide 走结构化的 {@link MemoryConfigContext} 端口,
33
+ * 调用方(index.ts)传真实 ctx;单测直接伪造。
34
+ */
35
+ import { PKG_VERSION } from "./version.js";
36
+ /** cordis 服务名(hiwork-memory 的 Host 半边按此名 `ctx.get` 探测)。 */
37
+ export const HIWORK_MEMORY_SERVICE = 'hiworkMemory';
38
+ /** 网关侧端点(跨仓冻结;与 `/api/me/office-config` 同一条会话鉴权)。 */
39
+ export const ME_MEMORY_CONFIG_PATH = '/api/me/memory-config';
40
+ /** 成功结果的缓存 TTL:5 分钟。 */
41
+ export const MEMORY_CONFIG_TTL_MS = 5 * 60 * 1000;
42
+ /** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
43
+ export const MEMORY_CONFIG_TIMEOUT_MS = 10_000;
44
+ /** 失败文案(给非技术员工看的中文;不含凭据、URL 原文、HTTP 状态码)。 */
45
+ const MESSAGE_NOT_CONFIGURED = '管理员还没开启团队记忆,请联系管理员开通';
46
+ const MESSAGE_UNAUTHENTICATED = '登录状态已过期,请重新登录';
47
+ const MESSAGE_UNREACHABLE = '记忆服务暂时不可用,请稍后重试';
48
+ const MESSAGE_NO_GATEWAY = '还没连上企业网关,请先登录后再试';
49
+ const MESSAGE_GENERIC = '记忆配置暂时无法获取,请稍后重试';
50
+ /** 按 reason 取文案(未登录的两条路径共用同一句,避免措辞分叉)。 */
51
+ function messageFor(reason) {
52
+ switch (reason) {
53
+ case 'not-configured': return MESSAGE_NOT_CONFIGURED;
54
+ case 'unauthenticated': return MESSAGE_UNAUTHENTICATED;
55
+ case 'unreachable': return MESSAGE_UNREACHABLE;
56
+ case 'error': return MESSAGE_GENERIC;
57
+ }
58
+ }
59
+ /** 失败视图(**只带 reason 与 message**,绝不带半个字段)。 */
60
+ function failure(reason, message) {
61
+ return { ok: false, reason, message: message ?? messageFor(reason) };
62
+ }
63
+ /** 裁掉两侧空白;非字符串或空白串一律视同缺省。 */
64
+ function textOrUndefined(value) {
65
+ if (typeof value !== 'string')
66
+ return undefined;
67
+ const text = value.trim();
68
+ return text === '' ? undefined : text;
69
+ }
70
+ /**
71
+ * 读团队列表:只保留非空字符串,**保持网关下发的顺序**(按优先级排好的,插件取第一个)。
72
+ * `[]` / 缺省 / 形状非法一律视同「只用个人域」——**不是错误**(spec §4.4 消费规则 3)。
73
+ */
74
+ function teamIdsOf(value) {
75
+ if (!Array.isArray(value))
76
+ return [];
77
+ const ids = [];
78
+ for (const item of value) {
79
+ const text = textOrUndefined(item);
80
+ if (text !== undefined)
81
+ ids.push(text);
82
+ }
83
+ return ids;
84
+ }
85
+ /**
86
+ * snake_case → camelCase 投影(**全仓唯一做这层转换的地方**)。
87
+ *
88
+ * 三个必需字段(`panel_url` / `service_id` / `api_key`)任一缺失即返回 undefined:
89
+ * 网关 200 却缺字段 = 契约破坏,宁可报「暂时无法获取」,也绝不把半份配置交给终端
90
+ * (半份配置的下场是插件拿空地址去打请求,错误信息离真正的原因十万八千里)。
91
+ *
92
+ * 同时宽容接受 camelCase 别名(`panelUrl` / `serviceId` / `apiKey` / `userKey`):
93
+ * 兄弟仓的 spec 里这两个名字都出现过(`hiwork-admin` 的
94
+ * `docs/superpowers/specs/2026-09-18-memory-provisioning-design.md` §4.1 写的是 camelCase
95
+ * 且字段名为 `userKey`),读取端宽容不会掩盖错误,却能让本服务在两种落地之间都能工作。
96
+ * **写出永远是 camelCase 视图**——插件侧不感知任何一种上游命名。
97
+ */
98
+ function projectMemoryConfig(body) {
99
+ const panelUrl = textOrUndefined(body['panel_url'] ?? body['panelUrl']);
100
+ const serviceId = textOrUndefined(body['service_id'] ?? body['serviceId']);
101
+ const apiKey = textOrUndefined(body['api_key'] ?? body['apiKey'] ?? body['userKey']);
102
+ if (panelUrl === undefined || serviceId === undefined || apiKey === undefined)
103
+ return undefined;
104
+ return {
105
+ ok: true,
106
+ panelUrl,
107
+ serviceId,
108
+ teamIds: teamIdsOf(body['team_ids'] ?? body['teamIds']),
109
+ apiKey,
110
+ };
111
+ }
112
+ /** 异常类型名(日志只记类型,不记消息,避免把凭据/URL 带进日志)。 */
113
+ function errorName(error) {
114
+ return error instanceof Error ? error.name : typeof error;
115
+ }
116
+ /**
117
+ * 把「会抛错的网关实现」的稳定 code 映射成 reason。
118
+ *
119
+ * 本模块自带的出网端口({@link createMemoryConfigGateway})**从不抛错**,这一支是为了
120
+ * 容忍接线侧传进来的其它实现(例如将来把本端点加进 `AiRouterClient` 后直接复用
121
+ * `gatewayFor(baseURL)`):按结构化 `code` 字段判别,不 import 对方的类,也不读 message
122
+ * (message 里可能带 URL 原文)。
123
+ */
124
+ function reasonOfErrorCode(error) {
125
+ const code = error?.code;
126
+ if (typeof code !== 'string')
127
+ return undefined;
128
+ switch (code) {
129
+ case 'SESSION_EXPIRED': return 'unauthenticated';
130
+ case 'MEMORY_NOT_CONFIGURED': return 'not-configured';
131
+ case 'MEMORY_UNREACHABLE':
132
+ case 'GATEWAY_UNREACHABLE': return 'unreachable';
133
+ default: return undefined;
134
+ }
135
+ }
136
+ /** 非 200 状态码 → reason(逐字对应契约:401 / 503 / 504 各有确定语义)。 */
137
+ function reasonOfStatus(status) {
138
+ if (status === 401)
139
+ return 'unauthenticated';
140
+ // 网关 503 `MEMORY_NOT_CONFIGURED`:管理员还没开启团队记忆(部署状态,不是故障)。
141
+ if (status === 503)
142
+ return 'not-configured';
143
+ // 网关 504 `MEMORY_UNREACHABLE`:记忆服务自己暂时不可用——语义上就是「够不着」。
144
+ if (status === 504)
145
+ return 'unreachable';
146
+ return 'error';
147
+ }
148
+ /** 视图投影 + 5 分钟内存缓存(无状态依赖,只有一条内存缓存)。 */
149
+ class MemoryConfigProvider {
150
+ deps;
151
+ cached;
152
+ constructor(deps) {
153
+ this.deps = deps;
154
+ }
155
+ async config(opts) {
156
+ const force = opts?.force === true;
157
+ let sessionCookie;
158
+ try {
159
+ sessionCookie = await this.deps.resolveSession();
160
+ }
161
+ catch (error) {
162
+ // 凭据服务读取失败是宿主故障,不是「没登录」:如实报 error,不诱导用户反复重登。
163
+ this.deps.log?.(`hiwork-core: 记忆配置获取失败(${errorName(error)})`);
164
+ return failure('error');
165
+ }
166
+ if (sessionCookie === undefined || sessionCookie === '')
167
+ return failure('unauthenticated');
168
+ if (!force) {
169
+ const hit = this.cached;
170
+ if (hit !== undefined && hit.sessionCookie === sessionCookie && this.now() - hit.at < MEMORY_CONFIG_TTL_MS) {
171
+ return hit.view;
172
+ }
173
+ }
174
+ const gateway = this.deps.gateway();
175
+ // 没有网关地址 = 还没完成登录(baseURL 是登录时写下的),与网络不通归同一档。
176
+ if (gateway === undefined)
177
+ return failure('unreachable', MESSAGE_NO_GATEWAY);
178
+ let outcome;
179
+ try {
180
+ outcome = await gateway.getMyMemoryConfig(sessionCookie);
181
+ }
182
+ catch (error) {
183
+ const mapped = reasonOfErrorCode(error);
184
+ if (mapped !== undefined)
185
+ return failure(mapped);
186
+ this.deps.log?.(`hiwork-core: 记忆配置获取失败(${errorName(error)})`);
187
+ return failure('error');
188
+ }
189
+ if (!outcome.ok) {
190
+ if (outcome.kind === 'network')
191
+ return failure('unreachable');
192
+ if (outcome.kind === 'http')
193
+ return failure(reasonOfStatus(outcome.status));
194
+ return failure('error');
195
+ }
196
+ const view = projectMemoryConfig(outcome.body);
197
+ if (view === undefined) {
198
+ // 只记事件与字段名,不记任何值(响应体里带凭据)。
199
+ this.deps.log?.('hiwork-core: 网关返回的 memory-config 不完整(缺 panel_url / service_id / api_key 之一)');
200
+ return failure('error');
201
+ }
202
+ // 只缓存成功结果:拉不到时下一次调用重新出网(一次抖动不该锁住一个 TTL)。
203
+ this.cached = { sessionCookie, view, at: this.now() };
204
+ return view;
205
+ }
206
+ now() {
207
+ return (this.deps.now ?? Date.now)();
208
+ }
209
+ }
210
+ /**
211
+ * 提供 host 侧服务 `hiworkMemory`;返回 disposer(交给 `ctx.effect` 管理生命周期,
212
+ * 插件卸载时服务随 fiber 一并注销)。
213
+ */
214
+ export function provideMemoryConfig(ctx, deps) {
215
+ const service = new MemoryConfigProvider(deps);
216
+ const dispose = ctx.provide(HIWORK_MEMORY_SERVICE, service);
217
+ return typeof dispose === 'function' ? dispose : () => { };
218
+ }
219
+ /**
220
+ * 本端点的出网实现(形状与 `AiRouterClient` 的 `request()` 同款:user-agent 带包版本、
221
+ * 会话 cookie 走 `cookie` 头、超时用 `AbortSignal.timeout`)。
222
+ *
223
+ * **不新造鉴权**:只复用 Host 侧已有的登录会话 cookie(由 `resolveSession` 从凭据库取),
224
+ * 没有任何密钥/令牌在这一层生成或保存。
225
+ *
226
+ * **永不抛异常、绝不回显响应原文**:失败收敛成 {@link MemoryConfigOutcome},日志与
227
+ * 异常里都不会出现 URL、状态码或响应体——响应体里带的是该员工自己的记忆凭据。
228
+ *
229
+ * `fetchImpl` / `timeoutMs` 仅用于测试注入。
230
+ */
231
+ export function createMemoryConfigGateway(baseURL, opts = {}) {
232
+ const root = baseURL.replace(/\/+$/, '');
233
+ const timeoutMs = opts.timeoutMs ?? MEMORY_CONFIG_TIMEOUT_MS;
234
+ const doFetch = opts.fetchImpl ?? fetch;
235
+ return {
236
+ async getMyMemoryConfig(sessionCookie) {
237
+ try {
238
+ const res = await doFetch(`${root}${ME_MEMORY_CONFIG_PATH}`, {
239
+ method: 'GET',
240
+ headers: {
241
+ 'user-agent': `hiwork-core/${PKG_VERSION}`,
242
+ cookie: sessionCookie,
243
+ },
244
+ signal: AbortSignal.timeout(timeoutMs),
245
+ });
246
+ if (!res.ok)
247
+ return { ok: false, kind: 'http', status: res.status };
248
+ let body;
249
+ try {
250
+ body = await res.json();
251
+ }
252
+ catch {
253
+ // 非 JSON 响应:只报「不可解析」,不把响应片段带进任何返回值或日志。
254
+ return { ok: false, kind: 'invalid' };
255
+ }
256
+ if (body === null || typeof body !== 'object')
257
+ return { ok: false, kind: 'invalid' };
258
+ return { ok: true, body };
259
+ }
260
+ catch {
261
+ // 超时(AbortSignal.timeout → TimeoutError)/ 连接失败 / DNS:一律按不可达。
262
+ return { ok: false, kind: 'network' };
263
+ }
264
+ },
265
+ };
266
+ }
@@ -158,8 +158,16 @@ export declare function browserLoginViewOf(value: AiRouterCardSettings | undefin
158
158
  browserLoginPending: boolean;
159
159
  };
160
160
  /**
161
- * 打开授权页(渲染层职责):桌面端 `shell.openExternal` 路由接管 https 外链,桌面端无需改动。
162
- * 命名空间值可被任意写入,因此只放行 http(s),其余静默忽略。
161
+ * 在系统浏览器里打开外链(渲染层职责):桌面端 `shell.openExternal` 路由接管 https 外链,
162
+ * 桌面端无需改动。
163
+ *
164
+ * 只放行 http(s):调用方给的可能是命名空间里被任意写入的值,也可能是将来变成用户可填的常量,
165
+ * `javascript:` / `file:` 之类一律静默忽略。URL 非法同样 no-op —— 外链打开失败不该抛错打断调用方。
166
+ * @param url - 目标地址;缺省 / 非 http(s) / 非法时为 no-op。
167
+ */
168
+ export declare function openExternalUrl(url: string | undefined): void;
169
+ /**
170
+ * 打开授权页。
163
171
  * @param url - Host 回写的授权页 URL;缺省 / 非法时为 no-op。
164
172
  */
165
173
  export declare function openAuthorizeUrl(url: string | undefined): void;
@@ -72,8 +72,10 @@ export declare function decorateDailyCard(doc: Document, quotes: DailyQuoteSourc
72
72
  export declare function removeStaleDailyCard(doc: Document): void;
73
73
  /**
74
74
  * 观察新建任务 hero 页并装饰同页组件:每日一句(服务端下发)+ hero 问候语轮换
75
- * (见 hero-greeting.ts)。DOM 变更按帧合并,避免流式输出时每个 token 都全文档扫描;
76
- * 页面完全空闲跨午夜时 MutationObserver 不会触发,靠周期轮询兜底换日(本地日变更
77
- * 触发重新拉取,服务端日期随之更新)。返回卸载清理函数。
75
+ * (见 hero-greeting.ts)+ 输入框右上方居右的「使用手册」问号按钮(见 manual-link.ts)。
76
+ * DOM 变更按帧合并,避免流式输出时每个 token 都全文档扫描;页面完全空闲跨午夜时
77
+ * MutationObserver 不会触发,靠周期轮询兜底换日(本地日变更触发重新拉取,服务端日期
78
+ * 随之更新)。三个装饰共用一个观察循环,不各自起 MutationObserver。
79
+ * 返回卸载清理函数。
78
80
  */
79
81
  export declare function observeHeroDecorations(doc: Document, quotes: DailyQuoteSource): () => void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 使用手册地址 —— **全仓唯一一处**写死这个域名的地方(与 pms-client 的 PMS_BASE_URL 同一约定)。
3
+ *
4
+ * 为什么固定而不是配置项:官网是组织自建且只有这一个生产部署,做成设置项只会多一个能填错、
5
+ * 又没人能集中改的地方;真要换部署,改这一个常量、发一版即可。**任何别处都不许再写这个域名**
6
+ * (包括错误消息、日志、文档示例之外的代码),否则「唯一的基址」立刻变成两处能漂移的真相。
7
+ */
8
+ export declare const MANUAL_URL = "https://hiworkapp.hivery.cn/manual.html";
9
+ export declare const MANUAL_STYLE_ID = "hiwork-core-manual-style";
10
+ /**
11
+ * 问号按钮:28×28 的圆形热区、无边框(与输入框自身的图标按钮同语言),hover 才浮出一层浅底。
12
+ * `margin-left:auto` 是「居右」的全部机制——该行是 flex 容器,自动外边距吃掉剩余空间。
13
+ * `flex:none` 保证窄窗口下被两个 chip 挤扁时不至于缩成一个点。
14
+ */
15
+ export declare const MANUAL_STYLE = "\n[data-hiwork-core-manual-entry]{box-sizing:border-box;flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:auto;padding:0;border:0;border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary,#71717a);font-family:inherit;font-size:14px;line-height:1;font-weight:600;cursor:pointer;user-select:none;transition:background .16s ease,color .16s ease}\n[data-hiwork-core-manual-entry]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(24,24,27,.06));color:var(--dsw-alias-label-secondary,#3f3f46)}\n[data-hiwork-core-manual-entry]:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#4c8dff);outline-offset:2px}\nbody[data-ds-dark-theme] [data-hiwork-core-manual-entry]{color:var(--dsw-alias-label-tertiary,#9ba3b4)}\nbody[data-ds-dark-theme] [data-hiwork-core-manual-entry]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));color:var(--dsw-alias-label-secondary,#a6adb8)}\n@media (prefers-reduced-motion: reduce){[data-hiwork-core-manual-entry]{transition:none}}\n";
16
+ export declare function ensureManualStyle(doc: Document): void;
17
+ /** 在系统浏览器里打开使用手册(桌面端 shell 接管 https 外链)。 */
18
+ export declare function openManual(): void;
19
+ /**
20
+ * hero 页里 workspace 行的定位:只在 `[data-phase="hero"]` 下找。
21
+ * 类名后缀稳定、前缀是构建哈希,故按 `*=` 匹配(同 daily-card 的 `[class$="_headline"]`)。
22
+ * 行本身缺失(非 hero 页 / 宿主改版)→ null,调用方只做清理。
23
+ */
24
+ export declare function findManualMount(doc: Document): HTMLElement | null;
25
+ /** 清掉非 hero 状态下(以及行被重建后)残留的按钮节点。 */
26
+ export declare function removeStaleManualEntry(doc: Document): void;
27
+ /**
28
+ * 幂等装饰:把问号按钮挂到 hero 输入框那一行的末尾(`margin-left:auto` 使其居右)。
29
+ *
30
+ * 已挂着且仍在该行内 → 不动(宿主流式重渲染时不做无谓的摘挂,避免按钮闪一下);
31
+ * 行不在场(会话记录页 / 登录门禁 / 宿主未渲染)→ 清掉残留。
32
+ * 只在**最后一个**子节点插入:宿主自己的 chip 顺序不受影响,将来它再往行里加东西,
33
+ * 我们的按钮仍在右侧。
34
+ */
35
+ export declare function decorateManualEntry(doc: Document): void;
@@ -0,0 +1,64 @@
1
+ import type { GatewayKey } from './ai-router-client.ts';
2
+ /** cordis 服务名(hiwork-memory 等消费方按此名 `ctx.get` 探测)。 */
3
+ export declare const HIWORK_IDENTITY_SERVICE = "hiworkIdentity";
4
+ /** 当前登录身份的稳定视图;未登录是 `{ loggedIn: false }`(其余字段可省略)。 */
5
+ export interface HiworkIdentityView {
6
+ /** 是否已登录(有账号名即视为已登录)。false 时其余字段无意义。 */
7
+ loggedIn: boolean;
8
+ /**
9
+ * 隔离键:`u_` + `sha256(账号名)` 前 16 位 hex。
10
+ * **恒不是账号名原文**;同一账号恒等、不同账号不同。消费方把它当不透明字符串用。
11
+ */
12
+ subject?: string;
13
+ /** 登录账号(网关 LDAP 账号名);供界面显示,**不得**当隔离键用。 */
14
+ username?: string;
15
+ /** 账号显示名(中文名);命名空间里没有时缺省(界面按既有约定回退 username)。 */
16
+ displayName?: string;
17
+ /** 员工姓名(来自 keys 的 `employee_name`);还没有 keys 或缺省时为 undefined。 */
18
+ employeeName?: string;
19
+ /** 团队名(来自 keys 的 `team_name`);还没有 keys 或缺省时为 undefined。 */
20
+ teamName?: string;
21
+ }
22
+ /** 本服务对宿主的可调用面(只读、同步、永不抛异常)。 */
23
+ export interface HiworkIdentityService {
24
+ /** 当前登录身份的稳定视图;未登录返回 { loggedIn: false }。 */
25
+ current(): HiworkIdentityView;
26
+ }
27
+ /** 身份的原始来源:`ai-router` 命名空间的相关字段 + 最近一次拉取到的 keys 列表。 */
28
+ export interface IdentityState {
29
+ username?: string | undefined;
30
+ /** 登录账号的显示名(网关 LDAP displayName);缺省时视图里也不出现。 */
31
+ displayName?: string | undefined;
32
+ /** 最近一次拉取到的当前用户全部虚拟 Key(`employee_name` / `team_name` 的来源)。 */
33
+ keys?: readonly GatewayKey[] | undefined;
34
+ }
35
+ /** Host 侧依赖:当前命名空间快照读取 + 日志出口。 */
36
+ export interface IdentityDeps {
37
+ /**
38
+ * 读一次当前 `ai-router` 命名空间值。**每次 `current()` 都会调用**(本服务不缓存);
39
+ * 传 `() => current()` 即可(`AiRouterNamespaceValue` 结构兼容 {@link IdentityState})。
40
+ */
41
+ current(): IdentityState;
42
+ /** Host 日志出口(只记错误类型,不记账号名之外的任何凭据)。 */
43
+ log?: ((message: string) => void) | undefined;
44
+ }
45
+ /** `ctx.provide` 的最小端口(结构兼容 cordis Context,便于单测伪造)。 */
46
+ export interface IdentityContext {
47
+ provide(name: string, value?: unknown): unknown;
48
+ }
49
+ /** 视图字段名的语义名(供消费方日志 / 文档引用,避免各处硬编码字符串)。 */
50
+ export declare const IDENTITY_SUBJECT_PREFIX = "u_";
51
+ /**
52
+ * 由登录账号派生稳定 subject:`u_` + `sha256(账号名).hex` 的前 16 位。
53
+ *
54
+ * 输入先裁掉两侧空白(与视图里的 `username` 同口径),因此 `' alice '` 与 `'alice'`
55
+ * 派生同一个 subject;大小写不折叠——账号名在网关侧就是区分大小写的。
56
+ * **本函数绝不返回账号名原文**:subject 会落进存储键 / 目录名,哈希是隔离键
57
+ * 既能稳定标识同一员工、又不外泄账号的那条界线。
58
+ */
59
+ export declare function deriveSubject(username: string): string;
60
+ /**
61
+ * 提供 host 侧服务 `hiworkIdentity`;返回 disposer(交给 `ctx.effect` 管理生命周期,
62
+ * 插件卸载时服务随 fiber 一并注销)。
63
+ */
64
+ export declare function provideIdentity(ctx: IdentityContext, deps: IdentityDeps): () => void;
@@ -0,0 +1,113 @@
1
+ /** cordis 服务名(hiwork-memory 的 Host 半边按此名 `ctx.get` 探测)。 */
2
+ export declare const HIWORK_MEMORY_SERVICE = "hiworkMemory";
3
+ /** 网关侧端点(跨仓冻结;与 `/api/me/office-config` 同一条会话鉴权)。 */
4
+ export declare const ME_MEMORY_CONFIG_PATH = "/api/me/memory-config";
5
+ /** 成功结果的缓存 TTL:5 分钟。 */
6
+ export declare const MEMORY_CONFIG_TTL_MS: number;
7
+ /** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
8
+ export declare const MEMORY_CONFIG_TIMEOUT_MS = 10000;
9
+ /**
10
+ * 失败原因(稳定枚举,消费方据此决定提示与自愈策略):
11
+ * - `not-configured`:管理员还没开通团队记忆(网关 503 `MEMORY_NOT_CONFIGURED`);
12
+ * - `unauthenticated`:没有可用会话(未登录 / 会话已失效);
13
+ * - `unreachable`:超时、网络不通、网关地址未配置,或记忆服务自身暂时不可用(504);
14
+ * - `error`:其余非 200、响应不符合契约、宿主内部故障等。
15
+ */
16
+ export type HiworkMemoryFailureReason = 'not-configured' | 'unauthenticated' | 'unreachable' | 'error';
17
+ /**
18
+ * 对外视图(camelCase,插件侧唯一接口面):
19
+ * 成功时 `ok: true` 且带 `panelUrl` / `serviceId` / `teamIds` / `apiKey`;
20
+ * 失败时 `ok: false` 且带 `reason` / `message`,**不带任何半个字段**。
21
+ */
22
+ export interface HiworkMemoryConfigView {
23
+ ok: boolean;
24
+ panelUrl?: string;
25
+ serviceId?: string;
26
+ teamIds?: string[];
27
+ apiKey?: string;
28
+ reason?: HiworkMemoryFailureReason;
29
+ message?: string;
30
+ }
31
+ /** 本服务对宿主的可调用面(只读、永不抛异常)。 */
32
+ export interface HiworkMemoryService {
33
+ /** 取当前登录员工的记忆配置;force=true 绕过缓存(用于「重新获取配置」与 401 自愈)。 */
34
+ config(opts?: {
35
+ force?: boolean;
36
+ }): Promise<HiworkMemoryConfigView>;
37
+ }
38
+ /**
39
+ * 网关侧原始响应(**snake_case**,跨仓冻结契约;本文件是唯一读它的地方)。
40
+ * 仅供文档与类型自证——真正的读取走宽容投影 {@link projectMemoryConfig}。
41
+ */
42
+ export interface GatewayMemoryConfig {
43
+ panel_url: string;
44
+ service_id: string;
45
+ /** 按优先级排好;**空数组 / 缺省 = 只用个人域**,不是错误。 */
46
+ team_ids?: string[];
47
+ api_key: string;
48
+ provisioned_at?: string;
49
+ }
50
+ /**
51
+ * 传输层结果:**失败是数据而不是异常**(本模块不依赖调用方捕获异常来判失败)。
52
+ * - `kind: 'http'`:拿到了非 200 状态码;
53
+ * - `kind: 'network'`:超时 / 连接失败 / DNS / 请求被中断;
54
+ * - `kind: 'invalid'`:200 但响应体不是可用的 JSON 对象(契约破坏)。
55
+ */
56
+ export type MemoryConfigOutcome = {
57
+ readonly ok: true;
58
+ readonly body: unknown;
59
+ } | {
60
+ readonly ok: false;
61
+ readonly kind: 'http';
62
+ readonly status: number;
63
+ } | {
64
+ readonly ok: false;
65
+ readonly kind: 'network';
66
+ } | {
67
+ readonly ok: false;
68
+ readonly kind: 'invalid';
69
+ };
70
+ /** 网关客户端的 memory-config 子集:只声明本模块真正调用的方法(便于单测打桩计数)。 */
71
+ export interface MemoryConfigGatewayPort {
72
+ /** 调 `GET /api/me/memory-config`,返回 snake_case 原始响应或一个稳定失败原因。 */
73
+ getMyMemoryConfig(sessionCookie: string): Promise<MemoryConfigOutcome>;
74
+ }
75
+ /** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关出网端口。 */
76
+ export interface MemoryConfigDeps {
77
+ /**
78
+ * 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
79
+ * **返回值只在本模块内部使用**(缓存判等 + 出网),绝不出现在视图与日志里。
80
+ */
81
+ resolveSession(): Promise<string | undefined>;
82
+ /** 当前 baseURL 对应的网关出网端口;baseURL 未配置时返回 undefined。 */
83
+ gateway(): MemoryConfigGatewayPort | undefined;
84
+ /** Host 日志出口(只记事件与异常类型名,绝不记凭据 / URL / 响应内容)。 */
85
+ log?: ((message: string) => void) | undefined;
86
+ /** 时钟注入(测试用);缺省 `Date.now`。 */
87
+ now?: (() => number) | undefined;
88
+ }
89
+ /** `ctx.provide` 的最小端口(结构兼容 cordis Context,便于单测伪造)。 */
90
+ export interface MemoryConfigContext {
91
+ provide(name: string, value?: unknown): unknown;
92
+ }
93
+ /**
94
+ * 提供 host 侧服务 `hiworkMemory`;返回 disposer(交给 `ctx.effect` 管理生命周期,
95
+ * 插件卸载时服务随 fiber 一并注销)。
96
+ */
97
+ export declare function provideMemoryConfig(ctx: MemoryConfigContext, deps: MemoryConfigDeps): () => void;
98
+ /**
99
+ * 本端点的出网实现(形状与 `AiRouterClient` 的 `request()` 同款:user-agent 带包版本、
100
+ * 会话 cookie 走 `cookie` 头、超时用 `AbortSignal.timeout`)。
101
+ *
102
+ * **不新造鉴权**:只复用 Host 侧已有的登录会话 cookie(由 `resolveSession` 从凭据库取),
103
+ * 没有任何密钥/令牌在这一层生成或保存。
104
+ *
105
+ * **永不抛异常、绝不回显响应原文**:失败收敛成 {@link MemoryConfigOutcome},日志与
106
+ * 异常里都不会出现 URL、状态码或响应体——响应体里带的是该员工自己的记忆凭据。
107
+ *
108
+ * `fetchImpl` / `timeoutMs` 仅用于测试注入。
109
+ */
110
+ export declare function createMemoryConfigGateway(baseURL: string, opts?: {
111
+ timeoutMs?: number;
112
+ fetchImpl?: typeof fetch;
113
+ }): MemoryConfigGatewayPort;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiwork-core",
3
- "version": "0.0.19",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)",
6
6
  "license": "UNLICENSED",