dsh-remote-bridge 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.cjs CHANGED
@@ -149,6 +149,19 @@ window.__ModuleLoader__.load({
149
149
  })
150
150
  });
151
151
  }
152
+ /**
153
+ * 只改名,不动权限。
154
+ *
155
+ * 走同一个 PATCH 但只带 `name` —— 服务端见到没有 scopes/rootIds 就按「纯改名」
156
+ * 处理。分成两个调用点(而不是把 name 塞进 `updateDevice`)是因为它们是两件
157
+ * 不同的事:改名不需要先想清楚权限,也不该因为权限校验失败而失败。
158
+ */
159
+ async renameDevice(deviceId, name) {
160
+ await this.manageJson(`/devices/${encodeURIComponent(deviceId)}`, {
161
+ method: "PATCH",
162
+ body: JSON.stringify({ name })
163
+ });
164
+ }
152
165
  async revokeDevice(deviceId) {
153
166
  await this.manageJson(`/devices/${encodeURIComponent(deviceId)}`, { method: "DELETE" });
154
167
  }
@@ -223,6 +236,33 @@ window.__ModuleLoader__.load({
223
236
  body: JSON.stringify({ subdir })
224
237
  });
225
238
  }
239
+ /**
240
+ * 读插件清单。
241
+ *
242
+ * `checkUpdates` 默认关:版本检查要打 npm registry,十次里九次用户只想看「装了啥」。
243
+ * 面板上那个「检查更新」按钮才把它打开。
244
+ */
245
+ async pluginInventory(checkUpdates = false) {
246
+ return this.json(`/settings/plugins${checkUpdates ? "?checkUpdates=1" : ""}`);
247
+ }
248
+ /**
249
+ * 启用 / 停用插件。
250
+ *
251
+ * 只对 `toggleable` 的条目有效;官方包(`@deepseek-ai/`)会被宿主拒绝并回
252
+ * `PLUGIN_OFFICIAL_IMMUTABLE` —— 它们随 DSH 运行时安装,改 bundles 也加载不出来。
253
+ *
254
+ * 返回 `restartRequired: true` 是常态:bundles 只在启动时读一次,改了文件不等于
255
+ * 改了运行中的进程。界面必须把这句话说出来,别让用户以为点了没反应。
256
+ */
257
+ async togglePlugin(name, loaded) {
258
+ return this.json("/settings/plugins", {
259
+ method: "PATCH",
260
+ body: JSON.stringify({
261
+ name,
262
+ loaded
263
+ })
264
+ });
265
+ }
226
266
  async json(path, init = {}) {
227
267
  const response = await this.fetch(path, init);
228
268
  await ensureOk(response);
@@ -19554,7 +19594,7 @@ window.__ModuleLoader__.load({
19554
19594
  key: "im3w4b"
19555
19595
  }]
19556
19596
  ]);
19557
- createLucideIcon("Package", [
19597
+ const Package = createLucideIcon("Package", [
19558
19598
  ["path", {
19559
19599
  d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",
19560
19600
  key: "1a0edw"
@@ -21676,7 +21716,7 @@ window.__ModuleLoader__.load({
21676
21716
  key: "1r8tn9"
21677
21717
  }]
21678
21718
  ]);
21679
- createLucideIcon("RefreshCw", [
21719
+ const RefreshCw = createLucideIcon("RefreshCw", [
21680
21720
  ["path", {
21681
21721
  d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
21682
21722
  key: "v9h5vc"
@@ -29791,6 +29831,8 @@ window.__ModuleLoader__.load({
29791
29831
  exports.HardDrive = HardDrive;
29792
29832
  exports.Image = Image;
29793
29833
  exports.Languages = Languages;
29834
+ exports.Package = Package;
29835
+ exports.RefreshCw = RefreshCw;
29794
29836
  exports.Smartphone = Smartphone;
29795
29837
  exports.Trash2 = Trash2;
29796
29838
  exports.Upload = Upload;
@@ -29938,6 +29980,34 @@ window.__ModuleLoader__.load({
29938
29980
  pairedDevices: "已配对设备",
29939
29981
  pluginTrash: "插件回收站",
29940
29982
  pluginTrashHint: "这里只列出来自手机 App 或独立文件页(/dsh-workspace)的删除记录 —— 桌面端这个面板不含文件树,无法删除文件。文件先移到这里而不是直接消失,可以在这里恢复;永久清理也由你在此发起。",
29983
+ pluginsTab: "插件",
29984
+ pluginsTabDesc: "看装了哪些插件、启用或停用,以及检查更新。",
29985
+ pluginsHint: "这里列出这台机器 profile 里装的所有插件。官方插件随 DSH 运行时安装、不能停用;其余可以在这里开关,改完要重启 DSH 才生效。",
29986
+ pluginsEmpty: "这个 profile 里没有插件。",
29987
+ pluginsUnavailable: "读不到插件清单:{reason}",
29988
+ pluginsProblems: "{count} 项需要留意",
29989
+ pluginsCheckUpdates: "检查更新",
29990
+ pluginsChecking: "正在检查…",
29991
+ pluginsOpenList: "打开插件列表",
29992
+ pluginsOpenListUnavailable: "当前 DSH 版本还不支持直接跳转插件列表。",
29993
+ pluginsEnable: "启用",
29994
+ pluginsDisable: "停用",
29995
+ pluginsEnabled: "已启用",
29996
+ pluginsDisabled: "已停用",
29997
+ pluginsToggled: "{name} {state}。",
29998
+ pluginsRestartRequired: "改动已写进 profile 的 package.json(原文件已备份)。插件列表只在 DSH 启动时读一次,重启后生效。",
29999
+ pluginsStateLoaded: "已加载",
30000
+ pluginsStateRuntimeProvided: "运行时自带",
30001
+ pluginsStateInstalledNotLoaded: "已安装未启用",
30002
+ pluginsStateDeclaredMissing: "声明加载但缺失",
30003
+ pluginsUpdateAvailable: "可更新到 {version}",
30004
+ pluginsLatest: "已是最新",
30005
+ pluginsNoDescription: "这个包没写描述",
30006
+ pluginsErrorOfficial: "官方插件随 DSH 运行时安装,不能在这里停用。",
30007
+ pluginsErrorNameInvalid: "插件名不合法。",
30008
+ pluginsErrorProfileUnknown: "找不到当前 profile 的目录,无法修改插件列表。",
30009
+ pluginsErrorManifestUnreadable: "读不懂 profile 的 package.json,为安全起见没有改动它。",
30010
+ pluginsErrorNotInstalled: "这个插件没有装进 profile,先安装再启用。",
29941
30011
  restore: "恢复",
29942
30012
  deletePermanently: "永久删除",
29943
30013
  purgeConfirm: "确定永久删除此回收项吗?此操作无法撤销。",
@@ -30017,6 +30087,11 @@ window.__ModuleLoader__.load({
30017
30087
  trashEmpty: "回收站是空的",
30018
30088
  deviceLastSeen: "最后使用 {time}",
30019
30089
  deviceNeverSeen: "从未连接",
30090
+ deviceRename: "改名",
30091
+ deviceRenamePlaceholder: "给这台设备起个名字",
30092
+ deviceRenameHint: "手动改过的名字不会被自动覆盖",
30093
+ deviceNameSaved: "设备名已保存",
30094
+ deviceDetectedName: "自动识别",
30020
30095
  imageLoadFailed: "图片加载失败",
30021
30096
  prevImage: "上一张",
30022
30097
  nextImage: "下一张",
@@ -30164,6 +30239,34 @@ window.__ModuleLoader__.load({
30164
30239
  pairedDevices: "Paired devices",
30165
30240
  pluginTrash: "Plugin trash",
30166
30241
  pluginTrashHint: "Only deletions made from the phone app or the standalone file page (/dsh-workspace) show up here — this desktop panel has no file tree and cannot delete anything. Files land here instead of vanishing, so they can be restored; permanent cleanup is also started from here.",
30242
+ pluginsTab: "Plugins",
30243
+ pluginsTabDesc: "See what is installed, enable or disable it, and check for updates.",
30244
+ pluginsHint: "Every plugin installed in this machine's profile is listed here. Official plugins ship with the DSH runtime and cannot be disabled; the rest can be toggled — changes take effect after DSH restarts.",
30245
+ pluginsEmpty: "No plugins in this profile.",
30246
+ pluginsUnavailable: "Cannot read the plugin inventory: {reason}",
30247
+ pluginsProblems: "{count} need attention",
30248
+ pluginsCheckUpdates: "Check for updates",
30249
+ pluginsChecking: "Checking…",
30250
+ pluginsOpenList: "Open plugin list",
30251
+ pluginsOpenListUnavailable: "This DSH version cannot jump to the plugin list yet.",
30252
+ pluginsEnable: "Enable",
30253
+ pluginsDisable: "Disable",
30254
+ pluginsEnabled: "enabled",
30255
+ pluginsDisabled: "disabled",
30256
+ pluginsToggled: "{name} {state}.",
30257
+ pluginsRestartRequired: "The change was written to the profile package.json (the original is backed up). The plugin list is read once at DSH startup, so restart to apply it.",
30258
+ pluginsStateLoaded: "Loaded",
30259
+ pluginsStateRuntimeProvided: "Shipped with the runtime",
30260
+ pluginsStateInstalledNotLoaded: "Installed, not loaded",
30261
+ pluginsStateDeclaredMissing: "Declared but missing",
30262
+ pluginsUpdateAvailable: "Update to {version}",
30263
+ pluginsLatest: "Up to date",
30264
+ pluginsNoDescription: "This package has no description",
30265
+ pluginsErrorOfficial: "Official plugins ship with the DSH runtime and cannot be disabled here.",
30266
+ pluginsErrorNameInvalid: "That plugin name is not valid.",
30267
+ pluginsErrorProfileUnknown: "The profile directory could not be found, so the plugin list was left untouched.",
30268
+ pluginsErrorManifestUnreadable: "The profile package.json could not be parsed, so it was left untouched.",
30269
+ pluginsErrorNotInstalled: "That plugin is not installed in this profile — install it before enabling it.",
30167
30270
  restore: "Restore",
30168
30271
  deletePermanently: "Delete permanently",
30169
30272
  purgeConfirm: "Permanently delete this trash item? This cannot be undone.",
@@ -30243,6 +30346,11 @@ window.__ModuleLoader__.load({
30243
30346
  trashEmpty: "The recycle bin is empty",
30244
30347
  deviceLastSeen: "Last seen {time}",
30245
30348
  deviceNeverSeen: "Never connected",
30349
+ deviceRename: "Rename",
30350
+ deviceRenamePlaceholder: "Name this device",
30351
+ deviceRenameHint: "A name you set is never overwritten automatically",
30352
+ deviceNameSaved: "Device name saved",
30353
+ deviceDetectedName: "Detected",
30246
30354
  imageLoadFailed: "Image failed to load",
30247
30355
  prevImage: "Previous image",
30248
30356
  nextImage: "Next image",
@@ -30803,82 +30911,6 @@ window.__ModuleLoader__.load({
30803
30911
  }
30804
30912
  };
30805
30913
  //#endregion
30806
- //#region src/client/styles.ts
30807
- const STYLE_ID = "dsh-workspace-style";
30808
- /**
30809
- * 插件的全部样式。
30810
- *
30811
- * 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
30812
- * `.daw-root` 与 `.daw-settings-section`**。
30813
- *
30814
- * 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
30815
- * 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
30816
- * 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
30817
- * 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
30818
- */
30819
- const WORKSPACE_CSS = `
30820
- .daw-root,.daw-settings-section{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
30821
- .daw-root *,.daw-settings-section *{box-sizing:border-box;letter-spacing:0}
30822
- /* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
30823
- 深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
30824
- .daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
30825
- .daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
30826
- .daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
30827
- .daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
30828
- .daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
30829
- .daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
30830
- .daw-icon:disabled{opacity:.45;cursor:not-allowed}
30831
- /* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
30832
- 全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
30833
- .daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
30834
- .daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30835
- .daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
30836
- /* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
30837
- .daw-command:disabled{opacity:.5;cursor:not-allowed}
30838
- .daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
30839
- .daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30840
- .daw-command.danger{color:var(--daw-danger)}
30841
- .daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
30842
- /* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
30843
- .daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
30844
- .daw-copy-label{font-size:12px}
30845
- .daw-empty{padding:14px 2px;text-align:center}
30846
- .daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
30847
- .daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
30848
- .daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30849
- .daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
30850
- .daw-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
30851
- .daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
30852
- .daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
30853
- .daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
30854
- .daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
30855
- .daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
30856
- .daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
30857
- .daw-card-chevron-open{transform:rotate(180deg)}
30858
- .daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
30859
- .daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
30860
- .daw-builtin-hint{text-align:right;align-self:center}
30861
- .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
30862
- /* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
30863
- .daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
30864
- .daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
30865
- .daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
30866
- .daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
30867
- /* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
30868
- 不再把「未知进度」伪装成恒定 30%。 */
30869
- .daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
30870
- @keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
30871
- @media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
30872
- `;
30873
- function installWorkspaceStyles() {
30874
- if (document.getElementById(STYLE_ID) !== null) return;
30875
- const style = document.createElement("style");
30876
- style.id = STYLE_ID;
30877
- style.dataset.plugin = "dsh-workspace";
30878
- style.textContent = WORKSPACE_CSS;
30879
- document.head.appendChild(style);
30880
- }
30881
- //#endregion
30882
30914
  //#region src/shared/contracts.ts
30883
30915
  const DEVICE_SCOPES = [
30884
30916
  "chat.read",
@@ -30889,6 +30921,23 @@ window.__ModuleLoader__.load({
30889
30921
  "settings.read",
30890
30922
  "settings.write"
30891
30923
  ];
30924
+ /**
30925
+ * 授予新设备时的默认权限(**五个非破坏性权限**)。
30926
+ *
30927
+ * 刻意不含 `files.delete` 与 `settings.write`:这两个是破坏性权限,应当是用户显式
30928
+ * 要求才授予。`settings.write` 尤其危险 —— 拿到它就能改供应商 `baseURL`,把模型
30929
+ * 请求连同真实凭据引到任意地址(SEC-H11),所以它绝不能是默认值。
30930
+ *
30931
+ * 三条路径必须共用这一份,否则「同一个产品里三处默认权限不一样」这种缺陷会反复
30932
+ * 复发:服务端生成配置文本时的缺省、Web UI 配对码面板的初始勾选、隧道一键路径。
30933
+ */
30934
+ const DEFAULT_DEVICE_SCOPES = [
30935
+ "chat.read",
30936
+ "chat.write",
30937
+ "files.read",
30938
+ "files.write",
30939
+ "settings.read"
30940
+ ];
30892
30941
  new Set(DEVICE_SCOPES);
30893
30942
  //#endregion
30894
30943
  //#region src/client/app-qr.ts
@@ -30966,6 +31015,7 @@ window.__ModuleLoader__.load({
30966
31015
  devices: "devicesTabDesc",
30967
31016
  image: "imageTabDesc",
30968
31017
  trash: "trashTabDesc",
31018
+ plugins: "pluginsTabDesc",
30969
31019
  "app-download": "appDownloadTabDesc"
30970
31020
  };
30971
31021
  function tabDescKey(tab) {
@@ -31008,7 +31058,8 @@ window.__ModuleLoader__.load({
31008
31058
  function PhoneSettingsBody(props) {
31009
31059
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminPanel, {
31010
31060
  api: props.api,
31011
- initialOpen: props.section
31061
+ initialOpen: props.section,
31062
+ onOpenPluginList: props.onOpenPluginList
31012
31063
  });
31013
31064
  }
31014
31065
  /**
@@ -31314,7 +31365,7 @@ window.__ModuleLoader__.load({
31314
31365
  }, [status?.configuredHost, status?.configuredPort]);
31315
31366
  const roots = status?.roots ?? [];
31316
31367
  const managementLocked = tunnel?.running === true;
31317
- const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEVICE_SCOPES]);
31368
+ const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEFAULT_DEVICE_SCOPES]);
31318
31369
  const toggleScope = (0, react.useCallback)((scope) => {
31319
31370
  setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
31320
31371
  }, []);
@@ -31916,6 +31967,12 @@ window.__ModuleLoader__.load({
31916
31967
  }, item.id))
31917
31968
  })]
31918
31969
  }, "trash"),
31970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginsSection, {
31971
+ api: props.api,
31972
+ open: isOpen("plugins"),
31973
+ onToggle: () => toggleCard("plugins"),
31974
+ onOpenPluginList: props.onOpenPluginList
31975
+ }),
31919
31976
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
31920
31977
  open: isOpen("app-download"),
31921
31978
  onToggle: () => toggleCard("app-download")
@@ -31924,6 +31981,190 @@ window.__ModuleLoader__.load({
31924
31981
  });
31925
31982
  }
31926
31983
  /**
31984
+ * 插件卡片:这台机器 profile 里装了哪些插件、能不能停用、有没有更新。
31985
+ *
31986
+ * ## 为什么加这张卡
31987
+ *
31988
+ * 用户的反馈是「功能单调点,无法查看装的插件情况」—— 想知道装了哪些插件、有没有出问题的,
31989
+ * 必须回到电脑前的 WebUI。宿主的 `GET /settings/plugins` 与 `PATCH /settings/plugins` 早就有,
31990
+ * 只是浏览器半边从来没有消费它们;这张卡补的就是那半截。
31991
+ *
31992
+ * ## 三个必须说清楚的地方
31993
+ *
31994
+ * 1. **官方包不能停**(`toggleable === false`)。它们随 DSH 运行时安装、不由 profile 管理,
31995
+ * 改 bundles 也加载不出来。服务端会拒绝并回 `PLUGIN_OFFICIAL_IMMUTABLE`,这里干脆不给按钮。
31996
+ * 2. **`runtime-provided` 不是故障**。官方包出现在 `dsh.profile.bundles` 里但不在 profile 的
31997
+ * node_modules 下,早期实现把它误判成「声明加载但缺失」,真机上一次冒出 5 条假警报。
31998
+ * 3. **改完要重启**。bundles 只在启动时读一次,所以成功提示不是「已停用」而是
31999
+ * 「已写进 package.json,重启后生效」—— 否则用户会以为点了没反应,再点几次。
32000
+ *
32001
+ * ## 更新检查为什么是按钮而不是自动
32002
+ *
32003
+ * 它要打 npm registry,十次里九次用户只想看「装了啥」。默认不查,`updateAvailable` 因此是
32004
+ * 三态(`null` = 没查),而不是压成 `false` 去谎称「已是最新」。
32005
+ */
32006
+ function PluginsSection(props) {
32007
+ const { t } = useWorkspaceI18n();
32008
+ const [inventory, setInventory] = (0, react.useState)(void 0);
32009
+ const [busy, setBusy] = (0, react.useState)(false);
32010
+ const [checking, setChecking] = (0, react.useState)(false);
32011
+ const [toggling, setToggling] = (0, react.useState)(void 0);
32012
+ const [error, setError] = (0, react.useState)(void 0);
32013
+ const [restartRequired, setRestartRequired] = (0, react.useState)(false);
32014
+ const [openListMissing, setOpenListMissing] = (0, react.useState)(false);
32015
+ const load = (0, react.useCallback)(async (checkUpdates) => {
32016
+ if (checkUpdates) setChecking(true);
32017
+ else setBusy(true);
32018
+ setError(void 0);
32019
+ try {
32020
+ setInventory(await props.api.pluginInventory(checkUpdates));
32021
+ } catch (cause) {
32022
+ setError(messageOf(cause, t));
32023
+ } finally {
32024
+ if (checkUpdates) setChecking(false);
32025
+ else setBusy(false);
32026
+ }
32027
+ }, [props.api, t]);
32028
+ (0, react.useEffect)(() => {
32029
+ if (!props.open || inventory !== void 0) return;
32030
+ load(false);
32031
+ }, [
32032
+ props.open,
32033
+ inventory,
32034
+ load
32035
+ ]);
32036
+ const toggle = (0, react.useCallback)(async (entry) => {
32037
+ setToggling(entry.name);
32038
+ setError(void 0);
32039
+ try {
32040
+ await props.api.togglePlugin(entry.name, !entry.loaded);
32041
+ setInventory(await props.api.pluginInventory(false));
32042
+ setRestartRequired(true);
32043
+ } catch (cause) {
32044
+ setError(messageOf(cause, t));
32045
+ } finally {
32046
+ setToggling(void 0);
32047
+ }
32048
+ }, [props.api, t]);
32049
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
32050
+ tab: "plugins",
32051
+ nameKey: tabLabelKey("plugins"),
32052
+ descKey: tabDescKey("plugins"),
32053
+ open: props.open,
32054
+ onToggle: props.onToggle,
32055
+ children: [
32056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32057
+ className: "daw-list-meta",
32058
+ children: t("pluginsHint")
32059
+ }),
32060
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32061
+ className: "daw-warning",
32062
+ role: "alert",
32063
+ children: error
32064
+ }),
32065
+ restartRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32066
+ className: "daw-success",
32067
+ role: "status",
32068
+ children: t("pluginsRestartRequired")
32069
+ }),
32070
+ openListMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32071
+ className: "daw-warning",
32072
+ role: "status",
32073
+ children: t("pluginsOpenListUnavailable")
32074
+ }),
32075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32076
+ className: "daw-form-row",
32077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32078
+ type: "button",
32079
+ className: "daw-command",
32080
+ disabled: checking,
32081
+ onClick: () => {
32082
+ load(true);
32083
+ },
32084
+ children: [
32085
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
32086
+ " ",
32087
+ checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
32088
+ ]
32089
+ }), props.onOpenPluginList !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32090
+ type: "button",
32091
+ className: "daw-command",
32092
+ onClick: () => {
32093
+ setOpenListMissing(props.onOpenPluginList?.() === false);
32094
+ },
32095
+ children: t("pluginsOpenList")
32096
+ })]
32097
+ }),
32098
+ inventory === void 0 ? busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32099
+ className: "daw-list-meta",
32100
+ children: t("loading")
32101
+ }) : null : !inventory.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32102
+ className: "daw-list-meta",
32103
+ children: t("pluginsUnavailable", { reason: inventory.reason ?? "" })
32104
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32105
+ className: "daw-list-meta",
32106
+ children: [
32107
+ inventory.profile !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [inventory.profile, " · "] }),
32108
+ inventory.loadedCount,
32109
+ " / ",
32110
+ inventory.items.length,
32111
+ inventory.problemCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsProblems", { count: inventory.problemCount })] })
32112
+ ]
32113
+ }), inventory.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32114
+ className: "daw-list-meta daw-empty",
32115
+ children: t("pluginsEmpty")
32116
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32117
+ className: "daw-list",
32118
+ children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32119
+ className: "daw-list-row",
32120
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
32121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32122
+ className: "daw-list-title",
32123
+ children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
32124
+ className: "daw-list-meta",
32125
+ children: [" · ", entry.installed]
32126
+ })]
32127
+ }),
32128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32129
+ className: "daw-list-meta",
32130
+ children: entry.description ?? t("pluginsNoDescription")
32131
+ }),
32132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32133
+ className: "daw-list-meta",
32134
+ children: [
32135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32136
+ className: `daw-pill${entry.loaded ? " on" : ""}`,
32137
+ children: t(pluginStateKey(entry.state))
32138
+ }),
32139
+ entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32140
+ className: "daw-pill",
32141
+ children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
32142
+ })] }),
32143
+ entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
32144
+ ]
32145
+ })
32146
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32147
+ type: "button",
32148
+ className: "daw-command",
32149
+ disabled: toggling === entry.name,
32150
+ onClick: () => {
32151
+ toggle(entry);
32152
+ },
32153
+ children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
32154
+ }) })]
32155
+ }, entry.name))
32156
+ })] })
32157
+ ]
32158
+ });
32159
+ }
32160
+ /** 四种 `PluginState` 各自的中文/英文说法 —— 状态本身是产品语义,不该在界面上露出裸码。 */
32161
+ function pluginStateKey(state) {
32162
+ if (state === "loaded") return "pluginsStateLoaded";
32163
+ if (state === "runtime-provided") return "pluginsStateRuntimeProvided";
32164
+ if (state === "installed-not-loaded") return "pluginsStateInstalledNotLoaded";
32165
+ return "pluginsStateDeclaredMissing";
32166
+ }
32167
+ /**
31927
32168
  * The App download page: one QR card per release host.
31928
32169
  *
31929
32170
  * The codes come from `APP_QR_CODES` as inlined SVG data URLs, so this page makes **no network
@@ -32103,9 +32344,19 @@ window.__ModuleLoader__.load({
32103
32344
  const [editing, setEditing] = (0, react.useState)(false);
32104
32345
  const [scopes, setScopes] = (0, react.useState)(props.device.scopes);
32105
32346
  const [rootIds, setRootIds] = (0, react.useState)(props.device.rootIds);
32347
+ const [name, setName] = (0, react.useState)(props.device.name);
32106
32348
  const toggle = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
32107
32349
  const shortId = props.device.id.length > 14 ? `${props.device.id.slice(0, 8)}…${props.device.id.slice(-4)}` : props.device.id;
32108
32350
  const lastSeen = props.device.lastSeenAt === void 0 ? t("deviceNeverSeen") : t("deviceLastSeen", { time: new Date(props.device.lastSeenAt).toLocaleString(locale) });
32351
+ const autoNamed = props.device.nameSource === "default" || props.device.nameSource === "detected";
32352
+ const saveAll = () => {
32353
+ const trimmed = name.trim();
32354
+ const rename = trimmed !== "" && trimmed !== props.device.name;
32355
+ (rename ? props.api.renameDevice(props.device.id, trimmed) : Promise.resolve()).then(() => props.api.updateDevice(props.device.id, scopes, rootIds)).then(props.onChanged).then(() => {
32356
+ setEditing(false);
32357
+ props.onNotice(rename ? "deviceNameSaved" : "permissionsSaved");
32358
+ }).catch(props.onError);
32359
+ };
32109
32360
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32110
32361
  className: "daw-list-row",
32111
32362
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
@@ -32113,10 +32364,14 @@ window.__ModuleLoader__.load({
32113
32364
  className: "daw-list-title",
32114
32365
  children: [
32115
32366
  props.device.name,
32116
- " ",
32117
32367
  props.device.revokedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32118
32368
  className: "daw-pill",
32119
32369
  children: t("revoked")
32370
+ }),
32371
+ autoNamed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32372
+ className: "daw-pill",
32373
+ title: t("deviceRenameHint"),
32374
+ children: t("deviceDetectedName")
32120
32375
  })
32121
32376
  ]
32122
32377
  }),
@@ -32127,47 +32382,66 @@ window.__ModuleLoader__.load({
32127
32382
  title: props.device.id,
32128
32383
  children: shortId
32129
32384
  }),
32385
+ props.device.platform !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
32386
+ " · ",
32387
+ props.device.platform,
32388
+ props.device.model === void 0 ? "" : ` · ${props.device.model}`
32389
+ ] }),
32130
32390
  " · ",
32131
32391
  lastSeen,
32132
32392
  " · ",
32133
32393
  props.device.scopes.length === 0 ? t("noScopes") : props.device.scopes.map((scope) => scopeLabel(locale, scope)).join(" · ")
32134
32394
  ]
32135
32395
  }),
32136
- editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32137
- className: "daw-scope-grid",
32138
- children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
32139
- className: "daw-check",
32140
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32141
- type: "checkbox",
32142
- checked: scopes.includes(scope),
32143
- onChange: () => setScopes((current) => toggle(current, scope))
32144
- }), scopeLabel(locale, scope)]
32145
- }, scope))
32146
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32147
- className: "daw-scope-grid",
32148
- children: props.roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
32149
- className: "daw-check",
32150
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32151
- type: "checkbox",
32152
- checked: rootIds.includes(root.id),
32153
- onChange: () => setRootIds((current) => toggle(current, root.id))
32154
- }), root.label]
32155
- }, root.id))
32156
- })] })
32396
+ editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
32397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
32398
+ className: "daw-field",
32399
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("deviceRename") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32400
+ className: "daw-input",
32401
+ value: name,
32402
+ placeholder: t("deviceRenamePlaceholder"),
32403
+ title: t("deviceRenameHint"),
32404
+ onChange: (event) => setName(event.target.value),
32405
+ onKeyDown: (event) => {
32406
+ if (event.key === "Enter") saveAll();
32407
+ }
32408
+ })]
32409
+ }),
32410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32411
+ className: "daw-scope-grid",
32412
+ children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
32413
+ className: "daw-check",
32414
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32415
+ type: "checkbox",
32416
+ checked: scopes.includes(scope),
32417
+ onChange: () => setScopes((current) => toggle(current, scope))
32418
+ }), scopeLabel(locale, scope)]
32419
+ }, scope))
32420
+ }),
32421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32422
+ className: "daw-scope-grid",
32423
+ children: props.roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
32424
+ className: "daw-check",
32425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
32426
+ type: "checkbox",
32427
+ checked: rootIds.includes(root.id),
32428
+ onChange: () => setRootIds((current) => toggle(current, root.id))
32429
+ }), root.label]
32430
+ }, root.id))
32431
+ })
32432
+ ] })
32157
32433
  ] }), props.device.revokedAt === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
32158
32434
  editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32159
32435
  className: "daw-command primary",
32160
- onClick: () => {
32161
- props.api.updateDevice(props.device.id, scopes, rootIds).then(props.onChanged).then(() => {
32162
- setEditing(false);
32163
- props.onNotice("permissionsSaved");
32164
- }).catch(props.onError);
32165
- },
32436
+ onClick: saveAll,
32166
32437
  children: t("save")
32167
32438
  }),
32168
32439
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32169
32440
  className: "daw-command",
32170
- onClick: () => setEditing((value) => !value),
32441
+ onClick: () => {
32442
+ setName(props.device.name);
32443
+ setEditing((value) => !value);
32444
+ },
32171
32445
  children: editing ? t("cancel") : t("permissions")
32172
32446
  }),
32173
32447
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -32188,6 +32462,7 @@ window.__ModuleLoader__.load({
32188
32462
  if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
32189
32463
  if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
32190
32464
  if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
32465
+ if (tab === "plugins") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Package, { size: 15 });
32191
32466
  if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
32192
32467
  if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
32193
32468
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.File, { size: 15 });
@@ -32222,6 +32497,11 @@ window.__ModuleLoader__.load({
32222
32497
  if (error.code === "ADMIN_PROXY_FORWARDED") return "errorAdminProxyForwarded";
32223
32498
  if (error.code === "ADMIN_HOST_NOT_LOOPBACK") return "errorAdminHostNotLoopback";
32224
32499
  if (error.code === "ADMIN_LOOPBACK_REQUIRED") return "errorAdminLoopbackRequired";
32500
+ if (error.code === "PLUGIN_OFFICIAL_IMMUTABLE") return "pluginsErrorOfficial";
32501
+ if (error.code === "PLUGIN_NAME_INVALID") return "pluginsErrorNameInvalid";
32502
+ if (error.code === "PROFILE_UNKNOWN") return "pluginsErrorProfileUnknown";
32503
+ if (error.code === "MANIFEST_UNREADABLE") return "pluginsErrorManifestUnreadable";
32504
+ if (error.code === "PLUGIN_NOT_INSTALLED") return "pluginsErrorNotInstalled";
32225
32505
  if (error.code.includes("PATH") || error.code.includes("SYMLINK") || error.code.includes("REPARSE")) return "errorPathInvalid";
32226
32506
  if (error.status === 403) return "errorForbidden";
32227
32507
  if (error.status === 404) return "errorNotFound";
@@ -32236,9 +32516,329 @@ window.__ModuleLoader__.load({
32236
32516
  return new Promise((resolve) => setTimeout(resolve, ms));
32237
32517
  }
32238
32518
  //#endregion
32519
+ //#region src/client/plugins.tsx
32520
+ /**
32521
+ * 插件清单的右侧栏形态:一个真正的 `SidebarRightTabDefinition`。
32522
+ *
32523
+ * ## 为什么不是「再注册一个 slot」
32524
+ *
32525
+ * 报告里第 2 项讲的是 DSH 0.2.1-rc.3 的三处 pin 机制(`view-pins` / `panel-pins` /
32526
+ * `guide-pins`)。pin 只作用于**注册了 panel / view / guide 的插件**——桥原来那两个座位
32527
+ * (`conversation.chat.turnTail` 与 `settings.section`)都不在这三者的管辖内,所以哪怕把
32528
+ * 三个新 slot 全注册一遍,桥自己的东西也一个都 pin 不到,只能消费别人的条目。
32529
+ *
32530
+ * 要真拿到 pin,唯一的路是**成为 tab 类型提供方**:往 `sidebarRightTabs` 注册一个
32531
+ * definition,并在 `sidebar.right.pane.tab` / `.title` 两个 keyed seat 上按 `definition.id`
32532
+ * 挂上 body 与标题。这样 guide 页会列出这张卡片,用户能在 rc.3 的 More 菜单里把它
32533
+ * 固定 / 取消固定(`guide-pins.d.ts` 的 `FIXED_GUIDE_KIND = "files"` 只豁免文件卡片,
32534
+ * 桥的 kind 不在豁免名单里)。
32535
+ *
32536
+ * ## 为什么单独一个文件
32537
+ *
32538
+ * `workspace.tsx` 已经 1200 多行,而 `docs/project/TECH-DEBT.md` 的行数预算写的是
32539
+ * 600 行/文件、超预算要先登记。把新 UI 面拆出来既不加剧那笔债,也让「设置页里的折叠卡」
32540
+ * 与「右侧栏的常驻 tab」这两种形态各自独立演进。
32541
+ *
32542
+ * ## rc.2 上的降级
32543
+ *
32544
+ * `sidebarRightTabs` 在 rc.2 与 rc.3 都提供,definition 契约两版一致,所以 tab 本身能用;
32545
+ * 但 rc.2 的 `GuideBody` 没有 `entryPins` / `entryActions` / More 菜单,卡片会正常列出、
32546
+ * 点开也正常,只是**不能 pin**。这是可接受的降级:能力随宿主版本生长,桥不做版本判断。
32547
+ */
32548
+ /** definition 的 id:tab 系统的实现身份,同时是 body/title 两个 keyed seat 的 key。 */
32549
+ const PLUGINS_TAB_ID = "dsh-remote-bridge-plugins";
32550
+ /**
32551
+ * tab 类型名。
32552
+ *
32553
+ * 必须与 DSH 内置类型不冲突 —— 内置的有 `guide` / `files` / `terminal` / `browser` /
32554
+ * `plan` / `schedule` / `deliverables` 等。冲突时 `sidebarRightTabs.register` 会抛
32555
+ * `sidebarRight: tab kind "…" is already registered`,整个插件起不来,所以带上插件前缀。
32556
+ */
32557
+ const PLUGINS_TAB_KIND = "remote-bridge-plugins";
32558
+ /**
32559
+ * 构造 tab 类型定义。
32560
+ *
32561
+ * 文案用 thunk(`() => string`)而不是现取的字符串:契约里写明 thunk 每次使用都会重读,
32562
+ * 所以切换语言不需要重新注册。`title` 是 tab chip 的初始文字,开 tab 的那一刻被捕获进
32563
+ * layout record。
32564
+ *
32565
+ * `patterns` 故意省略 —— 省略即「page type」,只能按 kind 打开(guide 卡片点一下就是这条路),
32566
+ * 不会被文件地址意外匹配到。`priority` 用默认的 `extension`。
32567
+ */
32568
+ function pluginsTabDefinition() {
32569
+ const label = () => translate(getWorkspaceLocale(), "pluginsTab");
32570
+ const description = () => translate(getWorkspaceLocale(), "pluginsTabDesc");
32571
+ return {
32572
+ id: PLUGINS_TAB_ID,
32573
+ kind: PLUGINS_TAB_KIND,
32574
+ title: label,
32575
+ guide: [{
32576
+ id: "plugins",
32577
+ order: 40,
32578
+ title: label,
32579
+ description,
32580
+ icon: import_lucide_react.Package
32581
+ }]
32582
+ };
32583
+ }
32584
+ /**
32585
+ * tab chip 里的文字。
32586
+ *
32587
+ * 不读 `useTabInfo` 是刻意的:标题在开 tab 时就被捕获了,跟着当前语言走没有意义;
32588
+ * 而且 rc.2 / rc.3 的 hook 注入形状不同,少依赖一处就少一处版本耦合。
32589
+ */
32590
+ function PluginsTabTitle() {
32591
+ const { t } = useWorkspaceI18n();
32592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("pluginsTab") });
32593
+ }
32594
+ /**
32595
+ * 右侧栏里的插件清单。
32596
+ *
32597
+ * 与设置页那张折叠卡读同一对端点(`GET/PATCH /settings/plugins`),但形态不同:
32598
+ * 这里是**常驻面板**,没有卡片外壳,清单直接铺开;刷新按钮常驻而不是「展开才拉」——
32599
+ * 用户既然把这张卡 pin 到了 guide 页,就是打算经常看。
32600
+ *
32601
+ * 三条与设置页一致的产品约束(详见 `workspace.tsx` 的 `PluginsSection` 注释):
32602
+ * 官方包不可停用、`runtime-provided` 不是故障、改完要重启才生效。
32603
+ */
32604
+ function makePluginsTabBody(api) {
32605
+ return function PluginsTabBody() {
32606
+ const { t } = useWorkspaceI18n();
32607
+ const [inventory, setInventory] = (0, react.useState)(void 0);
32608
+ const [busy, setBusy] = (0, react.useState)(false);
32609
+ const [checking, setChecking] = (0, react.useState)(false);
32610
+ const [toggling, setToggling] = (0, react.useState)(void 0);
32611
+ const [error, setError] = (0, react.useState)(void 0);
32612
+ const [restartRequired, setRestartRequired] = (0, react.useState)(false);
32613
+ const load = (0, react.useCallback)(async (checkUpdates) => {
32614
+ if (checkUpdates) setChecking(true);
32615
+ else setBusy(true);
32616
+ setError(void 0);
32617
+ try {
32618
+ setInventory(await api.pluginInventory(checkUpdates));
32619
+ } catch (cause) {
32620
+ setError(messageOf(cause, t));
32621
+ } finally {
32622
+ if (checkUpdates) setChecking(false);
32623
+ else setBusy(false);
32624
+ }
32625
+ }, [t]);
32626
+ (0, react.useEffect)(() => {
32627
+ load(false);
32628
+ }, [load]);
32629
+ const toggle = (0, react.useCallback)(async (entry) => {
32630
+ setToggling(entry.name);
32631
+ setError(void 0);
32632
+ try {
32633
+ await api.togglePlugin(entry.name, !entry.loaded);
32634
+ setInventory(await api.pluginInventory(false));
32635
+ setRestartRequired(true);
32636
+ } catch (cause) {
32637
+ setError(messageOf(cause, t));
32638
+ } finally {
32639
+ setToggling(void 0);
32640
+ }
32641
+ }, [t]);
32642
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32643
+ className: "daw-plugins-tab",
32644
+ children: [
32645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32646
+ className: "daw-list-meta",
32647
+ children: t("pluginsHint")
32648
+ }),
32649
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32650
+ className: "daw-warning",
32651
+ role: "alert",
32652
+ children: error
32653
+ }),
32654
+ restartRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32655
+ className: "daw-success",
32656
+ role: "status",
32657
+ children: t("pluginsRestartRequired")
32658
+ }),
32659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32660
+ className: "daw-form-row",
32661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32662
+ type: "button",
32663
+ className: "daw-command",
32664
+ disabled: busy,
32665
+ onClick: () => {
32666
+ load(false);
32667
+ },
32668
+ children: [
32669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.RefreshCw, { size: 14 }),
32670
+ " ",
32671
+ busy ? t("loading") : t("refresh")
32672
+ ]
32673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32674
+ type: "button",
32675
+ className: "daw-command",
32676
+ disabled: checking,
32677
+ onClick: () => {
32678
+ load(true);
32679
+ },
32680
+ children: checking ? t("pluginsChecking") : t("pluginsCheckUpdates")
32681
+ })]
32682
+ }),
32683
+ inventory === void 0 ? busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32684
+ className: "daw-list-meta",
32685
+ children: t("loading")
32686
+ }) : null : !inventory.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32687
+ className: "daw-list-meta",
32688
+ children: t("pluginsUnavailable", { reason: inventory.reason ?? "" })
32689
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32690
+ className: "daw-list-meta",
32691
+ children: [
32692
+ inventory.profile !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [inventory.profile, " · "] }),
32693
+ inventory.loadedCount,
32694
+ " / ",
32695
+ inventory.items.length,
32696
+ inventory.problemCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsProblems", { count: inventory.problemCount })] })
32697
+ ]
32698
+ }), inventory.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32699
+ className: "daw-list-meta daw-empty",
32700
+ children: t("pluginsEmpty")
32701
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32702
+ className: "daw-list",
32703
+ children: inventory.items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32704
+ className: "daw-list-row",
32705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
32706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32707
+ className: "daw-list-title",
32708
+ children: [entry.name, entry.installed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
32709
+ className: "daw-list-meta",
32710
+ children: [" · ", entry.installed]
32711
+ })]
32712
+ }),
32713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32714
+ className: "daw-list-meta",
32715
+ children: entry.description ?? t("pluginsNoDescription")
32716
+ }),
32717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32718
+ className: "daw-list-meta",
32719
+ children: [
32720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32721
+ className: `daw-pill${entry.loaded ? " on" : ""}`,
32722
+ children: t(pluginStateKey(entry.state))
32723
+ }),
32724
+ entry.updateAvailable === true && entry.latestVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32725
+ className: "daw-pill",
32726
+ children: t("pluginsUpdateAvailable", { version: entry.latestVersion })
32727
+ })] }),
32728
+ entry.updateAvailable === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", t("pluginsLatest")] })
32729
+ ]
32730
+ })
32731
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entry.toggleable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
32732
+ type: "button",
32733
+ className: "daw-command",
32734
+ disabled: toggling === entry.name,
32735
+ onClick: () => {
32736
+ toggle(entry);
32737
+ },
32738
+ children: entry.loaded ? t("pluginsDisable") : t("pluginsEnable")
32739
+ }) })]
32740
+ }, entry.name))
32741
+ })] })
32742
+ ]
32743
+ });
32744
+ };
32745
+ }
32746
+ //#endregion
32747
+ //#region src/client/styles.ts
32748
+ const STYLE_ID = "dsh-workspace-style";
32749
+ /**
32750
+ * 插件的全部样式。
32751
+ *
32752
+ * 导出出来是为了让测试能守住一条容易失守的不变量:**CSS 变量块必须同时覆盖
32753
+ * `.daw-root` 与 `.daw-settings-section`**。
32754
+ *
32755
+ * 这条防线来自一个真实发生过的隐形 bug:设置页内联渲染时,那一节没有 `.daw-root` 祖先,
32756
+ * 于是 `var(--daw-accent)` 解析失败 → `.daw-command.primary` 的 `background` 失效变透明、
32757
+ * 而 `color` 仍是 `#fff` → **白字落在白底上,按钮彻底看不见**。
32758
+ * 类型检查、单测、构建全都发现不了,只有肉眼看渲染或量计算样式才抓得到。
32759
+ */
32760
+ const WORKSPACE_CSS = `
32761
+ .daw-root,.daw-settings-section,.daw-plugins-tab{--daw-bg:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-2,#f7f8fa));--daw-panel:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));--daw-ink:var(--dsw-alias-label-primary,#18202a);--daw-muted:var(--dsw-alias-label-tertiary,#687381);--daw-line:var(--dsw-alias-border-l2,#dfe4e8);--daw-accent:var(--dsw-alias-brand-primary,#087f8c);--daw-accent-soft:var(--dsw-alias-bg-module-platform,#e5f5f6);--daw-good:var(--dsw-alias-state-success-primary,#2f7d4a);--daw-danger:var(--dsw-alias-state-error-primary,#b33a3a);color:var(--daw-ink);background:transparent;font:13px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:0;min-width:0}
32762
+ .daw-root *,.daw-settings-section *,.daw-plugins-tab *{box-sizing:border-box;letter-spacing:0}
32763
+ /* 输入控件基线(UI-04):此前只有 :focus-visible 一条,非聚焦态全靠浏览器默认(2px inset/Arial/灰底),
32764
+ 深色令牌下实测白底黑字与面板割裂。这里给齐边框/底/字/内边距,聚焦态只换描边。 */
32765
+ .daw-input{width:100%;min-width:0;height:30px;padding:4px 9px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit}
32766
+ .daw-input::placeholder{color:var(--dsw-alias-label-tertiary,var(--daw-muted))}
32767
+ .daw-input:focus-visible{border-color:var(--dsw-alias-brand-primary,var(--daw-accent));outline:none}
32768
+ .daw-input:disabled{opacity:.55;cursor:not-allowed}.daw-icon{width:30px;padding:0}
32769
+ .daw-icon:hover{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}
32770
+ .daw-icon.danger{color:var(--daw-danger);border-color:transparent;background:transparent}
32771
+ .daw-icon:disabled{opacity:.45;cursor:not-allowed}
32772
+ /* 普通命令按钮基线(UI-04):旧版写死 background:#fff/border:#cbd2d8,深色令牌下成了白块黑字。
32773
+ 全部改走 DSH 令牌;.primary 沿用面板底色做次按钮、真正的主操作由调用方加 .primary 配合。 */
32774
+ .daw-command{height:30px;padding:0 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));color:var(--dsw-alias-label-primary,var(--daw-ink));font:inherit;cursor:pointer}
32775
+ .daw-command:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32776
+ .daw-command:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:1px}
32777
+ /* 禁用态(UI-05):此前无任何 :disabled 样式,只剩浏览器默认的 30% 文字且仍是箭头手型。 */
32778
+ .daw-command:disabled{opacity:.5;cursor:not-allowed}
32779
+ .daw-command.primary{color:var(--dsw-alias-label-primary,var(--daw-ink));background:var(--dsw-alias-bg-module-platform,var(--daw-panel));border-color:var(--dsw-alias-border-l2,var(--daw-line));font-weight:500}
32780
+ .daw-command.primary:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32781
+ .daw-command.danger{color:var(--daw-danger)}
32782
+ .daw-command.danger:hover:not(:disabled){border-color:var(--daw-danger)}
32783
+ /* 图标+文字的小复制按钮:图标和文字对齐,窄屏只露图标也不改变按钮高度。 */
32784
+ .daw-command.icon-as-command{display:inline-flex;align-items:center;gap:6px;padding:0 10px;white-space:nowrap}
32785
+ .daw-copy-label{font-size:12px}
32786
+ .daw-empty{padding:14px 2px;text-align:center}
32787
+ .daw-admin{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.daw-admin h3{font-size:14px;margin:0 0 12px}
32788
+ .daw-card{border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-layer-3,var(--daw-panel));border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
32789
+ .daw-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32790
+ .daw-card-open{background:var(--dsw-alias-bg-layer-2,var(--daw-panel));border-color:var(--dsw-alias-label-dimmed,var(--daw-line))}
32791
+ .daw-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
32792
+ .daw-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--daw-accent));outline-offset:-2px}
32793
+ .daw-card-icon{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;display:flex;align-items:center}
32794
+ .daw-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
32795
+ .daw-card-name{color:var(--dsw-alias-label-primary,var(--daw-ink));font-size:15px;font-weight:600;line-height:1.4}
32796
+ .daw-card-desc{color:var(--dsw-alias-label-tertiary,var(--daw-muted));font-size:13px;line-height:1.5}
32797
+ .daw-card-chevron{color:var(--dsw-alias-label-tertiary,var(--daw-muted));flex:none;transition:transform .16s;font-size:12px;line-height:1}
32798
+ .daw-card-chevron-open{transform:rotate(180deg)}
32799
+ .daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-plugins-tab{overflow:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
32800
+ .daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
32801
+ .daw-builtin-hint{text-align:right;align-self:center}
32802
+ .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-command{margin-left:8px}
32803
+ /* 成功反馈条(UI-07):与 .daw-warning 同形,但走成功色,操作成功不再静默。 */
32804
+ .daw-success{flex:none;border:1px solid var(--dsw-alias-state-success-primary,var(--daw-good));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--dsw-alias-state-success-primary,var(--daw-good));margin-bottom:0}
32805
+ .daw-admin-topbar{display:flex;justify-content:flex-end;margin-bottom:8px}
32806
+ .daw-url-row{display:flex;align-items:center;gap:8px;margin-top:8px}.daw-url-row .daw-tunnel-url{flex:1 1 auto;margin:0;min-width:0}
32807
+ .daw-code-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0}.daw-code-row .daw-code{margin:0;flex:1 1 220px;min-width:0}.daw-copy-inline{flex:none}
32808
+ /* 隧道/监听进行中的流动条(UI-08):percent=null 时铺一段高光在轨道里循环滑动,
32809
+ 不再把「未知进度」伪装成恒定 30%。 */
32810
+ .daw-progress-fill.indeterminate{width:40%;background:linear-gradient(90deg,transparent,var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line))),transparent);animation:daw-indeterminate 1.1s ease-in-out infinite}
32811
+ @keyframes daw-indeterminate{0%{transform:translateX(-120%)}100%{transform:translateX(280%)}}
32812
+ @media (max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
32813
+ `;
32814
+ function installWorkspaceStyles() {
32815
+ if (document.getElementById(STYLE_ID) !== null) return;
32816
+ const style = document.createElement("style");
32817
+ style.id = STYLE_ID;
32818
+ style.dataset.plugin = "dsh-workspace";
32819
+ style.textContent = WORKSPACE_CSS;
32820
+ document.head.appendChild(style);
32821
+ }
32822
+ //#endregion
32239
32823
  //#region src/client/index.tsx
32240
32824
  const DEFAULT_SECTION = DEFAULT_PANEL_SECTION;
32241
32825
  const api = new WorkspaceApi("/dsh-workspace-api/api/v1", "/dsh-workspace-api/manage", void 0, true);
32826
+ /**
32827
+ * 打开 DSH 自己的插件主列表。
32828
+ *
32829
+ * **特性检测,不是版本判断**:`pluginNavigation.openList` 是 DSH 0.2.1-rc.3 才加的
32830
+ * (rc.2 只有 `openBundle`),而服务本身在更早的版本就有了。写死版本号会在
32831
+ * 自定义构建 / 未来版本上误判,直接看方法在不在更可靠。
32832
+ *
32833
+ * 返回 `false` 让调用方知道「这次没跳成」,卡片据此给出提示 —— 静默失败是最糟的
32834
+ * 交互,用户会以为按钮坏了。
32835
+ */
32836
+ function openPluginList(ctx) {
32837
+ const navigation = ctx.get?.("pluginNavigation", false);
32838
+ if (typeof navigation?.openList !== "function") return false;
32839
+ navigation.openList();
32840
+ return true;
32841
+ }
32242
32842
  const inject = ["slots", "uiConversation"];
32243
32843
  function apply(ctx) {
32244
32844
  installWorkspaceStyles();
@@ -32254,7 +32854,37 @@ window.__ModuleLoader__.load({
32254
32854
  name: "settings.section",
32255
32855
  id: "dsh-remote-bridge",
32256
32856
  order: 2
32257
- }, SettingsSection, "phoneSettings"), "dsh remote bridge settings section");
32857
+ }, makeSettingsSection(ctx), "phoneSettings"), "dsh remote bridge settings section");
32858
+ registerPluginsTab(ctx);
32859
+ }
32860
+ /**
32861
+ * 把插件清单注册成右侧栏的一个 tab 类型,并在 guide 页列出可 pin 的卡片。
32862
+ *
32863
+ * 三件事必须同时成立,少一件这张卡就「有入口、点开空白」或「根本不出现在 guide 页」:
32864
+ *
32865
+ * 1. `sidebarRightTabs.register(definition)` —— 声明类型身份与 guide 条目;
32866
+ * 2. `sidebar.right.pane.tab` 按 `key = definition.id` 挂 body;
32867
+ * 3. `sidebar.right.pane.tab.title` 同样按 `key` 挂标题。
32868
+ *
32869
+ * 两个 keyed seat 用的是 `definition?.id ?? tab.kind` 做 dispatch key,所以 `key` 必须等于
32870
+ * definition 的 `id`(不是 `kind`)—— 写错的表现是点开 tab 显示「不可用」占位。
32871
+ *
32872
+ * 依赖走 `ctx.inject` 而不是静态 `inject`:`sidebarRightTabs` 来自可选安装的
32873
+ * `@deepseek-ai/dsh-client-ui-sidebar-right`,宿主没装时这里整段静默不执行,
32874
+ * 设置页那张卡与交接图集照常工作。
32875
+ */
32876
+ function registerPluginsTab(ctx) {
32877
+ ctx.inject?.(["sidebarRightTabs"], (scope) => {
32878
+ scope.effect(() => scope.sidebarRightTabs.register(pluginsTabDefinition()), "dsh remote bridge plugins tab type");
32879
+ scope.effect(() => scope.slots.inject("sidebar.right.pane.tab", () => scope.slots.register({
32880
+ name: "sidebar.right.pane.tab",
32881
+ key: PLUGINS_TAB_ID
32882
+ }, makePluginsTabBody(api))), "dsh remote bridge plugins tab body");
32883
+ scope.effect(() => scope.slots.inject("sidebar.right.pane.tab.title", () => scope.slots.register({
32884
+ name: "sidebar.right.pane.tab.title",
32885
+ key: PLUGINS_TAB_ID
32886
+ }, PluginsTabTitle)), "dsh remote bridge plugins tab title");
32887
+ });
32258
32888
  }
32259
32889
  /**
32260
32890
  * DSH 设置页里的「手机设置」一节。
@@ -32268,20 +32898,24 @@ window.__ModuleLoader__.load({
32268
32898
  * 用户的另一条要求也靠内联才落地:二维码要**单独一页**(`app-download`),
32269
32899
  * 它是导航栏里可点的一项,而不是藏在按钮后面。
32270
32900
  */
32271
- function SettingsSection() {
32272
- const { t } = useWorkspaceI18n();
32273
- const [section, setSection] = (0, react.useState)(DEFAULT_SECTION);
32274
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32275
- className: "daw-settings-section",
32276
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32277
- className: "daw-list-meta",
32278
- children: t("phoneSettingsHint")
32279
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneSettingsBody, {
32280
- api,
32281
- section,
32282
- onSection: setSection
32283
- })]
32284
- });
32901
+ function makeSettingsSection(ctx) {
32902
+ return function SettingsSection() {
32903
+ const { t } = useWorkspaceI18n();
32904
+ const [section, setSection] = (0, react.useState)(DEFAULT_SECTION);
32905
+ const openList = (0, react.useCallback)(() => openPluginList(ctx), [ctx]);
32906
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32907
+ className: "daw-settings-section",
32908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
32909
+ className: "daw-list-meta",
32910
+ children: t("phoneSettingsHint")
32911
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneSettingsBody, {
32912
+ api,
32913
+ section,
32914
+ onSection: setSection,
32915
+ onOpenPluginList: openList
32916
+ })]
32917
+ });
32918
+ };
32285
32919
  }
32286
32920
  function localizedSlot(ctx, name, options, component, labelKey) {
32287
32921
  return ctx.slots.inject(name, () => {