@dsh-workbench/core 0.2.6 → 0.2.8

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.
Files changed (32) hide show
  1. package/README.md +11 -9
  2. package/bin/dsh-workbench +42 -4
  3. package/client.js +252 -36
  4. package/dist/client/client.body.js +252 -36
  5. package/dist/client/index.js +252 -36
  6. package/dist/host/index.js +1054 -737
  7. package/dist/types/app/client.d.ts +21 -15
  8. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +1 -0
  9. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-act.d.ts +2 -1
  12. package/dist/types/app/layouts/rightbar/market-rows.d.ts +3 -0
  13. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +13 -0
  14. package/dist/types/app/layouts/rightbar/market-settle.d.ts +22 -0
  15. package/dist/types/app/layouts/rightbar/market-transient-state.d.ts +22 -0
  16. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +26 -1
  17. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +3 -0
  18. package/dist/types/modules/panels/model/market-workbench-update.d.ts +19 -0
  19. package/dist/types/modules/panels/model/market.d.ts +4 -0
  20. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  21. package/dist/types/shared/api/config-sync.d.ts +0 -2
  22. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +1 -0
  23. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +1 -0
  24. package/dist/types/shared/api/host/bundle-manager.d.ts +1 -0
  25. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -1
  26. package/dist/types/shared/api/host/config-sync.d.ts +2 -2
  27. package/dist/types/shared/api/host/post-channels.d.ts +18 -0
  28. package/dist/types/shared/api/host/self-update.d.ts +45 -0
  29. package/docs/HANDOFF.md +12 -5
  30. package/docs/OPERATIONS.md +35 -4
  31. package/package.json +4 -4
  32. package/scripts/seed-profile.sh +61 -1
@@ -2456,6 +2456,10 @@ function readBundleManager(ctx) {
2456
2456
  async remove(name) {
2457
2457
  return asChange(await face.removeBundle?.(name));
2458
2458
  },
2459
+ async setEnabled(name, enabled) {
2460
+ if (typeof face.setBundleEnabled !== "function") return null;
2461
+ return asChange(await face.setBundleEnabled(name, enabled));
2462
+ },
2459
2463
  async cancel(requestId) {
2460
2464
  if (typeof face.cancelInstall !== "function") return null;
2461
2465
  return pick(CANCELLATIONS, (asRecord$6(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
@@ -2849,20 +2853,67 @@ var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
2849
2853
  "recon",
2850
2854
  ""
2851
2855
  ]);
2856
+ var SPEC_LIMIT = 80;
2857
+ function missing(id) {
2858
+ return id === void 0 || id === "";
2859
+ }
2860
+ function pad(token) {
2861
+ return token.startsWith("[") ? ` ${token} ` : token;
2862
+ }
2863
+ function workbenchModule(id) {
2864
+ return missing(id) ? {
2865
+ zh: "工作台模块",
2866
+ en: "the workbench module"
2867
+ } : {
2868
+ zh: `工作台${pad(`[${id}]`)}模块`,
2869
+ en: `the workbench [${id}] module`
2870
+ };
2871
+ }
2872
+ function anyWorkbenchModule(id) {
2873
+ return missing(id) ? {
2874
+ zh: "工作台的一个模块",
2875
+ en: "a workbench module"
2876
+ } : workbenchModule(id);
2877
+ }
2878
+ function workbenchPlugin(id) {
2879
+ return missing(id) ? {
2880
+ zh: "工作台的 opencli 插件",
2881
+ en: "a workbench opencli plugin"
2882
+ } : {
2883
+ zh: `工作台${pad(`[${id}]`)}opencli 插件`,
2884
+ en: `the workbench [${id}] opencli plugin`
2885
+ };
2886
+ }
2887
+ function workbenchDependency(id) {
2888
+ return missing(id) ? {
2889
+ zh: "工作台共用依赖",
2890
+ en: "a workbench shared dependency"
2891
+ } : {
2892
+ zh: `工作台共用依赖 [${id}]`,
2893
+ en: `the workbench shared dependency [${id}]`
2894
+ };
2895
+ }
2896
+ function workbenchPanelRow(id) {
2897
+ return missing(id) ? {
2898
+ zh: "工作台在当前项目的面板",
2899
+ en: "the workbench panel for this project"
2900
+ } : {
2901
+ zh: `工作台${pad(`[${id}]`)}模块这一行`,
2902
+ en: `the workbench [${id}] module row`
2903
+ };
2904
+ }
2852
2905
  function actionOf$1(call) {
2853
2906
  const raw = call.arguments;
2854
2907
  if (typeof raw !== "object" || raw === null) return {};
2855
2908
  const value = raw;
2856
2909
  const target = typeof value.plugin === "string" ? value.plugin : typeof value.name === "string" ? value.name : typeof value.id === "string" ? value.id : typeof value.unit === "string" ? value.unit : void 0;
2910
+ const spec = typeof value.spec === "string" ? value.spec.replace(/\s+/g, " ").trim() : "";
2857
2911
  return {
2858
2912
  ...typeof value.action === "string" ? { action: value.action } : {},
2859
- ...target === void 0 ? {} : { id: target }
2913
+ ...target === void 0 ? {} : { id: target },
2914
+ ...spec === "" ? {} : { spec: spec.length > SPEC_LIMIT ? `${spec.slice(0, SPEC_LIMIT)}…` : spec }
2860
2915
  };
2861
2916
  }
2862
- function whatOf(id) {
2863
- if (id === void 0 || id === "") return "一个模块";
2864
- return `「${id}」`;
2865
- }
2866
2917
  function askOf(reason, question, fallback) {
2867
2918
  return {
2868
2919
  kind: "ask",
@@ -2872,92 +2923,151 @@ function askOf(reason, question, fallback) {
2872
2923
  };
2873
2924
  }
2874
2925
  var panelsApproval = (call) => {
2875
- const { action, id } = actionOf$1(call);
2926
+ const { action, id, spec } = actionOf$1(call);
2876
2927
  const readAction = action !== void 0 && action !== "" && READ_ONLY_ACTIONS.has(action);
2877
- const what = whatOf(id);
2878
2928
  if (call.name === "workbench_modules") {
2879
2929
  if (readAction) return READ;
2880
2930
  if (action === "update-plugin" || action === "uninstall-plugin") {
2881
2931
  const verb = action === "update-plugin" ? "更新" : "卸载";
2882
- const verbEn = action === "update-plugin" ? "update" : "remove";
2932
+ const verbEn = action === "update-plugin" ? "update" : "uninstall";
2933
+ const target = workbenchPlugin(id);
2883
2934
  return askOf(`workbench_modules ${action} ${id ?? ""} changes an opencli plugin on this machine`, {
2884
- en: `it ${verbEn}s that opencli plugin on this machine. Allow?`,
2885
- zh: `会${verb}这个 opencli 插件。允许吗?`
2935
+ en: `it ${verbEn}s ${target.en}. Allow?`,
2936
+ zh: `会${verb}${target.zh},允许吗?`
2886
2937
  }, {
2887
- en: `The agent wants to ${verbEn} the opencli plugin ${what} (it changes what is installed on this machine). Allow?`,
2888
- zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2938
+ en: `The agent wants to ${verbEn} ${target.en}. Allow?`,
2939
+ zh: `agent 想${verb}${target.zh},允许吗?`
2889
2940
  });
2890
2941
  }
2891
2942
  if (action === "update-workbench") return askOf("workbench_modules update-workbench replaces the workbench itself with the new version from the registry", {
2892
- en: "it updates the workbench itself on this machine (the panel picks the new version up by itself). Allow?",
2893
- zh: "会把工作台自己更新到源上的新版(面板会自己换新)。允许吗?"
2943
+ en: "it updates the workbench itself to the new version from the registry. Allow?",
2944
+ zh: "会把工作台本身更新到源上的新版,允许吗?"
2894
2945
  }, {
2895
- en: "The agent wants to update the workbench itself to the new version from the registry (it changes what is installed on this machine; the panel picks the new version up by itself). Allow?",
2896
- zh: "agent 想把工作台自己更新到源上的新版(会改这台机器上装的东西,面板会自己换新)。允许吗?"
2946
+ en: "The agent wants to update the workbench itself to the new version from the registry. Allow?",
2947
+ zh: "agent 想把工作台本身更新到源上的新版,允许吗?"
2897
2948
  });
2898
- if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2899
- en: "it upgrades that shared dependency on this machine. Allow?",
2900
- zh: `会升级共用依赖 ${what}。允许吗?`
2949
+ if (action === "update-dependency") {
2950
+ const target = workbenchDependency(id);
2951
+ return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2952
+ en: `it upgrades ${target.en}. Allow?`,
2953
+ zh: `会升级${target.zh},允许吗?`
2954
+ }, {
2955
+ en: `The agent wants to upgrade ${target.en}. Allow?`,
2956
+ zh: `agent 想升级${target.zh},允许吗?`
2957
+ });
2958
+ }
2959
+ if (action === "install-bundle" && spec !== void 0) return askOf(`workbench_modules install-bundle ${spec} installs a workbench module from a spec`, {
2960
+ en: `it installs the workbench module from [${spec}]. Allow?`,
2961
+ zh: `会安装工作台模块 [${spec}],允许吗?`
2901
2962
  }, {
2902
- en: `The agent wants to upgrade the shared dependency ${what} (it changes what is installed on this machine). Allow?`,
2903
- zh: `agent 想升级共用依赖 ${what}(会改这台机器上装的东西)。允许吗?`
2963
+ en: `The agent wants to install the workbench module from [${spec}]. Allow?`,
2964
+ zh: `agent 想安装工作台模块 [${spec}],允许吗?`
2904
2965
  });
2905
- const verb = action === "install" ? "装" : action === "uninstall" ? "卸载" : action === "update" ? "更新" : "装 / 更新 / 卸载";
2906
- const verbEn = verb === "装" ? "install" : verb === "卸载" ? "remove" : verb === "更新" ? "update" : "install/update/remove";
2966
+ if (action === "install") {
2967
+ const target = workbenchModule(id);
2968
+ return askOf(`workbench_modules install ${id ?? ""} installs a workbench module on this machine`, {
2969
+ en: `it installs ${target.en}. Allow?`,
2970
+ zh: `会安装${target.zh},允许吗?`
2971
+ }, {
2972
+ en: `The agent wants to install ${target.en}. Allow?`,
2973
+ zh: `agent 想安装${target.zh},允许吗?`
2974
+ });
2975
+ }
2976
+ if (action === "update") {
2977
+ const target = workbenchModule(id);
2978
+ return askOf(`workbench_modules update ${id ?? ""} updates a workbench module on this machine`, {
2979
+ en: `it updates ${target.en}. Allow?`,
2980
+ zh: `会更新${target.zh},允许吗?`
2981
+ }, {
2982
+ en: `The agent wants to update ${target.en}. Allow?`,
2983
+ zh: `agent 想更新${target.zh},允许吗?`
2984
+ });
2985
+ }
2986
+ if (action === "uninstall") {
2987
+ const target = workbenchModule(id);
2988
+ return askOf(`workbench_modules uninstall ${id ?? ""} removes a workbench module from this machine`, {
2989
+ en: `it uninstalls ${target.en}. Allow?`,
2990
+ zh: `会卸载${target.zh},允许吗?`
2991
+ }, {
2992
+ en: `The agent wants to uninstall ${target.en}. Allow?`,
2993
+ zh: `agent 想卸载${target.zh},允许吗?`
2994
+ });
2995
+ }
2907
2996
  return askOf(`workbench_modules ${action ?? "(unknown action)"} ${id ?? ""} changes plugins on this machine`.trim(), {
2908
- en: "it changes plugins on this machine. Allow?",
2909
- zh: "会改这台机器上的插件。允许吗?"
2997
+ en: "it changes the plugins installed in the workbench. Allow?",
2998
+ zh: "会改动工作台装的插件,允许吗?"
2910
2999
  }, {
2911
- en: `The agent wants to ${verbEn} the module ${what} (it changes plugins on this machine). Allow?`,
2912
- zh: `agent 想${verb}模块 ${what}(会改这台机器上的插件)。允许吗?`
3000
+ en: "The agent wants to change the plugins installed in the workbench. Allow?",
3001
+ zh: "agent 想改动工作台装的插件,允许吗?"
2913
3002
  });
2914
3003
  }
2915
3004
  if (call.name === "workbench_diy") {
2916
3005
  if (readAction) return READ;
2917
- if (action === "open") return askOf("workbench_diy open puts an already-installed module on this project panel", {
2918
- en: `it puts ${what} on this project's panel (nothing new gets installed on this machine). Allow?`,
2919
- zh: `会把${what}摆到**这个项目**的面板上(这台机器上不装新东西)。允许吗?`
2920
- }, {
2921
- en: `The agent wants to show the module ${what} on this project's panel. Allow?`,
2922
- zh: `agent 想把${what}显示在**这个项目**的面板上。允许吗?`
2923
- });
3006
+ if (action === "open") {
3007
+ const target = anyWorkbenchModule(id);
3008
+ return askOf("workbench_diy open puts an already-installed module on this project panel", {
3009
+ en: `it shows ${target.en} on this project's panel. Allow?`,
3010
+ zh: `会把${target.zh}显示在当前项目的面板上,允许吗?`
3011
+ }, {
3012
+ en: `The agent wants to show ${target.en} on this project's panel. Allow?`,
3013
+ zh: `agent 想把${target.zh}显示在当前项目的面板上,允许吗?`
3014
+ });
3015
+ }
3016
+ const target = workbenchModule(id);
2924
3017
  return askOf(`workbench_diy ${action ?? "(unknown action)"} installs a module built in the workshop and opens it on this project's panel`, {
2925
- en: `it installs ${what} on this machine and opens it on this project's panel. Allow?`,
2926
- zh: `会把${what}装到这台机器上,并在**这个项目**的面板上打开。允许吗?`
3018
+ en: `it installs ${target.en} and shows it on this project's panel. Allow?`,
3019
+ zh: `会安装${target.zh},并显示在当前项目的面板上,允许吗?`
2927
3020
  }, {
2928
- en: `The agent wants to install the module ${what} built in the DIY workshop (it changes what is installed on this machine) and open it here. Allow?`,
2929
- zh: `agent 想把 DIY 工坊里造好的${what}装上(会改这台机器上装的东西),并在**这个项目**的面板上打开。允许吗?`
3021
+ en: `The agent wants to install ${target.en} built in the DIY workshop and show it on this project's panel. Allow?`,
3022
+ zh: `agent 想安装${target.zh},并显示在当前项目的面板上,允许吗?`
2930
3023
  });
2931
3024
  }
2932
3025
  if (call.name !== "workbench_panels") return void 0;
2933
3026
  if (readAction) return READ;
2934
- if (action === "hide") return askOf("workbench_panels hide removes a module from the panel", {
2935
- en: "it takes that module off this panel (its agent tools go offline here too). Allow?",
2936
- zh: `${what}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`
2937
- }, {
2938
- en: `The agent wants to remove ${what} from this project's panel (its agent tools go offline here too). Allow?`,
2939
- zh: `agent 想把 ${what} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`
2940
- });
2941
- if (action === "move") return askOf("workbench_panels move reorders the panel", {
2942
- en: `it reorders this panel (${what} goes first). Allow?`,
2943
- zh: `会调整这个项目面板的顺序(${what}放前面)。允许吗?`
2944
- }, {
2945
- en: `The agent wants to reorder this project's panel (put ${what} first). Allow?`,
2946
- zh: `agent 想调整这个项目面板的顺序(把 ${what} 放前面)。允许吗?`
2947
- });
3027
+ if (action === "hide") {
3028
+ const target = anyWorkbenchModule(id);
3029
+ return askOf("workbench_panels hide removes a module from the panel", {
3030
+ en: `it takes ${target.en} off this project's panel. Allow?`,
3031
+ zh: `会把${target.zh}从当前项目的面板上收起来,允许吗?`
3032
+ }, {
3033
+ en: `The agent wants to take ${target.en} off this project's panel. Allow?`,
3034
+ zh: `agent 想把${target.zh}从当前项目的面板上收起来,允许吗?`
3035
+ });
3036
+ }
3037
+ if (action === "move") {
3038
+ const target = anyWorkbenchModule(id);
3039
+ return askOf("workbench_panels move reorders the panel", {
3040
+ en: `it moves ${target.en} to the front of this project's panel. Allow?`,
3041
+ zh: `会把${target.zh}移到当前项目面板的最前面,允许吗?`
3042
+ }, {
3043
+ en: `The agent wants to move ${target.en} to the front of this project's panel. Allow?`,
3044
+ zh: `agent 想把${target.zh}移到当前项目面板的最前面,允许吗?`
3045
+ });
3046
+ }
2948
3047
  if (action === "reset") return askOf("workbench_panels reset clears the panel", {
2949
- en: "it clears this panel (every module goes offline here). Allow?",
2950
- zh: "会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?"
3048
+ en: `it clears the workbench panel for this project. Allow?`,
3049
+ zh: "会清空工作台在当前项目的面板,允许吗?"
2951
3050
  }, {
2952
- en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
2953
- zh: "agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?"
3051
+ en: "The agent wants to clear the workbench panel for this project. Allow?",
3052
+ zh: "agent 想清空工作台在当前项目的面板,允许吗?"
2954
3053
  });
3054
+ if (action === "follow") {
3055
+ const target = workbenchPanelRow(id);
3056
+ return askOf("workbench_panels follow puts a row back to following the machine-level board", {
3057
+ en: `it puts ${target.en} back to following the machine-level board. Allow?`,
3058
+ zh: `会把${target.zh}改回跟随机器级默认板,允许吗?`
3059
+ }, {
3060
+ en: `The agent wants to put ${target.en} back to following the machine-level board. Allow?`,
3061
+ zh: `agent 想把${target.zh}改回跟随机器级默认板,允许吗?`
3062
+ });
3063
+ }
3064
+ const target = anyWorkbenchModule(id);
2955
3065
  return askOf(`workbench_panels ${action ?? "(unknown action)"} changes the user's panel`, {
2956
- en: `it adds ${what} to this project's panel. Allow?`,
2957
- zh: `会把 ${what} 加到**这个项目**的面板上。允许吗?`
3066
+ en: `it adds ${target.en} to this project's panel. Allow?`,
3067
+ zh: `会把${target.zh}加到当前项目的面板上,允许吗?`
2958
3068
  }, {
2959
- en: `The agent wants to add ${what} to this project's panel. Allow?`,
2960
- zh: `agent 想把 ${what} 加到**这个项目**的面板上。允许吗?`
3069
+ en: `The agent wants to add ${target.en} to this project's panel. Allow?`,
3070
+ zh: `agent 想把${target.zh}加到当前项目的面板上,允许吗?`
2961
3071
  });
2962
3072
  };
2963
3073
  //#endregion
@@ -3586,26 +3696,26 @@ function registerDiyTools(ctx, market, panels) {
3586
3696
  }
3587
3697
  const tool = {
3588
3698
  name: "workbench_diy",
3589
- description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把六件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / **起得来没** / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。panel 是**面板上现在究竟是什么样**:这个工作区开了哪几张卡、每张最后是读到了还是读不到(带宿主回的原文)、有没有崩过 —— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先问它,别靠猜、也别让他截图给你。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3699
+ description: "DIY 工坊里的模块:看它走到哪一步,或把它装上 / 上板。只认工坊里造好的模块 —— 普通项目里装模块用 workbench_modules。status 是入口,照它给的\"下一步\"走。install 是这台机器上的持久变更,一个模块一辈子只装一次:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。",
3590
3700
  parameters: withApprovalNote({
3591
3701
  type: "object",
3592
3702
  properties: {
3593
3703
  action: {
3594
3704
  type: "string",
3595
- description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;panel=这个工作区的面板上现在是什么样(谁读到了 / 谁读不到 / 谁崩过);probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3705
+ description: "list=工坊里有哪些模块(默认);status=某个模块走到哪一步、下一步做什么;panel=面板此刻的真实样子(宿主回的原文都在)—— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先调它;recon=先查 opencli 有没有现成的(自带 adapter / 本机插件)—— opencli 是什么自己查,别问用户;probe=用用户浏览器里现有的登录探一条读(script 或 css);open=只上板(不装);install=装上并上板(第一次,会让用户点头)。",
3596
3706
  enum: [...ACTIONS$2]
3597
3707
  },
3598
3708
  unit: {
3599
3709
  type: "string",
3600
- description: "status / panel / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3710
+ description: "status / panel / open / install 填:单元 id(list 会给)。"
3601
3711
  },
3602
3712
  script: {
3603
3713
  type: "string",
3604
- description: "probe 要(与 css 二选一):在页面里跑的脚本(必须回 JSON)。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
3714
+ description: "probe 要(与 css 二选一):读接口用这个 —— 在页面里跑一段回 JSON 的脚本。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
3605
3715
  },
3606
3716
  css: {
3607
3717
  type: "string",
3608
- description: "probe 要(与 script 二选一):一个 CSS 选择器,回页面上匹配到的元素(每行的单元格按换行拼在 text 里)。页面里没有接口时用它 —— 走的是一条固定的 DOM 读(找不到会自己打开 url 再读一遍),别写 document.querySelectorAll。"
3718
+ description: "probe 要(与 script 二选一):读 DOM 用这个 —— 给一个 CSS 选择器,宿主回匹配到的元素文本(每行的单元格按换行拼在 text 里);不在站点会自己打开 url 再读一遍。"
3609
3719
  },
3610
3720
  limit: {
3611
3721
  type: "number",
@@ -3617,11 +3727,11 @@ function registerDiyTools(ctx, market, panels) {
3617
3727
  },
3618
3728
  url: {
3619
3729
  type: "string",
3620
- description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 拿它取域名去找现成的 adapter)。"
3730
+ description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 从它取域名)。"
3621
3731
  },
3622
3732
  keyword: {
3623
3733
  type: "string",
3624
- description: "recon 要:站点名 / 系统名 / 关键词(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。用户说的是\"我想看 X\",把它翻译成关键词是你的活 —— 别问他用哪个插件。"
3734
+ description: "recon 要:站点名 / 系统名 / 域名(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。"
3625
3735
  },
3626
3736
  loginUrl: {
3627
3737
  type: "string",
@@ -3629,7 +3739,7 @@ function registerDiyTools(ctx, market, panels) {
3629
3739
  },
3630
3740
  session: {
3631
3741
  type: "string",
3632
- description: "probe 用:浏览器桥上的会话名(默认 workbench,一般不用填)。"
3742
+ description: "probe 用:会话名(默认 workbench,一般不用填)。"
3633
3743
  },
3634
3744
  open: {
3635
3745
  type: "boolean",
@@ -4482,6 +4592,330 @@ async function installRouted(deps) {
4482
4592
  });
4483
4593
  }
4484
4594
  //#endregion
4595
+ //#region src/shared/api/host/runtime-seed.ts
4596
+ var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
4597
+ function runtimeSeedPaths(root) {
4598
+ const opencliRoot = join(root, "opencli");
4599
+ const opencliBinDir = join(opencliRoot, "bin");
4600
+ const workshop = join(root, "diy");
4601
+ return {
4602
+ root,
4603
+ entryDir: join(root, "bin"),
4604
+ opencliRoot,
4605
+ opencliBinDir,
4606
+ opencliBin: join(opencliBinDir, "opencli"),
4607
+ opencliHome: join(opencliRoot, "home"),
4608
+ workshop,
4609
+ workshopPackages: join(workshop, "packages"),
4610
+ workshopManifest: join(workshop, "package.json"),
4611
+ skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
4612
+ };
4613
+ }
4614
+ function runtimeSeedRootOf(ctx) {
4615
+ const homePath = ctx.get("dshHomePath");
4616
+ if (typeof homePath !== "function") return null;
4617
+ try {
4618
+ return homePath("workbench");
4619
+ } catch {
4620
+ return null;
4621
+ }
4622
+ }
4623
+ function packageRootOf(moduleUrl) {
4624
+ return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
4625
+ }
4626
+ function isInstalledCopy(packageRoot) {
4627
+ return packageRoot.split(sep).includes("node_modules");
4628
+ }
4629
+ function opencliWrapperScript(packageRoot, home) {
4630
+ const script = join(packageRoot, "bin", "opencli-isolated");
4631
+ return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
4632
+ }
4633
+ function workshopSeedPlan(packageRoot, home, root) {
4634
+ const script = join(packageRoot, "scripts", "seed-workshop.sh");
4635
+ const env = { DSH_HOME: home };
4636
+ const log = join(root, "seed.log");
4637
+ return {
4638
+ entry: {
4639
+ file: "bash",
4640
+ args: [script, "entry"],
4641
+ env,
4642
+ log
4643
+ },
4644
+ full: {
4645
+ file: "bash",
4646
+ args: [script],
4647
+ env,
4648
+ log
4649
+ }
4650
+ };
4651
+ }
4652
+ function runDetached(command, onError, spawnImpl = spawn) {
4653
+ let fd = null;
4654
+ try {
4655
+ fd = openSync(command.log, "a");
4656
+ const child = spawnImpl(command.file, [...command.args], {
4657
+ env: {
4658
+ ...process.env,
4659
+ ...command.env
4660
+ },
4661
+ detached: true,
4662
+ stdio: [
4663
+ "ignore",
4664
+ fd,
4665
+ fd
4666
+ ]
4667
+ });
4668
+ child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
4669
+ child.unref();
4670
+ } catch (error) {
4671
+ onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
4672
+ } finally {
4673
+ if (fd !== null) closeQuietly(fd);
4674
+ }
4675
+ }
4676
+ function closeQuietly(fd) {
4677
+ try {
4678
+ closeSync(fd);
4679
+ } catch {
4680
+ return;
4681
+ }
4682
+ }
4683
+ function prependPath(entryDir, env = process.env) {
4684
+ const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
4685
+ if (parts.includes(entryDir)) return false;
4686
+ env.PATH = [entryDir, ...parts].join(delimiter);
4687
+ return true;
4688
+ }
4689
+ function seedRuntimeFiles(options) {
4690
+ const paths = runtimeSeedPaths(options.root);
4691
+ const done = [];
4692
+ try {
4693
+ for (const dir of [
4694
+ paths.entryDir,
4695
+ paths.opencliBinDir,
4696
+ paths.opencliHome,
4697
+ paths.workshopPackages
4698
+ ]) mkdirSync(dir, { recursive: true });
4699
+ const wanted = opencliWrapperScript(options.packageRoot, options.home);
4700
+ if (readText(paths.opencliBin) !== wanted) {
4701
+ rmSync(paths.opencliBin, { force: true });
4702
+ writeFileSync(paths.opencliBin, wanted, "utf8");
4703
+ chmodSync(paths.opencliBin, 493);
4704
+ done.push(paths.opencliBin);
4705
+ }
4706
+ if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
4707
+ } catch (error) {
4708
+ options.log?.(`自举没做完:${String(error)}`);
4709
+ }
4710
+ return done;
4711
+ }
4712
+ function readText(file) {
4713
+ try {
4714
+ return readFileSync(file, "utf8");
4715
+ } catch {
4716
+ return null;
4717
+ }
4718
+ }
4719
+ function readLink(link) {
4720
+ try {
4721
+ return readlinkSync(link);
4722
+ } catch {
4723
+ return null;
4724
+ }
4725
+ }
4726
+ function linkSkills(link, source, log) {
4727
+ if (!existsSync(source) || readLink(link) === source) return false;
4728
+ try {
4729
+ mkdirSync(dirname(link), { recursive: true });
4730
+ rmSync(link, {
4731
+ force: true,
4732
+ recursive: true
4733
+ });
4734
+ symlinkSync(source, link, "dir");
4735
+ return true;
4736
+ } catch (error) {
4737
+ log?.(`DIY 指引没挂上(${link}):${String(error)}`);
4738
+ return false;
4739
+ }
4740
+ }
4741
+ //#endregion
4742
+ //#region src/shared/api/host/self-update.ts
4743
+ function runningVersionOf(moduleUrl) {
4744
+ try {
4745
+ const read = JSON.parse(readFileSync(new URL("../../package.json", moduleUrl), "utf8"));
4746
+ return typeof read.version === "string" && read.version !== "" ? read.version : void 0;
4747
+ } catch {
4748
+ return;
4749
+ }
4750
+ }
4751
+ function selfUpdatePaths(dshHome) {
4752
+ const dir = join(dshHome, "workbench");
4753
+ return {
4754
+ dir,
4755
+ script: join(dir, "self-update.sh"),
4756
+ marker: join(dir, "self-update.json"),
4757
+ log: join(dir, "self-update.log")
4758
+ };
4759
+ }
4760
+ function recoveryCommand(input) {
4761
+ return `dsh plugin --profile ${input.profile} add ${input.pkg}@${input.version}`;
4762
+ }
4763
+ function selfUpdateMarker(request, state, version, at) {
4764
+ return {
4765
+ ...request,
4766
+ state,
4767
+ version,
4768
+ at
4769
+ };
4770
+ }
4771
+ function quoted(value) {
4772
+ return `'${value.replaceAll("'", "'\\''")}'`;
4773
+ }
4774
+ function selfUpdateScript(request, paths) {
4775
+ const rescue = recoveryCommand({
4776
+ profile: request.profile,
4777
+ pkg: request.pkg,
4778
+ version: request.from
4779
+ });
4780
+ const lastsOne = recoveryCommand({
4781
+ profile: request.profile,
4782
+ pkg: request.pkg,
4783
+ version: request.to === "" ? "latest" : request.to
4784
+ });
4785
+ const target = request.to === "" ? "latest" : request.to;
4786
+ const landed = request.to === "" ? "is_other" : "is_to";
4787
+ return `#!/usr/bin/env bash
4788
+ set -uo pipefail
4789
+ DSH_HOME=${quoted(request.dshHome)}
4790
+ PKG=${quoted(request.pkg)}
4791
+ PROFILE=${quoted(request.profile)}
4792
+ FROM=${quoted(request.from)}
4793
+ TO=${quoted(target)}
4794
+ MARKER=${quoted(paths.marker)}
4795
+ LOG=${quoted(paths.log)}
4796
+ PROFILE_DIR="$DSH_HOME/profiles/$PROFILE"
4797
+ say() { printf '%s %s\\n' "$(date '+%F %T')" "$*" >>"$LOG"; }
4798
+ on_disk() {
4799
+ node -e 'const p=process.argv[1];try{process.stdout.write(require(p).version)}catch(e){}' \\
4800
+ "$PROFILE_DIR/node_modules/$PKG/package.json" 2>/dev/null
4801
+ }
4802
+ write_marker() {
4803
+ printf '{"pkg":"%s","profile":"%s","from":"%s","to":"%s","state":"%s","version":"%s","at":"%s"}\\n' \\
4804
+ "$PKG" "$PROFILE" "$FROM" "$TO" "$1" "$2" "$(date -u '+%FT%TZ')" >"$MARKER"
4805
+ }
4806
+ install() { dsh plugin --profile "$PROFILE" add "$PKG@$1" >>"$LOG" 2>&1; }
4807
+ is_to() { [ "$(on_disk)" = "$TO" ]; }
4808
+ is_other() { [ -n "$(on_disk)" ] && [ "$(on_disk)" != "$FROM" ]; }
4809
+ if ! command -v dsh >/dev/null 2>&1; then
4810
+ say "PATH 上没有 dsh —— 这一跳没跑。救法:${rescue}"
4811
+ write_marker failed "$FROM"
4812
+ exit 1
4813
+ fi
4814
+ say "开始 \${FROM} -> \${TO}(不卸任何一行)"
4815
+ write_marker running "$FROM"
4816
+ if install "$TO" && ${landed}; then
4817
+ say "换好了:$(on_disk) 在盘上(目标 \${TO}),重启一次 dsh 才用得上"
4818
+ write_marker ok "$(on_disk)"
4819
+ exit 0
4820
+ fi
4821
+ say "换版没成(盘上 $(on_disk))—— 把 \${FROM} 装回去"
4822
+ if install "$FROM" && [ "$(on_disk)" = "$FROM" ]; then
4823
+ say "已回滚:\${FROM} 还在盘上,工作台这一行没被动过"
4824
+ write_marker rolled-back "$FROM"
4825
+ exit 0
4826
+ fi
4827
+ say "回滚也没成。救法:${rescue}(或 ${lastsOne})"
4828
+ write_marker failed "$(on_disk)"
4829
+ exit 1
4830
+ `;
4831
+ }
4832
+ function planSelfUpdate(request, now = /* @__PURE__ */ new Date()) {
4833
+ const paths = selfUpdatePaths(request.dshHome);
4834
+ return {
4835
+ paths,
4836
+ script: selfUpdateScript(request, paths),
4837
+ marker: selfUpdateMarker(request, "running", request.from, now.toISOString()),
4838
+ command: {
4839
+ file: "bash",
4840
+ args: [paths.script],
4841
+ env: {},
4842
+ log: paths.log
4843
+ }
4844
+ };
4845
+ }
4846
+ function startSelfUpdate(request, onError, spawnImpl = spawn) {
4847
+ const plan = planSelfUpdate(request);
4848
+ mkdirSync(plan.paths.dir, { recursive: true });
4849
+ writeFileSync(plan.paths.script, plan.script, { mode: 493 });
4850
+ chmodSync(plan.paths.script, 493);
4851
+ writeFileSync(plan.paths.marker, `${JSON.stringify(plan.marker)}\n`);
4852
+ runDetached(plan.command, onError, spawnImpl);
4853
+ const forward = recoveryCommand({
4854
+ profile: request.profile,
4855
+ pkg: request.pkg,
4856
+ version: request.to === "" ? "latest" : request.to
4857
+ });
4858
+ return {
4859
+ started: true,
4860
+ paths: plan.paths,
4861
+ forward
4862
+ };
4863
+ }
4864
+ //#endregion
4865
+ //#region src/modules/panels/model/market-workbench-update.ts
4866
+ async function updateWorkbenchOnce(deps) {
4867
+ const asked = deps.asked.trim();
4868
+ const target = asked === "" ? PLUGIN_ID : asked;
4869
+ if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
4870
+ const read = deps.workbench?.(target);
4871
+ if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
4872
+ if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
4873
+ const shell = deps.shell;
4874
+ const registry = read.registry ?? await deps.profileRegistry?.();
4875
+ const askedLatest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
4876
+ shell,
4877
+ name: read.pkg,
4878
+ registry,
4879
+ force: true
4880
+ });
4881
+ if (shell !== void 0 && registry !== void 0 && askedLatest !== void 0) {
4882
+ const missing = await missingReleaseParts({
4883
+ shell,
4884
+ registry,
4885
+ version: askedLatest,
4886
+ force: true
4887
+ });
4888
+ if (missing.length > 0) throw new Error(`源上已经有 ${read.pkg}@${askedLatest},但这一份发布还没铺齐:${missing.join("、")} 在这一版上还拉不到。装上会缺依赖,稍等再试 —— 市场页那颗「重新检查」会重新探一次。`);
4889
+ }
4890
+ const home = deps.dshHome?.();
4891
+ const profile = deps.profileName?.();
4892
+ if (home === void 0 || profile === void 0 || profile === null) throw new Error("拿不到 DSH_HOME 或 profile 名字 —— 自更新要多一个进程在外头跑,这一步做不了。");
4893
+ const spec = askedLatest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${askedLatest}`;
4894
+ const started = (deps.selfUpdate ?? startSelfUpdate)({
4895
+ dshHome: home,
4896
+ profile,
4897
+ pkg: read.pkg,
4898
+ from: read.version,
4899
+ to: askedLatest ?? "",
4900
+ pinned: askedLatest !== void 0
4901
+ }, (message) => deps.logger?.warn?.(`[dsh-workbench] ${message}`));
4902
+ deps.logger?.info?.(`[dsh-workbench] 自更新交给外部执行器(${started.paths.script}):${read.version} → ${askedLatest ?? "最新版"},这一跳不掉自己那一行`);
4903
+ return {
4904
+ change: {
4905
+ application: "restart-required",
4906
+ stage: "install",
4907
+ changed: false,
4908
+ target: spec,
4909
+ bundle: read.pkg
4910
+ },
4911
+ module: deps.installed?.(read.pkg) ?? {
4912
+ kind: "unknown",
4913
+ pkg: read.pkg
4914
+ },
4915
+ supply: { state: "no-needs" }
4916
+ };
4917
+ }
4918
+ //#endregion
4485
4919
  //#region src/modules/panels/model/plugin-actions.ts
4486
4920
  function pluginKeptOf(retention, detail) {
4487
4921
  if (retention === "keep-held") return { kind: "held" };
@@ -4722,7 +5156,7 @@ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4722
5156
  //#endregion
4723
5157
  //#region src/modules/panels/model/market.ts
4724
5158
  function createMarketService(options) {
4725
- const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
5159
+ const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references, dshHome, profileName } = options;
4726
5160
  const command = options.command ?? "opencli";
4727
5161
  const catalog = () => manifestsNow?.() ?? manifests;
4728
5162
  function requireEntry(unit) {
@@ -4874,7 +5308,11 @@ function createMarketService(options) {
4874
5308
  async function removeInstalledBundle(manager, pkg) {
4875
5309
  const first = await manager.remove(pkg);
4876
5310
  if (first === null) throw new Error("插件管理器回的卸载结果看不懂。");
4877
- return retryAfterRegistryFix(registryFix, manager, pkg, first);
5311
+ const settled = await retryAfterRegistryFix(registryFix, manager, pkg, first);
5312
+ if (settled.application !== "failed" && settled.application !== "cancelled") return settled;
5313
+ const put = await manager.setEnabled?.(pkg, true);
5314
+ logger?.warn?.(`[dsh-workbench] ${pkg} 没卸成(${settled.errorCode ?? settled.application})—— 已把它那一行放回名单,别让一次失败留在"装着但关着"`);
5315
+ return put ?? settled;
4878
5316
  }
4879
5317
  function routeDeps(manager) {
4880
5318
  return {
@@ -5065,31 +5503,23 @@ function createMarketService(options) {
5065
5503
  return outcome;
5066
5504
  },
5067
5505
  async updateWorkbench(pkg) {
5068
- const target = pkg.trim() === "" ? PLUGIN_ID : pkg.trim();
5069
- if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
5070
- const read = workbench?.(target);
5071
- if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
5072
- if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
5073
- const registry = read.registry;
5074
- const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5075
- shell,
5076
- name: read.pkg,
5077
- registry,
5078
- force: true
5506
+ return updateWorkbenchOnce({
5507
+ asked: pkg,
5508
+ ...workbench === void 0 ? {} : { workbench },
5509
+ ...shell === void 0 ? {} : { shell },
5510
+ ...installed === void 0 ? {} : { installed },
5511
+ ...dshHome === void 0 ? {} : { dshHome },
5512
+ ...profileName === void 0 ? {} : { profileName },
5513
+ profileRegistry: async () => {
5514
+ try {
5515
+ return (await requireBundles().registries())?.resolved ?? void 0;
5516
+ } catch {
5517
+ return;
5518
+ }
5519
+ },
5520
+ ...options.selfUpdate === void 0 ? {} : { selfUpdate: options.selfUpdate },
5521
+ ...logger === void 0 ? {} : { logger }
5079
5522
  });
5080
- if (shell !== void 0 && registry !== void 0 && latest !== void 0) {
5081
- const missing = await missingReleaseParts({
5082
- shell,
5083
- registry,
5084
- version: latest,
5085
- force: true
5086
- });
5087
- if (missing.length > 0) throw new Error(`源上已经有 ${read.pkg}@${latest},但这一份发布还没铺齐:${missing.join("、")} 在这一版上还拉不到。装上会缺依赖,稍等再试 —— 市场页那颗「重新检查」会重新探一次。`);
5088
- }
5089
- const spec = latest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${latest}`;
5090
- const outcome = await replaceDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
5091
- logger?.info?.(`[dsh-workbench] 已更新工作台 ${read.version} → ${latest ?? "最新版"}`);
5092
- return outcome;
5093
5523
  },
5094
5524
  async updatePlugin(pkg) {
5095
5525
  const outcome = await plugins.update(pkg);
@@ -5293,30 +5723,30 @@ function registerMarketTools(ctx, market) {
5293
5723
  }
5294
5724
  const tool = {
5295
5725
  name: "workbench_modules",
5296
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
5726
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec 把模块包装进 profile;按模块 id 的 install / update 只装、升它声明的那份外部供给(opencli 插件与库),不换模块包本身;uninstall 连模块包一起卸。update-workbench 把工作台自己更新到源上的新版(一般自己换上,要重启时结果里会说;重启由用户来)。卸模块只动工作台运行时,不影响终端那套 dsh;共用依赖只有升级。",
5297
5727
  parameters: withApprovalNote({
5298
5728
  type: "object",
5299
5729
  properties: {
5300
5730
  action: {
5301
5731
  type: "string",
5302
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),update-workbench=把工作台自己更新到新版(不用别的参数),其余=对模块(要 id)。",
5732
+ description: "list=看状态(默认);install / update / uninstall 要 id;install-bundle 要 spec(换源给 registry);update-dependency 要 id;update-plugin / uninstall-plugin 要 plugin。",
5303
5733
  enum: [...ACTIONS$1]
5304
5734
  },
5305
5735
  id: {
5306
5736
  type: "string",
5307
- description: "模块 id(如 coding-issues);update-dependency 填共用依赖 id(如 plugin:coding)。"
5737
+ description: "模块 id(如 coding-issues);update-dependency 用共用依赖 id(如 plugin:coding)。"
5308
5738
  },
5309
5739
  spec: {
5310
5740
  type: "string",
5311
- description: "install-bundle 用:包名(可带 @版本),也可以是 git 地址 / tarball / 本地绝对路径。"
5741
+ description: "包名(可带 @版本),也可以是 git 地址 / tarball / 本地绝对路径。"
5312
5742
  },
5313
5743
  registry: {
5314
5744
  type: "string",
5315
- description: "install-bundle 用:先问哪个源(不填就用这个 profile 自己配的源)。"
5745
+ description: "去哪个源装(不填就用这份 profile 自己配的源)。"
5316
5746
  },
5317
5747
  plugin: {
5318
5748
  type: "string",
5319
- description: "update-plugin / uninstall-plugin 用:opencli 插件的包名或插件名(就是市场「已安装 opencli 插件」那一段里的名字,如 cmdb 或 opencli-plugin-cmdb)。"
5749
+ description: "opencli 插件的包名或插件名(如 cmdb 或 opencli-plugin-cmdb)。"
5320
5750
  }
5321
5751
  },
5322
5752
  required: ["action"],
@@ -5872,13 +6302,13 @@ function registerPanelTools(ctx, panels, values) {
5872
6302
  }
5873
6303
  const tool = {
5874
6304
  name: "workbench_panels",
5875
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的跟在后面);show 或 move 过就以那次为准。机器级默认板只回答\"默认开哪几行\",没有顺序这一说。scope=machine 动的是\"机器级默认板\" —— 它只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。",
6305
+ description: "看或调右侧面板:哪些套件开着、什么顺序。两份板:本工作区那份优先,没提过的行看机器级默认板 —— 那只是还没配过的工作区的默认值(已经自己配过的工作区保持自己的选择);本工作区只记改过的那几行,所以那份板以后改了,没改过的工作区跟着变。收起 = 这个工作区明确不要它:卡片与 agent 工具一起在这个工作区下线,连机器级默认板上开着的套件也照样收起;别的项目不受影响、不删数据,要放回来用 show。在机器级默认板上 hide 是写下\"不要它\"这一笔 —— 比名单更管用,show 会把这一笔划掉。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的在后),show 或 move 过就以那次为准。",
5876
6306
  parameters: withApprovalNote({
5877
6307
  type: "object",
5878
6308
  properties: {
5879
6309
  action: {
5880
6310
  type: "string",
5881
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调这个工作区的顺序(新开一行时它排在最后);reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
6311
+ description: "list=看现在这份板(默认);show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它;move=调顺序(新开一行时排最后);reset=清掉这个工作区改过的行(改完全部回到跟着机器级默认板走);follow=某一行放回跟随(带 id = 只那一行,不带 = 全部)。",
5882
6312
  enum: [
5883
6313
  "list",
5884
6314
  "show",
@@ -5890,7 +6320,7 @@ function registerPanelTools(ctx, panels, values) {
5890
6320
  },
5891
6321
  id: {
5892
6322
  type: "string",
5893
- description: "show / hide / move 必填;follow 可选(带 id = 只把那一行放回跟随)。"
6323
+ description: "show / hide / move 必填:要动的套件 id。"
5894
6324
  },
5895
6325
  index: {
5896
6326
  type: "integer",
@@ -5898,7 +6328,7 @@ function registerPanelTools(ctx, panels, values) {
5898
6328
  },
5899
6329
  scope: {
5900
6330
  type: "string",
5901
- description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。机器级板没有顺序这一说(move 会报错)。",
6331
+ description: "workspace(默认)=只动当前工作区;machine=动那份机器级默认板(只影响还没配过的工作区;没有顺序一说,move 会报错)。",
5902
6332
  enum: ["workspace", "machine"]
5903
6333
  }
5904
6334
  },
@@ -6034,6 +6464,8 @@ var OWNED = [{
6034
6464
  panels.accept(unit);
6035
6465
  },
6036
6466
  isReleased: (unit) => panels.isReleased(unit),
6467
+ dshHome: () => dshHome(),
6468
+ profileName: () => profileDir === null ? null : basename(profileDir),
6037
6469
  logger
6038
6470
  });
6039
6471
  return {
@@ -6077,155 +6509,311 @@ function assembleHostUnits(registry) {
6077
6509
  for (const face of OWNED) registry.registerOwned(face);
6078
6510
  }
6079
6511
  //#endregion
6080
- //#region src/shared/api/host/runtime-seed.ts
6081
- var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
6082
- function runtimeSeedPaths(root) {
6083
- const opencliRoot = join(root, "opencli");
6084
- const opencliBinDir = join(opencliRoot, "bin");
6085
- const workshop = join(root, "diy");
6512
+ //#region src/shared/api/host/unit-readiness.ts
6513
+ function unitReadiness(manifest, lookup) {
6514
+ const missingHard = manifest.host.inject.filter((name) => lookup(name) === void 0);
6515
+ const missingSoft = (manifest.host.optional ?? []).filter((name) => lookup(name) === void 0);
6086
6516
  return {
6087
- root,
6088
- entryDir: join(root, "bin"),
6089
- opencliRoot,
6090
- opencliBinDir,
6091
- opencliBin: join(opencliBinDir, "opencli"),
6092
- opencliHome: join(opencliRoot, "home"),
6093
- workshop,
6094
- workshopPackages: join(workshop, "packages"),
6095
- workshopManifest: join(workshop, "package.json"),
6096
- skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
6517
+ canMount: missingHard.length === 0,
6518
+ missingHard,
6519
+ missingSoft
6097
6520
  };
6098
6521
  }
6099
- function runtimeSeedRootOf(ctx) {
6100
- const homePath = ctx.get("dshHomePath");
6101
- if (typeof homePath !== "function") return null;
6102
- try {
6103
- return homePath("workbench");
6104
- } catch {
6105
- return null;
6106
- }
6107
- }
6108
- function packageRootOf(moduleUrl) {
6109
- return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
6110
- }
6111
- function isInstalledCopy(packageRoot) {
6112
- return packageRoot.split(sep).includes("node_modules");
6113
- }
6114
- function opencliWrapperScript(packageRoot, home) {
6115
- const script = join(packageRoot, "bin", "opencli-isolated");
6116
- return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
6117
- }
6118
- function workshopSeedPlan(packageRoot, home, root) {
6119
- const script = join(packageRoot, "scripts", "seed-workshop.sh");
6120
- const env = { DSH_HOME: home };
6121
- const log = join(root, "seed.log");
6522
+ //#endregion
6523
+ //#region src/shared/api/host/mount-units.ts
6524
+ function withRunsRoute(result, unit) {
6525
+ const runs = result.longTasks;
6526
+ if (runs === void 0) return result;
6122
6527
  return {
6123
- entry: {
6124
- file: "bash",
6125
- args: [script, "entry"],
6126
- env,
6127
- log
6128
- },
6129
- full: {
6130
- file: "bash",
6131
- args: [script],
6132
- env,
6133
- log
6528
+ ...result,
6529
+ routes: {
6530
+ ...result.routes,
6531
+ [runsRouteOf(unit)]: (search) => {
6532
+ const path = (param(search, "path") ?? "").trim();
6533
+ return {
6534
+ ok: true,
6535
+ runs: path === "" ? [] : runs.current(path)
6536
+ };
6537
+ }
6134
6538
  }
6135
6539
  };
6136
6540
  }
6137
- function runDetached(command, onError, spawnImpl = spawn) {
6138
- let fd = null;
6139
- try {
6140
- fd = openSync(command.log, "a");
6141
- const child = spawnImpl(command.file, [...command.args], {
6142
- env: {
6143
- ...process.env,
6144
- ...command.env
6145
- },
6146
- detached: true,
6147
- stdio: [
6148
- "ignore",
6149
- fd,
6150
- fd
6151
- ]
6541
+ function notedRegistry(registry, writesOf) {
6542
+ return { register(definition) {
6543
+ if (!writesOf(definition.name)) return registry.register(definition);
6544
+ const noted = withApprovalNote(definition.parameters, { required: true });
6545
+ return registry.register(noted === definition.parameters ? definition : {
6546
+ ...definition,
6547
+ parameters: noted
6152
6548
  });
6153
- child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
6154
- child.unref();
6155
- } catch (error) {
6156
- onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
6157
- } finally {
6158
- if (fd !== null) closeQuietly(fd);
6159
- }
6549
+ } };
6160
6550
  }
6161
- function closeQuietly(fd) {
6162
- try {
6163
- closeSync(fd);
6164
- } catch {
6165
- return;
6166
- }
6551
+ function scopedMountCtx(ctx, registry, writesOf) {
6552
+ const tools = notedRegistry(registry, writesOf);
6553
+ const get = (name) => {
6554
+ const value = ctx.get(name);
6555
+ return name === "tools" && value !== void 0 ? tools : value;
6556
+ };
6557
+ return new Proxy(ctx, { get(target, key) {
6558
+ if (key === "get") return get;
6559
+ const value = Reflect.get(target, key, target);
6560
+ return typeof value === "function" ? value.bind(target) : value;
6561
+ } });
6167
6562
  }
6168
- function prependPath(entryDir, env = process.env) {
6169
- const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
6170
- if (parts.includes(entryDir)) return false;
6171
- env.PATH = [entryDir, ...parts].join(delimiter);
6172
- return true;
6563
+ function failureReasonOf(error) {
6564
+ if (error instanceof Error) return error.message;
6565
+ return typeof error === "string" ? error : String(error);
6173
6566
  }
6174
- function seedRuntimeFiles(options) {
6175
- const paths = runtimeSeedPaths(options.root);
6176
- const done = [];
6177
- try {
6178
- for (const dir of [
6179
- paths.entryDir,
6180
- paths.opencliBinDir,
6181
- paths.opencliHome,
6182
- paths.workshopPackages
6183
- ]) mkdirSync(dir, { recursive: true });
6184
- const wanted = opencliWrapperScript(options.packageRoot, options.home);
6185
- if (readText(paths.opencliBin) !== wanted) {
6186
- rmSync(paths.opencliBin, { force: true });
6187
- writeFileSync(paths.opencliBin, wanted, "utf8");
6188
- chmodSync(paths.opencliBin, 493);
6189
- done.push(paths.opencliBin);
6190
- }
6191
- if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
6192
- } catch (error) {
6193
- options.log?.(`自举没做完:${String(error)}`);
6194
- }
6195
- return done;
6567
+ function fail(ledger, item, reason) {
6568
+ ledger.failed.add(item);
6569
+ ledger.failures.push({
6570
+ id: item.host.manifest.id,
6571
+ reason
6572
+ });
6196
6573
  }
6197
- function readText(file) {
6198
- try {
6199
- return readFileSync(file, "utf8");
6200
- } catch {
6201
- return null;
6574
+ function collectRoutes(mounted, ledger) {
6575
+ const routes = {};
6576
+ const owner = /* @__PURE__ */ new Map();
6577
+ const routesOfUnit = /* @__PURE__ */ new Map();
6578
+ for (const item of mounted) {
6579
+ if (ledger.failed.has(item)) continue;
6580
+ const unit = item.host.manifest.id;
6581
+ const accepted = [];
6582
+ let problem;
6583
+ for (const [key, handler] of Object.entries(item.result.routes)) {
6584
+ if (key !== unit && !key.startsWith(`${unit}/`)) {
6585
+ problem = `路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— 装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`;
6586
+ break;
6587
+ }
6588
+ const previous = owner.get(key);
6589
+ if (previous !== void 0) {
6590
+ problem = `路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`;
6591
+ break;
6592
+ }
6593
+ accepted.push([key, handler]);
6594
+ }
6595
+ if (problem !== void 0) {
6596
+ fail(ledger, item, problem);
6597
+ continue;
6598
+ }
6599
+ for (const [key, handler] of accepted) {
6600
+ owner.set(key, unit);
6601
+ routes[key] = handler;
6602
+ const own = routesOfUnit.get(unit);
6603
+ if (own === void 0) routesOfUnit.set(unit, [key]);
6604
+ else own.push(key);
6605
+ }
6202
6606
  }
6607
+ return {
6608
+ routes,
6609
+ owners: owner,
6610
+ routesOfUnit
6611
+ };
6203
6612
  }
6204
- function readLink(link) {
6205
- try {
6206
- return readlinkSync(link);
6207
- } catch {
6208
- return null;
6613
+ function collectNamed(mounted, ledger, pickMany, label) {
6614
+ const owner = /* @__PURE__ */ new Map();
6615
+ const out = [];
6616
+ for (const item of mounted) {
6617
+ if (ledger.failed.has(item)) continue;
6618
+ const unit = item.host.manifest.id;
6619
+ const entries = pickMany(item);
6620
+ if (entries.length === 0) continue;
6621
+ const clash = entries.find((entry) => owner.has(entry.resource));
6622
+ if (clash !== void 0) {
6623
+ fail(ledger, item, `${label}名冲突:${clash.resource} 由套件 ${owner.get(clash.resource)} 与 ${unit} 同时声明`);
6624
+ continue;
6625
+ }
6626
+ for (const entry of entries) {
6627
+ owner.set(entry.resource, unit);
6628
+ out.push({
6629
+ ...entry,
6630
+ unit
6631
+ });
6632
+ }
6209
6633
  }
6634
+ return out;
6210
6635
  }
6211
- function linkSkills(link, source, log) {
6212
- if (!existsSync(source) || readLink(link) === source) return false;
6213
- try {
6214
- mkdirSync(dirname(link), { recursive: true });
6215
- rmSync(link, {
6216
- force: true,
6217
- recursive: true
6218
- });
6219
- symlinkSync(source, link, "dir");
6220
- return true;
6221
- } catch (error) {
6222
- log?.(`DIY 指引没挂上(${link}):${String(error)}`);
6223
- return false;
6636
+ function collectContextRenderers(mounted, ledger) {
6637
+ const table = /* @__PURE__ */ new Map();
6638
+ const owner = /* @__PURE__ */ new Map();
6639
+ for (const item of mounted) {
6640
+ if (ledger.failed.has(item)) continue;
6641
+ const unit = item.host.manifest.id;
6642
+ const context = item.result.context;
6643
+ if (context === void 0) continue;
6644
+ const clash = context.kinds.find((kind) => owner.has(kind));
6645
+ if (clash !== void 0) {
6646
+ fail(ledger, item, `对话引用的 kind 冲突:${clash} 由套件 ${owner.get(clash)} 与 ${unit} 同时认领`);
6647
+ continue;
6648
+ }
6649
+ for (const kind of context.kinds) {
6650
+ owner.set(kind, unit);
6651
+ table.set(kind, (ref) => context.render(ref));
6652
+ }
6224
6653
  }
6654
+ return table;
6225
6655
  }
6226
- //#endregion
6227
- //#region src/shared/api/host/events.ts
6228
- var HEARTBEAT_MS = 25e3;
6656
+ function pickProvider(mounted, ledger, pick, label) {
6657
+ let chosen;
6658
+ let chosenBy;
6659
+ for (const item of mounted) {
6660
+ if (ledger.failed.has(item)) continue;
6661
+ const value = pick(item);
6662
+ if (value === void 0) continue;
6663
+ if (chosen === void 0) {
6664
+ chosen = value;
6665
+ chosenBy = item.host.manifest.id;
6666
+ continue;
6667
+ }
6668
+ fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.host.manifest.id}`);
6669
+ }
6670
+ return chosen;
6671
+ }
6672
+ function collectWarmReads(mounted) {
6673
+ const declarations = mounted.map((item) => item.result.warmReads).filter((declare) => declare !== void 0);
6674
+ return () => {
6675
+ const seen = /* @__PURE__ */ new Set();
6676
+ const reads = [];
6677
+ for (const read of declarations.flatMap((declare) => declare())) {
6678
+ const key = read.join(" ");
6679
+ if (seen.has(key)) continue;
6680
+ seen.add(key);
6681
+ reads.push(read);
6682
+ }
6683
+ return reads;
6684
+ };
6685
+ }
6686
+ function mountUnits(units, deps) {
6687
+ const store = deps.store ?? createWorkspaceStore({ root: null });
6688
+ const ownedUnits = deps.ownedUnits ?? (() => []);
6689
+ const mounted = [];
6690
+ const skipped = [];
6691
+ const longTaskDisposers = /* @__PURE__ */ new Map();
6692
+ const ledger = {
6693
+ failed: /* @__PURE__ */ new Set(),
6694
+ failures: []
6695
+ };
6696
+ for (const host of units) {
6697
+ const readiness = unitReadiness(host.manifest, (dep) => deps.ctx.get(dep));
6698
+ if (!readiness.canMount) {
6699
+ skipped.push({
6700
+ id: host.manifest.id,
6701
+ missing: readiness.missingHard
6702
+ });
6703
+ continue;
6704
+ }
6705
+ const unit = host.manifest.id;
6706
+ const tools = deps.ctx.get("tools");
6707
+ const writes = toolUnits(deps.manifests());
6708
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
6709
+ const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6710
+ scope: deps.longTasks.scope,
6711
+ unit,
6712
+ bump: deps.longTasks.bump
6713
+ });
6714
+ let result;
6715
+ try {
6716
+ result = host.face.mount({
6717
+ manifest: host.manifest,
6718
+ ctx: mountCtx,
6719
+ logger: deps.logger,
6720
+ manifests: deps.manifests,
6721
+ ownedUnits,
6722
+ ...deps.installable === void 0 ? {} : { installable: deps.installable },
6723
+ ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
6724
+ ...deps.probe === void 0 ? {} : { probe: deps.probe },
6725
+ ...deps.values === void 0 ? {} : { values: deps.values },
6726
+ ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
6727
+ store
6728
+ });
6729
+ } catch (error) {
6730
+ longTasks?.dispose();
6731
+ ledger.failures.push({
6732
+ id: unit,
6733
+ reason: failureReasonOf(error)
6734
+ });
6735
+ continue;
6736
+ }
6737
+ const item = {
6738
+ host,
6739
+ readiness,
6740
+ result: withRunsRoute(result, unit)
6741
+ };
6742
+ mounted.push(item);
6743
+ if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
6744
+ }
6745
+ const collected = collectRoutes(mounted, ledger);
6746
+ const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
6747
+ const actions = collectNamed(mounted, ledger, (item) => item.result.actions ?? [], "动作资源");
6748
+ const owners = new Map(collected.owners);
6749
+ for (const entry of [...prefs, ...actions]) owners.set(entry.resource, entry.unit);
6750
+ const factsOf = /* @__PURE__ */ new Map();
6751
+ for (const item of mounted) {
6752
+ if (ledger.failed.has(item)) continue;
6753
+ const declare = item.result.factsOf;
6754
+ if (declare !== void 0) factsOf.set(item.host.manifest.id, declare);
6755
+ }
6756
+ const gate = pickProvider(mounted, ledger, (item) => item.result.gate, "启停闸门");
6757
+ const provision = pickProvider(mounted, ledger, (item) => item.result.provision, "机器供给");
6758
+ const contextRenderers = collectContextRenderers(mounted, ledger);
6759
+ const alive = mounted.filter((item) => !ledger.failed.has(item));
6760
+ for (const item of mounted) {
6761
+ if (!ledger.failed.has(item)) continue;
6762
+ longTaskDisposers.get(item)?.();
6763
+ longTaskDisposers.delete(item);
6764
+ }
6765
+ return {
6766
+ mounted: alive,
6767
+ routes: collected.routes,
6768
+ owners,
6769
+ routesOfUnit: collected.routesOfUnit,
6770
+ factsOf,
6771
+ gate,
6772
+ prefs,
6773
+ actions,
6774
+ provision,
6775
+ contextRenderers,
6776
+ skipped,
6777
+ failed: ledger.failures,
6778
+ warmReads: collectWarmReads(alive),
6779
+ disposeLongTasks: () => {
6780
+ for (const dispose of [...longTaskDisposers.values()]) dispose();
6781
+ longTaskDisposers.clear();
6782
+ }
6783
+ };
6784
+ }
6785
+ //#endregion
6786
+ //#region src/shared/api/host/post-channels.ts
6787
+ function createPostChannels(deps) {
6788
+ const { ctx, mounted, facts, shellActions } = deps;
6789
+ let undo = [];
6790
+ const bind = (prefix, register) => {
6791
+ try {
6792
+ undo.push(register());
6793
+ } catch (error) {
6794
+ ctx.logger?.warn?.(`[dsh-workbench] ${prefix} 这条写通道没接上:${failureReasonOf(error)}`);
6795
+ }
6796
+ };
6797
+ return {
6798
+ rebind(wrote) {
6799
+ for (const dispose of undo.reverse()) dispose?.();
6800
+ undo = [];
6801
+ const prefs = [...mounted.values()].flatMap((item) => item.plan.prefs);
6802
+ const actions = [...shellActions, ...[...mounted.values()].flatMap((item) => item.plan.actions)];
6803
+ const provision = [...mounted.values()].find((item) => item.plan.provision !== void 0)?.plan.provision;
6804
+ if (prefs.length > 0) bind(PREFS_PREFIX, () => registerPrefs(ctx, prefs, wrote));
6805
+ if (actions.length > 0) bind(ACTIONS_PREFIX, () => registerActions(ctx, actions, wrote));
6806
+ if (provision !== void 0) bind(PROVISION_PREFIX, () => registerProvision(ctx, provision, facts));
6807
+ },
6808
+ dispose() {
6809
+ for (const dispose of undo) dispose?.();
6810
+ undo = [];
6811
+ }
6812
+ };
6813
+ }
6814
+ //#endregion
6815
+ //#region src/shared/api/host/events.ts
6816
+ var HEARTBEAT_MS = 25e3;
6229
6817
  function frame(payload) {
6230
6818
  return `data: ${JSON.stringify(payload)}\n\n`;
6231
6819
  }
@@ -6435,10 +7023,11 @@ async function workbenchVersion(input) {
6435
7023
  pkg: read.pkg,
6436
7024
  installed: read.version,
6437
7025
  ...latest === void 0 ? {} : { latest },
7026
+ ...input.running === void 0 ? {} : { running: input.running },
6438
7027
  ...read.local === true ? { local: true } : {}
6439
7028
  };
6440
7029
  }
6441
- function moduleVersionsHandler(ctx, shell) {
7030
+ function moduleVersionsHandler(ctx, shell, running) {
6442
7031
  return async (search) => {
6443
7032
  const manager = readBundleManager(ctx);
6444
7033
  if (manager === void 0) return {
@@ -6483,6 +7072,7 @@ function moduleVersionsHandler(ctx, shell) {
6483
7072
  shell,
6484
7073
  fallback,
6485
7074
  force,
7075
+ ...running === void 0 ? {} : { running },
6486
7076
  logger: ctx.logger
6487
7077
  });
6488
7078
  return {
@@ -7036,485 +7626,211 @@ async function probeLibs(options) {
7036
7626
  const installed = installedLibVersion(need.name);
7037
7627
  if (installed === void 0) {
7038
7628
  found.set(need.name, {
7039
- ...base,
7040
- status: "missing",
7041
- command: libInstallCommand({ need })
7042
- });
7043
- continue;
7044
- }
7045
- if (checkVersionRange(need.version, installed) !== "satisfied") {
7046
- found.set(need.name, {
7047
- ...base,
7048
- status: "unsatisfied",
7049
- installed,
7050
- command: libInstallCommand({ need })
7051
- });
7052
- continue;
7053
- }
7054
- const registry = process.env[need.registryEnv]?.trim() || need.registryDefault;
7055
- const latest = await latestVersion({
7056
- shell,
7057
- name: need.name,
7058
- registry,
7059
- force
7060
- });
7061
- const behind = latest !== void 0 && isNewerVersion(latest, installed);
7062
- found.set(need.name, {
7063
- ...base,
7064
- status: behind ? "outdated" : "ok",
7065
- installed,
7066
- ...latest === void 0 ? {} : { latest },
7067
- ...behind ? { command: libInstallCommand({
7068
- need,
7069
- version: latest
7070
- }) } : {}
7071
- });
7072
- }
7073
- return found;
7074
- }
7075
- //#endregion
7076
- //#region src/shared/api/host/probe-verdict.ts
7077
- function evaluateUnit(list, unit, failure, command = "opencli", releases = /* @__PURE__ */ new Map(), libs = []) {
7078
- const isPlugin = (item) => typeof item === "object" && item !== null;
7079
- const needs = unit.plugins ?? [];
7080
- const required = needs.flatMap((need) => need.commands ?? []);
7081
- const primary = needs[0]?.name;
7082
- if (failure !== void 0 || list === void 0) return {
7083
- unit: unit.unit,
7084
- ...primary === void 0 ? {} : { plugin: primary },
7085
- status: "unchecked",
7086
- required,
7087
- missing: [],
7088
- detail: failure ?? "探不动",
7089
- ...libs.length === 0 ? {} : { libs }
7090
- };
7091
- const plugins = list.filter(isPlugin);
7092
- const installed = new Map(plugins.map((item) => [String(item.name), item]));
7093
- const states = needs.map((need) => {
7094
- const hit = installed.get(need.name);
7095
- if (hit === void 0) {
7096
- const source = resolvePluginSource(need.installSource, process.env, {
7097
- command,
7098
- plugin: need.name
7099
- });
7100
- return {
7101
- name: need.name,
7102
- status: "missing",
7103
- missing: [],
7104
- install: source.ok ? "ready" : source.problem,
7105
- command: source.ok ? source.line : pluginInstallCommand(need.name)
7106
- };
7107
- }
7108
- const available = new Set((Array.isArray(hit.commands) ? hit.commands : []).map(String));
7109
- const missing = (need.commands ?? []).filter((name) => !available.has(name));
7110
- const release = releases.get(need.name);
7111
- const behind = release?.latest !== void 0 && isNewerVersion(release.latest, release.installed);
7112
- const commandsBehind = missing.length > 0;
7113
- const upgrade = behind && release?.latest !== void 0 ? upgradeLine(need, command, release.latest) : void 0;
7114
- return {
7115
- name: need.name,
7116
- status: commandsBehind || behind ? "outdated" : "ok",
7117
- missing,
7118
- ...release === void 0 ? {} : { release },
7119
- ...commandsBehind || behind ? { command: upgrade ?? pluginUpdateCommand(need.name) ?? pluginInstallCommand(need.name) } : {}
7120
- };
7121
- });
7122
- const absent = states.filter((state) => state.status === "missing");
7123
- const outdated = states.filter((state) => state.status === "outdated");
7124
- const missing = states.flatMap((state) => state.missing);
7125
- const libMissing = libs.filter((state) => state.status === "missing");
7126
- const libMismatch = libs.filter((state) => state.status === "unsatisfied");
7127
- const libBehind = libs.filter((state) => state.status === "outdated");
7128
- const status = absent.length > 0 || libMissing.length > 0 || libMismatch.length > 0 ? "missing-plugin" : missing.length > 0 ? "missing-commands" : "ok";
7129
- const blocked = absent.find((state) => state.install !== "ready");
7130
- const install = absent.length === 0 ? libMissing.length > 0 || libMismatch.length > 0 ? "ready" : void 0 : blocked === void 0 ? "ready" : blocked.install;
7131
- const fix = status === "missing-plugin" ? install === "ready" ? "install" : void 0 : outdated.length > 0 || libBehind.length > 0 ? "update" : void 0;
7132
- const first = absent[0] ?? outdated[0] ?? libMissing[0] ?? libMismatch[0] ?? libBehind[0];
7133
- const gaps = [...missing.length === 0 ? [] : [`缺必需:${missing.join("、")}`]];
7134
- const newer = states.filter((state) => state.release?.latest !== void 0 && isNewerVersion(state.release.latest, state.release.installed));
7135
- const versionText = newer.length === 0 ? "" : `(源上有新版:${newer.map((state) => `${state.release?.installed ?? ""} → ${state.release?.latest ?? ""}`).join("、")})`;
7136
- const absentNames = [...absent.map((state) => state.name), ...libMissing.map((state) => state.name)];
7137
- const mismatchText = libMismatch.map((state) => {
7138
- const declared = unit.libs?.find((need) => need.name === state.name)?.version?.trim();
7139
- return `${state.name}(装的是 ${state.installed ?? "未知"},声明要 ${declared === void 0 || declared === "" ? "任意版本" : declared})`;
7140
- });
7141
- return {
7142
- unit: unit.unit,
7143
- ...primary === void 0 ? {} : { plugin: primary },
7144
- status,
7145
- required,
7146
- missing,
7147
- detail: absentNames.length > 0 || mismatchText.length > 0 ? [...absentNames.length === 0 ? [] : [`${plugins.length} 个已装插件里没有 ${absentNames.join("、")}`], ...mismatchText].join(";") : `${needs.length} 份安装都在${gapsText(gaps)}${versionText}${libs.length === 0 ? "" : `;交叉依赖:${libs.map((state) => `${state.name}${state.installed === void 0 ? " 未装" : ` ${state.installed}`}`).join("、")}${libBehind.length === 0 ? "" : "(源上有新版)"}`}`,
7148
- ...fix === void 0 && first === void 0 ? {} : { command: fix === "install" ? (absent[0] ?? first)?.command : first?.command },
7149
- ...fix === void 0 ? {} : { fix },
7150
- ...install === void 0 ? {} : { install },
7151
- plugins: states,
7152
- ...libs.length === 0 ? {} : { libs }
7153
- };
7154
- }
7155
- function gapsText(gaps) {
7156
- return gaps.length === 0 ? "" : `(${gaps.join(";")})`;
7157
- }
7158
- function pluginUpdateCommand(plugin) {
7159
- return plugin === void 0 ? void 0 : `opencli plugin update ${plugin}`;
7160
- }
7161
- function pluginInstallCommand(plugin) {
7162
- return `opencli plugin install <${plugin ?? "插件"}源>`;
7163
- }
7164
- function upgradeLine(need, command, latest) {
7165
- const source = resolvePluginSource(need.installSource, process.env, {
7166
- command,
7167
- plugin: need.name,
7168
- version: latest,
7169
- mode: "update"
7170
- });
7171
- return source.ok ? source.line : void 0;
7172
- }
7173
- //#endregion
7174
- //#region src/shared/api/host/probe-units.ts
7175
- function parsePluginList(stdout) {
7176
- const parsed = parseJsonLoose(stdout);
7177
- if (Array.isArray(parsed)) return parsed;
7178
- return /no plugins? installed/i.test(stdout) ? [] : void 0;
7179
- }
7180
- async function probeReleases(units, options) {
7181
- const needs = /* @__PURE__ */ new Map();
7182
- for (const unit of units) for (const need of unit.plugins ?? []) {
7183
- const source = need.installSource;
7184
- if (source?.kind === "npm") needs.set(need.name, source);
7185
- }
7186
- const found = /* @__PURE__ */ new Map();
7187
- for (const [plugin, source] of needs) {
7188
- const registry = npmRegistryOf(source, process.env);
7189
- const signal = await probeRelease({
7190
- shell: options.shell,
7191
- package: source.package,
7192
- ...registry.ok ? { registry: registry.value } : {},
7193
- force: options.force
7194
- });
7195
- if (signal !== void 0) found.set(plugin, signal);
7196
- }
7197
- return found;
7198
- }
7199
- async function probeUnits(options) {
7200
- const { shell, command = "opencli", units, force = false } = options;
7201
- const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
7202
- if (!force && hits.every((hit) => hit !== void 0)) return hits;
7203
- const result = await runShell(shell, `${command} plugin list -f json`, { timeoutMs: OPENCLI_TIMEOUT_MS }).then((value) => ({
7204
- kind: "ok",
7205
- stdout: value.exitCode === 0 ? value.stdout : "",
7206
- stderr: value.stderr
7207
- }), (error) => ({
7208
- kind: "error",
7209
- stdout: "",
7210
- stderr: String(error)
7211
- }));
7212
- const list = result.kind === "ok" ? parsePluginList(result.stdout) : void 0;
7213
- const failure = result.kind === "error" ? result.stderr : list === void 0 ? "plugin list 的输出读不懂(不是 JSON 数组)" : void 0;
7214
- if (failure !== void 0) return units.map((unit) => {
7215
- const capability = {
7216
- unit: unit.unit,
7217
- ...unit.plugins?.[0] === void 0 ? {} : { plugin: unit.plugins[0].name },
7218
- status: "unchecked",
7219
- required: (unit.plugins ?? []).flatMap((need) => need.commands ?? []),
7220
- missing: [],
7221
- detail: failure
7222
- };
7223
- unitCache.set(`${command}:${unit.unit}`, capability);
7224
- return capability;
7225
- });
7226
- const [releases, libs] = await Promise.all([probeReleases(units, {
7227
- shell,
7228
- force
7229
- }), probeLibs({
7230
- needs: units.flatMap((unit) => unit.libs ?? []),
7231
- shell,
7232
- force
7233
- })]);
7234
- return units.map((unit) => {
7235
- const capability = evaluateUnit(list, unit, void 0, command, releases, libsOfUnit(unit, libs));
7236
- unitCache.set(`${command}:${unit.unit}`, capability);
7237
- return capability;
7238
- });
7239
- }
7240
- function libsOfUnit(unit, libs) {
7241
- return (unit.libs ?? []).map((need) => libs.get(need.name)).filter((state) => state !== void 0);
7242
- }
7243
- var unitCache = createTimedMemo(PROBE_TTL_MS);
7244
- //#endregion
7245
- //#region src/shared/api/host/unit-readiness.ts
7246
- function unitReadiness(manifest, lookup) {
7247
- const missingHard = manifest.host.inject.filter((name) => lookup(name) === void 0);
7248
- const missingSoft = (manifest.host.optional ?? []).filter((name) => lookup(name) === void 0);
7249
- return {
7250
- canMount: missingHard.length === 0,
7251
- missingHard,
7252
- missingSoft
7253
- };
7254
- }
7255
- //#endregion
7256
- //#region src/shared/api/host/mount-units.ts
7257
- function withRunsRoute(result, unit) {
7258
- const runs = result.longTasks;
7259
- if (runs === void 0) return result;
7260
- return {
7261
- ...result,
7262
- routes: {
7263
- ...result.routes,
7264
- [runsRouteOf(unit)]: (search) => {
7265
- const path = (param(search, "path") ?? "").trim();
7266
- return {
7267
- ok: true,
7268
- runs: path === "" ? [] : runs.current(path)
7269
- };
7270
- }
7271
- }
7272
- };
7273
- }
7274
- function notedRegistry(registry, writesOf) {
7275
- return { register(definition) {
7276
- if (!writesOf(definition.name)) return registry.register(definition);
7277
- const noted = withApprovalNote(definition.parameters, { required: true });
7278
- return registry.register(noted === definition.parameters ? definition : {
7279
- ...definition,
7280
- parameters: noted
7281
- });
7282
- } };
7283
- }
7284
- function scopedMountCtx(ctx, registry, writesOf) {
7285
- const tools = notedRegistry(registry, writesOf);
7286
- const get = (name) => {
7287
- const value = ctx.get(name);
7288
- return name === "tools" && value !== void 0 ? tools : value;
7289
- };
7290
- return new Proxy(ctx, { get(target, key) {
7291
- if (key === "get") return get;
7292
- const value = Reflect.get(target, key, target);
7293
- return typeof value === "function" ? value.bind(target) : value;
7294
- } });
7295
- }
7296
- function failureReasonOf(error) {
7297
- if (error instanceof Error) return error.message;
7298
- return typeof error === "string" ? error : String(error);
7299
- }
7300
- function fail(ledger, item, reason) {
7301
- ledger.failed.add(item);
7302
- ledger.failures.push({
7303
- id: item.host.manifest.id,
7304
- reason
7305
- });
7306
- }
7307
- function collectRoutes(mounted, ledger) {
7308
- const routes = {};
7309
- const owner = /* @__PURE__ */ new Map();
7310
- const routesOfUnit = /* @__PURE__ */ new Map();
7311
- for (const item of mounted) {
7312
- if (ledger.failed.has(item)) continue;
7313
- const unit = item.host.manifest.id;
7314
- const accepted = [];
7315
- let problem;
7316
- for (const [key, handler] of Object.entries(item.result.routes)) {
7317
- if (key !== unit && !key.startsWith(`${unit}/`)) {
7318
- problem = `路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— 装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`;
7319
- break;
7320
- }
7321
- const previous = owner.get(key);
7322
- if (previous !== void 0) {
7323
- problem = `路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`;
7324
- break;
7325
- }
7326
- accepted.push([key, handler]);
7327
- }
7328
- if (problem !== void 0) {
7329
- fail(ledger, item, problem);
7330
- continue;
7331
- }
7332
- for (const [key, handler] of accepted) {
7333
- owner.set(key, unit);
7334
- routes[key] = handler;
7335
- const own = routesOfUnit.get(unit);
7336
- if (own === void 0) routesOfUnit.set(unit, [key]);
7337
- else own.push(key);
7338
- }
7339
- }
7340
- return {
7341
- routes,
7342
- owners: owner,
7343
- routesOfUnit
7344
- };
7345
- }
7346
- function collectNamed(mounted, ledger, pickMany, label) {
7347
- const owner = /* @__PURE__ */ new Map();
7348
- const out = [];
7349
- for (const item of mounted) {
7350
- if (ledger.failed.has(item)) continue;
7351
- const unit = item.host.manifest.id;
7352
- const entries = pickMany(item);
7353
- if (entries.length === 0) continue;
7354
- const clash = entries.find((entry) => owner.has(entry.resource));
7355
- if (clash !== void 0) {
7356
- fail(ledger, item, `${label}名冲突:${clash.resource} 由套件 ${owner.get(clash.resource)} 与 ${unit} 同时声明`);
7357
- continue;
7358
- }
7359
- for (const entry of entries) {
7360
- owner.set(entry.resource, unit);
7361
- out.push({
7362
- ...entry,
7363
- unit
7364
- });
7365
- }
7366
- }
7367
- return out;
7368
- }
7369
- function collectContextRenderers(mounted, ledger) {
7370
- const table = /* @__PURE__ */ new Map();
7371
- const owner = /* @__PURE__ */ new Map();
7372
- for (const item of mounted) {
7373
- if (ledger.failed.has(item)) continue;
7374
- const unit = item.host.manifest.id;
7375
- const context = item.result.context;
7376
- if (context === void 0) continue;
7377
- const clash = context.kinds.find((kind) => owner.has(kind));
7378
- if (clash !== void 0) {
7379
- fail(ledger, item, `对话引用的 kind 冲突:${clash} 由套件 ${owner.get(clash)} 与 ${unit} 同时认领`);
7380
- continue;
7381
- }
7382
- for (const kind of context.kinds) {
7383
- owner.set(kind, unit);
7384
- table.set(kind, (ref) => context.render(ref));
7385
- }
7386
- }
7387
- return table;
7388
- }
7389
- function pickProvider(mounted, ledger, pick, label) {
7390
- let chosen;
7391
- let chosenBy;
7392
- for (const item of mounted) {
7393
- if (ledger.failed.has(item)) continue;
7394
- const value = pick(item);
7395
- if (value === void 0) continue;
7396
- if (chosen === void 0) {
7397
- chosen = value;
7398
- chosenBy = item.host.manifest.id;
7399
- continue;
7400
- }
7401
- fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.host.manifest.id}`);
7402
- }
7403
- return chosen;
7404
- }
7405
- function collectWarmReads(mounted) {
7406
- const declarations = mounted.map((item) => item.result.warmReads).filter((declare) => declare !== void 0);
7407
- return () => {
7408
- const seen = /* @__PURE__ */ new Set();
7409
- const reads = [];
7410
- for (const read of declarations.flatMap((declare) => declare())) {
7411
- const key = read.join(" ");
7412
- if (seen.has(key)) continue;
7413
- seen.add(key);
7414
- reads.push(read);
7415
- }
7416
- return reads;
7417
- };
7418
- }
7419
- function mountUnits(units, deps) {
7420
- const store = deps.store ?? createWorkspaceStore({ root: null });
7421
- const ownedUnits = deps.ownedUnits ?? (() => []);
7422
- const mounted = [];
7423
- const skipped = [];
7424
- const longTaskDisposers = /* @__PURE__ */ new Map();
7425
- const ledger = {
7426
- failed: /* @__PURE__ */ new Set(),
7427
- failures: []
7428
- };
7429
- for (const host of units) {
7430
- const readiness = unitReadiness(host.manifest, (dep) => deps.ctx.get(dep));
7431
- if (!readiness.canMount) {
7432
- skipped.push({
7433
- id: host.manifest.id,
7434
- missing: readiness.missingHard
7435
- });
7436
- continue;
7437
- }
7438
- const unit = host.manifest.id;
7439
- const tools = deps.ctx.get("tools");
7440
- const writes = toolUnits(deps.manifests());
7441
- const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
7442
- const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
7443
- scope: deps.longTasks.scope,
7444
- unit,
7445
- bump: deps.longTasks.bump
7446
- });
7447
- let result;
7448
- try {
7449
- result = host.face.mount({
7450
- manifest: host.manifest,
7451
- ctx: mountCtx,
7452
- logger: deps.logger,
7453
- manifests: deps.manifests,
7454
- ownedUnits,
7455
- ...deps.installable === void 0 ? {} : { installable: deps.installable },
7456
- ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
7457
- ...deps.probe === void 0 ? {} : { probe: deps.probe },
7458
- ...deps.values === void 0 ? {} : { values: deps.values },
7459
- ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
7460
- store
7629
+ ...base,
7630
+ status: "missing",
7631
+ command: libInstallCommand({ need })
7461
7632
  });
7462
- } catch (error) {
7463
- longTasks?.dispose();
7464
- ledger.failures.push({
7465
- id: unit,
7466
- reason: failureReasonOf(error)
7633
+ continue;
7634
+ }
7635
+ if (checkVersionRange(need.version, installed) !== "satisfied") {
7636
+ found.set(need.name, {
7637
+ ...base,
7638
+ status: "unsatisfied",
7639
+ installed,
7640
+ command: libInstallCommand({ need })
7467
7641
  });
7468
7642
  continue;
7469
7643
  }
7470
- const item = {
7471
- host,
7472
- readiness,
7473
- result: withRunsRoute(result, unit)
7474
- };
7475
- mounted.push(item);
7476
- if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
7477
- }
7478
- const collected = collectRoutes(mounted, ledger);
7479
- const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
7480
- const actions = collectNamed(mounted, ledger, (item) => item.result.actions ?? [], "动作资源");
7481
- const owners = new Map(collected.owners);
7482
- for (const entry of [...prefs, ...actions]) owners.set(entry.resource, entry.unit);
7483
- const factsOf = /* @__PURE__ */ new Map();
7484
- for (const item of mounted) {
7485
- if (ledger.failed.has(item)) continue;
7486
- const declare = item.result.factsOf;
7487
- if (declare !== void 0) factsOf.set(item.host.manifest.id, declare);
7488
- }
7489
- const gate = pickProvider(mounted, ledger, (item) => item.result.gate, "启停闸门");
7490
- const provision = pickProvider(mounted, ledger, (item) => item.result.provision, "机器供给");
7491
- const contextRenderers = collectContextRenderers(mounted, ledger);
7492
- const alive = mounted.filter((item) => !ledger.failed.has(item));
7493
- for (const item of mounted) {
7494
- if (!ledger.failed.has(item)) continue;
7495
- longTaskDisposers.get(item)?.();
7496
- longTaskDisposers.delete(item);
7644
+ const registry = process.env[need.registryEnv]?.trim() || need.registryDefault;
7645
+ const latest = await latestVersion({
7646
+ shell,
7647
+ name: need.name,
7648
+ registry,
7649
+ force
7650
+ });
7651
+ const behind = latest !== void 0 && isNewerVersion(latest, installed);
7652
+ found.set(need.name, {
7653
+ ...base,
7654
+ status: behind ? "outdated" : "ok",
7655
+ installed,
7656
+ ...latest === void 0 ? {} : { latest },
7657
+ ...behind ? { command: libInstallCommand({
7658
+ need,
7659
+ version: latest
7660
+ }) } : {}
7661
+ });
7497
7662
  }
7498
- return {
7499
- mounted: alive,
7500
- routes: collected.routes,
7501
- owners,
7502
- routesOfUnit: collected.routesOfUnit,
7503
- factsOf,
7504
- gate,
7505
- prefs,
7506
- actions,
7507
- provision,
7508
- contextRenderers,
7509
- skipped,
7510
- failed: ledger.failures,
7511
- warmReads: collectWarmReads(alive),
7512
- disposeLongTasks: () => {
7513
- for (const dispose of [...longTaskDisposers.values()]) dispose();
7514
- longTaskDisposers.clear();
7663
+ return found;
7664
+ }
7665
+ //#endregion
7666
+ //#region src/shared/api/host/probe-verdict.ts
7667
+ function evaluateUnit(list, unit, failure, command = "opencli", releases = /* @__PURE__ */ new Map(), libs = []) {
7668
+ const isPlugin = (item) => typeof item === "object" && item !== null;
7669
+ const needs = unit.plugins ?? [];
7670
+ const required = needs.flatMap((need) => need.commands ?? []);
7671
+ const primary = needs[0]?.name;
7672
+ if (failure !== void 0 || list === void 0) return {
7673
+ unit: unit.unit,
7674
+ ...primary === void 0 ? {} : { plugin: primary },
7675
+ status: "unchecked",
7676
+ required,
7677
+ missing: [],
7678
+ detail: failure ?? "探不动",
7679
+ ...libs.length === 0 ? {} : { libs }
7680
+ };
7681
+ const plugins = list.filter(isPlugin);
7682
+ const installed = new Map(plugins.map((item) => [String(item.name), item]));
7683
+ const states = needs.map((need) => {
7684
+ const hit = installed.get(need.name);
7685
+ if (hit === void 0) {
7686
+ const source = resolvePluginSource(need.installSource, process.env, {
7687
+ command,
7688
+ plugin: need.name
7689
+ });
7690
+ return {
7691
+ name: need.name,
7692
+ status: "missing",
7693
+ missing: [],
7694
+ install: source.ok ? "ready" : source.problem,
7695
+ command: source.ok ? source.line : pluginInstallCommand(need.name)
7696
+ };
7515
7697
  }
7698
+ const available = new Set((Array.isArray(hit.commands) ? hit.commands : []).map(String));
7699
+ const missing = (need.commands ?? []).filter((name) => !available.has(name));
7700
+ const release = releases.get(need.name);
7701
+ const behind = release?.latest !== void 0 && isNewerVersion(release.latest, release.installed);
7702
+ const commandsBehind = missing.length > 0;
7703
+ const upgrade = behind && release?.latest !== void 0 ? upgradeLine(need, command, release.latest) : void 0;
7704
+ return {
7705
+ name: need.name,
7706
+ status: commandsBehind || behind ? "outdated" : "ok",
7707
+ missing,
7708
+ ...release === void 0 ? {} : { release },
7709
+ ...commandsBehind || behind ? { command: upgrade ?? pluginUpdateCommand(need.name) ?? pluginInstallCommand(need.name) } : {}
7710
+ };
7711
+ });
7712
+ const absent = states.filter((state) => state.status === "missing");
7713
+ const outdated = states.filter((state) => state.status === "outdated");
7714
+ const missing = states.flatMap((state) => state.missing);
7715
+ const libMissing = libs.filter((state) => state.status === "missing");
7716
+ const libMismatch = libs.filter((state) => state.status === "unsatisfied");
7717
+ const libBehind = libs.filter((state) => state.status === "outdated");
7718
+ const status = absent.length > 0 || libMissing.length > 0 || libMismatch.length > 0 ? "missing-plugin" : missing.length > 0 ? "missing-commands" : "ok";
7719
+ const blocked = absent.find((state) => state.install !== "ready");
7720
+ const install = absent.length === 0 ? libMissing.length > 0 || libMismatch.length > 0 ? "ready" : void 0 : blocked === void 0 ? "ready" : blocked.install;
7721
+ const fix = status === "missing-plugin" ? install === "ready" ? "install" : void 0 : outdated.length > 0 || libBehind.length > 0 ? "update" : void 0;
7722
+ const first = absent[0] ?? outdated[0] ?? libMissing[0] ?? libMismatch[0] ?? libBehind[0];
7723
+ const gaps = [...missing.length === 0 ? [] : [`缺必需:${missing.join("、")}`]];
7724
+ const newer = states.filter((state) => state.release?.latest !== void 0 && isNewerVersion(state.release.latest, state.release.installed));
7725
+ const versionText = newer.length === 0 ? "" : `(源上有新版:${newer.map((state) => `${state.release?.installed ?? ""} → ${state.release?.latest ?? ""}`).join("、")})`;
7726
+ const absentNames = [...absent.map((state) => state.name), ...libMissing.map((state) => state.name)];
7727
+ const mismatchText = libMismatch.map((state) => {
7728
+ const declared = unit.libs?.find((need) => need.name === state.name)?.version?.trim();
7729
+ return `${state.name}(装的是 ${state.installed ?? "未知"},声明要 ${declared === void 0 || declared === "" ? "任意版本" : declared})`;
7730
+ });
7731
+ return {
7732
+ unit: unit.unit,
7733
+ ...primary === void 0 ? {} : { plugin: primary },
7734
+ status,
7735
+ required,
7736
+ missing,
7737
+ detail: absentNames.length > 0 || mismatchText.length > 0 ? [...absentNames.length === 0 ? [] : [`${plugins.length} 个已装插件里没有 ${absentNames.join("、")}`], ...mismatchText].join(";") : `${needs.length} 份安装都在${gapsText(gaps)}${versionText}${libs.length === 0 ? "" : `;交叉依赖:${libs.map((state) => `${state.name}${state.installed === void 0 ? " 未装" : ` ${state.installed}`}`).join("、")}${libBehind.length === 0 ? "" : "(源上有新版)"}`}`,
7738
+ ...fix === void 0 && first === void 0 ? {} : { command: fix === "install" ? (absent[0] ?? first)?.command : first?.command },
7739
+ ...fix === void 0 ? {} : { fix },
7740
+ ...install === void 0 ? {} : { install },
7741
+ plugins: states,
7742
+ ...libs.length === 0 ? {} : { libs }
7516
7743
  };
7517
7744
  }
7745
+ function gapsText(gaps) {
7746
+ return gaps.length === 0 ? "" : `(${gaps.join(";")})`;
7747
+ }
7748
+ function pluginUpdateCommand(plugin) {
7749
+ return plugin === void 0 ? void 0 : `opencli plugin update ${plugin}`;
7750
+ }
7751
+ function pluginInstallCommand(plugin) {
7752
+ return `opencli plugin install <${plugin ?? "插件"}源>`;
7753
+ }
7754
+ function upgradeLine(need, command, latest) {
7755
+ const source = resolvePluginSource(need.installSource, process.env, {
7756
+ command,
7757
+ plugin: need.name,
7758
+ version: latest,
7759
+ mode: "update"
7760
+ });
7761
+ return source.ok ? source.line : void 0;
7762
+ }
7763
+ //#endregion
7764
+ //#region src/shared/api/host/probe-units.ts
7765
+ function parsePluginList(stdout) {
7766
+ const parsed = parseJsonLoose(stdout);
7767
+ if (Array.isArray(parsed)) return parsed;
7768
+ return /no plugins? installed/i.test(stdout) ? [] : void 0;
7769
+ }
7770
+ async function probeReleases(units, options) {
7771
+ const needs = /* @__PURE__ */ new Map();
7772
+ for (const unit of units) for (const need of unit.plugins ?? []) {
7773
+ const source = need.installSource;
7774
+ if (source?.kind === "npm") needs.set(need.name, source);
7775
+ }
7776
+ const found = /* @__PURE__ */ new Map();
7777
+ for (const [plugin, source] of needs) {
7778
+ const registry = npmRegistryOf(source, process.env);
7779
+ const signal = await probeRelease({
7780
+ shell: options.shell,
7781
+ package: source.package,
7782
+ ...registry.ok ? { registry: registry.value } : {},
7783
+ force: options.force
7784
+ });
7785
+ if (signal !== void 0) found.set(plugin, signal);
7786
+ }
7787
+ return found;
7788
+ }
7789
+ async function probeUnits(options) {
7790
+ const { shell, command = "opencli", units, force = false } = options;
7791
+ const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
7792
+ if (!force && hits.every((hit) => hit !== void 0)) return hits;
7793
+ const result = await runShell(shell, `${command} plugin list -f json`, { timeoutMs: OPENCLI_TIMEOUT_MS }).then((value) => ({
7794
+ kind: "ok",
7795
+ stdout: value.exitCode === 0 ? value.stdout : "",
7796
+ stderr: value.stderr
7797
+ }), (error) => ({
7798
+ kind: "error",
7799
+ stdout: "",
7800
+ stderr: String(error)
7801
+ }));
7802
+ const list = result.kind === "ok" ? parsePluginList(result.stdout) : void 0;
7803
+ const failure = result.kind === "error" ? result.stderr : list === void 0 ? "plugin list 的输出读不懂(不是 JSON 数组)" : void 0;
7804
+ if (failure !== void 0) return units.map((unit) => {
7805
+ const capability = {
7806
+ unit: unit.unit,
7807
+ ...unit.plugins?.[0] === void 0 ? {} : { plugin: unit.plugins[0].name },
7808
+ status: "unchecked",
7809
+ required: (unit.plugins ?? []).flatMap((need) => need.commands ?? []),
7810
+ missing: [],
7811
+ detail: failure
7812
+ };
7813
+ unitCache.set(`${command}:${unit.unit}`, capability);
7814
+ return capability;
7815
+ });
7816
+ const [releases, libs] = await Promise.all([probeReleases(units, {
7817
+ shell,
7818
+ force
7819
+ }), probeLibs({
7820
+ needs: units.flatMap((unit) => unit.libs ?? []),
7821
+ shell,
7822
+ force
7823
+ })]);
7824
+ return units.map((unit) => {
7825
+ const capability = evaluateUnit(list, unit, void 0, command, releases, libsOfUnit(unit, libs));
7826
+ unitCache.set(`${command}:${unit.unit}`, capability);
7827
+ return capability;
7828
+ });
7829
+ }
7830
+ function libsOfUnit(unit, libs) {
7831
+ return (unit.libs ?? []).map((need) => libs.get(need.name)).filter((state) => state !== void 0);
7832
+ }
7833
+ var unitCache = createTimedMemo(PROBE_TTL_MS);
7518
7834
  //#endregion
7519
7835
  //#region src/shared/api/host/unit-registry.ts
7520
7836
  function installableUnitsOf(registry) {
@@ -7876,6 +8192,7 @@ var SCHEMA_TYPE = {
7876
8192
  string: "string"
7877
8193
  };
7878
8194
  var KEY_ARG_LIMIT = 3;
8195
+ var NO_KEY_ARGS = "没有带参数";
7879
8196
  var RUN_CODE_TOOL = "run_code";
7880
8197
  var PLUGIN_PROMPT_BUDGET = 4e3;
7881
8198
  function pluginToolName(plugin, command) {
@@ -7953,13 +8270,15 @@ function pluginKeyArgs(command, raw) {
7953
8270
  parts.push(`${arg.name}=${String(value)}`);
7954
8271
  if (parts.length === KEY_ARG_LIMIT) break;
7955
8272
  }
7956
- return parts.length === 0 ? "没有带参数" : parts.join("、");
8273
+ return parts.length === 0 ? NO_KEY_ARGS : parts.join("、");
7957
8274
  }
7958
8275
  function pluginApprovalQuestion(command, raw) {
7959
8276
  const key = pluginKeyArgs(command, raw);
8277
+ const zhKey = key === NO_KEY_ARGS ? "这次没带参数" : key;
8278
+ const enKey = key === NO_KEY_ARGS ? "no arguments" : key;
7960
8279
  return {
7961
- zh: `允许执行 ${command.plugin} 的 ${command.name}(${key})吗?`,
7962
- en: `Run ${command.plugin} ${command.name} (${key})?`
8280
+ zh: `agent 想执行工作台 [${command.plugin}] opencli 插件的 [${command.name}] 命令(${zhKey}),允许吗?`,
8281
+ en: `The agent wants to run the [${command.name}] command of the workbench opencli plugin [${command.plugin}] (${enKey}). Allow?`
7963
8282
  };
7964
8283
  }
7965
8284
  function pluginApprovalFor(command) {
@@ -8423,8 +8742,7 @@ function configSyncSnapshot(deps, asked) {
8423
8742
  const others = profileNames(profilesDir).filter((name) => name !== deps.self);
8424
8743
  const base = {
8425
8744
  self: deps.self,
8426
- home: deps.home,
8427
- restart: true
8745
+ home: deps.home
8428
8746
  };
8429
8747
  if (deps.profileDir === null) return {
8430
8748
  ...base,
@@ -8536,8 +8854,13 @@ function configSyncApply(deps, payload) {
8536
8854
  return {
8537
8855
  ok: true,
8538
8856
  written: merged.written,
8539
- skipped: merged.skipped,
8540
- restart: true
8857
+ skipped: merged.skipped
8858
+ };
8859
+ }
8860
+ function configSyncAction(deps) {
8861
+ return {
8862
+ resource: CONFIG_SYNC_RESOURCE,
8863
+ handle: (payload) => configSyncApply(deps, payload)
8541
8864
  };
8542
8865
  }
8543
8866
  //#endregion
@@ -8665,7 +8988,7 @@ function apply(ctx) {
8665
8988
  [DSH_PLUGINS_ROUTE]: dshPluginsHandler(ctx),
8666
8989
  [BUNDLE_REGISTRIES_ROUTE]: bundleRegistriesHandler(ctx),
8667
8990
  [BUNDLE_INSPECT_ROUTE]: bundleInspectHandler(ctx),
8668
- [MODULE_VERSIONS_ROUTE]: moduleVersionsHandler(ctx, ctx.get("shell"))
8991
+ [MODULE_VERSIONS_ROUTE]: moduleVersionsHandler(ctx, ctx.get("shell"), runningVersionOf(import.meta.url))
8669
8992
  };
8670
8993
  const renderers = /* @__PURE__ */ new Map();
8671
8994
  const shell = ctx.get("shell");
@@ -8750,10 +9073,23 @@ function apply(ctx) {
8750
9073
  const factsOfMap = /* @__PURE__ */ new Map();
8751
9074
  const routesOfUnitMap = /* @__PURE__ */ new Map();
8752
9075
  const ownersMap = /* @__PURE__ */ new Map();
8753
- let undoPost = [];
8754
9076
  const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
8755
9077
  const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
8756
9078
  const deniedNow = (workspace) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(workspace) ?? []))];
9079
+ const wrote = (resource, write) => {
9080
+ const unit = ownersMap.get(resource);
9081
+ if (unit !== void 0) facts.bump(callFacts(unit, {
9082
+ source: resource,
9083
+ args: asArgs(write.args),
9084
+ result: write.result
9085
+ }));
9086
+ };
9087
+ const postChannels = createPostChannels({
9088
+ ctx,
9089
+ mounted,
9090
+ facts,
9091
+ shellActions: [configSyncAction(configSync)]
9092
+ });
8757
9093
  function unmountUnit(id) {
8758
9094
  const item = mounted.get(id);
8759
9095
  if (item === void 0) return;
@@ -8843,22 +9179,7 @@ function apply(ctx) {
8843
9179
  }
8844
9180
  }
8845
9181
  function rebindPost() {
8846
- for (const dispose of undoPost.reverse()) dispose?.();
8847
- undoPost = [];
8848
- const wrote = (resource, write) => {
8849
- const unit = ownersMap.get(resource);
8850
- if (unit !== void 0) facts.bump(callFacts(unit, {
8851
- source: resource,
8852
- args: asArgs(write.args),
8853
- result: write.result
8854
- }));
8855
- };
8856
- const prefs = [...mounted.values()].flatMap((item) => item.plan.prefs);
8857
- const actions = [...mounted.values()].flatMap((item) => item.plan.actions);
8858
- const provision = [...mounted.values()].find((item) => item.plan.provision !== void 0)?.plan.provision;
8859
- if (prefs.length > 0) undoPost.push(registerPrefs(ctx, prefs, wrote));
8860
- if (actions.length > 0) undoPost.push(registerActions(ctx, actions, wrote));
8861
- if (provision !== void 0) undoPost.push(registerProvision(ctx, provision, facts));
9182
+ postChannels.rebind(wrote);
8862
9183
  }
8863
9184
  function sync() {
8864
9185
  const present = new Set(units.all().map((one) => one.unit));
@@ -8925,7 +9246,7 @@ function apply(ctx) {
8925
9246
  }
8926
9247
  ctx.effect(() => () => {
8927
9248
  for (const id of [...mounted.keys()]) unmountUnit(id);
8928
- for (const dispose of undoPost) dispose?.();
9249
+ postChannels.dispose();
8929
9250
  }, `${PLUGIN_ID}: 卸载时把单元摘干净`);
8930
9251
  ctx.effect(() => units.subscribe(sync), `${PLUGIN_ID}: 注册表变了就增删(装/卸一个模块 = 挂/卸它的面)`);
8931
9252
  ctx.effect(() => bindRosterFacts({
@@ -9013,10 +9334,6 @@ function apply(ctx) {
9013
9334
  return () => off?.();
9014
9335
  }, `${PLUGIN_ID}: 对话引用注入`);
9015
9336
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
9016
- ctx.effect(() => registerActions(ctx, [{
9017
- resource: CONFIG_SYNC_RESOURCE,
9018
- handle: (payload) => configSyncApply(configSync, payload)
9019
- }]), `${PLUGIN_ID}: 配置同步那一跳(把来源 profile 的行写进工作台自己的 patch)`);
9020
9337
  ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
9021
9338
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
9022
9339
  }