hiwork-core 0.0.16 → 0.0.18
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 +373 -83
- package/lib/index.js +102 -1
- package/lib/plugin-updates-rpc.js +187 -0
- package/lib/types/client/plugin-updates.d.ts +150 -0
- package/lib/types/client/user-badge.d.ts +12 -2
- package/lib/types/plugin-updates-rpc.d.ts +169 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -350,6 +350,146 @@ function AiRouterSection(props) {
|
|
|
350
350
|
var import_react2 = require("react");
|
|
351
351
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
352
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
|
+
|
|
353
493
|
// src/client/settings-navigation.ts
|
|
354
494
|
function pickSettingsSectionButton(buttons, labels) {
|
|
355
495
|
for (const label of labels) {
|
|
@@ -576,6 +716,90 @@ var confirmActionsStyle = {
|
|
|
576
716
|
gap: 8,
|
|
577
717
|
marginTop: 8
|
|
578
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;
|
|
579
803
|
function LogoutIcon() {
|
|
580
804
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
581
805
|
"svg",
|
|
@@ -631,6 +855,41 @@ function LogoutConfirm(props) {
|
|
|
631
855
|
function UserBadge(props) {
|
|
632
856
|
const state = props.useAiRouterCard((snapshot) => snapshot);
|
|
633
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]);
|
|
634
893
|
if (state.status !== "logged-in" || state.username === void 0) return null;
|
|
635
894
|
const displayName = state.displayName ?? state.username;
|
|
636
895
|
const initial = displayName.slice(0, 1).toUpperCase();
|
|
@@ -638,22 +897,51 @@ function UserBadge(props) {
|
|
|
638
897
|
setConfirming(false);
|
|
639
898
|
void props.actions.logout();
|
|
640
899
|
} }) : null;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
title: displayName,
|
|
649
|
-
"aria-label": "\u6253\u5F00\u8BBE\u7F6E",
|
|
650
|
-
onClick: openGatewaySettings,
|
|
651
|
-
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);
|
|
652
907
|
}
|
|
653
|
-
)
|
|
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
|
+
] });
|
|
654
942
|
}
|
|
655
943
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
656
|
-
/* @__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: [
|
|
657
945
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
658
946
|
"button",
|
|
659
947
|
{
|
|
@@ -694,7 +982,8 @@ function UserBadge(props) {
|
|
|
694
982
|
}
|
|
695
983
|
)
|
|
696
984
|
] }),
|
|
697
|
-
confirm
|
|
985
|
+
confirm,
|
|
986
|
+
updatesCard
|
|
698
987
|
] });
|
|
699
988
|
}
|
|
700
989
|
|
|
@@ -2876,28 +3165,28 @@ function toAssignment(value) {
|
|
|
2876
3165
|
const record = value;
|
|
2877
3166
|
const authRaw = record["auth"];
|
|
2878
3167
|
const auth = typeof authRaw === "object" && authRaw !== null ? authRaw : void 0;
|
|
2879
|
-
const assignmentId =
|
|
2880
|
-
const serverName =
|
|
2881
|
-
const url =
|
|
2882
|
-
const kind =
|
|
2883
|
-
const 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"]);
|
|
2884
3173
|
if (assignmentId === void 0 || serverName === void 0 || url === void 0 || kind === void 0 || mintUrl === void 0) {
|
|
2885
3174
|
return void 0;
|
|
2886
3175
|
}
|
|
2887
3176
|
return {
|
|
2888
3177
|
assignmentId,
|
|
2889
3178
|
revision: typeof record["revision"] === "number" ? record["revision"] : 0,
|
|
2890
|
-
slug:
|
|
2891
|
-
displayName:
|
|
3179
|
+
slug: text2(record["slug"]) ?? serverName,
|
|
3180
|
+
displayName: text2(record["displayName"]) ?? serverName,
|
|
2892
3181
|
serverName,
|
|
2893
|
-
transport:
|
|
3182
|
+
transport: text2(record["transport"]) ?? "",
|
|
2894
3183
|
url,
|
|
2895
3184
|
extraHeaders: stringMap(record["extraHeaders"]),
|
|
2896
|
-
auth: { kind, mintUrl, revokeUrl:
|
|
2897
|
-
updatedAt:
|
|
3185
|
+
auth: { kind, mintUrl, revokeUrl: text2(auth?.["revokeUrl"]) ?? "" },
|
|
3186
|
+
updatedAt: text2(record["updatedAt"]) ?? ""
|
|
2898
3187
|
};
|
|
2899
3188
|
}
|
|
2900
|
-
function
|
|
3189
|
+
function text2(value) {
|
|
2901
3190
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2902
3191
|
}
|
|
2903
3192
|
function stringMap(raw) {
|
|
@@ -2961,17 +3250,17 @@ var ManagedMcpStore = class {
|
|
|
2961
3250
|
throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
|
|
2962
3251
|
}
|
|
2963
3252
|
const record = value;
|
|
2964
|
-
const assertion =
|
|
2965
|
-
const mintUrl =
|
|
3253
|
+
const assertion = text2(record["assertion"]);
|
|
3254
|
+
const mintUrl = text2(record["mintUrl"]);
|
|
2966
3255
|
if (assertion === void 0 || mintUrl === void 0) {
|
|
2967
3256
|
throw new ManagedMcpRpcError("internal", "MCP \u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002");
|
|
2968
3257
|
}
|
|
2969
3258
|
return {
|
|
2970
3259
|
assertion,
|
|
2971
3260
|
mintUrl,
|
|
2972
|
-
revokeUrl:
|
|
2973
|
-
expiresAt:
|
|
2974
|
-
serverName:
|
|
3261
|
+
revokeUrl: text2(record["revokeUrl"]) ?? "",
|
|
3262
|
+
expiresAt: text2(record["expiresAt"]) ?? "",
|
|
3263
|
+
serverName: text2(record["serverName"]) ?? ""
|
|
2975
3264
|
};
|
|
2976
3265
|
}
|
|
2977
3266
|
/** 订阅镜像变化(连接器域据此重渲染 / 重排同步)。返回退订函数。 */
|
|
@@ -3020,29 +3309,29 @@ var ManagedSkillsRpcError = class extends Error {
|
|
|
3020
3309
|
this.name = "ManagedSkillsRpcError";
|
|
3021
3310
|
}
|
|
3022
3311
|
};
|
|
3023
|
-
var
|
|
3024
|
-
var
|
|
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";
|
|
3025
3314
|
function unwrapManagedSkillsResult(value) {
|
|
3026
3315
|
if (typeof value !== "object" || value === null || !("ok" in value)) {
|
|
3027
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3316
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3028
3317
|
}
|
|
3029
3318
|
const result = value;
|
|
3030
3319
|
if (result.ok === true) return result.value;
|
|
3031
3320
|
if (result.ok === false) {
|
|
3032
3321
|
const code = typeof result.error?.code === "string" ? result.error.code : "internal";
|
|
3033
|
-
const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message :
|
|
3322
|
+
const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE2;
|
|
3034
3323
|
throw new ManagedSkillsRpcError(code, message);
|
|
3035
3324
|
}
|
|
3036
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3325
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3037
3326
|
}
|
|
3038
3327
|
function toSnapshot2(value) {
|
|
3039
3328
|
if (typeof value !== "object" || value === null) {
|
|
3040
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3329
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3041
3330
|
}
|
|
3042
3331
|
const record = value;
|
|
3043
3332
|
const rawList = record["assignments"];
|
|
3044
3333
|
if (!Array.isArray(rawList)) {
|
|
3045
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3334
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3046
3335
|
}
|
|
3047
3336
|
const assignments = [];
|
|
3048
3337
|
for (const item of rawList) {
|
|
@@ -3060,7 +3349,7 @@ function toSnapshot2(value) {
|
|
|
3060
3349
|
const detail = lastError;
|
|
3061
3350
|
snapshot.lastError = {
|
|
3062
3351
|
code: typeof detail.code === "string" ? detail.code : "internal",
|
|
3063
|
-
message: typeof detail.message === "string" ? detail.message :
|
|
3352
|
+
message: typeof detail.message === "string" ? detail.message : GENERIC_FAILURE_MESSAGE2
|
|
3064
3353
|
};
|
|
3065
3354
|
}
|
|
3066
3355
|
return snapshot;
|
|
@@ -3068,24 +3357,24 @@ function toSnapshot2(value) {
|
|
|
3068
3357
|
function toAssignment2(value) {
|
|
3069
3358
|
if (typeof value !== "object" || value === null) return void 0;
|
|
3070
3359
|
const record = value;
|
|
3071
|
-
const assignmentId =
|
|
3072
|
-
const skillName =
|
|
3073
|
-
const contentSha256 =
|
|
3360
|
+
const assignmentId = text3(record["assignmentId"]);
|
|
3361
|
+
const skillName = text3(record["skillName"]);
|
|
3362
|
+
const contentSha256 = text3(record["contentSha256"]);
|
|
3074
3363
|
if (assignmentId === void 0 || skillName === void 0 || contentSha256 === void 0) return void 0;
|
|
3075
3364
|
return {
|
|
3076
3365
|
assignmentId,
|
|
3077
3366
|
revision: typeof record["revision"] === "number" ? record["revision"] : 0,
|
|
3078
|
-
slug:
|
|
3079
|
-
displayName:
|
|
3367
|
+
slug: text3(record["slug"]) ?? skillName,
|
|
3368
|
+
displayName: text3(record["displayName"]) ?? skillName,
|
|
3080
3369
|
skillName,
|
|
3081
|
-
description:
|
|
3370
|
+
description: text3(record["description"]) ?? "",
|
|
3082
3371
|
contentSha256,
|
|
3083
3372
|
sizeBytes: typeof record["sizeBytes"] === "number" ? record["sizeBytes"] : 0,
|
|
3084
|
-
version:
|
|
3085
|
-
updatedAt:
|
|
3373
|
+
version: text3(record["version"]) ?? "",
|
|
3374
|
+
updatedAt: text3(record["updatedAt"]) ?? ""
|
|
3086
3375
|
};
|
|
3087
3376
|
}
|
|
3088
|
-
function
|
|
3377
|
+
function text3(value) {
|
|
3089
3378
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
3090
3379
|
}
|
|
3091
3380
|
var ManagedSkillsStore = class {
|
|
@@ -3155,15 +3444,15 @@ var ManagedSkillsStore = class {
|
|
|
3155
3444
|
const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "package", { assignmentId }, signal);
|
|
3156
3445
|
const value = unwrapManagedSkillsResult(raw);
|
|
3157
3446
|
if (typeof value !== "object" || value === null) {
|
|
3158
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3447
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3159
3448
|
}
|
|
3160
3449
|
const record = value;
|
|
3161
|
-
const id =
|
|
3162
|
-
const skillName =
|
|
3163
|
-
const contentSha256 =
|
|
3164
|
-
const archiveBase64 =
|
|
3450
|
+
const id = text3(record["assignmentId"]);
|
|
3451
|
+
const skillName = text3(record["skillName"]);
|
|
3452
|
+
const contentSha256 = text3(record["contentSha256"]);
|
|
3453
|
+
const archiveBase64 = text3(record["archiveBase64"]);
|
|
3165
3454
|
if (id === void 0 || skillName === void 0 || contentSha256 === void 0 || archiveBase64 === void 0) {
|
|
3166
|
-
throw new ManagedSkillsRpcError("internal",
|
|
3455
|
+
throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE2);
|
|
3167
3456
|
}
|
|
3168
3457
|
return { assignmentId: id, skillName, contentSha256, archiveBase64 };
|
|
3169
3458
|
}
|
|
@@ -3251,25 +3540,25 @@ var ManagedWorkbenchRpcError = class extends Error {
|
|
|
3251
3540
|
this.name = "ManagedWorkbenchRpcError";
|
|
3252
3541
|
}
|
|
3253
3542
|
};
|
|
3254
|
-
var
|
|
3255
|
-
var
|
|
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";
|
|
3256
3545
|
function unwrapManagedWorkbenchResult(value) {
|
|
3257
3546
|
if (typeof value !== "object" || value === null || !("ok" in value)) {
|
|
3258
|
-
throw new ManagedWorkbenchRpcError("internal",
|
|
3547
|
+
throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
|
|
3259
3548
|
}
|
|
3260
3549
|
const result = value;
|
|
3261
3550
|
if (result.ok === true) return result.value;
|
|
3262
3551
|
if (result.ok === false) {
|
|
3263
3552
|
const code = typeof result.error?.code === "string" ? result.error.code : "internal";
|
|
3264
|
-
const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message :
|
|
3553
|
+
const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE3;
|
|
3265
3554
|
throw new ManagedWorkbenchRpcError(code, message);
|
|
3266
3555
|
}
|
|
3267
|
-
throw new ManagedWorkbenchRpcError("internal",
|
|
3556
|
+
throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
|
|
3268
3557
|
}
|
|
3269
|
-
function
|
|
3558
|
+
function text4(value) {
|
|
3270
3559
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
3271
3560
|
}
|
|
3272
|
-
function
|
|
3561
|
+
function isRecord2(value) {
|
|
3273
3562
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3274
3563
|
}
|
|
3275
3564
|
function stringArray(value) {
|
|
@@ -3277,45 +3566,45 @@ function stringArray(value) {
|
|
|
3277
3566
|
return value.filter((item) => typeof item === "string");
|
|
3278
3567
|
}
|
|
3279
3568
|
function toApp(value) {
|
|
3280
|
-
if (!
|
|
3281
|
-
const code =
|
|
3569
|
+
if (!isRecord2(value)) return void 0;
|
|
3570
|
+
const code = text4(value["code"]);
|
|
3282
3571
|
const executor = value["executor"];
|
|
3283
|
-
if (code === void 0 || !
|
|
3284
|
-
const kind =
|
|
3572
|
+
if (code === void 0 || !isRecord2(executor)) return void 0;
|
|
3573
|
+
const kind = text4(executor["kind"]);
|
|
3285
3574
|
if (kind === void 0) return void 0;
|
|
3286
3575
|
const normalizedExecutor = { ...executor, kind };
|
|
3287
|
-
const mode =
|
|
3576
|
+
const mode = text4(executor["mode"]);
|
|
3288
3577
|
if (mode === void 0) delete normalizedExecutor.mode;
|
|
3289
3578
|
else normalizedExecutor.mode = mode;
|
|
3290
|
-
const category =
|
|
3579
|
+
const category = isRecord2(value["category"]) ? value["category"] : {};
|
|
3291
3580
|
const inputs = [];
|
|
3292
3581
|
for (const item of Array.isArray(value["inputs"]) ? value["inputs"] : []) {
|
|
3293
|
-
if (!
|
|
3294
|
-
const key =
|
|
3582
|
+
if (!isRecord2(item)) continue;
|
|
3583
|
+
const key = text4(item["key"]);
|
|
3295
3584
|
if (key === void 0) continue;
|
|
3296
3585
|
inputs.push({
|
|
3297
3586
|
...item,
|
|
3298
3587
|
key,
|
|
3299
|
-
type:
|
|
3300
|
-
label:
|
|
3588
|
+
type: text4(item["type"]) ?? "",
|
|
3589
|
+
label: text4(item["label"]) ?? key,
|
|
3301
3590
|
required: item["required"] === true
|
|
3302
3591
|
});
|
|
3303
3592
|
}
|
|
3304
3593
|
const outputs = [];
|
|
3305
3594
|
for (const item of Array.isArray(value["outputs"]) ? value["outputs"] : []) {
|
|
3306
|
-
if (!
|
|
3307
|
-
const key =
|
|
3595
|
+
if (!isRecord2(item)) continue;
|
|
3596
|
+
const key = text4(item["key"]);
|
|
3308
3597
|
if (key === void 0) continue;
|
|
3309
|
-
outputs.push({ ...item, key, type:
|
|
3598
|
+
outputs.push({ ...item, key, type: text4(item["type"]) ?? "" });
|
|
3310
3599
|
}
|
|
3311
|
-
const iconUrl =
|
|
3600
|
+
const iconUrl = text4(value["iconUrl"]);
|
|
3312
3601
|
const revision = value["revision"];
|
|
3313
3602
|
return {
|
|
3314
3603
|
code,
|
|
3315
|
-
displayName:
|
|
3316
|
-
summary:
|
|
3604
|
+
displayName: text4(value["displayName"]) ?? code,
|
|
3605
|
+
summary: text4(value["summary"]) ?? "",
|
|
3317
3606
|
...iconUrl === void 0 ? {} : { iconUrl },
|
|
3318
|
-
category: { code:
|
|
3607
|
+
category: { code: text4(category["code"]) ?? "", name: text4(category["name"]) ?? "" },
|
|
3319
3608
|
tags: stringArray(value["tags"]),
|
|
3320
3609
|
featured: value["featured"] === true,
|
|
3321
3610
|
revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
|
|
@@ -3326,9 +3615,9 @@ function toApp(value) {
|
|
|
3326
3615
|
};
|
|
3327
3616
|
}
|
|
3328
3617
|
function toSnapshot3(value) {
|
|
3329
|
-
if (!
|
|
3618
|
+
if (!isRecord2(value)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
|
|
3330
3619
|
const rawApps = value["apps"];
|
|
3331
|
-
if (!Array.isArray(rawApps)) throw new ManagedWorkbenchRpcError("internal",
|
|
3620
|
+
if (!Array.isArray(rawApps)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE3);
|
|
3332
3621
|
const apps = [];
|
|
3333
3622
|
for (const item of rawApps) {
|
|
3334
3623
|
const app = toApp(item);
|
|
@@ -3341,10 +3630,10 @@ function toSnapshot3(value) {
|
|
|
3341
3630
|
stale: value["stale"] === true
|
|
3342
3631
|
};
|
|
3343
3632
|
const lastError = value["lastError"];
|
|
3344
|
-
if (
|
|
3633
|
+
if (isRecord2(lastError)) {
|
|
3345
3634
|
snapshot.lastError = {
|
|
3346
|
-
code:
|
|
3347
|
-
message: typeof lastError["message"] === "string" ? lastError["message"] :
|
|
3635
|
+
code: text4(lastError["code"]) ?? "internal",
|
|
3636
|
+
message: typeof lastError["message"] === "string" ? lastError["message"] : GENERIC_FAILURE_MESSAGE3
|
|
3348
3637
|
};
|
|
3349
3638
|
}
|
|
3350
3639
|
return snapshot;
|
|
@@ -3509,6 +3798,7 @@ function apply(ctx) {
|
|
|
3509
3798
|
const managedSkills = new ManagedSkillsStore(connection.rpc);
|
|
3510
3799
|
const managedWorkbench = new ManagedWorkbenchStore(connection.rpc);
|
|
3511
3800
|
const appVersion = createAppVersion(connection.rpc);
|
|
3801
|
+
const pluginUpdates = createPluginUpdates(connection.rpc);
|
|
3512
3802
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3513
3803
|
name: "settings.section",
|
|
3514
3804
|
id: "hiwork-usage",
|
|
@@ -3542,7 +3832,7 @@ function apply(ctx) {
|
|
|
3542
3832
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
3543
3833
|
name: "sidebar.footer.action",
|
|
3544
3834
|
id: AI_ROUTER_NS,
|
|
3545
|
-
inject: () => controller.inject()
|
|
3835
|
+
inject: () => ({ ...controller.inject(), pluginUpdates })
|
|
3546
3836
|
}, UserBadge));
|
|
3547
3837
|
ctx.effect(() => mountLoginGate(scope, storeCredential), "hiwork-core: login gate");
|
|
3548
3838
|
ctx.effect(() => observeHeroDecorations(document, dailyQuotes), "hiwork-core: hero decorations");
|
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.
|
|
3140
|
+
var PKG_VERSION = false ? "dev" : "0.0.18";
|
|
3141
3141
|
|
|
3142
3142
|
// src/ai-router-client.ts
|
|
3143
3143
|
var AUTH_AUTHORIZE_PATH = "/api/auth/authorize";
|
|
@@ -5446,6 +5446,101 @@ function registerWorkbenchRpc(ctx, deps, store) {
|
|
|
5446
5446
|
};
|
|
5447
5447
|
}
|
|
5448
5448
|
|
|
5449
|
+
// src/plugin-updates-rpc.ts
|
|
5450
|
+
var PLUGIN_UPDATES_RPC_CHANNEL = "/hiwork-plugin-updates";
|
|
5451
|
+
var PLUGIN_UPDATES_RPC_ENDPOINTS = ["snapshot", "apply"];
|
|
5452
|
+
var PluginUpdatesRequestError = class extends Error {
|
|
5453
|
+
constructor(code, message) {
|
|
5454
|
+
super(message);
|
|
5455
|
+
this.code = code;
|
|
5456
|
+
this.name = "PluginUpdatesRequestError";
|
|
5457
|
+
}
|
|
5458
|
+
};
|
|
5459
|
+
var PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE = "\u63D2\u4EF6\u66F4\u65B0\u72B6\u6001\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
5460
|
+
function isPluginUpdatesRpcEndpoint(endpoint) {
|
|
5461
|
+
return PLUGIN_UPDATES_RPC_ENDPOINTS.includes(endpoint);
|
|
5462
|
+
}
|
|
5463
|
+
function isRecord3(value) {
|
|
5464
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5465
|
+
}
|
|
5466
|
+
function textOrUndefined4(value) {
|
|
5467
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
5468
|
+
}
|
|
5469
|
+
function isAborted7(signal) {
|
|
5470
|
+
return signal?.aborted === true;
|
|
5471
|
+
}
|
|
5472
|
+
function parsePluginUpdatesPayload(endpoint, raw) {
|
|
5473
|
+
if (raw === void 0 || raw === null) return {};
|
|
5474
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
5475
|
+
throw new PluginUpdatesRequestError("bad-request", "\u63D2\u4EF6\u66F4\u65B0\u8BF7\u6C42\u53C2\u6570\u4E0D\u5408\u6CD5\u3002");
|
|
5476
|
+
}
|
|
5477
|
+
const keys = Object.keys(raw);
|
|
5478
|
+
const first = keys[0];
|
|
5479
|
+
if (first !== void 0) {
|
|
5480
|
+
throw new PluginUpdatesRequestError("bad-request", `\u63D2\u4EF6\u66F4\u65B0\u8BF7\u6C42\u5305\u542B\u672A\u77E5\u5B57\u6BB5 '${first}'\u3002`);
|
|
5481
|
+
}
|
|
5482
|
+
return {};
|
|
5483
|
+
}
|
|
5484
|
+
function toPackageUpdate(raw) {
|
|
5485
|
+
if (!isRecord3(raw)) return void 0;
|
|
5486
|
+
const packageName = textOrUndefined4(raw["packageName"]);
|
|
5487
|
+
if (packageName === void 0) return void 0;
|
|
5488
|
+
return {
|
|
5489
|
+
packageName,
|
|
5490
|
+
displayName: textOrUndefined4(raw["displayName"]) ?? packageName,
|
|
5491
|
+
from: textOrUndefined4(raw["from"]) ?? "",
|
|
5492
|
+
to: textOrUndefined4(raw["to"]) ?? ""
|
|
5493
|
+
};
|
|
5494
|
+
}
|
|
5495
|
+
function toSnapshot(raw) {
|
|
5496
|
+
if (!isRecord3(raw)) return void 0;
|
|
5497
|
+
const packages = [];
|
|
5498
|
+
for (const item of Array.isArray(raw["packages"]) ? raw["packages"] : []) {
|
|
5499
|
+
const normalized = toPackageUpdate(item);
|
|
5500
|
+
if (normalized !== void 0) packages.push(normalized);
|
|
5501
|
+
}
|
|
5502
|
+
const revision = raw["revision"];
|
|
5503
|
+
const fetchedAt = raw["fetchedAt"];
|
|
5504
|
+
return {
|
|
5505
|
+
packages,
|
|
5506
|
+
fetchedAt: typeof fetchedAt === "number" && Number.isFinite(fetchedAt) ? fetchedAt : 0,
|
|
5507
|
+
revision: typeof revision === "number" && Number.isFinite(revision) ? revision : null
|
|
5508
|
+
};
|
|
5509
|
+
}
|
|
5510
|
+
async function handlePluginUpdatesEndpoint(endpoint, rawPayload, deps, signal) {
|
|
5511
|
+
try {
|
|
5512
|
+
if (isAborted7(signal)) return failure7("cancelled", "\u63D2\u4EF6\u66F4\u65B0\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002");
|
|
5513
|
+
if (!isPluginUpdatesRpcEndpoint(endpoint)) {
|
|
5514
|
+
throw new PluginUpdatesRequestError("bad-request", `\u672A\u77E5\u7684\u63D2\u4EF6\u66F4\u65B0\u7AEF\u70B9 '${endpoint}'`);
|
|
5515
|
+
}
|
|
5516
|
+
parsePluginUpdatesPayload(endpoint, rawPayload);
|
|
5517
|
+
const plugins = deps.desktopPlugins();
|
|
5518
|
+
if (endpoint === "snapshot") {
|
|
5519
|
+
const value = plugins === void 0 ? null : toSnapshot(await plugins.pending()) ?? null;
|
|
5520
|
+
return { ok: true, value };
|
|
5521
|
+
}
|
|
5522
|
+
if (plugins === void 0) {
|
|
5523
|
+
return { ok: true, value: { requested: false } };
|
|
5524
|
+
}
|
|
5525
|
+
await plugins.applyNow();
|
|
5526
|
+
return { ok: true, value: { requested: true } };
|
|
5527
|
+
} catch (error) {
|
|
5528
|
+
if (error instanceof PluginUpdatesRequestError) return failure7(error.code, error.message);
|
|
5529
|
+
deps.log?.(`hiwork-core: \u63D2\u4EF6\u66F4\u65B0 RPC '${endpoint}' \u5931\u8D25\uFF08${error instanceof Error ? error.name : typeof error}\uFF09`);
|
|
5530
|
+
return failure7("internal", PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
function failure7(code, message) {
|
|
5534
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
5535
|
+
}
|
|
5536
|
+
function registerPluginUpdatesRpc(ctx, deps) {
|
|
5537
|
+
const handler = (endpoint, payload, signal) => handlePluginUpdatesEndpoint(endpoint, payload, deps, signal);
|
|
5538
|
+
const remove = ctx.connection.rpc.handle(PLUGIN_UPDATES_RPC_CHANNEL, handler, { authority: "loopback" });
|
|
5539
|
+
return async () => {
|
|
5540
|
+
await remove();
|
|
5541
|
+
};
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5449
5544
|
// src/office-credentials.ts
|
|
5450
5545
|
var OFFICE_CREDENTIALS_SERVICE = "hiworkOfficeCredentials";
|
|
5451
5546
|
var OFFICE_MESSAGE_FALLBACK = {
|
|
@@ -6058,6 +6153,12 @@ function apply(ctx, config) {
|
|
|
6058
6153
|
ctx.logger.warn(message);
|
|
6059
6154
|
}
|
|
6060
6155
|
}), "hiwork-core: app version rpc");
|
|
6156
|
+
ctx.effect(() => registerPluginUpdatesRpc({ connection, logger: ctx.logger }, {
|
|
6157
|
+
desktopPlugins: () => ctx.get("desktopPlugins"),
|
|
6158
|
+
log: (message) => {
|
|
6159
|
+
ctx.logger.warn(message);
|
|
6160
|
+
}
|
|
6161
|
+
}), "hiwork-core: plugin updates rpc");
|
|
6061
6162
|
ctx.effect(() => registerIpcMessageHandler({
|
|
6062
6163
|
source: process,
|
|
6063
6164
|
complete: (input) => {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/hiwork-plugin-updates` loopback RPC 的 Host 实现:把**桌面端**的 `desktopPlugins`
|
|
3
|
+
* (受管插件清单的只读面 + 唯一的回收入口)搬到渲染端够得着的地方。
|
|
4
|
+
*
|
|
5
|
+
* 为什么必须有这条 RPC(spec §4.5 的「数据来源」一段):`desktopPlugins` 是桌面端的
|
|
6
|
+
* `desktop-bridge.mts` 用 `ctx.provide('desktopPlugins', …)` 提供到 **DSH host** ctx 上的,
|
|
7
|
+
* 而 hiwork-core 的客户端跑在 **renderer**——两个进程。客户端现有的全部数据层(usage /
|
|
8
|
+
* session-cost / daily / managed-mcp / managed-skills / managed-workbench / app-version)
|
|
9
|
+
* 无一例外都经 `ctx.get('connection').rpc` 走 `/hiwork-*` loopback RPC,**没有任何
|
|
10
|
+
* client→host 的服务直连通道**。而「重启并更新」是个**点击**,它发生在客户端。所以取数与
|
|
11
|
+
* 触发都得过这条 RPC。
|
|
12
|
+
*
|
|
13
|
+
* 骨架照 `src/workbench-bridge.ts`(同 `src/managed-skills.ts` / `src/app-version-rpc.ts`):
|
|
14
|
+
* 导出频道名 + 端点白名单(`as const`)+ 结构化 deps 端口(**不 import cordis 运行时**,
|
|
15
|
+
* 便于单测直接伪造)+ 一个 `register…Rpc(...)` 返回 disposer。
|
|
16
|
+
*
|
|
17
|
+
* 三条本频道特有的纪律:
|
|
18
|
+
* - ⚠️ **`desktopPlugins` 是可选的**(不变量:依赖必须是可选的)。老桌面端上没有它 —— 那正是
|
|
19
|
+
* 「新 core + 老桌面端」这条共存路径。拿不到时 `snapshot` 回**成功信封 + `null`**
|
|
20
|
+
* (「不可用」**不是错误**,与 `app-version-rpc.ts` 的「桌面端没推过不是错误」同一条口径),
|
|
21
|
+
* 客户端据此**不渲染任何东西**。抛错会把共存路径变成一条报错日志。
|
|
22
|
+
* - ⚠️ **只搬运 `packages` / `revision` / `fetchedAt` 这三样**,**不搬 `held`**(plan T11
|
|
23
|
+
* Step 0)。它们三个是真的:落在 `.dsh-pending-updates.json` 里,跨进程可用。`held` /
|
|
24
|
+
* `from` / `displayName` 在生产路径上是空的——根因是 `desktop-bridge.mts` 调
|
|
25
|
+
* `createDesktopHostServices()` 时**没传 `pluginManifest`**,于是 `desktop-host.ts` 里那条
|
|
26
|
+
* `memory = options.pluginManifest?.current()` 恒 `undefined`、`held` 退到 `?? []`。
|
|
27
|
+
* 那条通道要修得补一条**主进程 → 子进程**的路(新 IPC 信封或 sidecar 文件),而**本 RPC 是
|
|
28
|
+
* client→host 的**,它跨的是「renderer↔DSH」,**跨不过「DSH↔主进程」这一层**。所以别「顺手把
|
|
29
|
+
* `held` 也传过去」——传过去也永远是空的,只会让下一个读这段的人以为服务端没下发降级项。
|
|
30
|
+
* - ⚠️ **`apply` 不是「更新成功」的回执**:它只把触发递下去(`desktopPlugins.applyNow()` →
|
|
31
|
+
* `APPLY_PLUGIN_UPDATES_IPC` → 主进程的 `recycleDshForPluginUpdate()`),重启由**主进程**执行、
|
|
32
|
+
* 客户端那一刻就没了。按钮语义是「已请求重启」。所以返回值是 `{ requested }`,
|
|
33
|
+
* **不是** `{ applied: true }`。
|
|
34
|
+
*
|
|
35
|
+
* 回收红线(spec §6.1 不变量②):本文件的 `apply` 是**唯一**允许触发回收的新入口,且只能由
|
|
36
|
+
* 用户点击到达(客户端只在「重启并更新」按钮的 onClick 里调它)。`snapshot` **绝不触碰**
|
|
37
|
+
* `applyNow()`——清单拉取、轮询、启动流程都不许触发回收。
|
|
38
|
+
*/
|
|
39
|
+
/** RPC 频道;必须与 client 半边 `src/client/plugin-updates.ts` 的 `PLUGIN_UPDATES_CHANNEL` 一致(由测试锁定)。 */
|
|
40
|
+
export const PLUGIN_UPDATES_RPC_CHANNEL = '/hiwork-plugin-updates';
|
|
41
|
+
/**
|
|
42
|
+
* 端点白名单:只有两个。
|
|
43
|
+
*
|
|
44
|
+
* `snapshot` 是「现在有没有待生效的更新」(桌面端读 pending 文件,**不拉清单、不写文件、
|
|
45
|
+
* 不触发回收**),`apply` 是「现在就去重启并应用」——它只在用户点击时被调用。
|
|
46
|
+
* 白名单是客户端可用的能力边界,多一项就等于先发布了一个没有实现的承诺。
|
|
47
|
+
*/
|
|
48
|
+
export const PLUGIN_UPDATES_RPC_ENDPOINTS = ['snapshot', 'apply'];
|
|
49
|
+
/** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
|
|
50
|
+
export class PluginUpdatesRequestError extends Error {
|
|
51
|
+
code;
|
|
52
|
+
constructor(code, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.name = 'PluginUpdatesRequestError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 通用失败文案:不暴露任何桌面端细节。 */
|
|
59
|
+
export const PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE = '插件更新状态暂时无法获取,请稍后重试';
|
|
60
|
+
/** 是否为白名单端点。 */
|
|
61
|
+
export function isPluginUpdatesRpcEndpoint(endpoint) {
|
|
62
|
+
return PLUGIN_UPDATES_RPC_ENDPOINTS.includes(endpoint);
|
|
63
|
+
}
|
|
64
|
+
/** 宽松的记录判定(数组与 null 都不算)。 */
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
/** 非空字符串或 undefined。 */
|
|
69
|
+
function textOrUndefined(value) {
|
|
70
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** 取消判定(用函数包一层,避免 TS 把属性窄化固化到 `await` 之后)。 */
|
|
73
|
+
function isAborted(signal) {
|
|
74
|
+
return signal?.aborted === true;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* payload 校验:两个端点都**只放行空对象**(本频道没有任何入参)。
|
|
78
|
+
*
|
|
79
|
+
* 与 workbench / app-version 的空入参分支逐字同款:出现任何字段都是客户端实现缺陷,直接
|
|
80
|
+
* `bad-request`——一个 `{ force: true }` 被静默忽略,会让调用方以为「强制重启」生效了。
|
|
81
|
+
*/
|
|
82
|
+
export function parsePluginUpdatesPayload(endpoint, raw) {
|
|
83
|
+
if (raw === undefined || raw === null)
|
|
84
|
+
return {};
|
|
85
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
86
|
+
throw new PluginUpdatesRequestError('bad-request', '插件更新请求参数不合法。');
|
|
87
|
+
}
|
|
88
|
+
const keys = Object.keys(raw);
|
|
89
|
+
const first = keys[0];
|
|
90
|
+
if (first !== undefined) {
|
|
91
|
+
throw new PluginUpdatesRequestError('bad-request', `插件更新请求包含未知字段 '${first}'。`);
|
|
92
|
+
}
|
|
93
|
+
// endpoint 已在调用方过白名单;这里保留参数是为了类型上与其它频道的 parse* 同形。
|
|
94
|
+
void endpoint;
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 一条更新的规范化:**只在缺 `packageName` 时丢条目**,其余兜底。
|
|
99
|
+
*
|
|
100
|
+
* 为什么判据是 `packageName` 而不是「`to` 是不是精确版本」:界面**只**依赖
|
|
101
|
+
* `packages.length > 0`(spec §4.3 的 ⚠️)。在这里按更严的形状丢条目,等于把一份真的待生效
|
|
102
|
+
* 更新静默吃掉——用户就再也等不到重启提示了。所以「形状兜底」在此处的方向是**保住计数**:
|
|
103
|
+
* 只要这条还指得出是哪个包就留下,`to` 缺了就记空串(界面本来也不渲染它)。
|
|
104
|
+
* 反过来,缺 `packageName` 的条目连「有没有东西要装」都回答不了,留着只会让界面误报。
|
|
105
|
+
*/
|
|
106
|
+
function toPackageUpdate(raw) {
|
|
107
|
+
if (!isRecord(raw))
|
|
108
|
+
return undefined;
|
|
109
|
+
const packageName = textOrUndefined(raw['packageName']);
|
|
110
|
+
if (packageName === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
return {
|
|
113
|
+
packageName,
|
|
114
|
+
displayName: textOrUndefined(raw['displayName']) ?? packageName,
|
|
115
|
+
from: textOrUndefined(raw['from']) ?? '',
|
|
116
|
+
to: textOrUndefined(raw['to']) ?? '',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 把 `desktopPlugins.pending()` 的返回值规范化成 {@link PluginUpdatesSnapshot}。
|
|
121
|
+
*
|
|
122
|
+
* `undefined` / 非对象 → `undefined`(= 没有待生效项,界面不渲染)。**只有降级项(`held` 非空、
|
|
123
|
+
* `packages` 为空)时同样落成空 `packages`**,由客户端据此不渲染——回退对员工是要解释的事,
|
|
124
|
+
* 不该混在「更新已就绪」里(spec §4.5)。`held` 本身不搬运,理由见文件头。
|
|
125
|
+
*/
|
|
126
|
+
export function toSnapshot(raw) {
|
|
127
|
+
if (!isRecord(raw))
|
|
128
|
+
return undefined;
|
|
129
|
+
const packages = [];
|
|
130
|
+
for (const item of Array.isArray(raw['packages']) ? raw['packages'] : []) {
|
|
131
|
+
const normalized = toPackageUpdate(item);
|
|
132
|
+
if (normalized !== undefined)
|
|
133
|
+
packages.push(normalized);
|
|
134
|
+
}
|
|
135
|
+
const revision = raw['revision'];
|
|
136
|
+
const fetchedAt = raw['fetchedAt'];
|
|
137
|
+
return {
|
|
138
|
+
packages,
|
|
139
|
+
fetchedAt: typeof fetchedAt === 'number' && Number.isFinite(fetchedAt) ? fetchedAt : 0,
|
|
140
|
+
revision: typeof revision === 'number' && Number.isFinite(revision) ? revision : null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
|
|
145
|
+
* 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
|
|
146
|
+
*/
|
|
147
|
+
export async function handlePluginUpdatesEndpoint(endpoint, rawPayload, deps, signal) {
|
|
148
|
+
try {
|
|
149
|
+
if (isAborted(signal))
|
|
150
|
+
return failure('cancelled', '插件更新请求已取消。');
|
|
151
|
+
if (!isPluginUpdatesRpcEndpoint(endpoint)) {
|
|
152
|
+
throw new PluginUpdatesRequestError('bad-request', `未知的插件更新端点 '${endpoint}'`);
|
|
153
|
+
}
|
|
154
|
+
parsePluginUpdatesPayload(endpoint, rawPayload);
|
|
155
|
+
const plugins = deps.desktopPlugins();
|
|
156
|
+
if (endpoint === 'snapshot') {
|
|
157
|
+
// 「不可用」是**成功信封 + null**,不是错误(老桌面端共存的正常路径,见文件头)。
|
|
158
|
+
const value = plugins === undefined ? null : toSnapshot(await plugins.pending()) ?? null;
|
|
159
|
+
return { ok: true, value };
|
|
160
|
+
}
|
|
161
|
+
// apply:**唯一**的回收触发点,只在用户点击时到达这里。
|
|
162
|
+
if (plugins === undefined) {
|
|
163
|
+
// 不可用时不调 applyNow(没有可调的对象),同样按「不是错误」回执:
|
|
164
|
+
// `requested: false` 如实告诉客户端「这次请求没有被递下去」。
|
|
165
|
+
return { ok: true, value: { requested: false } };
|
|
166
|
+
}
|
|
167
|
+
await plugins.applyNow();
|
|
168
|
+
return { ok: true, value: { requested: true } };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error instanceof PluginUpdatesRequestError)
|
|
172
|
+
return failure(error.code, error.message);
|
|
173
|
+
deps.log?.(`hiwork-core: 插件更新 RPC '${endpoint}' 失败(${error instanceof Error ? error.name : typeof error})`);
|
|
174
|
+
return failure('internal', PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function failure(code, message) {
|
|
178
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 注册 `/hiwork-plugin-updates` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
|
|
182
|
+
*/
|
|
183
|
+
export function registerPluginUpdatesRpc(ctx, deps) {
|
|
184
|
+
const handler = (endpoint, payload, signal) => handlePluginUpdatesEndpoint(endpoint, payload, deps, signal);
|
|
185
|
+
const remove = ctx.connection.rpc.handle(PLUGIN_UPDATES_RPC_CHANNEL, handler, { authority: 'loopback' });
|
|
186
|
+
return async () => { await remove(); };
|
|
187
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「插件更新待重启」取数层:经 `/hiwork-plugin-updates` loopback RPC 问 Host
|
|
3
|
+
* 「现在有没有待生效的插件更新」,并在用户点「重启并更新」时触发那次重启。
|
|
4
|
+
*
|
|
5
|
+
* Host 半边见 `src/plugin-updates-rpc.ts`(照 `src/workbench-bridge.ts` 的骨架)。
|
|
6
|
+
* 为什么必须有这条 RPC:`desktopPlugins` 在 **DSH host** ctx 上,而本文件跑在 **renderer**
|
|
7
|
+
* ——客户端没有任何 client→host 的服务直连通道,只有 `ctx.get('connection').rpc`。
|
|
8
|
+
*
|
|
9
|
+
* 四条本层特有的纪律:
|
|
10
|
+
* - ⚠️ **不可用不是错误**(老桌面端共存路径):Host 回 `ok:true` + `null` 时本层归一为
|
|
11
|
+
* `undefined`,界面据此**不渲染任何东西**。RPC 失败同样是「这一轮没拿到」,
|
|
12
|
+
* **不覆写上一次镜像**——提示条不该因为一次读盘 / 传输抖动或一次网关重启而凭空消失。
|
|
13
|
+
* - ⚠️ **只依赖 `packages.length > 0` 与 `revision`**(spec §4.3 的 ⚠️):`from` /
|
|
14
|
+
* `displayName` 在生产路径恒为空 / 退化成包名,`held` 则恒为 `[]`(根因在桌面端
|
|
15
|
+
* `desktop-bridge.mts` 没传 `pluginManifest`,见 `src/plugin-updates-rpc.ts` 文件头)。
|
|
16
|
+
* 所以本层虽然按桌面端契约透传 `packages[i]` 的四个字段,但**界面不许拿它们渲染文案**。
|
|
17
|
+
* - ⚠️ **「关闭」按「本次会话 + 该 revision」记**(内存,不落盘):同一个 revision 不再提示,
|
|
18
|
+
* `revision` 变了要能重新提示——否则管理员推完修复、员工关过一次就再也看不见,又变成
|
|
19
|
+
* 「推了没反应」。`revision` 为 `null`(服务端没给)时 `null` 自成**一个桶**,语义同上。
|
|
20
|
+
* - ⚠️ **`requestRestart` 不是「更新成功」的回执**:它只把触发递下去,重启由主进程执行,
|
|
21
|
+
* 客户端那一刻就没了。`true` 的语义是「已请求重启」。
|
|
22
|
+
*
|
|
23
|
+
* client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import Host 代码
|
|
24
|
+
* (响应字段在这里按结构重声明)。
|
|
25
|
+
*/
|
|
26
|
+
/** RPC 频道;必须与 Host `src/plugin-updates-rpc.ts` 的 `PLUGIN_UPDATES_RPC_CHANNEL` 一致(由测试锁定)。 */
|
|
27
|
+
export declare const PLUGIN_UPDATES_CHANNEL = "/hiwork-plugin-updates";
|
|
28
|
+
/** 端点白名单;必须与 Host `PLUGIN_UPDATES_RPC_ENDPOINTS` 一致(由测试锁定)。 */
|
|
29
|
+
export declare const PLUGIN_UPDATES_ENDPOINTS: readonly ["snapshot", "apply"];
|
|
30
|
+
export type PluginUpdatesEndpoint = (typeof PLUGIN_UPDATES_ENDPOINTS)[number];
|
|
31
|
+
/**
|
|
32
|
+
* 轮询间隔:5 分钟。
|
|
33
|
+
*
|
|
34
|
+
* 为什么必须轮询:清单是**主进程**在**运行期**每 6h 拉一次、**只写 pending 文件不应用**
|
|
35
|
+
* (spec §4.2 的时机表)——也就是说「有待重启的更新」这件事**只会在运行期出现**(启动边界那
|
|
36
|
+
* 一次拉完就当场应用掉了)。不轮询的话侧栏永远等不到提示条(验收 E3:员工不退出、保持运行 →
|
|
37
|
+
* 提示条出现)。
|
|
38
|
+
*
|
|
39
|
+
* 为什么是 5 分钟:它只是一次 loopback RPC + 桌面端一次读盘,代价远低于「更新提示迟到 6 小时」;
|
|
40
|
+
* 取值与 client 半边既有的本地镜像 TTL 同量级(`MANAGED_WORKBENCH_STALE_MS` = 5 分钟)。
|
|
41
|
+
*/
|
|
42
|
+
export declare const PLUGIN_UPDATES_POLL_INTERVAL_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* 一条待生效的更新(**冻结契约**:与桌面端 `ManagedPluginUpdate` 逐字同形)。
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ `from` / `displayName` **不得用于渲染**(见文件头)。
|
|
47
|
+
*/
|
|
48
|
+
export interface PendingPluginUpdate {
|
|
49
|
+
packageName: string;
|
|
50
|
+
displayName: string;
|
|
51
|
+
from: string;
|
|
52
|
+
to: string;
|
|
53
|
+
}
|
|
54
|
+
/** 一次待更新快照。`revision` = 清单级版本号(服务端没给就是 `null`),供「关闭」去重。 */
|
|
55
|
+
export interface PluginUpdatesSnapshot {
|
|
56
|
+
packages: PendingPluginUpdate[];
|
|
57
|
+
fetchedAt: number;
|
|
58
|
+
revision: number | null;
|
|
59
|
+
}
|
|
60
|
+
/** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
|
|
61
|
+
export interface PluginUpdatesRpcPort {
|
|
62
|
+
call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
63
|
+
}
|
|
64
|
+
/** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
|
|
65
|
+
export declare class PluginUpdatesRpcError extends Error {
|
|
66
|
+
readonly code: string;
|
|
67
|
+
constructor(code: string, message: string);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 解开 DSH Connection 的结果信封。
|
|
71
|
+
*
|
|
72
|
+
* `ok:false` 一律**抛错**(由调用方决定「保留上一次镜像」)——这是「这一轮没拿到」与
|
|
73
|
+
* 「拿到了一份空的」之间的那道闸,把它读成 `value` 会让一次 RPC 抖动变成「更新没了」。
|
|
74
|
+
* 只有 `ok === true` 才返回 `value`(`value` 可以是 `null`:那是**不可用**,不是错误)。
|
|
75
|
+
*/
|
|
76
|
+
export declare function unwrapPluginUpdatesResult<T>(value: unknown): T;
|
|
77
|
+
/** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
|
|
78
|
+
export declare function pluginUpdatesErrorOf(error: unknown): {
|
|
79
|
+
code: string;
|
|
80
|
+
message: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* 快照形状校验:`null`(不可用)/ 形状不合法一律归一为 `undefined`。
|
|
84
|
+
*
|
|
85
|
+
* `undefined` 是**唯一**的「界面什么都不渲染」信号,所以「不可用」与「响应坏了」在这里合并
|
|
86
|
+
* ——两者对界面是同一件事,区分它们只会多一条没有消费者的分支。
|
|
87
|
+
*/
|
|
88
|
+
export declare function toPluginUpdatesSnapshot(value: unknown): PluginUpdatesSnapshot | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* 「插件更新待重启」的取数仓库。
|
|
91
|
+
*
|
|
92
|
+
* 状态有三样,全部只活在内存里(**不落盘**):
|
|
93
|
+
* - `current`:最近一次**成功**取到的快照(失败不覆写,见 {@link refresh});
|
|
94
|
+
* - `dismissed`:用户关掉过的那一个 `revision`(`undefined` = 从没关过;`null` = 关过
|
|
95
|
+
* 「服务端没给 revision」的那一份)。比较是**按值**的,所以 `revision` 一变就自动重新提示;
|
|
96
|
+
* - `version`:变化计数,供 `useSyncExternalStore` 当 `getSnapshot` 用(值变了才重渲染)。
|
|
97
|
+
*/
|
|
98
|
+
export declare class PluginUpdatesStore {
|
|
99
|
+
private readonly rpc;
|
|
100
|
+
private current;
|
|
101
|
+
private dismissed;
|
|
102
|
+
private version;
|
|
103
|
+
private readonly listeners;
|
|
104
|
+
constructor(rpc: PluginUpdatesRpcPort);
|
|
105
|
+
/** 最近一次成功取到的快照(同步读,可能比服务端现状旧)。没有 / 不可用时 `undefined`。 */
|
|
106
|
+
snapshot(): PluginUpdatesSnapshot | undefined;
|
|
107
|
+
/** 变化计数:`useSyncExternalStore` 的 `getSnapshot`(值变化即重渲染)。 */
|
|
108
|
+
getVersion: () => number;
|
|
109
|
+
/** 订阅变化(快照更新 / 关闭)。返回退订函数。 */
|
|
110
|
+
subscribe: (listener: () => void) => (() => void);
|
|
111
|
+
/** 这个 revision 是否已经被用户关掉过(同 revision 不再提示;换 revision 会重新提示)。 */
|
|
112
|
+
isDismissed(revision: number | null): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* 现在是否该渲染提示卡片。
|
|
115
|
+
*
|
|
116
|
+
* 三个条件缺一不可:**有快照**(不可用 / 从没取到 → 不渲染)、**`packages` 非空**
|
|
117
|
+
* (只有降级项时桌面端返回的也是空 `packages`,spec §4.5 明确不弹)、**这个 revision 没被关过**。
|
|
118
|
+
* ⚠️ 判据里**没有** `held` / `from` / `displayName`(spec §4.3 的 ⚠️)。
|
|
119
|
+
*/
|
|
120
|
+
shouldPrompt(): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* 记住「用户关掉了这一个 revision 的提示」。**只记内存**(spec §4.5:本次会话 + 该 revision),
|
|
123
|
+
* 不落盘、不调任何 apply。
|
|
124
|
+
*/
|
|
125
|
+
dismiss(): void;
|
|
126
|
+
/**
|
|
127
|
+
* 向 Host 取一次「有没有待生效的更新」。
|
|
128
|
+
*
|
|
129
|
+
* 返回 `undefined` 有两种来源,对调用方是同一件事(**不渲染**):Host 说「不可用」(`null`),
|
|
130
|
+
* 或响应形状不合法。⚠️ **失败(`ok:false` / 传输异常)一律抛错且不覆写镜像**——提示条不该
|
|
131
|
+
* 因为一次抖动而消失,而「更新已经在排队」这件事不会因为读不出来就不存在。
|
|
132
|
+
*/
|
|
133
|
+
refresh(signal?: AbortSignal): Promise<PluginUpdatesSnapshot | undefined>;
|
|
134
|
+
/**
|
|
135
|
+
* 请求「重启并更新」。**这是全链路唯一的回收入口,且只能由用户点击到达。**
|
|
136
|
+
*
|
|
137
|
+
* 返回 `true` 表示触发**已经递下去**(最终落到主进程的 `recycleDshForPluginUpdate()`),
|
|
138
|
+
* **不是**「更新成功」——主进程会重建窗口,客户端那一刻就没了。
|
|
139
|
+
* `false` 只在「老桌面端上没有 `desktopPlugins`」时出现(Host 回 `requested: false`)。
|
|
140
|
+
*/
|
|
141
|
+
requestRestart(signal?: AbortSignal): Promise<boolean>;
|
|
142
|
+
private bump;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 造一份取数面:直接返回仓库本身(组件的 `inject` 面要的就是它)。
|
|
146
|
+
*
|
|
147
|
+
* 保留这层构造函数是为了与 client 半边既有的 `createAppVersion(connection.rpc)` 同形——
|
|
148
|
+
* 装配点只看这一个入口,不直接 new。
|
|
149
|
+
*/
|
|
150
|
+
export declare function createPluginUpdates(rpc: PluginUpdatesRpcPort): PluginUpdatesStore;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
2
|
import { type AiRouterCardFace } from './controller.ts';
|
|
3
|
+
import { type PluginUpdatesStore } from './plugin-updates.ts';
|
|
3
4
|
/**
|
|
4
5
|
* 侧边栏底部「设置」上方的账号信息行:显示当前登录网关的显示名(网关 LDAP
|
|
5
6
|
* 中文名,缺省回退登录账号)。「头像 + 用户名」是一个合并钮,hover 高亮与点击
|
|
@@ -9,6 +10,15 @@ import { type AiRouterCardFace } from './controller.ts';
|
|
|
9
10
|
* 设置入口。行几何(高度/留白/字号/图标尺寸)与 ui-settings-general 的设置触发
|
|
10
11
|
* 行完全一致。
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* 「插件更新待重启」的取数面(`/hiwork-plugin-updates`,见 `src/client/plugin-updates.ts`)。
|
|
15
|
+
*
|
|
16
|
+
* 单独成一格而**不是**塞进 `AiRouterCardFace`:那个面是登录卡片的控制器,与插件更新无关;
|
|
17
|
+
* 合成面在这里做,注册点只负责把两者拼起来(`src/client/index.ts`)。
|
|
18
|
+
*/
|
|
19
|
+
export interface PluginUpdatesFace {
|
|
20
|
+
pluginUpdates: PluginUpdatesStore;
|
|
21
|
+
}
|
|
22
|
+
export type UserBadgeProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<AiRouterCardFace & PluginUpdatesFace>;
|
|
23
|
+
/** 底部账号信息行(宽栏 / 折叠栏共用;插件更新提示卡片也由它渲染,见下)。 */
|
|
14
24
|
export declare function UserBadge(props: UserBadgeProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/hiwork-plugin-updates` loopback RPC 的 Host 实现:把**桌面端**的 `desktopPlugins`
|
|
3
|
+
* (受管插件清单的只读面 + 唯一的回收入口)搬到渲染端够得着的地方。
|
|
4
|
+
*
|
|
5
|
+
* 为什么必须有这条 RPC(spec §4.5 的「数据来源」一段):`desktopPlugins` 是桌面端的
|
|
6
|
+
* `desktop-bridge.mts` 用 `ctx.provide('desktopPlugins', …)` 提供到 **DSH host** ctx 上的,
|
|
7
|
+
* 而 hiwork-core 的客户端跑在 **renderer**——两个进程。客户端现有的全部数据层(usage /
|
|
8
|
+
* session-cost / daily / managed-mcp / managed-skills / managed-workbench / app-version)
|
|
9
|
+
* 无一例外都经 `ctx.get('connection').rpc` 走 `/hiwork-*` loopback RPC,**没有任何
|
|
10
|
+
* client→host 的服务直连通道**。而「重启并更新」是个**点击**,它发生在客户端。所以取数与
|
|
11
|
+
* 触发都得过这条 RPC。
|
|
12
|
+
*
|
|
13
|
+
* 骨架照 `src/workbench-bridge.ts`(同 `src/managed-skills.ts` / `src/app-version-rpc.ts`):
|
|
14
|
+
* 导出频道名 + 端点白名单(`as const`)+ 结构化 deps 端口(**不 import cordis 运行时**,
|
|
15
|
+
* 便于单测直接伪造)+ 一个 `register…Rpc(...)` 返回 disposer。
|
|
16
|
+
*
|
|
17
|
+
* 三条本频道特有的纪律:
|
|
18
|
+
* - ⚠️ **`desktopPlugins` 是可选的**(不变量:依赖必须是可选的)。老桌面端上没有它 —— 那正是
|
|
19
|
+
* 「新 core + 老桌面端」这条共存路径。拿不到时 `snapshot` 回**成功信封 + `null`**
|
|
20
|
+
* (「不可用」**不是错误**,与 `app-version-rpc.ts` 的「桌面端没推过不是错误」同一条口径),
|
|
21
|
+
* 客户端据此**不渲染任何东西**。抛错会把共存路径变成一条报错日志。
|
|
22
|
+
* - ⚠️ **只搬运 `packages` / `revision` / `fetchedAt` 这三样**,**不搬 `held`**(plan T11
|
|
23
|
+
* Step 0)。它们三个是真的:落在 `.dsh-pending-updates.json` 里,跨进程可用。`held` /
|
|
24
|
+
* `from` / `displayName` 在生产路径上是空的——根因是 `desktop-bridge.mts` 调
|
|
25
|
+
* `createDesktopHostServices()` 时**没传 `pluginManifest`**,于是 `desktop-host.ts` 里那条
|
|
26
|
+
* `memory = options.pluginManifest?.current()` 恒 `undefined`、`held` 退到 `?? []`。
|
|
27
|
+
* 那条通道要修得补一条**主进程 → 子进程**的路(新 IPC 信封或 sidecar 文件),而**本 RPC 是
|
|
28
|
+
* client→host 的**,它跨的是「renderer↔DSH」,**跨不过「DSH↔主进程」这一层**。所以别「顺手把
|
|
29
|
+
* `held` 也传过去」——传过去也永远是空的,只会让下一个读这段的人以为服务端没下发降级项。
|
|
30
|
+
* - ⚠️ **`apply` 不是「更新成功」的回执**:它只把触发递下去(`desktopPlugins.applyNow()` →
|
|
31
|
+
* `APPLY_PLUGIN_UPDATES_IPC` → 主进程的 `recycleDshForPluginUpdate()`),重启由**主进程**执行、
|
|
32
|
+
* 客户端那一刻就没了。按钮语义是「已请求重启」。所以返回值是 `{ requested }`,
|
|
33
|
+
* **不是** `{ applied: true }`。
|
|
34
|
+
*
|
|
35
|
+
* 回收红线(spec §6.1 不变量②):本文件的 `apply` 是**唯一**允许触发回收的新入口,且只能由
|
|
36
|
+
* 用户点击到达(客户端只在「重启并更新」按钮的 onClick 里调它)。`snapshot` **绝不触碰**
|
|
37
|
+
* `applyNow()`——清单拉取、轮询、启动流程都不许触发回收。
|
|
38
|
+
*/
|
|
39
|
+
/** RPC 频道;必须与 client 半边 `src/client/plugin-updates.ts` 的 `PLUGIN_UPDATES_CHANNEL` 一致(由测试锁定)。 */
|
|
40
|
+
export declare const PLUGIN_UPDATES_RPC_CHANNEL = "/hiwork-plugin-updates";
|
|
41
|
+
/**
|
|
42
|
+
* 端点白名单:只有两个。
|
|
43
|
+
*
|
|
44
|
+
* `snapshot` 是「现在有没有待生效的更新」(桌面端读 pending 文件,**不拉清单、不写文件、
|
|
45
|
+
* 不触发回收**),`apply` 是「现在就去重启并应用」——它只在用户点击时被调用。
|
|
46
|
+
* 白名单是客户端可用的能力边界,多一项就等于先发布了一个没有实现的承诺。
|
|
47
|
+
*/
|
|
48
|
+
export declare const PLUGIN_UPDATES_RPC_ENDPOINTS: readonly ["snapshot", "apply"];
|
|
49
|
+
export type PluginUpdatesRpcEndpoint = (typeof PLUGIN_UPDATES_RPC_ENDPOINTS)[number];
|
|
50
|
+
/**
|
|
51
|
+
* 一条待生效的更新(**冻结契约**:与桌面端 `ManagedPluginUpdate` 逐字同形)。
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ `from` / `displayName` **不得用于渲染任何东西**(spec §4.3 的 ⚠️):生产路径上 `from` 恒空串、
|
|
54
|
+
* `displayName` 退化成包名(`pendingUpdatesFromDisk()`)。保留它们是为了贴合桌面端契约,
|
|
55
|
+
* 不是因为它们可用——界面只认 `packages.length > 0`。
|
|
56
|
+
*/
|
|
57
|
+
export interface PendingPluginUpdate {
|
|
58
|
+
packageName: string;
|
|
59
|
+
displayName: string;
|
|
60
|
+
from: string;
|
|
61
|
+
to: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 一次待更新快照。
|
|
65
|
+
*
|
|
66
|
+
* `revision` 供提示条去重「关过一次的提示」(spec §4.5):**服务端没给就是 `null`**,
|
|
67
|
+
* 客户端把 `null` 当作一个独立的桶(关过它就不再提示,换了值就重新提示)。
|
|
68
|
+
*
|
|
69
|
+
* **本对象里没有 `held`**:见文件头第二条纪律。
|
|
70
|
+
*/
|
|
71
|
+
export interface PluginUpdatesSnapshot {
|
|
72
|
+
packages: PendingPluginUpdate[];
|
|
73
|
+
/** 桌面端最近一次清单的时刻(epoch ms);拿不到就是 0。 */
|
|
74
|
+
fetchedAt: number;
|
|
75
|
+
/** 清单级版本号;服务端没给就是 `null`。 */
|
|
76
|
+
revision: number | null;
|
|
77
|
+
}
|
|
78
|
+
/** `snapshot` 端点的响应:`null` = 不可用(老桌面端没有 `desktopPlugins`)。**不是错误**。 */
|
|
79
|
+
export type PluginUpdatesSnapshotValue = PluginUpdatesSnapshot | null;
|
|
80
|
+
/** `apply` 端点的响应:`requested` = 触发是否递下去了(**不是**「更新是否成功」)。 */
|
|
81
|
+
export interface PluginUpdatesApplyValue {
|
|
82
|
+
requested: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** 客户端可安全展示的失败。 */
|
|
85
|
+
export interface PluginUpdatesFailure {
|
|
86
|
+
code: string;
|
|
87
|
+
message: string;
|
|
88
|
+
/** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
|
|
89
|
+
details: object;
|
|
90
|
+
}
|
|
91
|
+
/** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
|
|
92
|
+
export type PluginUpdatesRpcResult<T> = {
|
|
93
|
+
readonly ok: true;
|
|
94
|
+
readonly value: T;
|
|
95
|
+
} | {
|
|
96
|
+
readonly ok: false;
|
|
97
|
+
readonly error: PluginUpdatesFailure;
|
|
98
|
+
};
|
|
99
|
+
/** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
|
|
100
|
+
export declare class PluginUpdatesRequestError extends Error {
|
|
101
|
+
readonly code: 'bad-request';
|
|
102
|
+
constructor(code: 'bad-request', message: string);
|
|
103
|
+
}
|
|
104
|
+
/** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
|
|
105
|
+
export interface PluginUpdatesRpcConnection {
|
|
106
|
+
rpc: {
|
|
107
|
+
handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
|
|
108
|
+
authority: 'loopback';
|
|
109
|
+
}): () => Promise<void> | void;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export interface PluginUpdatesRpcContext {
|
|
113
|
+
readonly connection: PluginUpdatesRpcConnection;
|
|
114
|
+
readonly logger: {
|
|
115
|
+
warn(message: string): void;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 桌面端 `desktopPlugins` 服务在本频道里真正用到的那两个成员(结构兼容,便于单测伪造)。
|
|
120
|
+
*
|
|
121
|
+
* 返回值一律按 `unknown` 收:它来自**另一个包**在运行时 provide 的对象,本文件不假设它的形状
|
|
122
|
+
* ——形状由 {@link toSnapshot} 兜底(这正是「不变量:拿不到就当不可用」在取值一侧的落实)。
|
|
123
|
+
*/
|
|
124
|
+
export interface DesktopPluginsPort {
|
|
125
|
+
/** 待生效的更新;没有待生效项(含只有 `held`)时返回 `undefined`。 */
|
|
126
|
+
pending(): Promise<unknown>;
|
|
127
|
+
/** 等价于用户点插件市场的「更新」:立刻回收。**只能由用户点击触发**。 */
|
|
128
|
+
applyNow(): Promise<unknown>;
|
|
129
|
+
}
|
|
130
|
+
/** Host 侧取数依赖:桌面端的 `desktopPlugins` 服务(老桌面端上没有它)。 */
|
|
131
|
+
export interface PluginUpdatesRpcDeps {
|
|
132
|
+
/**
|
|
133
|
+
* 现取 `ctx.get('desktopPlugins')`;拿不到返回 `undefined`(老桌面端)。
|
|
134
|
+
*
|
|
135
|
+
* 做成**函数**而不是值:cordis 的服务可能在插件生命周期中途才被 provide / 被撤下,
|
|
136
|
+
* 一次性取到的值会把「还没 provide」误判成「永远没有」。
|
|
137
|
+
*/
|
|
138
|
+
desktopPlugins(): DesktopPluginsPort | undefined;
|
|
139
|
+
/** Host 日志出口(只记事件与错误类型)。 */
|
|
140
|
+
log?: ((message: string) => void) | undefined;
|
|
141
|
+
}
|
|
142
|
+
/** 通用失败文案:不暴露任何桌面端细节。 */
|
|
143
|
+
export declare const PLUGIN_UPDATES_GENERIC_FAILURE_MESSAGE = "\u63D2\u4EF6\u66F4\u65B0\u72B6\u6001\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
144
|
+
/** 是否为白名单端点。 */
|
|
145
|
+
export declare function isPluginUpdatesRpcEndpoint(endpoint: string): endpoint is PluginUpdatesRpcEndpoint;
|
|
146
|
+
/**
|
|
147
|
+
* payload 校验:两个端点都**只放行空对象**(本频道没有任何入参)。
|
|
148
|
+
*
|
|
149
|
+
* 与 workbench / app-version 的空入参分支逐字同款:出现任何字段都是客户端实现缺陷,直接
|
|
150
|
+
* `bad-request`——一个 `{ force: true }` 被静默忽略,会让调用方以为「强制重启」生效了。
|
|
151
|
+
*/
|
|
152
|
+
export declare function parsePluginUpdatesPayload(endpoint: PluginUpdatesRpcEndpoint, raw: unknown): Record<string, never>;
|
|
153
|
+
/**
|
|
154
|
+
* 把 `desktopPlugins.pending()` 的返回值规范化成 {@link PluginUpdatesSnapshot}。
|
|
155
|
+
*
|
|
156
|
+
* `undefined` / 非对象 → `undefined`(= 没有待生效项,界面不渲染)。**只有降级项(`held` 非空、
|
|
157
|
+
* `packages` 为空)时同样落成空 `packages`**,由客户端据此不渲染——回退对员工是要解释的事,
|
|
158
|
+
* 不该混在「更新已就绪」里(spec §4.5)。`held` 本身不搬运,理由见文件头。
|
|
159
|
+
*/
|
|
160
|
+
export declare function toSnapshot(raw: unknown): PluginUpdatesSnapshot | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
|
|
163
|
+
* 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
|
|
164
|
+
*/
|
|
165
|
+
export declare function handlePluginUpdatesEndpoint(endpoint: string, rawPayload: unknown, deps: PluginUpdatesRpcDeps, signal?: AbortSignal): Promise<PluginUpdatesRpcResult<unknown>>;
|
|
166
|
+
/**
|
|
167
|
+
* 注册 `/hiwork-plugin-updates` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
|
|
168
|
+
*/
|
|
169
|
+
export declare function registerPluginUpdatesRpc(ctx: PluginUpdatesRpcContext, deps: PluginUpdatesRpcDeps): () => Promise<void>;
|