hiwork-core 0.0.8 → 0.0.11

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.
@@ -12,6 +12,15 @@ const ME_COST_ESTIMATE_PATH = '/api/me/cost-estimate';
12
12
  /** 每日一句端点:当日条目(东八区取模轮换)与换一句(排除当前条目的随机)。 */
13
13
  const ME_DAILY_QUOTE_PATH = '/api/me/daily-quote';
14
14
  const ME_DAILY_QUOTE_RANDOM_PATH = '/api/me/daily-quote/random';
15
+ /**
16
+ * MCP 分发的自助端点(管理员在 ai-hub 登记 MCP、下发给全员或某人后,员工端从这里拉清单)。
17
+ * 两条都在网关 `/api/me` 会话组下,鉴权是 `SessionAuth`(只读 Cookie)。
18
+ *
19
+ * 清单响应里**没有任何令牌**:客户端拿断言去 MCP 服务自己换 per-user 令牌,
20
+ * 网关全程不经手明文,所以这里也没有「查看我的令牌」这类端点。
21
+ */
22
+ const ME_MCP_ASSIGNMENTS_PATH = '/api/me/mcp-assignments';
23
+ const ME_MCP_ASSERTIONS_PATH = '/api/me/mcp-assertions';
15
24
  /** 授权请求默认值:桌面端 client_id 与自定义协议回调地址(网关侧精确匹配)。 */
16
25
  const DEFAULT_CLIENT_ID = 'hiwork-desktop';
17
26
  const DEFAULT_REDIRECT_URI = 'hiwork://auth/callback';
@@ -208,6 +217,52 @@ export class AiRouterClient {
208
217
  throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
209
218
  return await parseMeJson(res);
210
219
  }
220
+ /**
221
+ * GET /api/me/mcp-assignments:管理员下发给当前账号的 MCP 清单。
222
+ *
223
+ * 不复用 {@link getMeJson}:那条路径把 403 映射成 `NO_KEY_ASSIGNED`(「尚未被分配
224
+ * 虚拟 KEY」),而本端点的鉴权只有会话——一个还没被分配 Key 的账号照样应该有
225
+ * 清单(分 MCP 与分 Key 是两件事)。沿用它的 403 文案会把「没有 Key」错报成原因。
226
+ */
227
+ async getMyMcpAssignments(sessionCookie) {
228
+ const res = await this.requestOrUnreachable(ME_MCP_ASSIGNMENTS_PATH, { method: 'GET', sessionCookie });
229
+ if (res.status === 401)
230
+ throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
231
+ // 403 / 5xx / 反代错误页一律「网关不可达」:这条路径没有需要单独解释的拒绝语义。
232
+ if (!res.ok)
233
+ throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
234
+ return await parseMeJson(res);
235
+ }
236
+ /**
237
+ * POST /api/me/mcp-assertions:为某条分配签一张 120 秒断言。
238
+ *
239
+ * 三种拒绝各有确定含义(服务端 handler 的 404/409/503),必须分开映射——否则
240
+ * 客户端只会看到一句「取断言失败」,既分不清「被撤销了」(该卸连接)还是
241
+ * 「服务停用了」(该重试)还是「部署没配私钥」(找管理员)。
242
+ */
243
+ async issueMyMcpAssertion(sessionCookie, assignmentId) {
244
+ const res = await this.requestOrUnreachable(ME_MCP_ASSERTIONS_PATH, {
245
+ method: 'POST',
246
+ sessionCookie,
247
+ body: { assignmentId },
248
+ });
249
+ if (res.status === 401)
250
+ throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
251
+ // 400 与 404 合并成同一个 code:对客户端而言两者的行为一致(这条 id 用不了了,
252
+ // 只能等下一次成功同步),文案也不该暗示「是你发错了 id」。
253
+ if (res.status === 400 || res.status === 404) {
254
+ throw new GatewayError('MCP_ASSIGNMENT_GONE', '这条分配已失效,稍后同步会重新核对', res.status);
255
+ }
256
+ if (res.status === 409) {
257
+ throw new GatewayError('MCP_NOT_DELIVERABLE', '该 MCP 服务当前不可下发,请联系管理员', 409);
258
+ }
259
+ if (res.status === 503) {
260
+ throw new GatewayError('MCP_ASSERTION_DISABLED', '本部署未启用 MCP 断言签发,请联系管理员', 503);
261
+ }
262
+ if (!res.ok)
263
+ throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
264
+ return await parseMeJson(res);
265
+ }
211
266
  /**
212
267
  * /api/me 自助端点的 GET 公共路径:网络异常 → GATEWAY_UNREACHABLE,
213
268
  * 401/403/400 映射为稳定 code,响应体非 JSON 时不把原文带进异常。
@@ -15,6 +15,8 @@ export const DEFAULT_LOGIN_MODE = 'both';
15
15
  export const CLIENT_POLICY_PATH = '/api/client/policy';
16
16
  /** 策略拉取默认超时:免登录只读接口,超过 5s 视同不可达(fail-open)。 */
17
17
  export const CLIENT_POLICY_TIMEOUT_MS = 5_000;
18
+ /** fail-open 缺省:旧网关不返回 passwordLogin 时,密码表单仍视作可用。 */
19
+ export const DEFAULT_PASSWORD_LOGIN = true;
18
20
  /**
19
21
  * 拼策略 URL:`{baseURL}/api/client/policy?platform=&arch=&version=&device_id=`。
20
22
  * 参数固定顺序、逐个 `encodeURIComponent`(device_id 可能是任意 ASCII,含 `+/=` 也必须安全);
@@ -47,6 +49,16 @@ function parseLoginMode(payload) {
47
49
  const value = payload.loginMode;
48
50
  return value === 'browser' || value === 'form' || value === 'both' ? value : undefined;
49
51
  }
52
+ /**
53
+ * 解析 `passwordLogin`:只认布尔值;缺失 / 非法值一律 {@link DEFAULT_PASSWORD_LOGIN}
54
+ * (fail-open:旧网关不返回该字段,不能因此收起密码入口)。
55
+ */
56
+ function parsePasswordLogin(payload) {
57
+ if (typeof payload !== 'object' || payload === null)
58
+ return DEFAULT_PASSWORD_LOGIN;
59
+ const value = payload.passwordLogin;
60
+ return typeof value === 'boolean' ? value : DEFAULT_PASSWORD_LOGIN;
61
+ }
50
62
  /**
51
63
  * 拉取并解析客户端策略。**永不抛异常**:任何失败都返回 undefined(fail-open)。
52
64
  *
@@ -73,7 +85,7 @@ export async function fetchClientPolicy(opts) {
73
85
  return undefined;
74
86
  }
75
87
  const loginMode = parseLoginMode(payload);
76
- return loginMode === undefined ? undefined : { loginMode };
88
+ return loginMode === undefined ? undefined : { loginMode, passwordLogin: parsePasswordLogin(payload) };
77
89
  }
78
90
  catch {
79
91
  // 网络异常 / 超时(AbortSignal.timeout 抛 TimeoutError)/ 非法 URL:一律 fail-open。
package/lib/client.js CHANGED
@@ -78,7 +78,11 @@ var textInput = {
78
78
  var frameSurfaceSelector = '[class*="_frame"]:has(> [class*="_sidebarCol"])';
79
79
  var frameCss = [
80
80
  `${frameSurfaceSelector} { background: var(--dsw-specific-sidebar-fill, #f9fafb); }`,
81
+ // 原生会话区根节点:两代链路并存,见上方注释。
81
82
  `html[data-dsh-shell="win32"] [class*="_centerCol"] > div > [class*="_root"] { border-top-left-radius: 16px; }`,
83
+ `html[data-dsh-shell="win32"] [class*="_centerCol"] [data-slot="main.conversation"] > [class*="_root"] { border-top-left-radius: 16px; }`,
84
+ // HiWork 扩展页 takeover 根节点(中心列直接子元素)。
85
+ `html[data-dsh-shell="win32"] [class*="_centerCol"] > [data-hiwork-feature-root] { border-top-left-radius: 16px; }`,
82
86
  `[class*="_sidebarCol"] { border-right: 0 !important; }`
83
87
  ].join("\n");
84
88
  var hostHideCss = [
@@ -125,6 +129,9 @@ function authorizeUrlOf(value) {
125
129
  function browserLoginPendingOf(value) {
126
130
  return authorizeUrlOf(value) !== void 0 || value?.browserLoginRequest !== void 0;
127
131
  }
132
+ function passwordLoginOf(value) {
133
+ return value?.passwordLogin !== false;
134
+ }
128
135
  function browserLoginViewOf(value) {
129
136
  const loginMode = normalizeLoginMode(value?.loginMode);
130
137
  if (loginMode === "form") return { loginMode, browserLoginUrl: void 0, browserLoginPending: false };
@@ -162,7 +169,8 @@ function project(value) {
162
169
  budgets: value?.budgets,
163
170
  keys: value?.keys,
164
171
  activeKeyId: value?.keyId ?? value?.keys?.[0]?.id,
165
- ...browserLoginViewOf(value)
172
+ ...browserLoginViewOf(value),
173
+ passwordLogin: passwordLoginOf(value)
166
174
  };
167
175
  if (value?.pendingLogin !== void 0) return { ...base, status: "logging-in" };
168
176
  if (value?.lastLogin !== void 0) {
@@ -189,6 +197,7 @@ var AiRouterCardController = class {
189
197
  keys: void 0,
190
198
  activeKeyId: void 0,
191
199
  loginMode: "both",
200
+ passwordLogin: true,
192
201
  browserLoginUrl: void 0,
193
202
  browserLoginPending: false
194
203
  });
@@ -1016,12 +1025,15 @@ function removeStaleHeroGreeting(doc) {
1016
1025
 
1017
1026
  // src/client/daily-card.ts
1018
1027
  var DAILY_CARD_STYLE_ID = "dcu-daily-card-style";
1028
+ var DAILY_MAIN_FONT_SIZE_MAX = 64;
1029
+ var DAILY_MAIN_FONT_SIZE_MIN = 24;
1019
1030
  var DAILY_CARD_STYLE = `
1020
1031
  [data-dcu-daily-card]{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;width:100%;max-width:min(880px,100%);margin:6px auto 44px;cursor:pointer;user-select:none;text-align:center}
1021
1032
  [data-dcu-daily-prefix]{display:inline-flex;align-items:center;gap:14px;font-size:20px;line-height:1.4;font-weight:500;letter-spacing:.22em;text-indent:.22em;color:rgba(24,24,27,.35);white-space:nowrap}
1022
1033
  [data-dcu-daily-prefix]::before,[data-dcu-daily-prefix]::after{content:"";display:inline-block;width:44px;height:2px;border-radius:1px;background:currentColor;opacity:.5}
1023
1034
  [data-dcu-daily-prefix][hidden]{display:none}
1024
- [data-dcu-daily-main]{margin-top:6px;font-size:64px;line-height:1.25;font-weight:650;letter-spacing:.005em;background:linear-gradient(100deg,#2f6bf0,#1d4ed8 45%,#0e7fc2);-webkit-background-clip:text;background-clip:text;color:transparent;transition:filter .18s ease}
1035
+ [data-dcu-daily-main]{margin-top:6px;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:${DAILY_MAIN_FONT_SIZE_MAX}px;line-height:1.25;font-weight:650;letter-spacing:.005em;background:linear-gradient(100deg,#2f6bf0,#1d4ed8 45%,#0e7fc2);-webkit-background-clip:text;background-clip:text;color:transparent;transition:filter .18s ease}
1036
+ [data-dcu-daily-main][data-dcu-daily-truncated]{text-align:left}
1025
1037
  [data-dcu-daily-card]:hover [data-dcu-daily-main]{filter:brightness(1.12)}
1026
1038
  [data-dcu-daily-card]:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#4c8dff);outline-offset:4px;border-radius:6px}
1027
1039
  [data-dcu-daily-shuffle]{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:5px;margin-top:10px;padding:3px 10px;border-radius:8px;color:var(--dsw-alias-label-tertiary,#71717a);font-size:13px;line-height:20px;letter-spacing:.08em;text-indent:.08em;cursor:pointer;transition:color .18s ease}
@@ -1060,6 +1072,31 @@ function ensureDailyCardStyle(doc) {
1060
1072
  style.textContent = DAILY_CARD_STYLE;
1061
1073
  doc.head.append(style);
1062
1074
  }
1075
+ function solveDailyMainFontSize(availableWidth, measure) {
1076
+ if (!(availableWidth > 0)) return null;
1077
+ if (measure(DAILY_MAIN_FONT_SIZE_MAX) <= availableWidth) return null;
1078
+ if (measure(DAILY_MAIN_FONT_SIZE_MIN) > availableWidth) return DAILY_MAIN_FONT_SIZE_MIN;
1079
+ let low = DAILY_MAIN_FONT_SIZE_MIN;
1080
+ let high = DAILY_MAIN_FONT_SIZE_MAX;
1081
+ while (high - low > 1) {
1082
+ const mid = Math.floor((low + high) / 2);
1083
+ if (measure(mid) <= availableWidth) low = mid;
1084
+ else high = mid;
1085
+ }
1086
+ return low;
1087
+ }
1088
+ function fitDailyMainText(card) {
1089
+ const main = card.querySelector("[data-dcu-daily-main]");
1090
+ if (main === null) return null;
1091
+ const availableWidth = card.clientWidth;
1092
+ const size = solveDailyMainFontSize(availableWidth, (fontSize) => {
1093
+ main.style.fontSize = `${fontSize}px`;
1094
+ return main.scrollWidth;
1095
+ });
1096
+ main.style.fontSize = size === null ? "" : `${size}px`;
1097
+ main.toggleAttribute("data-dcu-daily-truncated", size === DAILY_MAIN_FONT_SIZE_MIN && main.scrollWidth > availableWidth);
1098
+ return size;
1099
+ }
1063
1100
  function findDailyCardMount(doc) {
1064
1101
  const hero = doc.querySelector('[data-phase="hero"]');
1065
1102
  if (hero === null) return null;
@@ -1068,6 +1105,7 @@ function findDailyCardMount(doc) {
1068
1105
  return headline;
1069
1106
  }
1070
1107
  var modalKeyHandlers = /* @__PURE__ */ new WeakMap();
1108
+ var cardHandles = /* @__PURE__ */ new WeakMap();
1071
1109
  function modalHeadline(item) {
1072
1110
  return item.title !== "" ? item.title : item.content;
1073
1111
  }
@@ -1107,6 +1145,36 @@ function buildCard(doc, initial, date, quotes) {
1107
1145
  shuffle.setAttribute("aria-label", "\u6362\u4E00\u53E5");
1108
1146
  shuffle.append(createRefreshIcon(doc), doc.createTextNode("\u6362\u4E00\u53E5"));
1109
1147
  root.append(prefix, main, shuffle);
1148
+ let lastFitWidth = -1;
1149
+ let fitFrame;
1150
+ const fit = (reflow = false) => {
1151
+ const width = root.clientWidth;
1152
+ if (!reflow && width === lastFitWidth) return;
1153
+ lastFitWidth = width;
1154
+ fitDailyMainText(root);
1155
+ };
1156
+ const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1157
+ if (fitFrame !== void 0) return;
1158
+ fitFrame = window.requestAnimationFrame(() => {
1159
+ fitFrame = void 0;
1160
+ fit();
1161
+ });
1162
+ }) : null;
1163
+ if (resizeObserver !== null) resizeObserver.observe(root);
1164
+ void doc.fonts?.ready.then(() => {
1165
+ fit(true);
1166
+ }).catch(() => {
1167
+ });
1168
+ cardHandles.set(root, {
1169
+ fit,
1170
+ dispose: () => {
1171
+ if (resizeObserver !== null) resizeObserver.disconnect();
1172
+ if (fitFrame !== void 0) {
1173
+ window.cancelAnimationFrame(fitFrame);
1174
+ fitFrame = void 0;
1175
+ }
1176
+ }
1177
+ });
1110
1178
  let current = initial;
1111
1179
  const applyItem = (next) => {
1112
1180
  current = next;
@@ -1114,6 +1182,7 @@ function buildCard(doc, initial, date, quotes) {
1114
1182
  prefix.hidden = next.subtitle === "";
1115
1183
  main.textContent = next.content;
1116
1184
  root.setAttribute("aria-label", `\u6BCF\u65E5\u4E00\u53E5\uFF1A${modalHeadline(next)}`);
1185
+ fit(true);
1117
1186
  };
1118
1187
  applyItem(initial);
1119
1188
  root.addEventListener("click", (e) => {
@@ -1228,12 +1297,16 @@ function decorateDailyCard(doc, quotes) {
1228
1297
  removeStaleDailyCard(doc);
1229
1298
  const card = buildCard(doc, view, view.date, quotes);
1230
1299
  parent.insertBefore(card, anchor);
1300
+ cardHandles.get(card)?.fit(true);
1231
1301
  }).catch(() => {
1232
1302
  });
1233
1303
  }
1234
1304
  function removeStaleDailyCard(doc) {
1235
1305
  closeDailyModal(doc);
1236
- for (const stale of Array.from(doc.querySelectorAll("[data-dcu-daily-card]"))) stale.remove();
1306
+ for (const stale of Array.from(doc.querySelectorAll("[data-dcu-daily-card]"))) {
1307
+ cardHandles.get(stale)?.dispose();
1308
+ stale.remove();
1309
+ }
1237
1310
  }
1238
1311
  function observeHeroDecorations(doc, quotes) {
1239
1312
  if (doc.head === null) return () => {
@@ -1450,9 +1523,10 @@ function LoginGateModal(props) {
1450
1523
  }, [props.browserLoginUrl]);
1451
1524
  const showBrowserLogin = props.loginMode !== "form";
1452
1525
  const waiting = showBrowserLogin && props.browserLoginPending;
1453
- const formVisible = props.loginMode === "form" || formOpen;
1526
+ const passwordFormOffered = props.passwordLogin || props.loginMode === "form";
1527
+ const formVisible = props.loginMode === "form" || passwordFormOffered && formOpen;
1454
1528
  const noKey = props.lastError?.code === "NO_KEY_ASSIGNED";
1455
- const error = describeLoginError(props.lastError) ?? submitError;
1529
+ const error = (props.lastError?.code === "AUTH_FAILED" && !passwordFormOffered ? "\u5F53\u524D\u7F51\u5173\u5DF2\u542F\u7528\u7EDF\u4E00\u8BA4\u8BC1\uFF0C\u8BF7\u70B9\u51FB\u300C\u4F7F\u7528\u6D4F\u89C8\u5668\u767B\u5F55\u300D\u5B8C\u6210\u767B\u5F55" : describeLoginError(props.lastError)) ?? submitError;
1456
1530
  const loginDisabled = props.loggingIn || username === "" || password === "";
1457
1531
  const submit = () => {
1458
1532
  if (loginDisabled) return;
@@ -1486,7 +1560,7 @@ function LoginGateModal(props) {
1486
1560
  ] }),
1487
1561
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-brand": true, type: "submit", style: loginDisabled ? loginButtonDisabled : loginButton, disabled: loginDisabled, children: props.loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B \u5F55" }),
1488
1562
  noKey ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-secondary": true, type: "button", style: linkButton, onClick: () => void props.scope.set("refreshRequest", { at: (/* @__PURE__ */ new Date()).toISOString() }), children: "\u6211\u5DF2\u5206\u914D KEY\uFF0C\u5237\u65B0" }) : null
1489
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-secondary": true, "data-hiwork-core-form-toggle": true, type: "button", style: toggleStyle, onClick: () => setFormOpen(true), children: props.loginMode === "browser" ? "\u6D4F\u89C8\u5668\u767B\u5F55\u4E0D\u53EF\u7528\uFF1F\u4F7F\u7528\u8D26\u53F7\u5BC6\u7801\u767B\u5F55" : "\u4F7F\u7528\u8D26\u53F7\u5BC6\u7801\u767B\u5F55" });
1563
+ ] }) : !passwordFormOffered ? null : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { "data-hiwork-core-secondary": true, "data-hiwork-core-form-toggle": true, type: "button", style: toggleStyle, onClick: () => setFormOpen(true), children: props.loginMode === "browser" ? "\u6D4F\u89C8\u5668\u767B\u5F55\u4E0D\u53EF\u7528\uFF1F\u4F7F\u7528\u8D26\u53F7\u5BC6\u7801\u767B\u5F55" : "\u4F7F\u7528\u8D26\u53F7\u5BC6\u7801\u767B\u5F55" });
1490
1564
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: overlayStyle, children: [
1491
1565
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(OceanWaves, {}),
1492
1566
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("main", { id: "hiwork-core-frame", "data-hiwork-core-frame": true, style: contentFrameStyle, children: [
@@ -1553,7 +1627,7 @@ function mountLoginGate(scope, storeCredential) {
1553
1627
  root = (0, import_client.createRoot)(gateContainer);
1554
1628
  if (appRoot !== null) appRoot.inert = true;
1555
1629
  }
1556
- root.render(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LoginGateModal, { scope, storeCredential, lastError: value?.lastError, loggingIn: value?.pendingLogin !== void 0, initialUsername: value?.username, initialRememberPassword: value?.rememberPassword === true, ...browserLoginViewOf(value) }));
1630
+ root.render(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LoginGateModal, { scope, storeCredential, lastError: value?.lastError, loggingIn: value?.pendingLogin !== void 0, initialUsername: value?.username, initialRememberPassword: value?.rememberPassword === true, passwordLogin: passwordLoginOf(value), ...browserLoginViewOf(value) }));
1557
1631
  };
1558
1632
  render();
1559
1633
  const dispose = scope.subscribe(() => render());
@@ -2120,8 +2194,15 @@ function rfc3339Of(time) {
2120
2194
  const date = new Date(time);
2121
2195
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
2122
2196
  }
2123
- function selectSessionCost(owner) {
2124
- return sessionCostMatchOf(owner.turn.data.get("turn-tail"));
2197
+ function closingTurnTailData(snapshot, messageId) {
2198
+ for (const key of snapshot.order) {
2199
+ const node = snapshot.nodes.get(key);
2200
+ if (node === void 0 || node.kind !== "turn-tail") continue;
2201
+ const data = node.data;
2202
+ if (data === void 0) continue;
2203
+ if (data.closing?.finalNode?.messageId === messageId) return data;
2204
+ }
2205
+ return void 0;
2125
2206
  }
2126
2207
  function sessionCostKey(sessionId, match) {
2127
2208
  return `${sessionId}\0${match.turn}\0${JSON.stringify(match.items)}`;
@@ -2178,30 +2259,35 @@ var SessionCostStore = class {
2178
2259
  }
2179
2260
  };
2180
2261
  var costStyle = {
2262
+ // 字号/颜色与同一行的「用量」「用时」胶囊同源变量,保证在动作行里不显得突兀。
2181
2263
  color: textTertiary,
2182
- fontSize: 12,
2264
+ fontSize: "var(--dsh-content-font-size-secondary, 13px)",
2183
2265
  lineHeight: "18px",
2184
2266
  whiteSpace: "nowrap",
2185
2267
  cursor: "default"
2186
2268
  };
2187
- function SessionCost({ matched, sessionId, store }) {
2188
- const key = sessionCostKey(sessionId, matched);
2189
- const [result, setResult] = (0, import_react6.useState)(void 0);
2269
+ function SessionCost({ messageId, sessionId, useChat, store }) {
2270
+ const data = useChat((snapshot) => closingTurnTailData(snapshot, messageId));
2271
+ const match = sessionCostMatchOf(data);
2272
+ const key = match === null ? null : sessionCostKey(sessionId, match);
2273
+ const [shown, setShown] = (0, import_react6.useState)(void 0);
2190
2274
  (0, import_react6.useEffect)(() => {
2275
+ if (match === null || key === null) return;
2191
2276
  let disposed = false;
2192
2277
  const controller = new AbortController();
2193
- void store.estimate({ sessionId, turn: matched.turn, items: matched.items, signal: controller.signal }).then((value) => {
2194
- if (!disposed) setResult(value);
2278
+ void store.estimate({ sessionId, turn: match.turn, items: match.items, signal: controller.signal }).then((value) => {
2279
+ if (!disposed) setShown({ key, result: value });
2195
2280
  }).catch(() => {
2196
- if (!disposed) setResult(void 0);
2281
+ if (!disposed) setShown({ key, result: void 0 });
2197
2282
  });
2198
2283
  return () => {
2199
2284
  disposed = true;
2200
2285
  controller.abort();
2201
2286
  };
2202
2287
  }, [store, key]);
2203
- if (result === void 0) return null;
2204
- const estimated = !result.channelResolved || matched.ambiguous;
2288
+ if (match === null || key === null || shown === void 0 || shown.key !== key || shown.result === void 0) return null;
2289
+ const result = shown.result;
2290
+ const estimated = !result.channelResolved || match.ambiguous;
2205
2291
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: costStyle, "data-hiwork-session-cost": estimated ? "estimated" : "exact", title: SESSION_COST_HINT, children: `\u2248 ${formatYuan(result.cost)}${estimated ? SESSION_COST_ESTIMATED_SUFFIX : ""}` });
2206
2292
  }
2207
2293
 
@@ -2726,6 +2812,198 @@ function mountFeatureCenter(options) {
2726
2812
  // src/client/contracts.ts
2727
2813
  var HIWORK_FEATURE_CENTER_SERVICE = "hiworkFeatureCenter";
2728
2814
 
2815
+ // src/client/managed-mcp.ts
2816
+ var MANAGED_MCP_CHANNEL = "/hiwork-managed-mcp";
2817
+ var MANAGED_MCP_STALE_MS = 5 * 6e4;
2818
+ var ManagedMcpRpcError = class extends Error {
2819
+ constructor(code, message) {
2820
+ super(message);
2821
+ this.code = code;
2822
+ this.name = "ManagedMcpRpcError";
2823
+ }
2824
+ };
2825
+ function unwrapManagedMcpResult(value) {
2826
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
2827
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2828
+ }
2829
+ const result = value;
2830
+ if (result.ok === true) return result.value;
2831
+ if (result.ok === false) {
2832
+ const code = typeof result.error?.code === "string" ? result.error.code : "internal";
2833
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : "MCP \u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
2834
+ throw new ManagedMcpRpcError(code, message);
2835
+ }
2836
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2837
+ }
2838
+ function toSnapshot(value) {
2839
+ if (typeof value !== "object" || value === null) {
2840
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2841
+ }
2842
+ const record = value;
2843
+ const rawList = record["assignments"];
2844
+ if (!Array.isArray(rawList)) {
2845
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2846
+ }
2847
+ const assignments = [];
2848
+ for (const item of rawList) {
2849
+ const normalized = toAssignment(item);
2850
+ if (normalized !== void 0) assignments.push(normalized);
2851
+ }
2852
+ const lastError = record["lastError"];
2853
+ const snapshot = {
2854
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
2855
+ fetchedAt: typeof record["fetchedAt"] === "string" ? record["fetchedAt"] : "",
2856
+ assignments,
2857
+ stale: record["stale"] === true
2858
+ };
2859
+ if (typeof lastError === "object" && lastError !== null) {
2860
+ const detail = lastError;
2861
+ snapshot.lastError = {
2862
+ code: typeof detail.code === "string" ? detail.code : "internal",
2863
+ message: typeof detail.message === "string" ? detail.message : "MCP \u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
2864
+ };
2865
+ }
2866
+ return snapshot;
2867
+ }
2868
+ function toAssignment(value) {
2869
+ if (typeof value !== "object" || value === null) return void 0;
2870
+ const record = value;
2871
+ const authRaw = record["auth"];
2872
+ const auth = typeof authRaw === "object" && authRaw !== null ? authRaw : void 0;
2873
+ const assignmentId = text(record["assignmentId"]);
2874
+ const serverName = text(record["serverName"]);
2875
+ const url = text(record["url"]);
2876
+ const kind = text(auth?.["kind"]);
2877
+ const mintUrl = text(auth?.["mintUrl"]);
2878
+ if (assignmentId === void 0 || serverName === void 0 || url === void 0 || kind === void 0 || mintUrl === void 0) {
2879
+ return void 0;
2880
+ }
2881
+ return {
2882
+ assignmentId,
2883
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
2884
+ slug: text(record["slug"]) ?? serverName,
2885
+ displayName: text(record["displayName"]) ?? serverName,
2886
+ serverName,
2887
+ transport: text(record["transport"]) ?? "",
2888
+ url,
2889
+ extraHeaders: stringMap(record["extraHeaders"]),
2890
+ auth: { kind, mintUrl, revokeUrl: text(auth?.["revokeUrl"]) ?? "" },
2891
+ updatedAt: text(record["updatedAt"]) ?? ""
2892
+ };
2893
+ }
2894
+ function text(value) {
2895
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
2896
+ }
2897
+ function stringMap(raw) {
2898
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
2899
+ const out = {};
2900
+ for (const [key, value] of Object.entries(raw)) {
2901
+ if (typeof value === "string") out[key] = value;
2902
+ }
2903
+ return out;
2904
+ }
2905
+ var ManagedMcpStore = class {
2906
+ constructor(rpc) {
2907
+ this.rpc = rpc;
2908
+ }
2909
+ current;
2910
+ listeners = /* @__PURE__ */ new Set();
2911
+ inFlight;
2912
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做删除决策**(见文件头)。 */
2913
+ snapshot() {
2914
+ return this.current;
2915
+ }
2916
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
2917
+ isStale(ttlMs = MANAGED_MCP_STALE_MS, now = Date.now()) {
2918
+ if (this.current === void 0 || this.current.fetchedAt === "") return true;
2919
+ const fetchedAt = Date.parse(this.current.fetchedAt);
2920
+ if (Number.isNaN(fetchedAt)) return true;
2921
+ return now - fetchedAt >= ttlMs;
2922
+ }
2923
+ /**
2924
+ * 强制同步一次并返回**刚拉到的**清单。
2925
+ *
2926
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次
2927
+ * 网络请求成功了」,用它做删除决策才是安全的;读镜像可能读到上一次的结果。
2928
+ * 失败时抛错且**不动镜像**——调用方据此保持现有的连接一条不删。
2929
+ */
2930
+ async refresh(signal) {
2931
+ return await this.dedupe(async () => {
2932
+ const raw = await this.rpc.call(MANAGED_MCP_CHANNEL, "refresh", {}, signal);
2933
+ return this.adopt(unwrapManagedMcpResult(raw));
2934
+ });
2935
+ }
2936
+ /**
2937
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
2938
+ *
2939
+ * 与 {@link refresh} 的分工:这里是「启动时先把界面填上」的便宜入口——命中缓存时
2940
+ * 一次网络请求都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功
2941
+ * 拉取的结果」,但**可能比服务端现状旧**(最旧不超过 Host 的上一次成功拉取),
2942
+ * 所以调用方按 `stale` / `isStale()` 判定它够不够权威,不够就再 `refresh()` 一次。
2943
+ */
2944
+ async warm(signal) {
2945
+ return await this.dedupe(async () => {
2946
+ const raw = await this.rpc.call(MANAGED_MCP_CHANNEL, "snapshot", {}, signal);
2947
+ return this.adopt(unwrapManagedMcpResult(raw));
2948
+ });
2949
+ }
2950
+ /** 取一张新鲜断言(120 秒)。失败抛错;本层不缓存、不换令牌。 */
2951
+ async assertion(assignmentId, signal) {
2952
+ const raw = await this.rpc.call(MANAGED_MCP_CHANNEL, "assertion", { assignmentId }, signal);
2953
+ const value = unwrapManagedMcpResult(raw);
2954
+ if (typeof value !== "object" || value === null) {
2955
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2956
+ }
2957
+ const record = value;
2958
+ const assertion = text(record["assertion"]);
2959
+ const mintUrl = text(record["mintUrl"]);
2960
+ if (assertion === void 0 || mintUrl === void 0) {
2961
+ throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2962
+ }
2963
+ return {
2964
+ assertion,
2965
+ mintUrl,
2966
+ revokeUrl: text(record["revokeUrl"]) ?? "",
2967
+ expiresAt: text(record["expiresAt"]) ?? "",
2968
+ serverName: text(record["serverName"]) ?? ""
2969
+ };
2970
+ }
2971
+ /** 订阅镜像变化(连接器域据此重渲染 / 重排同步)。返回退订函数。 */
2972
+ subscribe(listener) {
2973
+ this.listeners.add(listener);
2974
+ return () => {
2975
+ this.listeners.delete(listener);
2976
+ };
2977
+ }
2978
+ /** 清空镜像(登出 / 换账号:清单是按人的,留着会串号)。 */
2979
+ clear() {
2980
+ this.current = void 0;
2981
+ this.emit();
2982
+ }
2983
+ /**
2984
+ * 并发去重:`warm` 与 `refresh` 撞在一起(启动同步与定时同步常常同拍)时共用一次
2985
+ * 请求,且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
2986
+ */
2987
+ async dedupe(task) {
2988
+ if (this.inFlight !== void 0) return await this.inFlight;
2989
+ const promise = task();
2990
+ this.inFlight = promise;
2991
+ return await promise.finally(() => {
2992
+ this.inFlight = void 0;
2993
+ });
2994
+ }
2995
+ /** 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。 */
2996
+ adopt(value) {
2997
+ const snapshot = toSnapshot(value);
2998
+ this.current = snapshot;
2999
+ this.emit();
3000
+ return snapshot;
3001
+ }
3002
+ emit() {
3003
+ for (const listener of [...this.listeners]) listener();
3004
+ }
3005
+ };
3006
+
2729
3007
  // src/client/index.ts
2730
3008
  var inject = ["slots", "locale", "remote", "remote.credentials", "settingsScope", "connection"];
2731
3009
  var INTERACTION_STYLE_ID = "hiwork-core-interaction-styles";
@@ -2763,6 +3041,7 @@ function apply(ctx) {
2763
3041
  const usage = new UsageDataStore(connection.rpc);
2764
3042
  const sessionCost = new SessionCostStore(connection.rpc);
2765
3043
  const dailyQuotes = new DailyDataStore(connection.rpc);
3044
+ const managedMcp = new ManagedMcpStore(connection.rpc);
2766
3045
  const appVersion = createAppVersion(connection.rpc);
2767
3046
  ctx.slots.inject("settings.section", () => ctx.slots.register({
2768
3047
  name: "settings.section",
@@ -2771,14 +3050,16 @@ function apply(ctx) {
2771
3050
  label: () => "\u7528\u91CF\u4E0E\u6210\u672C",
2772
3051
  inject: () => ({ hooks: controller.inject().hooks, usage })
2773
3052
  }, UsageSection));
2774
- ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
2775
- name: "conversation.chat.turnTail",
2776
- select: selectSessionCost,
3053
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
3054
+ name: "conversation.chat.assistant-actions",
3055
+ id: "hiwork-session-cost",
3056
+ order: 20,
2777
3057
  inject: () => ({ store: sessionCost })
2778
3058
  }, SessionCost));
2779
3059
  ctx.effect(() => observeAccountChange(controller.inject().hooks.aiRouterCard, () => {
2780
3060
  usage.clear();
2781
3061
  sessionCost.clear();
3062
+ managedMcp.clear();
2782
3063
  }), "hiwork-core: \u7528\u91CF\u7F13\u5B58\u968F\u8D26\u53F7 / Key \u53D8\u5316\u6E05\u7A7A");
2783
3064
  ctx.effect(() => observeAboutNavIcon(), "hiwork-core: about nav icon");
2784
3065
  ctx.slots.inject("sidebar.brand.mark", () => ctx.slots.register({
@@ -2797,9 +3078,9 @@ function apply(ctx) {
2797
3078
  }, UserBadge));
2798
3079
  ctx.effect(() => mountLoginGate(scope, storeCredential), "hiwork-core: login gate");
2799
3080
  ctx.effect(() => observeHeroDecorations(document, dailyQuotes), "hiwork-core: hero decorations");
2800
- installFeatureCenter(ctx);
3081
+ installFeatureCenter(ctx, managedMcp);
2801
3082
  }
2802
- function installFeatureCenter(ctx) {
3083
+ function installFeatureCenter(ctx, managedMcp) {
2803
3084
  const featureRegistry = createFeatureRegistry();
2804
3085
  let center;
2805
3086
  const sessions = ctx.get("sessions");
@@ -2818,6 +3099,16 @@ function installFeatureCenter(ctx) {
2818
3099
  return;
2819
3100
  }
2820
3101
  featureRegistry.openNative();
3102
+ },
3103
+ // 受管 MCP 面:连接器域(hiwork-capabilities)按这个成员判断「有没有这个能力」,
3104
+ // 老 core 上它是 undefined,能力中心照常工作、只是没有受管连接。
3105
+ managedMcp: {
3106
+ snapshot: () => managedMcp.snapshot(),
3107
+ isStale: (ttlMs) => managedMcp.isStale(ttlMs),
3108
+ warm: async (signal) => await managedMcp.warm(signal),
3109
+ refresh: async (signal) => await managedMcp.refresh(signal),
3110
+ assertion: async (assignmentId, signal) => await managedMcp.assertion(assignmentId, signal),
3111
+ subscribe: (listener) => managedMcp.subscribe(listener)
2821
3112
  }
2822
3113
  };
2823
3114
  ctx.provide(HIWORK_FEATURE_CENTER_SERVICE, face);