hiwork-core 0.0.11 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -90,6 +90,11 @@ var hostHideCss = [
90
90
  `header [class*="moreButton"] { display: none !important; }`,
91
91
  `[class*="settingsArea"] [aria-haspopup="dialog"] { display: none !important; }`
92
92
  ].join("\n");
93
+ var composerCss = [
94
+ `[data-composer-card]:has([data-composer-input]) { --dsw-elevation-stroke-color: var(--dsw-alias-border-l4); }`,
95
+ `[data-composer-card]:has([data-composer-input]):focus-within { --dsw-elevation-stroke-color: ${brandAccent}; }`,
96
+ `[data-composer-card]:has([data-composer-input]) > * { --dsw-elevation-stroke-color: var(--dsw-alias-border-l4); }`
97
+ ].join("\n");
93
98
  var brandCss = [
94
99
  `body[data-ds-dark-theme] .hiwork-brand-wordmark { filter: invert(1) brightness(1.35); }`
95
100
  ].join("\n");
@@ -345,6 +350,146 @@ function AiRouterSection(props) {
345
350
  var import_react2 = require("react");
346
351
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
347
352
 
353
+ // src/client/plugin-updates.ts
354
+ var PLUGIN_UPDATES_CHANNEL = "/hiwork-plugin-updates";
355
+ var PLUGIN_UPDATES_POLL_INTERVAL_MS = 5 * 6e4;
356
+ var PluginUpdatesRpcError = class extends Error {
357
+ constructor(code, message) {
358
+ super(message);
359
+ this.code = code;
360
+ this.name = "PluginUpdatesRpcError";
361
+ }
362
+ };
363
+ var GENERIC_FAILURE_MESSAGE = "\u63D2\u4EF6\u66F4\u65B0\u72B6\u6001\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
364
+ var INVALID_RESPONSE_MESSAGE = "\u63D2\u4EF6\u66F4\u65B0\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002";
365
+ function isRecord(value) {
366
+ return typeof value === "object" && value !== null && !Array.isArray(value);
367
+ }
368
+ function text(value) {
369
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
370
+ }
371
+ function unwrapPluginUpdatesResult(value) {
372
+ if (!isRecord(value) || !("ok" in value)) {
373
+ throw new PluginUpdatesRpcError("internal", INVALID_RESPONSE_MESSAGE);
374
+ }
375
+ if (value["ok"] === true) return value["value"];
376
+ if (value["ok"] === false) {
377
+ const error = isRecord(value["error"]) ? value["error"] : void 0;
378
+ const code = text(error?.["code"]) ?? "internal";
379
+ const message = typeof error?.["message"] === "string" && error["message"].trim() !== "" ? error["message"] : GENERIC_FAILURE_MESSAGE;
380
+ throw new PluginUpdatesRpcError(code, message);
381
+ }
382
+ throw new PluginUpdatesRpcError("internal", INVALID_RESPONSE_MESSAGE);
383
+ }
384
+ function toPackageUpdate(value) {
385
+ if (!isRecord(value)) return void 0;
386
+ const packageName = text(value["packageName"]);
387
+ if (packageName === void 0) return void 0;
388
+ return {
389
+ packageName,
390
+ displayName: text(value["displayName"]) ?? packageName,
391
+ from: text(value["from"]) ?? "",
392
+ to: text(value["to"]) ?? ""
393
+ };
394
+ }
395
+ function toPluginUpdatesSnapshot(value) {
396
+ if (!isRecord(value)) return void 0;
397
+ const packages = [];
398
+ for (const item of Array.isArray(value["packages"]) ? value["packages"] : []) {
399
+ const normalized = toPackageUpdate(item);
400
+ if (normalized !== void 0) packages.push(normalized);
401
+ }
402
+ const revision = value["revision"];
403
+ const fetchedAt = value["fetchedAt"];
404
+ return {
405
+ packages,
406
+ fetchedAt: typeof fetchedAt === "number" && Number.isFinite(fetchedAt) ? fetchedAt : 0,
407
+ revision: typeof revision === "number" && Number.isFinite(revision) ? revision : null
408
+ };
409
+ }
410
+ var PluginUpdatesStore = class {
411
+ constructor(rpc) {
412
+ this.rpc = rpc;
413
+ }
414
+ current;
415
+ dismissed;
416
+ version = 0;
417
+ listeners = /* @__PURE__ */ new Set();
418
+ /** 最近一次成功取到的快照(同步读,可能比服务端现状旧)。没有 / 不可用时 `undefined`。 */
419
+ snapshot() {
420
+ return this.current;
421
+ }
422
+ /** 变化计数:`useSyncExternalStore` 的 `getSnapshot`(值变化即重渲染)。 */
423
+ getVersion = () => this.version;
424
+ /** 订阅变化(快照更新 / 关闭)。返回退订函数。 */
425
+ subscribe = (listener) => {
426
+ this.listeners.add(listener);
427
+ return () => {
428
+ this.listeners.delete(listener);
429
+ };
430
+ };
431
+ /** 这个 revision 是否已经被用户关掉过(同 revision 不再提示;换 revision 会重新提示)。 */
432
+ isDismissed(revision) {
433
+ return this.dismissed !== void 0 && this.dismissed === revision;
434
+ }
435
+ /**
436
+ * 现在是否该渲染提示卡片。
437
+ *
438
+ * 三个条件缺一不可:**有快照**(不可用 / 从没取到 → 不渲染)、**`packages` 非空**
439
+ * (只有降级项时桌面端返回的也是空 `packages`,spec §4.5 明确不弹)、**这个 revision 没被关过**。
440
+ * ⚠️ 判据里**没有** `held` / `from` / `displayName`(spec §4.3 的 ⚠️)。
441
+ */
442
+ shouldPrompt() {
443
+ const snapshot = this.current;
444
+ if (snapshot === void 0 || snapshot.packages.length === 0) return false;
445
+ return !this.isDismissed(snapshot.revision);
446
+ }
447
+ /**
448
+ * 记住「用户关掉了这一个 revision 的提示」。**只记内存**(spec §4.5:本次会话 + 该 revision),
449
+ * 不落盘、不调任何 apply。
450
+ */
451
+ dismiss() {
452
+ const snapshot = this.current;
453
+ if (snapshot === void 0) return;
454
+ if (this.dismissed === snapshot.revision) return;
455
+ this.dismissed = snapshot.revision;
456
+ this.bump();
457
+ }
458
+ /**
459
+ * 向 Host 取一次「有没有待生效的更新」。
460
+ *
461
+ * 返回 `undefined` 有两种来源,对调用方是同一件事(**不渲染**):Host 说「不可用」(`null`),
462
+ * 或响应形状不合法。⚠️ **失败(`ok:false` / 传输异常)一律抛错且不覆写镜像**——提示条不该
463
+ * 因为一次抖动而消失,而「更新已经在排队」这件事不会因为读不出来就不存在。
464
+ */
465
+ async refresh(signal) {
466
+ const raw = await this.rpc.call(PLUGIN_UPDATES_CHANNEL, "snapshot", {}, signal);
467
+ const next = toPluginUpdatesSnapshot(unwrapPluginUpdatesResult(raw));
468
+ this.current = next;
469
+ this.bump();
470
+ return next;
471
+ }
472
+ /**
473
+ * 请求「重启并更新」。**这是全链路唯一的回收入口,且只能由用户点击到达。**
474
+ *
475
+ * 返回 `true` 表示触发**已经递下去**(最终落到主进程的 `recycleDshForPluginUpdate()`),
476
+ * **不是**「更新成功」——主进程会重建窗口,客户端那一刻就没了。
477
+ * `false` 只在「老桌面端上没有 `desktopPlugins`」时出现(Host 回 `requested: false`)。
478
+ */
479
+ async requestRestart(signal) {
480
+ const raw = await this.rpc.call(PLUGIN_UPDATES_CHANNEL, "apply", {}, signal);
481
+ const value = unwrapPluginUpdatesResult(raw);
482
+ return isRecord(value) && value["requested"] === true;
483
+ }
484
+ bump() {
485
+ this.version += 1;
486
+ for (const listener of [...this.listeners]) listener();
487
+ }
488
+ };
489
+ function createPluginUpdates(rpc) {
490
+ return new PluginUpdatesStore(rpc);
491
+ }
492
+
348
493
  // src/client/settings-navigation.ts
349
494
  function pickSettingsSectionButton(buttons, labels) {
350
495
  for (const label of labels) {
@@ -571,6 +716,90 @@ var confirmActionsStyle = {
571
716
  gap: 8,
572
717
  marginTop: 8
573
718
  };
719
+ var UPDATES_CARD_GAP = 8;
720
+ var UPDATES_CARD_FALLBACK_BOTTOM = 64;
721
+ var UPDATES_CARD_MIN_BOTTOM = 8;
722
+ var updatesCardStyle = (bottom) => ({
723
+ position: "fixed",
724
+ left: 8,
725
+ bottom,
726
+ zIndex: 1200,
727
+ boxSizing: "border-box",
728
+ width: 268,
729
+ maxWidth: "calc(100vw - 16px)",
730
+ padding: "12px 12px 10px",
731
+ display: "flex",
732
+ flexDirection: "column",
733
+ gap: 6,
734
+ background: bgLayer2,
735
+ borderRadius: 12,
736
+ border: `1px solid ${borderL2}`,
737
+ boxShadow: "var(--dsw-shadow-lv3, 0 12px 32px rgba(15,17,21,0.18))"
738
+ });
739
+ var updatesTitleStyle = {
740
+ display: "flex",
741
+ alignItems: "center",
742
+ gap: 6,
743
+ fontSize: 13,
744
+ fontWeight: 600,
745
+ color: textPrimary
746
+ };
747
+ var updatesBodyStyle = { fontSize: 12, lineHeight: 1.6, color: textSecondary };
748
+ var updatesErrorStyle = { fontSize: 12, lineHeight: 1.6, color: warnText };
749
+ var updatesActionsStyle = {
750
+ display: "flex",
751
+ justifyContent: "flex-end",
752
+ gap: 8,
753
+ marginTop: 4
754
+ };
755
+ function PluginUpdatesCard(props) {
756
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
757
+ "div",
758
+ {
759
+ style: updatesCardStyle(props.bottom),
760
+ "data-hiwork-plugin-updates": true,
761
+ role: "status",
762
+ "aria-label": "\u63D2\u4EF6\u66F4\u65B0\u63D0\u793A",
763
+ children: [
764
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: updatesTitleStyle, children: "\u63D2\u4EF6\u66F4\u65B0\u5DF2\u5C31\u7EEA" }),
765
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: updatesBodyStyle, children: "\u91CD\u542F\u540E\u751F\u6548\u3002" }),
766
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: updatesBodyStyle, children: [
767
+ "\u70B9\u300C\u91CD\u542F\u5E76\u66F4\u65B0\u300D\u4F1A\u7ACB\u523B\u91CD\u542F\u5E94\u7528\uFF1A",
768
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "\u5F53\u524D\u6B63\u5728\u8F93\u5165\u7684\u5185\u5BB9\u4F1A\u4E22\u5931" }),
769
+ "\uFF08\u4F1A\u8BDD\u8BB0\u5F55\u4FDD\u5B58\u5728\u78C1\u76D8\u4E0A\uFF0C\u4E0D\u4F1A\u4E22\uFF09\u3002"
770
+ ] }),
771
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: updatesBodyStyle, children: "\u4E5F\u53EF\u4EE5\u7A0D\u540E\u624B\u52A8\u91CD\u542F\uFF1A\u9000\u51FA HiWork \u540E\u91CD\u65B0\u6253\u5F00\uFF08macOS \u6309 Cmd+Q\uFF0C\u5173\u95ED\u7A97\u53E3\u4E0D\u7B97\u9000\u51FA\uFF09\u3002" }),
772
+ props.restartFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: updatesErrorStyle, children: "\u6682\u65F6\u65E0\u6CD5\u89E6\u53D1\u91CD\u542F\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u6309\u4E0A\u9762\u7684\u65B9\u5F0F\u624B\u52A8\u9000\u51FA\u540E\u91CD\u65B0\u6253\u5F00\u3002" }) : null,
773
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: updatesActionsStyle, children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
775
+ "button",
776
+ {
777
+ type: "button",
778
+ "data-hiwork-core-secondary": true,
779
+ "data-hiwork-plugin-updates-dismiss": true,
780
+ style: secondaryButton,
781
+ onClick: props.onDismiss,
782
+ children: "\u5173\u95ED"
783
+ }
784
+ ),
785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
786
+ "button",
787
+ {
788
+ type: "button",
789
+ "data-hiwork-core-primary": true,
790
+ "data-hiwork-plugin-updates-restart": true,
791
+ style: primaryButton,
792
+ disabled: props.restarting,
793
+ onClick: props.onRestart,
794
+ children: props.restarting ? "\u5DF2\u8BF7\u6C42\u91CD\u542F\u2026" : "\u91CD\u542F\u5E76\u66F4\u65B0"
795
+ }
796
+ )
797
+ ] })
798
+ ]
799
+ }
800
+ );
801
+ }
802
+ var serverUpdatesVersion = () => 0;
574
803
  function LogoutIcon() {
575
804
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
576
805
  "svg",
@@ -626,6 +855,41 @@ function LogoutConfirm(props) {
626
855
  function UserBadge(props) {
627
856
  const state = props.useAiRouterCard((snapshot) => snapshot);
628
857
  const [confirming, setConfirming] = (0, import_react2.useState)(false);
858
+ const updates = props.pluginUpdates;
859
+ const [restarting, setRestarting] = (0, import_react2.useState)(false);
860
+ const [restartFailed, setRestartFailed] = (0, import_react2.useState)(false);
861
+ const [cardBottom, setCardBottom] = (0, import_react2.useState)(UPDATES_CARD_FALLBACK_BOTTOM);
862
+ const wideRowRef = (0, import_react2.useRef)(null);
863
+ const railRowRef = (0, import_react2.useRef)(null);
864
+ const loggedIn = state.status === "logged-in" && state.username !== void 0;
865
+ (0, import_react2.useSyncExternalStore)(updates.subscribe, updates.getVersion, serverUpdatesVersion);
866
+ (0, import_react2.useEffect)(() => {
867
+ if (!loggedIn) return;
868
+ const tick = () => {
869
+ void updates.refresh().catch(() => {
870
+ });
871
+ };
872
+ tick();
873
+ const timer = setInterval(tick, PLUGIN_UPDATES_POLL_INTERVAL_MS);
874
+ return () => {
875
+ clearInterval(timer);
876
+ };
877
+ }, [loggedIn, updates]);
878
+ const showUpdates = loggedIn && updates.shouldPrompt();
879
+ (0, import_react2.useLayoutEffect)(() => {
880
+ if (!showUpdates) return;
881
+ const measure = () => {
882
+ const node = props.wide ? wideRowRef.current : railRowRef.current;
883
+ if (node === null) return;
884
+ const rect = node.getBoundingClientRect();
885
+ setCardBottom(Math.max(UPDATES_CARD_MIN_BOTTOM, Math.round(window.innerHeight - rect.top + UPDATES_CARD_GAP)));
886
+ };
887
+ measure();
888
+ window.addEventListener("resize", measure);
889
+ return () => {
890
+ window.removeEventListener("resize", measure);
891
+ };
892
+ }, [showUpdates, props.wide]);
629
893
  if (state.status !== "logged-in" || state.username === void 0) return null;
630
894
  const displayName = state.displayName ?? state.username;
631
895
  const initial = displayName.slice(0, 1).toUpperCase();
@@ -633,22 +897,51 @@ function UserBadge(props) {
633
897
  setConfirming(false);
634
898
  void props.actions.logout();
635
899
  } }) : null;
636
- if (!props.wide) {
637
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
638
- "button",
639
- {
640
- type: "button",
641
- "data-hiwork-core-icon": true,
642
- style: railRowStyle,
643
- title: displayName,
644
- "aria-label": "\u6253\u5F00\u8BBE\u7F6E",
645
- onClick: openGatewaySettings,
646
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: avatarStyle(24), children: initial })
900
+ const onRestart = () => {
901
+ setRestartFailed(false);
902
+ setRestarting(true);
903
+ void updates.requestRestart().then((requested) => {
904
+ if (!requested) {
905
+ setRestarting(false);
906
+ setRestartFailed(true);
647
907
  }
648
- );
908
+ }).catch(() => {
909
+ setRestarting(false);
910
+ setRestartFailed(true);
911
+ });
912
+ };
913
+ const updatesCard = showUpdates ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
914
+ PluginUpdatesCard,
915
+ {
916
+ bottom: cardBottom,
917
+ restarting,
918
+ restartFailed,
919
+ onRestart,
920
+ onDismiss: () => {
921
+ updates.dismiss();
922
+ }
923
+ }
924
+ ) : null;
925
+ if (!props.wide) {
926
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
927
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
928
+ "button",
929
+ {
930
+ ref: railRowRef,
931
+ type: "button",
932
+ "data-hiwork-core-icon": true,
933
+ style: railRowStyle,
934
+ title: displayName,
935
+ "aria-label": "\u6253\u5F00\u8BBE\u7F6E",
936
+ onClick: openGatewaySettings,
937
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: avatarStyle(24), children: initial })
938
+ }
939
+ ),
940
+ updatesCard
941
+ ] });
649
942
  }
650
943
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
651
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: wideRowStyle, title: displayName, children: [
944
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: wideRowRef, style: wideRowStyle, title: displayName, children: [
652
945
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
653
946
  "button",
654
947
  {
@@ -689,7 +982,8 @@ function UserBadge(props) {
689
982
  }
690
983
  )
691
984
  ] }),
692
- confirm
985
+ confirm,
986
+ updatesCard
693
987
  ] });
694
988
  }
695
989
 
@@ -724,7 +1018,7 @@ function BrandMark(props) {
724
1018
  }
725
1019
  var brandNameStyle = {
726
1020
  display: "inline-flex",
727
- alignItems: "center",
1021
+ alignItems: "flex-end",
728
1022
  gap: 8,
729
1023
  minWidth: 0
730
1024
  };
@@ -739,7 +1033,8 @@ var versionStyle = {
739
1033
  flex: "none",
740
1034
  fontSize: 12,
741
1035
  fontWeight: 400,
742
- lineHeight: "24px",
1036
+ lineHeight: "normal",
1037
+ marginBottom: 1,
743
1038
  whiteSpace: "nowrap",
744
1039
  color: textTertiary
745
1040
  };
@@ -2870,28 +3165,28 @@ function toAssignment(value) {
2870
3165
  const record = value;
2871
3166
  const authRaw = record["auth"];
2872
3167
  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"]);
3168
+ const assignmentId = text2(record["assignmentId"]);
3169
+ const serverName = text2(record["serverName"]);
3170
+ const url = text2(record["url"]);
3171
+ const kind = text2(auth?.["kind"]);
3172
+ const mintUrl = text2(auth?.["mintUrl"]);
2878
3173
  if (assignmentId === void 0 || serverName === void 0 || url === void 0 || kind === void 0 || mintUrl === void 0) {
2879
3174
  return void 0;
2880
3175
  }
2881
3176
  return {
2882
3177
  assignmentId,
2883
3178
  revision: typeof record["revision"] === "number" ? record["revision"] : 0,
2884
- slug: text(record["slug"]) ?? serverName,
2885
- displayName: text(record["displayName"]) ?? serverName,
3179
+ slug: text2(record["slug"]) ?? serverName,
3180
+ displayName: text2(record["displayName"]) ?? serverName,
2886
3181
  serverName,
2887
- transport: text(record["transport"]) ?? "",
3182
+ transport: text2(record["transport"]) ?? "",
2888
3183
  url,
2889
3184
  extraHeaders: stringMap(record["extraHeaders"]),
2890
- auth: { kind, mintUrl, revokeUrl: text(auth?.["revokeUrl"]) ?? "" },
2891
- updatedAt: text(record["updatedAt"]) ?? ""
3185
+ auth: { kind, mintUrl, revokeUrl: text2(auth?.["revokeUrl"]) ?? "" },
3186
+ updatedAt: text2(record["updatedAt"]) ?? ""
2892
3187
  };
2893
3188
  }
2894
- function text(value) {
3189
+ function text2(value) {
2895
3190
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
2896
3191
  }
2897
3192
  function stringMap(raw) {
@@ -2955,17 +3250,17 @@ var ManagedMcpStore = class {
2955
3250
  throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2956
3251
  }
2957
3252
  const record = value;
2958
- const assertion = text(record["assertion"]);
2959
- const mintUrl = text(record["mintUrl"]);
3253
+ const assertion = text2(record["assertion"]);
3254
+ const mintUrl = text2(record["mintUrl"]);
2960
3255
  if (assertion === void 0 || mintUrl === void 0) {
2961
3256
  throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
2962
3257
  }
2963
3258
  return {
2964
3259
  assertion,
2965
3260
  mintUrl,
2966
- revokeUrl: text(record["revokeUrl"]) ?? "",
2967
- expiresAt: text(record["expiresAt"]) ?? "",
2968
- serverName: text(record["serverName"]) ?? ""
3261
+ revokeUrl: text2(record["revokeUrl"]) ?? "",
3262
+ expiresAt: text2(record["expiresAt"]) ?? "",
3263
+ serverName: text2(record["serverName"]) ?? ""
2969
3264
  };
2970
3265
  }
2971
3266
  /** 订阅镜像变化(连接器域据此重渲染 / 重排同步)。返回退订函数。 */
@@ -3004,6 +3299,464 @@ var ManagedMcpStore = class {
3004
3299
  }
3005
3300
  };
3006
3301
 
3302
+ // src/client/managed-skills.ts
3303
+ var MANAGED_SKILLS_CHANNEL = "/hiwork-managed-skills";
3304
+ var MANAGED_SKILLS_STALE_MS = 5 * 6e4;
3305
+ var ManagedSkillsRpcError = class extends Error {
3306
+ constructor(code, message) {
3307
+ super(message);
3308
+ this.code = code;
3309
+ this.name = "ManagedSkillsRpcError";
3310
+ }
3311
+ };
3312
+ var GENERIC_FAILURE_MESSAGE2 = "\u6280\u80FD\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3313
+ var INVALID_RESPONSE_MESSAGE2 = "\u6280\u80FD\u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002";
3314
+ function unwrapManagedSkillsResult(value) {
3315
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
3316
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3317
+ }
3318
+ const result = value;
3319
+ if (result.ok === true) return result.value;
3320
+ if (result.ok === false) {
3321
+ const code = typeof result.error?.code === "string" ? result.error.code : "internal";
3322
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE2;
3323
+ throw new ManagedSkillsRpcError(code, message);
3324
+ }
3325
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3326
+ }
3327
+ function toSnapshot2(value) {
3328
+ if (typeof value !== "object" || value === null) {
3329
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3330
+ }
3331
+ const record = value;
3332
+ const rawList = record["assignments"];
3333
+ if (!Array.isArray(rawList)) {
3334
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3335
+ }
3336
+ const assignments = [];
3337
+ for (const item of rawList) {
3338
+ const normalized = toAssignment2(item);
3339
+ if (normalized !== void 0) assignments.push(normalized);
3340
+ }
3341
+ const lastError = record["lastError"];
3342
+ const snapshot = {
3343
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
3344
+ fetchedAt: typeof record["fetchedAt"] === "string" ? record["fetchedAt"] : "",
3345
+ assignments,
3346
+ stale: record["stale"] === true
3347
+ };
3348
+ if (typeof lastError === "object" && lastError !== null) {
3349
+ const detail = lastError;
3350
+ snapshot.lastError = {
3351
+ code: typeof detail.code === "string" ? detail.code : "internal",
3352
+ message: typeof detail.message === "string" ? detail.message : GENERIC_FAILURE_MESSAGE2
3353
+ };
3354
+ }
3355
+ return snapshot;
3356
+ }
3357
+ function toAssignment2(value) {
3358
+ if (typeof value !== "object" || value === null) return void 0;
3359
+ const record = value;
3360
+ const assignmentId = text3(record["assignmentId"]);
3361
+ const skillName = text3(record["skillName"]);
3362
+ const contentSha256 = text3(record["contentSha256"]);
3363
+ if (assignmentId === void 0 || skillName === void 0 || contentSha256 === void 0) return void 0;
3364
+ return {
3365
+ assignmentId,
3366
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
3367
+ slug: text3(record["slug"]) ?? skillName,
3368
+ displayName: text3(record["displayName"]) ?? skillName,
3369
+ skillName,
3370
+ description: text3(record["description"]) ?? "",
3371
+ contentSha256,
3372
+ sizeBytes: typeof record["sizeBytes"] === "number" ? record["sizeBytes"] : 0,
3373
+ version: text3(record["version"]) ?? "",
3374
+ updatedAt: text3(record["updatedAt"]) ?? ""
3375
+ };
3376
+ }
3377
+ function text3(value) {
3378
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
3379
+ }
3380
+ var ManagedSkillsStore = class {
3381
+ constructor(rpc) {
3382
+ this.rpc = rpc;
3383
+ }
3384
+ current;
3385
+ listeners = /* @__PURE__ */ new Set();
3386
+ /**
3387
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
3388
+ *
3389
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,
3390
+ * 它的清单属于上一个人。复用它等于让新账号的同步拿到旧账号的清单,而 `refresh` 的返回值
3391
+ * 是技能域唯一被允许用来删本地受管技能的依据(spec §6 的唯一安全不变量)⇒ 会把新账号
3392
+ * 被分配的技能**硬删掉**。所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
3393
+ */
3394
+ inFlight;
3395
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
3396
+ generation = 0;
3397
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做删除决策**(见文件头)。 */
3398
+ snapshot() {
3399
+ return this.current;
3400
+ }
3401
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
3402
+ isStale(ttlMs = MANAGED_SKILLS_STALE_MS, now = Date.now()) {
3403
+ if (this.current === void 0 || this.current.fetchedAt === "") return true;
3404
+ const fetchedAt = Date.parse(this.current.fetchedAt);
3405
+ if (Number.isNaN(fetchedAt)) return true;
3406
+ return now - fetchedAt >= ttlMs;
3407
+ }
3408
+ /**
3409
+ * 强制同步一次并返回**刚拉到的**清单。
3410
+ *
3411
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次
3412
+ * 网络请求成功了」,用它做删除决策才是安全的;读镜像可能读到上一次的结果。
3413
+ * 失败时抛错且**不动镜像**——调用方据此保持现有的受管技能一条不删(spec §6 的
3414
+ * 唯一安全不变量)。
3415
+ */
3416
+ async refresh(signal) {
3417
+ return await this.dedupe(async (generation) => {
3418
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "refresh", {}, signal);
3419
+ return this.adopt(unwrapManagedSkillsResult(raw), generation);
3420
+ });
3421
+ }
3422
+ /**
3423
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
3424
+ *
3425
+ * 与 {@link refresh} 的分工:这里是「启动时先把界面填上」的便宜入口——命中缓存时
3426
+ * 一次网络请求都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功
3427
+ * 拉取的结果」,但**可能比服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它
3428
+ * 够不够权威,不够就再 `refresh()` 一次。
3429
+ */
3430
+ async warm(signal) {
3431
+ return await this.dedupe(async (generation) => {
3432
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "snapshot", {}, signal);
3433
+ return this.adopt(unwrapManagedSkillsResult(raw), generation);
3434
+ });
3435
+ }
3436
+ /**
3437
+ * 取一条分配的技能包(base64 字节)。
3438
+ *
3439
+ * **本层不缓存包体、也不解码**:它是一条分配的内容,不是「服务端此刻认什么」的投影;
3440
+ * 缓存下来等于把每次同步装过的所有技能包都钉在内存里(base64 后每条约 5 MiB)。
3441
+ * 校验与落盘在技能域(spec §4.6)。
3442
+ */
3443
+ async package(assignmentId, signal) {
3444
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "package", { assignmentId }, signal);
3445
+ const value = unwrapManagedSkillsResult(raw);
3446
+ if (typeof value !== "object" || value === null) {
3447
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3448
+ }
3449
+ const record = value;
3450
+ const id = text3(record["assignmentId"]);
3451
+ const skillName = text3(record["skillName"]);
3452
+ const contentSha256 = text3(record["contentSha256"]);
3453
+ const archiveBase64 = text3(record["archiveBase64"]);
3454
+ if (id === void 0 || skillName === void 0 || contentSha256 === void 0 || archiveBase64 === void 0) {
3455
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3456
+ }
3457
+ return { assignmentId: id, skillName, contentSha256, archiveBase64 };
3458
+ }
3459
+ /** 订阅镜像变化(技能域据此重渲染 / 重排同步)。返回退订函数。 */
3460
+ subscribe(listener) {
3461
+ this.listeners.add(listener);
3462
+ return () => {
3463
+ this.listeners.delete(listener);
3464
+ };
3465
+ }
3466
+ /**
3467
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
3468
+ *
3469
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的
3470
+ * 清单属于上一个人——清空之后到来的调用者若复用了它,就会拿旧账号的清单去对账本地受管
3471
+ * 技能,把新账号被分配的技能全删掉(`refresh` 的返回值是唯一被允许用来删的那条路,
3472
+ * spec §6)。所以世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
3473
+ *
3474
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把
3475
+ * **新**发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据
3476
+ * 天然避开这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/managed-skills.ts`
3477
+ * 的 `load()` 同形)。
3478
+ *
3479
+ * ⚠️ 这条修复**刻意只落在技能这半**:`src/client/managed-mcp.ts` 是逐字同形的既有缺口,
3480
+ * **用户已裁定 MCP 那半本轮不动**。别为了「两半对齐」把这里改回原样。
3481
+ */
3482
+ clear() {
3483
+ this.generation += 1;
3484
+ this.current = void 0;
3485
+ this.emit();
3486
+ }
3487
+ /**
3488
+ * 并发去重:`warm` 与 `refresh` 撞在一起(启动同步与定时同步常常同拍)时共用一次
3489
+ * 请求,且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
3490
+ *
3491
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
3492
+ * 于是删除得更少——偏差落在「该删的这次没删」这一侧,正是 spec §6 允许的那一侧。
3493
+ *
3494
+ * 上面这条讲的是**新旧次序**,管不到**身份**:并发合并还有第二个维度——「这几次调用是不是
3495
+ * 同一个人的」。世代号({@link clear})负责的就是这一格:换账号之后账上那条属于上一个人,
3496
+ * 谁都不许复用,必须各自重新发起。
3497
+ *
3498
+ * 传给 `task` 的 `generation` 是**发起这一次请求时**的世代号,由它决定这次结果能不能写进镜像
3499
+ * (见 {@link adopt})——晚到的旧世代结果照旧返回给它自己的调用方,但不许覆盖新世代的镜像。
3500
+ */
3501
+ async dedupe(task) {
3502
+ const inFlight = this.inFlight;
3503
+ if (inFlight !== void 0 && inFlight.generation === this.generation) return await inFlight.task;
3504
+ const generation = this.generation;
3505
+ const promise = task(generation);
3506
+ this.inFlight = { generation, task: promise };
3507
+ try {
3508
+ return await promise;
3509
+ } finally {
3510
+ if (this.inFlight?.task === promise) this.inFlight = void 0;
3511
+ }
3512
+ }
3513
+ /**
3514
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
3515
+ *
3516
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
3517
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步
3518
+ * 就靠 `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示
3519
+ * 新账号的技能,也不触发同步。
3520
+ */
3521
+ adopt(value, generation) {
3522
+ const snapshot = toSnapshot2(value);
3523
+ if (generation !== this.generation) return snapshot;
3524
+ this.current = snapshot;
3525
+ this.emit();
3526
+ return snapshot;
3527
+ }
3528
+ emit() {
3529
+ for (const listener of [...this.listeners]) listener();
3530
+ }
3531
+ };
3532
+
3533
+ // src/client/managed-workbench.ts
3534
+ var MANAGED_WORKBENCH_CHANNEL = "/hiwork-workbench";
3535
+ var MANAGED_WORKBENCH_STALE_MS = 5 * 6e4;
3536
+ var ManagedWorkbenchRpcError = class extends Error {
3537
+ constructor(code, message) {
3538
+ super(message);
3539
+ this.code = code;
3540
+ this.name = "ManagedWorkbenchRpcError";
3541
+ }
3542
+ };
3543
+ var GENERIC_FAILURE_MESSAGE3 = "\u5DE5\u4F5C\u53F0\u5E94\u7528\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3544
+ var INVALID_RESPONSE_MESSAGE3 = "\u5DE5\u4F5C\u53F0\u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002";
3545
+ function unwrapManagedWorkbenchResult(value) {
3546
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
3547
+ throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
3548
+ }
3549
+ const result = value;
3550
+ if (result.ok === true) return result.value;
3551
+ if (result.ok === false) {
3552
+ const code = typeof result.error?.code === "string" ? result.error.code : "internal";
3553
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE3;
3554
+ throw new ManagedWorkbenchRpcError(code, message);
3555
+ }
3556
+ throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
3557
+ }
3558
+ function text4(value) {
3559
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
3560
+ }
3561
+ function isRecord2(value) {
3562
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3563
+ }
3564
+ function stringArray(value) {
3565
+ if (!Array.isArray(value)) return [];
3566
+ return value.filter((item) => typeof item === "string");
3567
+ }
3568
+ function toApp(value) {
3569
+ if (!isRecord2(value)) return void 0;
3570
+ const code = text4(value["code"]);
3571
+ const executor = value["executor"];
3572
+ if (code === void 0 || !isRecord2(executor)) return void 0;
3573
+ const kind = text4(executor["kind"]);
3574
+ if (kind === void 0) return void 0;
3575
+ const normalizedExecutor = { ...executor, kind };
3576
+ const mode = text4(executor["mode"]);
3577
+ if (mode === void 0) delete normalizedExecutor.mode;
3578
+ else normalizedExecutor.mode = mode;
3579
+ const category = isRecord2(value["category"]) ? value["category"] : {};
3580
+ const inputs = [];
3581
+ for (const item of Array.isArray(value["inputs"]) ? value["inputs"] : []) {
3582
+ if (!isRecord2(item)) continue;
3583
+ const key = text4(item["key"]);
3584
+ if (key === void 0) continue;
3585
+ inputs.push({
3586
+ ...item,
3587
+ key,
3588
+ type: text4(item["type"]) ?? "",
3589
+ label: text4(item["label"]) ?? key,
3590
+ required: item["required"] === true
3591
+ });
3592
+ }
3593
+ const outputs = [];
3594
+ for (const item of Array.isArray(value["outputs"]) ? value["outputs"] : []) {
3595
+ if (!isRecord2(item)) continue;
3596
+ const key = text4(item["key"]);
3597
+ if (key === void 0) continue;
3598
+ outputs.push({ ...item, key, type: text4(item["type"]) ?? "" });
3599
+ }
3600
+ const iconUrl = text4(value["iconUrl"]);
3601
+ const revision = value["revision"];
3602
+ return {
3603
+ code,
3604
+ displayName: text4(value["displayName"]) ?? code,
3605
+ summary: text4(value["summary"]) ?? "",
3606
+ ...iconUrl === void 0 ? {} : { iconUrl },
3607
+ category: { code: text4(category["code"]) ?? "", name: text4(category["name"]) ?? "" },
3608
+ tags: stringArray(value["tags"]),
3609
+ featured: value["featured"] === true,
3610
+ revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
3611
+ executor: normalizedExecutor,
3612
+ inputs,
3613
+ outputs,
3614
+ effects: stringArray(value["effects"])
3615
+ };
3616
+ }
3617
+ function toSnapshot3(value) {
3618
+ if (!isRecord2(value)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
3619
+ const rawApps = value["apps"];
3620
+ if (!Array.isArray(rawApps)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
3621
+ const apps = [];
3622
+ for (const item of rawApps) {
3623
+ const app = toApp(item);
3624
+ if (app !== void 0) apps.push(app);
3625
+ }
3626
+ const snapshot = {
3627
+ revision: typeof value["revision"] === "number" && Number.isFinite(value["revision"]) ? value["revision"] : 0,
3628
+ fetchedAt: typeof value["fetchedAt"] === "string" ? value["fetchedAt"] : "",
3629
+ apps,
3630
+ stale: value["stale"] === true
3631
+ };
3632
+ const lastError = value["lastError"];
3633
+ if (isRecord2(lastError)) {
3634
+ snapshot.lastError = {
3635
+ code: text4(lastError["code"]) ?? "internal",
3636
+ message: typeof lastError["message"] === "string" ? lastError["message"] : GENERIC_FAILURE_MESSAGE3
3637
+ };
3638
+ }
3639
+ return snapshot;
3640
+ }
3641
+ var ManagedWorkbenchStore = class {
3642
+ constructor(rpc) {
3643
+ this.rpc = rpc;
3644
+ }
3645
+ current;
3646
+ listeners = /* @__PURE__ */ new Set();
3647
+ /**
3648
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
3649
+ *
3650
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,它的清单
3651
+ * 属于上一个人。复用它等于让新账号的货架显示旧账号的应用(管理员给谁分配是按人的)。
3652
+ * 所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
3653
+ */
3654
+ inFlight;
3655
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
3656
+ generation = 0;
3657
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做下架决策**(见文件头)。 */
3658
+ snapshot() {
3659
+ return this.current;
3660
+ }
3661
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
3662
+ isStale(ttlMs = MANAGED_WORKBENCH_STALE_MS, now = Date.now()) {
3663
+ if (this.current === void 0 || this.current.fetchedAt === "") return true;
3664
+ const fetchedAt = Date.parse(this.current.fetchedAt);
3665
+ if (Number.isNaN(fetchedAt)) return true;
3666
+ return now - fetchedAt >= ttlMs;
3667
+ }
3668
+ /**
3669
+ * 强制同步一次并返回**刚拉到的**清单。
3670
+ *
3671
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次网络请求成功了」,
3672
+ * 用它判断「这个应用还在不在」才是安全的;读镜像可能读到上一次的结果。失败时抛错且**不动镜像**
3673
+ * ——调用方据此保持货架原样(离线态显示上次成功缓存 + 「可能不是最新」)。
3674
+ */
3675
+ async refresh(signal) {
3676
+ return await this.dedupe(async (generation) => {
3677
+ const raw = await this.rpc.call(MANAGED_WORKBENCH_CHANNEL, "refresh", {}, signal);
3678
+ return this.adopt(unwrapManagedWorkbenchResult(raw), generation);
3679
+ });
3680
+ }
3681
+ /**
3682
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
3683
+ *
3684
+ * 与 {@link refresh} 的分工:这里是「挂载时先把界面填上」的便宜入口——命中缓存时一次网络请求
3685
+ * 都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功拉取的结果」,但**可能比
3686
+ * 服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它够不够权威,不够就再 `refresh()`。
3687
+ */
3688
+ async warm(signal) {
3689
+ return await this.dedupe(async (generation) => {
3690
+ const raw = await this.rpc.call(MANAGED_WORKBENCH_CHANNEL, "snapshot", {}, signal);
3691
+ return this.adopt(unwrapManagedWorkbenchResult(raw), generation);
3692
+ });
3693
+ }
3694
+ /** 订阅镜像变化(货架页据此重渲染)。返回退订函数。 */
3695
+ subscribe(listener) {
3696
+ this.listeners.add(listener);
3697
+ return () => {
3698
+ this.listeners.delete(listener);
3699
+ };
3700
+ }
3701
+ /**
3702
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
3703
+ *
3704
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的清单
3705
+ * 属于上一个人——清空之后到来的调用者若复用了它,新账号的货架就会显示旧账号的应用。所以
3706
+ * 世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
3707
+ *
3708
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把**新**
3709
+ * 发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据天然避开
3710
+ * 这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/workbench-bridge.ts`
3711
+ * 的 `load()` 同形,也与 `src/client/managed-skills.ts` 的同名方法逐字同款)。
3712
+ */
3713
+ clear() {
3714
+ this.generation += 1;
3715
+ this.current = void 0;
3716
+ this.emit();
3717
+ }
3718
+ /**
3719
+ * 并发去重:`warm` 与 `refresh` 撞在一起(挂载预热与手动刷新常常同拍)时共用一次请求,
3720
+ * 且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
3721
+ *
3722
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
3723
+ * 于是「该消失的这次没消失」——偏差落在保守的那一侧。
3724
+ *
3725
+ * 上面这条讲的是**新旧次序**,管不到**身份**:世代号({@link clear})负责的就是这一格——
3726
+ * 换账号之后账上那条属于上一个人,谁都不许复用,必须各自重新发起。
3727
+ */
3728
+ async dedupe(task) {
3729
+ const inFlight = this.inFlight;
3730
+ if (inFlight !== void 0 && inFlight.generation === this.generation) return await inFlight.task;
3731
+ const generation = this.generation;
3732
+ const promise = task(generation);
3733
+ this.inFlight = { generation, task: promise };
3734
+ try {
3735
+ return await promise;
3736
+ } finally {
3737
+ if (this.inFlight?.task === promise) this.inFlight = void 0;
3738
+ }
3739
+ }
3740
+ /**
3741
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
3742
+ *
3743
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
3744
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步就靠
3745
+ * `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示新账号的
3746
+ * 应用,也不触发同步。
3747
+ */
3748
+ adopt(value, generation) {
3749
+ const snapshot = toSnapshot3(value);
3750
+ if (generation !== this.generation) return snapshot;
3751
+ this.current = snapshot;
3752
+ this.emit();
3753
+ return snapshot;
3754
+ }
3755
+ emit() {
3756
+ for (const listener of [...this.listeners]) listener();
3757
+ }
3758
+ };
3759
+
3007
3760
  // src/client/index.ts
3008
3761
  var inject = ["slots", "locale", "remote", "remote.credentials", "settingsScope", "connection"];
3009
3762
  var INTERACTION_STYLE_ID = "hiwork-core-interaction-styles";
@@ -3012,7 +3765,7 @@ function injectInteractionStyles() {
3012
3765
  if (document.getElementById(INTERACTION_STYLE_ID) !== null) return;
3013
3766
  const style = document.createElement("style");
3014
3767
  style.id = INTERACTION_STYLE_ID;
3015
- style.textContent = [interactionCss, brandCss, frameCss, hostHideCss].join("\n");
3768
+ style.textContent = [interactionCss, brandCss, frameCss, hostHideCss, composerCss].join("\n");
3016
3769
  document.head.appendChild(style);
3017
3770
  }
3018
3771
  function apply(ctx) {
@@ -3042,7 +3795,10 @@ function apply(ctx) {
3042
3795
  const sessionCost = new SessionCostStore(connection.rpc);
3043
3796
  const dailyQuotes = new DailyDataStore(connection.rpc);
3044
3797
  const managedMcp = new ManagedMcpStore(connection.rpc);
3798
+ const managedSkills = new ManagedSkillsStore(connection.rpc);
3799
+ const managedWorkbench = new ManagedWorkbenchStore(connection.rpc);
3045
3800
  const appVersion = createAppVersion(connection.rpc);
3801
+ const pluginUpdates = createPluginUpdates(connection.rpc);
3046
3802
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3047
3803
  name: "settings.section",
3048
3804
  id: "hiwork-usage",
@@ -3060,6 +3816,8 @@ function apply(ctx) {
3060
3816
  usage.clear();
3061
3817
  sessionCost.clear();
3062
3818
  managedMcp.clear();
3819
+ managedSkills.clear();
3820
+ managedWorkbench.clear();
3063
3821
  }), "hiwork-core: \u7528\u91CF\u7F13\u5B58\u968F\u8D26\u53F7 / Key \u53D8\u5316\u6E05\u7A7A");
3064
3822
  ctx.effect(() => observeAboutNavIcon(), "hiwork-core: about nav icon");
3065
3823
  ctx.slots.inject("sidebar.brand.mark", () => ctx.slots.register({
@@ -3074,13 +3832,13 @@ function apply(ctx) {
3074
3832
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
3075
3833
  name: "sidebar.footer.action",
3076
3834
  id: AI_ROUTER_NS,
3077
- inject: () => controller.inject()
3835
+ inject: () => ({ ...controller.inject(), pluginUpdates })
3078
3836
  }, UserBadge));
3079
3837
  ctx.effect(() => mountLoginGate(scope, storeCredential), "hiwork-core: login gate");
3080
3838
  ctx.effect(() => observeHeroDecorations(document, dailyQuotes), "hiwork-core: hero decorations");
3081
- installFeatureCenter(ctx, managedMcp);
3839
+ installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench);
3082
3840
  }
3083
- function installFeatureCenter(ctx, managedMcp) {
3841
+ function installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench) {
3084
3842
  const featureRegistry = createFeatureRegistry();
3085
3843
  let center;
3086
3844
  const sessions = ctx.get("sessions");
@@ -3109,6 +3867,27 @@ function installFeatureCenter(ctx, managedMcp) {
3109
3867
  refresh: async (signal) => await managedMcp.refresh(signal),
3110
3868
  assertion: async (assignmentId, signal) => await managedMcp.assertion(assignmentId, signal),
3111
3869
  subscribe: (listener) => managedMcp.subscribe(listener)
3870
+ },
3871
+ // 受管技能面:技能域(hiwork-capabilities)按这个成员判断「有没有这个能力」,老 core 上
3872
+ // 它是 undefined,能力中心照常工作、只是没有受管技能那一块(可选解耦,见 contracts.ts)。
3873
+ // 六项与 spec §4.5 逐字对应;核心不落盘、不解包、不持有会话 cookie——包体由 Host 取。
3874
+ managedSkills: {
3875
+ snapshot: () => managedSkills.snapshot(),
3876
+ isStale: (ttlMs) => managedSkills.isStale(ttlMs),
3877
+ warm: async (signal) => await managedSkills.warm(signal),
3878
+ refresh: async (signal) => await managedSkills.refresh(signal),
3879
+ package: async (assignmentId, signal) => await managedSkills.package(assignmentId, signal),
3880
+ subscribe: (listener) => managedSkills.subscribe(listener)
3881
+ },
3882
+ // 工作台应用清单面:工作台域(hiwork-workbench)按这个成员判断「有没有这个能力」,老 core 上
3883
+ // 它是 undefined,货架按「本部署尚未启用工作台」降级(spec §10 的降级矩阵)。M0 只有清单
3884
+ // 相关的方法——`submit` / `poll` / `cancel` / `materialize` 属 M1(见 contracts.ts)。
3885
+ managedWorkbench: {
3886
+ snapshot: () => managedWorkbench.snapshot(),
3887
+ isStale: (ttlMs) => managedWorkbench.isStale(ttlMs),
3888
+ warm: async (signal) => await managedWorkbench.warm(signal),
3889
+ refresh: async (signal) => await managedWorkbench.refresh(signal),
3890
+ subscribe: (listener) => managedWorkbench.subscribe(listener)
3112
3891
  }
3113
3892
  };
3114
3893
  ctx.provide(HIWORK_FEATURE_CENTER_SERVICE, face);