@wenbin_wb/dsh-bridge 2.10.9 → 2.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v2.10.11] - 2026-09-16
8
+
9
+ ### 🐞 修复
10
+
11
+ - **移动端顶栏压住 `position: fixed` 全屏面板顶部 52px**(issue #41):桥的让位方式此前是给内容框架加 `padding-top: var(--dsh-mobile-header-h)`,但 `position: fixed` 元素的包含块是视口(CSS 2.1 §10.1),不跟随祖先内边距。官方右侧栏在窄视口(`viewportWidth < 768`)自动全屏(`.P3OORG_panel[data-sidebar-right-panel=fullscreen]`,即 `fixed; inset:0; z-index:40`),其顶部标签栏因此落进顶栏(`z-index: 9998`)覆盖区。由于顶栏背景透明且容器自身 `pointer-events: none`,实际表现并非「被不透明条挡住」,而是顶栏标题/图标与面板标签压字,外加两处 40×40 命中区抢占点击——面板右端的「全屏 / 收起」按钮右半(14×28px)会被顶栏「+」按钮夺走,误触即触发新建会话。现按 `data-sidebar-right-panel="fullscreen"` 直接位移面板容器本身(`top` 下移 52px 并同步收窄 `height` / `max-height`),不依赖宿主 CSS-module 哈希类名,宿主升级不失效。若用户此前用 `top` 自行让位过,本次修复与之同属性、级联只生效一条,**不会叠加**;但若用的是 `padding-top` / `margin-top` / `transform` 位移,请撤掉旧规则,否则会叠加下移。
12
+ - **移动端断点与宿主判据对齐**:桥原按 `max-width: 768px`(运行时亦为 `innerWidth <= 768`),而宿主用 `viewportWidth < 768` 决定右侧栏是否自动全屏,恰在 768px 这一宽度上出现「桥渲染移动端顶栏、宿主按桌面(push)布局」的错位。现移动端统一为 `<= 767px`(新增 `MOBILE_MAX_WIDTH = 767` 常量,消除散落的魔法值),`768px` 起彻底交还桌面布局。
13
+ - **鸿蒙(HuaweiBrowser / ArkWeb)上文档预览永远显示「文件资源服务不可用。」**:该内核解析非特殊 scheme 时 `new URL('dsh-resource://file/session/<id>/<path>').hostname` 返回空串(Chrome/Safari 返回 `file`),而 DSH 的客户端资源注册表 `@deepseek-ai/dsh-client-resources` 正是拿 URL 的 hostname 当「协议键」——取不到就把记录判成「没有 provider」,状态永久停在 `none`;注册表只在 provider 注册那一刻回挂一次已有记录,之后不再重试,所以刷新也不恢复。表现为右侧栏文件与对话生成文档的预览全部打不开(而日志/探针里 `resources.providers` 其实一直有 `file`,容易误判成插件没生效)。现桥侧新增 `client/resource-url-compat.js`:**先做特性探测**,只有引擎确有差异时才包一层 `window.URL`,且仅对 `dsh-resource:` 协议、原生 hostname 为空、原始串能解析出 host 的实例补上 `hostname`/`host`(写成实例自有访问器,读取时原生值优先、为空才回退现解析,赋值/改 href 后立即反映新值;setter 仍委托原生原型);引擎正常时一个字节都不动(判定 `not-needed`)。根因在宿主侧(用 URL.hostname 当协议键),已另行整理上游材料;上游若改为从原始串解析协议,本兼容层自动空转。
14
+ - **iOS 16 / 旧 Safari 完全打不开 DSH(页面红屏 `Failed to load plugins … Can't find variable: Iterator`)**:DSH 自带的 pdf.js 在**模块顶层**就执行 `if (typeof Iterator.prototype.join !== 'function') Iterator.prototype.join = …`,而 `Iterator` 这个全局是 Safari 18.4 才随「迭代器辅助方法」引入的——iOS 16 上该引用直接抛 `Can't find variable: Iterator`,导致 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` 导入失败,进而整棵客户端插件树加载失败(实测于 iOS 16 真机,并在本机以同构环境复现)。同一批缺口还有 `Promise.withResolvers`(Safari 17.4+):DSH 宿主 HTML 末尾的启动握手内联脚本、以及审批 / 用户提问 / cordis 运行时 `ctx.timeout`·`ctx.interval` / workspace-files 变更订阅等运行期路径都在用它。现桥在反向代理注入 HTML 时、于宿主所有脚本之前补齐两枚垫片:`Promise.withResolvers`(完整语义)与 `Iterator`(把真实的内置迭代器共享原型 `%IteratorPrototype%` 挂到 `Iterator.prototype`,使 pdf.js 的补丁落到真实迭代器原型而不是一个没用的空对象)。两者都**只在缺失时安装**,iOS 18.4+ / Android / 桌面端零改动;至此 iOS 16 可正常启动并使用。
15
+ - **「重启 DSH」按钮在 systemd 托管下点了没用,DSH 再也没起来(只能手动重启),且不留任何日志**:桥原实现是不识别托管器的「自派生子进程 + `process.exit(0)`」。在 systemd 单元(如 `dsh-web.service`,用户在 user slice 下)里这必然失败:`Restart=on-failure` 不认干净退出的 code 0(systemd 不会拉起),而默认 `KillMode=control-group` 会在主进程退出时把刚派生的子进程一并杀掉;旧实现还把子进程 stdio 设成 `ignore`,所以连失败原因都看不到。现按托管方式分流:从 `/proc/self/cgroup` 取出**最内层** systemd 单元名 → 交给 `systemctl [--user] restart --no-block <unit>`(stop+start 由 systemd 负责,新进程仍在正确 cgroup 内,并尊重单元自身的 Restart 策略);`DSH_DAEMON` / `PM2_HOME` 仍走守护进程退出;无托管器时派生独立助手 `lib/restart-helper.mjs`,**等旧进程真正退出、端口释放后**才拉起新 DSH,并在最长 120s 内确认端口可连接,全过程写入 `~/.dsh/dsh-bridge/restart.log`(子进程输出另存 `restart-child.log`)。前端在宿主明确返回 `ok:false`(例如 `systemctl` 与助手都派生失败)时**直接报错**,不再假装「正在重连」让用户面对永远转圈。注意:systemd 托管的环境需先手动重启一次让本修复生效,之后按钮即可正常使用。
16
+
17
+ ---
18
+
19
+ ## [v2.10.10] - 2026-09-14
20
+
21
+ ### ✨ 新功能
22
+
23
+ - **设置页新增「高级设置」**(每个 IM 平台卡片内):可直接配置该平台远程会话的 **Agent 预设**(下拉列出本机可用预设,留空使用 DSH 默认预设)、**工作区目录**、**模型提供方 / 模型**,以及进度摘要间隔、审批超时、单条消息上限、分块发送间隔。保存后立即生效,并随宿主重启保留——此前这四个会话级配置没有任何界面入口,只能手改 `config.json`,节奏参数的面板也已在早前重构中丢失。
24
+
25
+ ### 🐞 修复
26
+
27
+ - **DSH 0.1.5 系列上远程 IM 会话工具集残缺**(issue #40):DSH 从 `0.1.5` 起把 agent 侧的工具、提示词与技能目录整体挪到了 agent preset 后面(宿主层不再挂载 `tool-fs` / `tool-bash` / `tool-jobs` / `tool-skill` 等),改由每个会话在创建时挂载预设提供。桥此前只把预设名写进会话元数据、从未真正挂载,会话因此落在「空预设层」——表现为远程 Agent 缺少工具(例如没有读文件的工具)。现在新建与恢复会话都会真实挂载预设:未配置时使用 DSH 自身的默认预设;配置的预设在本机已不存在时回退默认预设,并在会话创建提示里明确告知,不再静默降级。
28
+ - **宿主重启后给已有会话发消息报 `session "..." already exists`、随后卡在「当前没有活动会话」**(issue #39):判断会话是否已持久化时沿用了旧的数据结构假设,而 DSH 0.1.5 的会话列表返回的是 `{ header, revision, sizeBytes }` 快照,`entry.id` 恒为空——已存在的历史会话被误判成新会话,走 `agents.create` 建同名会话而失败。现按真实结构取会话头,重启后能正常恢复续聊。
29
+ - **远程会话的工作区与预设配置重启即丢**(issue #40):`cwd`、`agentPreset`、`agentProvider`、`agentModel` 写进配置后,宿主重启即失效(恢复链路只回写了白名单里的少数字段),远程会话会掉到默认目录与默认预设。现已在重启恢复与配置备份导入时一并读回;同时补上 QQ / 飞书此前遗漏的摘要间隔、审批超时、分块延时恢复。配置的工作区目录不可用(被删、换机器、写成相对路径)时回落到已注册工作区并提示,不再把会话建在不存在的目录上。
30
+ - **`/rename` 改名重启即失效**:此前调用的是 `sessionPersistence.update()`——DSH 的持久化服务根本没有这个方法,被可选链静默跳过,改名只改了内存对象。现改用 DSH 原生会话标题服务(追加 `session/title` 事件并落盘),重启与 Web 侧边栏都能看到新标题;会话尚未恢复时会明确提示而不是假报成功。
31
+
32
+ ---
33
+
7
34
  ## [v2.10.9] - 2026-09-12
8
35
 
9
36
  ### 🐞 修复
package/client/client.js CHANGED
@@ -51,7 +51,10 @@ var MOBILE_STYLES_CSS = `
51
51
  --dsh-mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
52
52
  }
53
53
 
54
- @media (max-width: 768px) {
54
+ /* \u65AD\u70B9\u4E0E\u5BBF\u4E3B\u5224\u636E\u5BF9\u9F50\uFF1A\u5BBF\u4E3B\u7528 viewportWidth < 768 \u51B3\u5B9A\u53F3\u4FA7\u680F\u81EA\u52A8\u5168\u5C4F
55
+ \uFF08dsh-client-ui-sidebar-right/lib/client.js\uFF09\uFF0C\u6545\u79FB\u52A8\u7AEF\u6837\u5F0F\u53D6 <=767px\uFF0C
56
+ 768px \u8D77\u5F7B\u5E95\u4EA4\u8FD8\u684C\u9762\u5E03\u5C40\uFF0C\u907F\u514D"\u6865\u6E32\u67D3\u9876\u680F\u3001\u5BBF\u4E3B\u5374\u672A\u5168\u5C4F"\u7684\u9519\u4F4D\u3002 */
57
+ @media (max-width: 767px) {
55
58
  /* 1. \u4E3B\u6846\u67B6\u4E3A Header \u817E\u51FA\u9876\u90E8\u7A7A\u95F4 */
56
59
  div[class*="_frame"] {
57
60
  display: flex !important;
@@ -66,6 +69,19 @@ var MOBILE_STYLES_CSS = `
66
69
  box-sizing: border-box !important;
67
70
  }
68
71
 
72
+ /* 1.1 fixed \u5168\u5C4F\u9762\u677F\u5355\u72EC\u8BA9\u4F4D\uFF1Aposition:fixed \u7684\u5305\u542B\u5757\u662F viewport\uFF08CSS 2.1 \xA710.1\uFF09\uFF0C
73
+ \u4E0D\u8DDF\u968F\u4E0A\u9762 frame \u7684 padding-top\uFF0C\u56E0\u6B64\u9876\u90E8 52px \u4F1A\u843D\u8FDB\u9876\u680F\u8986\u76D6\u533A\uFF08#41\uFF09\u3002
74
+ \u5B98\u65B9\u53F3\u4FA7\u680F\u5728 <768px \u81EA\u52A8\u5168\u5C4F\uFF08fixed; inset:0; z-index:40\uFF09\uFF0C\u8FD9\u91CC\u6309 data \u5C5E\u6027
75
+ \u76F4\u63A5\u4F4D\u79FB\u5BB9\u5668\u672C\u8EAB\uFF0C\u4E0D\u4F9D\u8D56\u5BBF\u4E3B CSS-module \u54C8\u5E0C\u7C7B\u540D\u3002
76
+ \u771F\u6B63\u8D77\u4F5C\u7528\u7684\u662F top\uFF1Bheight/max-height \u4E0E\u4E0B\u9762\u5DE5\u4F5C\u53F0\u9762\u677F\u90A3\u6BB5\u4FDD\u6301\u540C\u4E00\u5199\u6CD5\uFF1A
77
+ \u5BBF\u4E3B\u5F53\u524D\u7528 inset:0\uFF08\u65E0\u663E\u5F0F\u9AD8\u5EA6\uFF09\u65F6 top \u5355\u72EC\u5373\u53EF\uFF0C\u4F46\u5BBF\u4E3B\u5C06\u6765\u82E5\u7ED9\u51FA\u663E\u5F0F\u9AD8\u5EA6\uFF0C
78
+ \u663E\u5F0F height \u4ECD\u80FD\u628A\u76D2\u5B50\u6536\u5728\u9876\u680F\u4E4B\u4E0B\u3002 */
79
+ [data-sidebar-right-panel="fullscreen"] {
80
+ top: var(--dsh-mobile-header-h, 52px) !important;
81
+ height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
82
+ max-height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
83
+ }
84
+
69
85
  /* 2. \u9876\u90E8\u539F\u751F\u5BFC\u822A\u6761\uFF1A100% \u8FD8\u539F DeepSeek App (\u5DE6\u4FA7\u53CC\u6A2A\u7EBF\uFF0C\u53F3\u4FA7(+)\uFF0C\u4E2D\u95F4\u7559\u767D\uFF0C\u65E0\u591A\u4F59\u8BBE\u7F6E\u6309\u94AE) */
70
86
  .dsh-mobile-app-header {
71
87
  position: fixed !important;
@@ -923,7 +939,7 @@ var MOBILE_STYLES_CSS = `
923
939
  --dsw-alias-label-primary-foreground: #0f1115;
924
940
  }
925
941
 
926
- @media (min-width: 769px) {
942
+ @media (min-width: 768px) {
927
943
  .dsh-mobile-app-header,
928
944
  .dsh-mobile-backdrop,
929
945
  .dsh-mobile-panel-close-btn {
@@ -1091,6 +1107,8 @@ var BRIDGE_ENDPOINTS = {
1091
1107
  platformStop: "platformStop",
1092
1108
  platformStart: "platformStart",
1093
1109
  platformUnbind: "platformUnbind",
1110
+ // 本机可用的 DSH agent preset(设置页下拉用;老版本 DSH 返回 available:false)
1111
+ listAgentPresets: "listAgentPresets",
1094
1112
  // 微信 Bot(v1.x 向后兼容别名,deprecated)
1095
1113
  wechatGetStatus: "wechatGetStatus",
1096
1114
  wechatLogin: "wechatLogin",
@@ -1101,6 +1119,81 @@ var BRIDGE_ENDPOINTS = {
1101
1119
  wechatUnbind: "wechatUnbind"
1102
1120
  };
1103
1121
 
1122
+ // client/resource-url-compat.js
1123
+ function installResourceUrlCompat(win = typeof window === "undefined" ? void 0 : window) {
1124
+ if (!win || typeof win.URL !== "function") return "no-url";
1125
+ const NativeURL = win.URL;
1126
+ let probe;
1127
+ try {
1128
+ probe = new NativeURL("dsh-resource://file/__dsh_probe__/x");
1129
+ } catch {
1130
+ return "probe-throw";
1131
+ }
1132
+ if (probe.hostname === "file") return "not-needed";
1133
+ const hostOf = (raw) => {
1134
+ const m = /^dsh-resource:\/\/([^/?#]*)/i.exec(String(raw || ""));
1135
+ return m && m[1] ? m[1].toLowerCase() : "";
1136
+ };
1137
+ function CompatURL(...args) {
1138
+ const url = new NativeURL(...args);
1139
+ try {
1140
+ if (String(url.protocol) === "dsh-resource:" && !url.hostname) {
1141
+ const proto = NativeURL.prototype;
1142
+ const hostnameDesc = Object.getOwnPropertyDescriptor(proto, "hostname");
1143
+ const hostDesc = Object.getOwnPropertyDescriptor(proto, "host");
1144
+ const hostOfUrl = (u) => {
1145
+ let native;
1146
+ try {
1147
+ native = hostnameDesc && hostnameDesc.get ? hostnameDesc.get.call(u) : u.hostname || "";
1148
+ } catch {
1149
+ native = "";
1150
+ }
1151
+ if (native) return native;
1152
+ let href;
1153
+ try {
1154
+ href = String(u.href);
1155
+ } catch {
1156
+ href = "";
1157
+ }
1158
+ return hostOf(href);
1159
+ };
1160
+ if (hostOfUrl(url)) {
1161
+ Object.defineProperty(url, "hostname", {
1162
+ configurable: true,
1163
+ enumerable: hostnameDesc ? hostnameDesc.enumerable : true,
1164
+ get: () => hostOfUrl(url),
1165
+ set: (v) => {
1166
+ if (hostnameDesc && hostnameDesc.set) hostnameDesc.set.call(url, v);
1167
+ }
1168
+ });
1169
+ if (hostDesc) {
1170
+ Object.defineProperty(url, "host", {
1171
+ configurable: true,
1172
+ enumerable: hostDesc.enumerable,
1173
+ get: () => hostOfUrl(url),
1174
+ set: (v) => {
1175
+ if (hostDesc.set) hostDesc.set.call(url, v);
1176
+ }
1177
+ });
1178
+ }
1179
+ }
1180
+ }
1181
+ } catch {
1182
+ }
1183
+ return url;
1184
+ }
1185
+ CompatURL.prototype = NativeURL.prototype;
1186
+ for (const key of Object.getOwnPropertyNames(NativeURL)) {
1187
+ if (key === "length" || key === "name" || key === "prototype") continue;
1188
+ try {
1189
+ CompatURL[key] = NativeURL[key];
1190
+ } catch {
1191
+ }
1192
+ }
1193
+ win.URL = CompatURL;
1194
+ return "installed";
1195
+ }
1196
+
1104
1197
  // client/index.js
1105
1198
  if (typeof window !== "undefined") {
1106
1199
  if (!window.crypto) {
@@ -1121,6 +1214,9 @@ if (typeof window !== "undefined") {
1121
1214
  };
1122
1215
  }
1123
1216
  }
1217
+ var RESOURCE_URL_COMPAT = installResourceUrlCompat();
1218
+ if (typeof window !== "undefined") window.__dshResourceUrlCompat = RESOURCE_URL_COMPAT;
1219
+ var MOBILE_MAX_WIDTH = 767;
1124
1220
  function isLocalEnvironment() {
1125
1221
  if (typeof window === "undefined") return true;
1126
1222
  const host = window.location.hostname || "";
@@ -2652,10 +2748,29 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
2652
2748
  appSecret: "",
2653
2749
  domain: platform.config.domain ?? "feishu",
2654
2750
  botToken: "",
2655
- proxy: platform.config.proxy ?? ""
2751
+ proxy: platform.config.proxy ?? "",
2752
+ // 会话级配置:留空 = 使用 DSH 默认值
2753
+ agentPreset: platform.config.agentPreset ?? "",
2754
+ cwd: platform.config.cwd ?? "",
2755
+ agentProvider: platform.config.agentProvider ?? "",
2756
+ agentModel: platform.config.agentModel ?? ""
2656
2757
  });
2657
2758
  }
2658
2759
  }, [platform?.config, platformId]);
2760
+ const [presetOptions, setPresetOptions] = React.useState(null);
2761
+ React.useEffect(() => {
2762
+ let cancelled = false;
2763
+ (async () => {
2764
+ try {
2765
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listAgentPresets, {});
2766
+ if (!cancelled && r?.ok) setPresetOptions(r.value ?? { available: false, presets: [] });
2767
+ } catch {
2768
+ }
2769
+ })();
2770
+ return () => {
2771
+ cancelled = true;
2772
+ };
2773
+ }, [rpcCall]);
2659
2774
  const loadInFlightRef = React.useRef(false);
2660
2775
  const seqRef = React.useRef(0);
2661
2776
  const load = React.useCallback(async (quiet = false) => {
@@ -2746,7 +2861,12 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
2746
2861
  digestIntervalSec: num(cfgDraft.digestIntervalSec, platform?.config?.digestIntervalSec ?? 300),
2747
2862
  approvalTimeoutSec: num(cfgDraft.approvalTimeoutSec, platform?.config?.approvalTimeoutSec ?? 600),
2748
2863
  maxMessageChars: num(cfgDraft.maxMessageChars, platform?.config?.maxMessageChars ?? 2e3),
2749
- sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500)
2864
+ sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500),
2865
+ // 会话级配置:空串表示清除该设置、回落 DSH 默认值
2866
+ agentPreset: (cfgDraft.agentPreset ?? "").trim(),
2867
+ cwd: (cfgDraft.cwd ?? "").trim(),
2868
+ agentProvider: (cfgDraft.agentProvider ?? "").trim(),
2869
+ agentModel: (cfgDraft.agentModel ?? "").trim()
2750
2870
  };
2751
2871
  if (platformId === "qq") {
2752
2872
  payload.appId = cfgDraft.appId.trim();
@@ -2761,7 +2881,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
2761
2881
  }
2762
2882
  await act(BRIDGE_ENDPOINTS.platformSetConfig, payload);
2763
2883
  }, [act, cfgDraft, platformId]);
2764
- const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")) || platformId === "feishu" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.appSecret !== (platform.config.appSecret ?? "")) || platformId === "telegram" && (cfgDraft.botToken !== "" || cfgDraft.proxy !== (platform.config.proxy ?? "")));
2884
+ const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || (cfgDraft.agentPreset ?? "") !== (platform.config.agentPreset ?? "") || (cfgDraft.cwd ?? "") !== (platform.config.cwd ?? "") || (cfgDraft.agentProvider ?? "") !== (platform.config.agentProvider ?? "") || (cfgDraft.agentModel ?? "") !== (platform.config.agentModel ?? "") || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")) || platformId === "feishu" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.appSecret !== (platform.config.appSecret ?? "")) || platformId === "telegram" && (cfgDraft.botToken !== "" || cfgDraft.proxy !== (platform.config.proxy ?? "")));
2765
2885
  if (!platform && !err) {
2766
2886
  return React.createElement(
2767
2887
  "div",
@@ -3006,6 +3126,121 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
3006
3126
  }),
3007
3127
  React.createElement("span", null, "\u7FA4\u804A\u81EA\u52A8\u6388\u6743\uFF08\u65B0\u7FA4\u9996\u6B21 @\u673A\u5668\u4EBA \u81EA\u52A8\u52A0\u5165\u767D\u540D\u5355\uFF0C\u9ED8\u8BA4\u5173\u95ED\uFF09")
3008
3128
  ),
3129
+ // 高级设置:会话级配置(工作区 / Agent 预设 / 模型路由)+ 会话节奏参数
3130
+ React.createElement(
3131
+ "div",
3132
+ { style: { marginTop: 12 } },
3133
+ React.createElement("button", {
3134
+ style: s.btnGhost,
3135
+ onClick: () => setShowAdvanced((v) => !v)
3136
+ }, showAdvanced ? "\u6536\u8D77\u9AD8\u7EA7\u8BBE\u7F6E" : "\u2699\uFE0F \u9AD8\u7EA7\u8BBE\u7F6E"),
3137
+ showAdvanced && React.createElement(
3138
+ "div",
3139
+ { style: { ...s.block, marginTop: 8, display: "flex", flexDirection: "column", gap: 10 } },
3140
+ React.createElement(
3141
+ "div",
3142
+ { style: { ...s.muted, fontSize: 12, lineHeight: 1.6 } },
3143
+ "\u4EE5\u4E0B\u8BBE\u7F6E\u6309\u5E73\u53F0\u4FDD\u5B58\uFF0C\u53EA\u5F71\u54CD\u8BE5\u5E73\u53F0\u901A\u8FC7 Bot \u65B0\u5EFA\u7684\u4F1A\u8BDD\uFF08\u7559\u7A7A = \u4F7F\u7528 DSH \u9ED8\u8BA4\u503C\uFF09\u3002"
3144
+ ),
3145
+ // Agent 预设:决定远程会话挂载哪些工具/技能
3146
+ React.createElement(
3147
+ "div",
3148
+ null,
3149
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "Agent \u9884\u8BBE \u2014 \u51B3\u5B9A\u8FDC\u7A0B\u4F1A\u8BDD\u53EF\u7528\u7684\u5DE5\u5177\u4E0E\u6280\u80FD\uFF0C\u7559\u7A7A\u7528 DSH \u9ED8\u8BA4\u9884\u8BBE"),
3150
+ React.createElement("input", {
3151
+ style: { ...s.input, width: "100%" },
3152
+ list: "dsh-bridge-agent-presets",
3153
+ placeholder: presetOptions?.default ? `\u7559\u7A7A = DSH \u9ED8\u8BA4\uFF08${presetOptions.default}\uFF09` : "\u7559\u7A7A = DSH \u9ED8\u8BA4\u9884\u8BBE",
3154
+ value: cfgDraft?.agentPreset ?? "",
3155
+ onChange: (e) => setCfgDraft((d) => ({ ...d, agentPreset: e.target.value }))
3156
+ }),
3157
+ React.createElement(
3158
+ "datalist",
3159
+ { id: "dsh-bridge-agent-presets" },
3160
+ (presetOptions?.presets ?? []).map((p) => React.createElement("option", { key: p.id, value: p.id }, p.name && p.name !== p.id ? p.name : null))
3161
+ ),
3162
+ React.createElement(
3163
+ "div",
3164
+ { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
3165
+ presetOptions == null ? "\u6B63\u5728\u8BFB\u53D6\u672C\u673A\u53EF\u7528\u9884\u8BBE\u2026" : presetOptions.available ? `\u672C\u673A\u53EF\u7528\uFF1A${(presetOptions.presets ?? []).map((p) => p.id).join(" / ") || "\uFF08\u65E0\uFF09"}` : "\u5F53\u524D DSH \u7248\u672C\u672A\u63D0\u4F9B\u9884\u8BBE\u5217\u8868\uFF0C\u8BF7\u624B\u52A8\u586B\u5199\u9884\u8BBE\u540D"
3166
+ )
3167
+ ),
3168
+ // 工作区
3169
+ React.createElement(
3170
+ "div",
3171
+ null,
3172
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5DE5\u4F5C\u533A\u76EE\u5F55 \u2014 \u8BE5\u5E73\u53F0 /new \u65B0\u5EFA\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u76EE\u5F55\uFF0C\u7559\u7A7A\u7528\u9996\u4E2A\u5DF2\u6CE8\u518C\u5DE5\u4F5C\u533A"),
3173
+ React.createElement("input", {
3174
+ style: { ...s.input, width: "100%" },
3175
+ placeholder: "\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u4F8B\u5982 /home/me/project \u6216 D:\\project",
3176
+ value: cfgDraft?.cwd ?? "",
3177
+ onChange: (e) => setCfgDraft((d) => ({ ...d, cwd: e.target.value }))
3178
+ })
3179
+ ),
3180
+ // 模型路由
3181
+ React.createElement(
3182
+ "div",
3183
+ { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 8 } },
3184
+ React.createElement(
3185
+ "div",
3186
+ null,
3187
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u6A21\u578B\u63D0\u4F9B\u65B9\uFF08\u53EF\u9009\uFF09"),
3188
+ React.createElement("input", {
3189
+ style: { ...s.input, width: "100%" },
3190
+ placeholder: "\u7559\u7A7A = DSH \u9ED8\u8BA4",
3191
+ value: cfgDraft?.agentProvider ?? "",
3192
+ onChange: (e) => setCfgDraft((d) => ({ ...d, agentProvider: e.target.value }))
3193
+ })
3194
+ ),
3195
+ React.createElement(
3196
+ "div",
3197
+ null,
3198
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u6A21\u578B\uFF08\u53EF\u9009\uFF09"),
3199
+ React.createElement("input", {
3200
+ style: { ...s.input, width: "100%" },
3201
+ placeholder: "\u7559\u7A7A = DSH \u9ED8\u8BA4",
3202
+ value: cfgDraft?.agentModel ?? "",
3203
+ onChange: (e) => setCfgDraft((d) => ({ ...d, agentModel: e.target.value }))
3204
+ })
3205
+ )
3206
+ ),
3207
+ // 会话节奏参数
3208
+ React.createElement(
3209
+ "div",
3210
+ { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 8 } },
3211
+ [
3212
+ ["digestIntervalSec", "\u8FDB\u5EA6\u6458\u8981\u95F4\u9694\uFF08\u79D2\uFF09"],
3213
+ ["approvalTimeoutSec", "\u5BA1\u6279\u8D85\u65F6\uFF08\u79D2\uFF09"],
3214
+ ["maxMessageChars", "\u5355\u6761\u6D88\u606F\u4E0A\u9650\uFF08\u5B57\u7B26\uFF09"],
3215
+ ["sendChunkDelayMs", "\u5206\u5757\u53D1\u9001\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09"]
3216
+ ].map(([key, label]) => React.createElement(
3217
+ "div",
3218
+ { key },
3219
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, label),
3220
+ React.createElement("input", {
3221
+ style: { ...s.input, width: "100%" },
3222
+ inputMode: "numeric",
3223
+ value: cfgDraft?.[key] ?? "",
3224
+ onChange: (e) => setCfgDraft((d) => ({ ...d, [key]: e.target.value }))
3225
+ })
3226
+ ))
3227
+ ),
3228
+ React.createElement(
3229
+ "div",
3230
+ { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
3231
+ React.createElement("button", {
3232
+ style: { ...s.btnPri, opacity: busy || !cfgDirty ? 0.5 : 1 },
3233
+ onClick: saveConfig,
3234
+ disabled: busy || !cfgDirty
3235
+ }, busy ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u9AD8\u7EA7\u8BBE\u7F6E"),
3236
+ React.createElement("button", {
3237
+ style: { ...s.btnGhost },
3238
+ onClick: resetDefaults,
3239
+ disabled: busy
3240
+ }, "\u6062\u590D\u63A8\u8350\u8282\u594F\u53C2\u6570")
3241
+ )
3242
+ )
3243
+ ),
3009
3244
  // 飞书 / Telegram 扫码直达对话引导卡片
3010
3245
  (platformId === "feishu" || platformId === "telegram") && platform.botQr && React.createElement(
3011
3246
  "div",
@@ -3558,7 +3793,12 @@ function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
3558
3793
  setRestarting(true);
3559
3794
  setStatus({ phase: "restarting", text: T.restarting });
3560
3795
  try {
3561
- await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
3796
+ const r = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
3797
+ if (r && r.ok === false) {
3798
+ setStatus({ phase: "timeout", text: `${T.timeout}\uFF1A${r.error || "\u672A\u77E5\u539F\u56E0"}` });
3799
+ setRestarting(false);
3800
+ return;
3801
+ }
3562
3802
  } catch {
3563
3803
  }
3564
3804
  setStatus({ phase: "reconnecting", text: T.reconnecting });
@@ -5123,7 +5363,7 @@ function setupMobileExperience(rpcCall, ctx) {
5123
5363
  if (document.body.classList.contains("dsh-drawer-open")) {
5124
5364
  document.body.classList.remove("dsh-drawer-open");
5125
5365
  }
5126
- if (typeof window !== "undefined" && window.innerWidth <= 768) {
5366
+ if (typeof window !== "undefined" && window.innerWidth <= MOBILE_MAX_WIDTH) {
5127
5367
  document.body.classList.remove("dsh-workbench-open");
5128
5368
  const openPanels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
5129
5369
  openPanels.forEach((p) => p.classList.add("nArs4W_panelHidden"));
@@ -5132,7 +5372,7 @@ function setupMobileExperience(rpcCall, ctx) {
5132
5372
  }
5133
5373
  const ensurePanelCloseButton = () => {
5134
5374
  if (typeof window === "undefined") return;
5135
- if (window.innerWidth > 768) {
5375
+ if (window.innerWidth > MOBILE_MAX_WIDTH) {
5136
5376
  document.querySelectorAll(".dsh-mobile-panel-close-btn").forEach((btn) => btn.remove());
5137
5377
  return;
5138
5378
  }
@@ -5158,14 +5398,14 @@ function setupMobileExperience(rpcCall, ctx) {
5158
5398
  panelObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
5159
5399
  window.addEventListener("resize", ensurePanelCloseButton);
5160
5400
  document.addEventListener("click", (e) => {
5161
- if (typeof window === "undefined" || window.innerWidth > 768) return;
5401
+ if (typeof window === "undefined" || window.innerWidth > MOBILE_MAX_WIDTH) return;
5162
5402
  const trigger = e.target.closest('button[aria-label*="\u9762\u677F"], button[aria-label*="\u5DE5\u4F5C\u533A"], div[class*="toggleCluster"] button, button[class*="subagent"], div[class*="headerActions"] button, div[class*="titleRow"] button');
5163
5403
  if (trigger && !trigger.classList.contains("dsh-mobile-panel-close-btn") && !trigger.classList.contains("dsh-header-menu-btn") && !trigger.classList.contains("dsh-header-new-btn")) {
5164
5404
  document.body.classList.add("dsh-workbench-open");
5165
5405
  }
5166
5406
  }, true);
5167
5407
  document.addEventListener("click", (e) => {
5168
- if (typeof window === "undefined" || window.innerWidth > 768) return;
5408
+ if (typeof window === "undefined" || window.innerWidth > MOBILE_MAX_WIDTH) return;
5169
5409
  const toggle = e.target.closest('button[aria-label*="\u6536\u8D77\u4FA7\u8FB9\u680F"], button[title*="\u6536\u8D77\u4FA7\u8FB9\u680F"]');
5170
5410
  if (toggle) {
5171
5411
  document.body.classList.remove("dsh-drawer-open");
@@ -6007,7 +6247,7 @@ function setupIosKeyboardAdapter() {
6007
6247
  }
6008
6248
  function setupComposerCollapse() {
6009
6249
  if (typeof window === "undefined" || typeof document === "undefined") return;
6010
- if (window.innerWidth > 768) return;
6250
+ if (window.innerWidth > MOBILE_MAX_WIDTH) return;
6011
6251
  const LS_KEY = "dsh-composer-fold";
6012
6252
  let bar = null;
6013
6253
  let busy = false;
package/client/index.js CHANGED
@@ -29,6 +29,17 @@ if (typeof window !== 'undefined') {
29
29
 
30
30
  import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from '../lib/bridge-rpc-constants.js';
31
31
 
32
+ import { installResourceUrlCompat } from './resource-url-compat.js';
33
+
34
+ const RESOURCE_URL_COMPAT = installResourceUrlCompat();
35
+ if (typeof window !== 'undefined') window.__dshResourceUrlCompat = RESOURCE_URL_COMPAT;
36
+
37
+ // 移动端断点上限:宿主用 viewportWidth < 768 判定右侧栏自动全屏
38
+ // (@deepseek-ai/dsh-client-ui-sidebar-right 的 autoFullscreen),
39
+ // 故桥的移动端分支一律取「<= 767」,与 mobile-styles.js 的 @media (max-width: 767px) 对齐,
40
+ // 避免 768px 单点上"桥渲染顶栏、宿主却按桌面(push)布局"的错位。
41
+ const MOBILE_MAX_WIDTH = 767;
42
+
32
43
  function isLocalEnvironment() {
33
44
  if (typeof window === 'undefined') return true;
34
45
  const host = window.location.hostname || '';
@@ -1396,10 +1407,28 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
1396
1407
  domain: platform.config.domain ?? 'feishu',
1397
1408
  botToken: '',
1398
1409
  proxy: platform.config.proxy ?? '',
1410
+ // 会话级配置:留空 = 使用 DSH 默认值
1411
+ agentPreset: platform.config.agentPreset ?? '',
1412
+ cwd: platform.config.cwd ?? '',
1413
+ agentProvider: platform.config.agentProvider ?? '',
1414
+ agentModel: platform.config.agentModel ?? '',
1399
1415
  });
1400
1416
  }
1401
1417
  }, [platform?.config, platformId]);
1402
1418
 
1419
+ // 本机可用的 DSH agent preset(旧版本 DSH 返回 available:false → 退化为手填)
1420
+ const [presetOptions, setPresetOptions] = React.useState(null);
1421
+ React.useEffect(() => {
1422
+ let cancelled = false;
1423
+ (async () => {
1424
+ try {
1425
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listAgentPresets, {});
1426
+ if (!cancelled && r?.ok) setPresetOptions(r.value ?? { available: false, presets: [] });
1427
+ } catch { /* 读取失败保持手填 */ }
1428
+ })();
1429
+ return () => { cancelled = true; };
1430
+ }, [rpcCall]);
1431
+
1403
1432
  const loadInFlightRef = React.useRef(false);
1404
1433
  const seqRef = React.useRef(0);
1405
1434
  const load = React.useCallback(async (quiet = false) => {
@@ -1507,6 +1536,11 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
1507
1536
  approvalTimeoutSec: num(cfgDraft.approvalTimeoutSec, platform?.config?.approvalTimeoutSec ?? 600),
1508
1537
  maxMessageChars: num(cfgDraft.maxMessageChars, platform?.config?.maxMessageChars ?? 2000),
1509
1538
  sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500),
1539
+ // 会话级配置:空串表示清除该设置、回落 DSH 默认值
1540
+ agentPreset: (cfgDraft.agentPreset ?? '').trim(),
1541
+ cwd: (cfgDraft.cwd ?? '').trim(),
1542
+ agentProvider: (cfgDraft.agentProvider ?? '').trim(),
1543
+ agentModel: (cfgDraft.agentModel ?? '').trim(),
1510
1544
  };
1511
1545
  // QQ / 飞书 / Telegram 平台额外携带凭证
1512
1546
  if (platformId === 'qq') {
@@ -1527,6 +1561,10 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
1527
1561
  Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec ||
1528
1562
  Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars ||
1529
1563
  Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs ||
1564
+ (cfgDraft.agentPreset ?? '') !== (platform.config.agentPreset ?? '') ||
1565
+ (cfgDraft.cwd ?? '') !== (platform.config.cwd ?? '') ||
1566
+ (cfgDraft.agentProvider ?? '') !== (platform.config.agentProvider ?? '') ||
1567
+ (cfgDraft.agentModel ?? '') !== (platform.config.agentModel ?? '') ||
1530
1568
  (platformId === 'qq' && (
1531
1569
  cfgDraft.appId !== (platform.config.appId ?? '') ||
1532
1570
  cfgDraft.clientSecret !== (platform.config.clientSecret ?? '')
@@ -1747,6 +1785,99 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
1747
1785
  }),
1748
1786
  React.createElement('span', null, '群聊自动授权(新群首次 @机器人 自动加入白名单,默认关闭)'),
1749
1787
  ),
1788
+ // 高级设置:会话级配置(工作区 / Agent 预设 / 模型路由)+ 会话节奏参数
1789
+ React.createElement('div', { style: { marginTop: 12 } },
1790
+ React.createElement('button', {
1791
+ style: s.btnGhost,
1792
+ onClick: () => setShowAdvanced(v => !v),
1793
+ }, showAdvanced ? '收起高级设置' : '⚙️ 高级设置'),
1794
+ showAdvanced && React.createElement('div', { style: { ...s.block, marginTop: 8, display: 'flex', flexDirection: 'column', gap: 10 } },
1795
+ React.createElement('div', { style: { ...s.muted, fontSize: 12, lineHeight: 1.6 } },
1796
+ '以下设置按平台保存,只影响该平台通过 Bot 新建的会话(留空 = 使用 DSH 默认值)。'),
1797
+ // Agent 预设:决定远程会话挂载哪些工具/技能
1798
+ React.createElement('div', null,
1799
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'Agent 预设 — 决定远程会话可用的工具与技能,留空用 DSH 默认预设'),
1800
+ React.createElement('input', {
1801
+ style: { ...s.input, width: '100%' },
1802
+ list: 'dsh-bridge-agent-presets',
1803
+ placeholder: presetOptions?.default ? `留空 = DSH 默认(${presetOptions.default})` : '留空 = DSH 默认预设',
1804
+ value: cfgDraft?.agentPreset ?? '',
1805
+ onChange: (e) => setCfgDraft(d => ({ ...d, agentPreset: e.target.value })),
1806
+ }),
1807
+ React.createElement('datalist', { id: 'dsh-bridge-agent-presets' },
1808
+ (presetOptions?.presets ?? []).map((p) =>
1809
+ React.createElement('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? p.name : null)),
1810
+ ),
1811
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
1812
+ presetOptions == null
1813
+ ? '正在读取本机可用预设…'
1814
+ : presetOptions.available
1815
+ ? `本机可用:${(presetOptions.presets ?? []).map((p) => p.id).join(' / ') || '(无)'}`
1816
+ : '当前 DSH 版本未提供预设列表,请手动填写预设名'),
1817
+ ),
1818
+ // 工作区
1819
+ React.createElement('div', null,
1820
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '工作区目录 — 该平台 /new 新建会话的默认目录,留空用首个已注册工作区'),
1821
+ React.createElement('input', {
1822
+ style: { ...s.input, width: '100%' },
1823
+ placeholder: '绝对路径,例如 /home/me/project 或 D:\\project',
1824
+ value: cfgDraft?.cwd ?? '',
1825
+ onChange: (e) => setCfgDraft(d => ({ ...d, cwd: e.target.value })),
1826
+ }),
1827
+ ),
1828
+ // 模型路由
1829
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 8 } },
1830
+ React.createElement('div', null,
1831
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '模型提供方(可选)'),
1832
+ React.createElement('input', {
1833
+ style: { ...s.input, width: '100%' },
1834
+ placeholder: '留空 = DSH 默认',
1835
+ value: cfgDraft?.agentProvider ?? '',
1836
+ onChange: (e) => setCfgDraft(d => ({ ...d, agentProvider: e.target.value })),
1837
+ }),
1838
+ ),
1839
+ React.createElement('div', null,
1840
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '模型(可选)'),
1841
+ React.createElement('input', {
1842
+ style: { ...s.input, width: '100%' },
1843
+ placeholder: '留空 = DSH 默认',
1844
+ value: cfgDraft?.agentModel ?? '',
1845
+ onChange: (e) => setCfgDraft(d => ({ ...d, agentModel: e.target.value })),
1846
+ }),
1847
+ ),
1848
+ ),
1849
+ // 会话节奏参数
1850
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 8 } },
1851
+ [
1852
+ ['digestIntervalSec', '进度摘要间隔(秒)'],
1853
+ ['approvalTimeoutSec', '审批超时(秒)'],
1854
+ ['maxMessageChars', '单条消息上限(字符)'],
1855
+ ['sendChunkDelayMs', '分块发送间隔(毫秒)'],
1856
+ ].map(([key, label]) =>
1857
+ React.createElement('div', { key },
1858
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, label),
1859
+ React.createElement('input', {
1860
+ style: { ...s.input, width: '100%' },
1861
+ inputMode: 'numeric',
1862
+ value: cfgDraft?.[key] ?? '',
1863
+ onChange: (e) => setCfgDraft(d => ({ ...d, [key]: e.target.value })),
1864
+ }),
1865
+ )),
1866
+ ),
1867
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1868
+ React.createElement('button', {
1869
+ style: { ...s.btnPri, opacity: (busy || !cfgDirty) ? 0.5 : 1 },
1870
+ onClick: saveConfig,
1871
+ disabled: busy || !cfgDirty,
1872
+ }, busy ? '保存中…' : '保存高级设置'),
1873
+ React.createElement('button', {
1874
+ style: { ...s.btnGhost },
1875
+ onClick: resetDefaults,
1876
+ disabled: busy,
1877
+ }, '恢复推荐节奏参数'),
1878
+ ),
1879
+ ),
1880
+ ),
1750
1881
  // 飞书 / Telegram 扫码直达对话引导卡片
1751
1882
  (platformId === 'feishu' || platformId === 'telegram') && platform.botQr && React.createElement('div', {
1752
1883
  style: {
@@ -2207,9 +2338,16 @@ function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
2207
2338
  setRestarting(true);
2208
2339
  setStatus({ phase: 'restarting', text: T.restarting });
2209
2340
  try {
2210
- await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
2341
+ const r = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
2342
+ // 宿主已明确告知"没能安排重启"(例如 systemd 调用失败、助手都派生不出来):
2343
+ // 这时绝不能再假装"正在重连"——直接报错并停下,否则用户只会看到永远转圈。
2344
+ if (r && r.ok === false) {
2345
+ setStatus({ phase: 'timeout', text: `${T.timeout}:${r.error || '未知原因'}` });
2346
+ setRestarting(false);
2347
+ return;
2348
+ }
2211
2349
  } catch {
2212
- // 服务可能瞬间关闭导致连接断开,忽略
2350
+ // 服务可能瞬间关闭导致连接断开,忽略;下面的轮询会判断是否真的起来了
2213
2351
  }
2214
2352
 
2215
2353
  setStatus({ phase: 'reconnecting', text: T.reconnecting });
@@ -3705,7 +3843,7 @@ function setupMobileExperience(rpcCall, ctx) {
3705
3843
  document.body.classList.remove('dsh-drawer-open');
3706
3844
  }
3707
3845
  // 仅在移动端切换会话时自动收起右侧面板回到对话(PC端绝不干扰)
3708
- if (typeof window !== 'undefined' && window.innerWidth <= 768) {
3846
+ if (typeof window !== 'undefined' && window.innerWidth <= MOBILE_MAX_WIDTH) {
3709
3847
  document.body.classList.remove('dsh-workbench-open');
3710
3848
  const openPanels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
3711
3849
  openPanels.forEach((p) => p.classList.add('nArs4W_panelHidden'));
@@ -3713,10 +3851,10 @@ function setupMobileExperience(rpcCall, ctx) {
3713
3851
  });
3714
3852
  }
3715
3853
 
3716
- // 1.1 移动端右侧边栏 / Workbench 面板管理:仅在移动端(<= 768px)挂载“返回对话”收起按钮,PC端保持纯净
3854
+ // 1.1 移动端右侧边栏 / Workbench 面板管理:仅在移动端(<= 767px)挂载“返回对话”收起按钮,PC端保持纯净
3717
3855
  const ensurePanelCloseButton = () => {
3718
3856
  if (typeof window === 'undefined') return;
3719
- if (window.innerWidth > 768) {
3857
+ if (window.innerWidth > MOBILE_MAX_WIDTH) {
3720
3858
  document.querySelectorAll('.dsh-mobile-panel-close-btn').forEach(btn => btn.remove());
3721
3859
  return;
3722
3860
  }
@@ -3745,7 +3883,7 @@ function setupMobileExperience(rpcCall, ctx) {
3745
3883
 
3746
3884
  // 移动端点击面板/工作区触发按钮时,自动激活 dsh-workbench-open
3747
3885
  document.addEventListener('click', (e) => {
3748
- if (typeof window === 'undefined' || window.innerWidth > 768) return;
3886
+ if (typeof window === 'undefined' || window.innerWidth > MOBILE_MAX_WIDTH) return;
3749
3887
  const trigger = e.target.closest('button[aria-label*="面板"], button[aria-label*="工作区"], div[class*="toggleCluster"] button, button[class*="subagent"], div[class*="headerActions"] button, div[class*="titleRow"] button');
3750
3888
  if (trigger && !trigger.classList.contains('dsh-mobile-panel-close-btn') && !trigger.classList.contains('dsh-header-menu-btn') && !trigger.classList.contains('dsh-header-new-btn')) {
3751
3889
  document.body.classList.add('dsh-workbench-open');
@@ -3754,7 +3892,7 @@ function setupMobileExperience(rpcCall, ctx) {
3754
3892
 
3755
3893
  // 移动端点击 DSH 自带的收起侧边栏图标时,自动收起抽屉
3756
3894
  document.addEventListener('click', (e) => {
3757
- if (typeof window === 'undefined' || window.innerWidth > 768) return;
3895
+ if (typeof window === 'undefined' || window.innerWidth > MOBILE_MAX_WIDTH) return;
3758
3896
  const toggle = e.target.closest('button[aria-label*="收起侧边栏"], button[title*="收起侧边栏"]');
3759
3897
  if (toggle) {
3760
3898
  document.body.classList.remove('dsh-drawer-open');
@@ -4764,7 +4902,7 @@ function setupIosKeyboardAdapter() {
4764
4902
  // 折叠态记忆到 localStorage(dsh-composer-fold),跨会话保持用户偏好。
4765
4903
  function setupComposerCollapse() {
4766
4904
  if (typeof window === 'undefined' || typeof document === 'undefined') return;
4767
- if (window.innerWidth > 768) return; // 仅移动端
4905
+ if (window.innerWidth > MOBILE_MAX_WIDTH) return; // 仅移动端
4768
4906
  const LS_KEY = 'dsh-composer-fold';
4769
4907
 
4770
4908
  let bar = null; // 折叠态底部"点击输入"细条