dsh-vscode-mode 0.1.52 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +71 -0
  2. package/assets/shell/dsh-open.cs +182 -0
  3. package/assets/shell/dsh-open.ps1 +52 -0
  4. package/assets/shell/dsh-open.sh +70 -0
  5. package/assets/shell/dsh-whale.ico +0 -0
  6. package/lib/client.js +1038 -13
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +1130 -7
  9. package/lib/index.js.map +1 -1
  10. package/package.json +2 -1
  11. package/src/client/chooseDialog.ts +70 -0
  12. package/src/client/compat.ts +31 -0
  13. package/src/client/externalOpen.ts +130 -0
  14. package/src/client/imagePreview.ts +45 -0
  15. package/src/client/index.ts +50 -8
  16. package/src/client/openFlow.ts +330 -0
  17. package/src/client/remoteOpenRouter.ts +78 -0
  18. package/src/client/styles/editor.css +6 -0
  19. package/src/client/ui/EditorView.ts +116 -1
  20. package/src/client/ui/IntegrationSettings.ts +243 -0
  21. package/src/client/ui/McpSettings.ts +2 -0
  22. package/src/compat.ts +1 -1
  23. package/src/dshVersion.ts +1 -0
  24. package/src/externalHandoff.ts +70 -0
  25. package/src/fileOpenSettings.ts +10 -2
  26. package/src/index.ts +3 -0
  27. package/src/integrate.ts +576 -0
  28. package/src/rpc.ts +97 -3
  29. package/src/shared/externalOpen.ts +62 -0
  30. package/src/shared/integration.ts +73 -0
  31. package/src/shared/rpc.ts +58 -5
  32. package/src/unityBridge.ts +234 -0
  33. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +267 -0
  34. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs.meta +11 -0
  35. package/unity/com.dsh.editor/Editor/com.dsh.editor.asmdef +15 -0
  36. package/unity/com.dsh.editor/Editor/com.dsh.editor.asmdef.meta +7 -0
  37. package/unity/com.dsh.editor/Editor.meta +8 -0
  38. package/unity/com.dsh.editor/README.md +33 -0
  39. package/unity/com.dsh.editor/README.md.meta +7 -0
  40. package/unity/com.dsh.editor/package.json +7 -0
  41. package/unity/com.dsh.editor/package.json.meta +7 -0
package/README.md CHANGED
@@ -45,6 +45,14 @@
45
45
  `compaction-basic`/`tool-result-pruner` 阈值写入 profile `cordis.patch.yml`(带备份与撤销,重启生效);
46
46
  ④ 侧车摘要接口指引。对话 header 另有**会话体积指示器**(`edrv-perf-size`,≥1MB 显示、超 2MB 琥珀、超 8MB 红),
47
47
  引导"一次任务一个短会话"。
48
+ - **系统集成**(v0.1.53,设置 → VSCodeMode → 通用「系统集成」卡片):
49
+ ① **文件管理器右键菜单**——一键把「在 DSH 文件编辑中打开」注册进系统(Windows 资源管理器三类入口 / Linux
50
+ Nautilus+Dolphin / macOS Automator 配方),插件卸载自动清理、更新自动恢复;点击经 launcher 打开默认浏览器深链
51
+ `?edrvOpen=1&edrvPaths=…`,编辑器直接打开所选文件;DSH 未运行时弹提示。
52
+ ② **Unity 外部脚本编辑器**——内置 UPM 包 `com.dsh.editor`(参照 com.unity.ide.traeCN 机制实现
53
+ `IExternalEditor` 虚拟安装,无需本地 exe),设置页**一键安装/更新**为 Unity 内嵌包
54
+ (复制到 `<项目>/Packages/com.dsh.editor`,自动发现,不改 manifest.json),Unity Preferences →
55
+ External Tools 下拉选「DSH 文件编辑」后,双击脚本/Console 报错跳转即在浏览器打开对应文件与行列。
48
56
 
49
57
  ## 界面截图
50
58
 
@@ -212,6 +220,69 @@ V8 展开约 10×)。会话越多越大,启动内存越高,可能冲爆堆
212
220
  > 治理原则:存量靠「移出到归档」;增量靠「压缩调优 + 短会话习惯 + 体积指示器」;packChunks(chunk 打包存储)为
213
221
  > DSH 默认开启,无需再配置。
214
222
 
223
+ ## 系统集成(Windows 右键菜单 + Unity 外部脚本编辑器)
224
+
225
+ 设置 → VSCodeMode → 通用 →「系统集成」卡片。深链契约(launcher / Unity 包 / client 三端共用):
226
+ `http://127.0.0.1:3080/?edrvOpen=1&edrvPaths=<enc1>[,<enc2>…][&edrvLine=N][&edrvColumn=M]`
227
+ (每段路径独立 `encodeURIComponent`,逗号连接;client 解析后按下方「打开规则」路由,处理完剥离参数防刷新重开,
228
+ 跨源 referrer 守卫防外部网页诱导)。
229
+
230
+ ### 打开规则(智能路由)
231
+
232
+ 按首路径类型分派(其余路径:文件进编辑器、文件夹补引用):
233
+
234
+ | 场景 | 行为 |
235
+ |---|---|
236
+ | **文件夹** 且位于某已注册工作区内 | 页面内弹窗二选一(同「添加 MCP」模态):**使用最近的工作区** = 跳转该工作区 + 新增对话 + 添加文件夹引用 + 打开文件编辑页;**新建工作区** = 以该文件夹注册新工作区 + 新增对话 + 引用 + 编辑页;取消则中止 |
237
+ | **文件夹** 不在任何已注册工作区 | 不弹窗,直接以**该文件夹本身**为根注册新工作区 + 新增对话 + 文件夹引用 + 编辑页 |
238
+ | **文件** 且位于某已注册工作区内(工作区亲和) | 该工作区**有会话** → 打开其最近一次对话 + 编辑器展开该文件(行列透传);**无会话** → 在该工作区**新建对话** + 添加文件引用 + 编辑器展开 |
239
+ | **文件** 不在任何已注册工作区 | 打开最近一次对话 + 文件编辑器展开该文件 |
240
+ | **文件** 且无任何工作区/对话 | 以文件父目录注册工作区 + 新增对话 + 添加文件引用到对话 + 编辑器打开该文件 |
241
+
242
+ **页面复用**:launcher 先向 host 投递待打开请求(`edrv.external.handoff`)——已打开的 DSH 页面
243
+ (3s 移交轮询)就地执行打开规则,**不重复开新页**;2s 未领取(页面刚关/后台节流)或无活跃页面时
244
+ 回退打开新页(URL 深链)。
245
+
246
+ 时序:等待会话/工作区列表就绪(≤15s);`sessions.create/workspaces.create` 为 DSH 官方服务方法
247
+ (与 New Session 同路径);引用插入轮询输入门面就绪(忙态自动降级纯文本)。
248
+
249
+ ### 文件管理器右键菜单(Windows / Linux / macOS)
250
+
251
+ - **Windows**:launcher 源 `assets/shell/dsh-open.cs` 复制到 `~/.dsh/dsh-vscode-mode/shell/` 并用 .NET Framework 4.x
252
+ `csc.exe` 编译为 `dsh-open.exe`(WinExe 无闪窗;csc 缺失自动降级 `dsh-open.ps1`);随后写三类 HKCU 键
253
+ `HKCU\Software\Classes\{\*,Directory,Directory\Background}\shell\DSHEditor`(显示名 + Icon(DSH 小鲸鱼
254
+ `dsh-whale.ico`)+ command,首次注册前 `reg export` 备份到安装目录)。全部 reg/csc 调用走 host
255
+ `subprocess.spawn` argv 数组(stdio `inherit`,受管环境禁管道),无 shell 插值。
256
+ - **Linux**(纯文件写入,无需管理员):GNOME Files(Nautilus)右键脚本
257
+ `~/.local/share/nautilus/scripts/在 DSH 文件编辑中打开`(`NAUTILUS_SCRIPT_SELECTED_FILE_PATHS` 多选)+
258
+ KDE Dolphin 服务菜单 `~/.local/share/kio/servicemenus/dsh-editor.desktop`(兼容旧 `kservices5/ServiceMenus/`,
259
+ `%F` 多选);其他文件管理器暂未支持。
260
+ - **macOS**:Finder 菜单不做自动生成——设置页「复制 Automator 配方」按步骤创建快速操作(约 1 分钟,脚本指向
261
+ `~/.dsh/dsh-vscode-mode/shell/dsh-open.sh`),创建后面板自动检测显示 ✓;「移除注册」仅删除引用本插件
262
+ launcher 的 workflow。
263
+ - **行为**:launcher 读同目录 `dsh-open.ini`(`base=<深链基址>`,注册时按设置写入)→ 探测端口(Windows TcpClient /
264
+ POSIX curl,1s)→ 未运行弹提示(MessageBox / osascript / notify-send / zenity)→ 把路径编码合并为一个 URL
265
+ 交给默认浏览器(POSIX 侧 `LC_ALL=C` 逐字节 percent-encode,中文/空格/任意字符安全)。改「DSH 服务地址」后点「注册」刷新 ini。
266
+ - **生命周期**:注册成功写 marker(`shell/registered.json`)→ 插件卸载/reload 自动清理注册痕迹,重启/更新后自动恢复
267
+ (开发态反复 reload 不丢注册);点「移除注册」删除 marker,此后不再自动恢复;强杀进程跳过清理时残留键会被
268
+ 下一次启动的幂等重写与「移除注册」兜底。
269
+ - RPC:`edrv.integration.status / register / unregister`。
270
+
271
+ ### Unity 一键安装 / 更新(内嵌包)
272
+
273
+ - 包源随插件分发于 `unity/com.dsh.editor/`(UPM 包:`package.json` + `Editor/DshCodeEditor.cs` +
274
+ `Editor/com.dsh.editor.asmdef`,`"unity": "2019.4"` 基线;机制参照 com.unity.ide.traeCN 的
275
+ `IExternalCodeEditor`:虚拟安装 `dsh-editor://vscode-mode`,`OpenProject` → `Application.OpenURL(深链)`,
276
+ 不需要本地可执行文件,不生成 csproj)。
277
+ - **一键安装/更新**:设置页登记 Unity 项目根(校验 `Assets` + `ProjectSettings` 特征)后,点「安装/更新」把包源
278
+ 整目录复制为 `<项目>/Packages/com.dsh.editor`(Unity **内嵌包**自动发现,无需改 manifest.json);更新 = 整目录
279
+ 替换,列表显示已装版本与「可更新」徽标;目标已存在且 `package.json` name 不是 `com.dsh.editor` 时拒绝覆盖。
280
+ Unity 已打开时切回窗口自动刷新生效。卸载 = 删除该目录。
281
+ - 手动兜底:Package Manager → Add package from disk 选 `unity/com.dsh.editor`(面板「复制包源路径」)。
282
+ - RPC:`edrv.unity.list / add / remove / install`;登记清单存 `~/.dsh/dsh-vscode-mode/unity-projects.json`。
283
+ - 安全说明:深链可在编辑器中查看任意绝对路径文件(与用户手动打开等价);保存仍受会话沙箱 `policyOf` 约束,
284
+ 工作区外保存被拒是预期行为。
285
+
215
286
  ## 开发 / 卸载(超级模组注入器,开发期可选)
216
287
 
217
288
  - 热装配:`dev_install_package {dir: packages/dsh-edit-review}`;更新:`dev_reload_package {packageName: "dsh-vscode-mode"}`
@@ -0,0 +1,182 @@
1
+ // dsh-open.cs — DSH 文件编辑 shell launcher(Windows 资源管理器右键菜单入口)。
2
+ // 由 dsh-vscode-mode 设置页一键注册时经 csc 编译为 WinExe(无控制台闪窗)。
3
+ // 流程:读 dsh-open.ini base → TcpClient 探测(1s 超时,未运行弹 MessageBox)
4
+ // → 投递待打开请求到 host(edrv.external.handoff):有已打开 DSH 页面时由其就地
5
+ // 执行打开规则(不重复开页);2s 未领取(页面刚关/后台节流)则取回请求并回退打开新页。
6
+ // 深链契约见插件 src/shared/externalOpen.ts(edrvOpen/edrvPaths/edrvLine/edrvColumn)。
7
+ // 作者 ddj 2026-09-08
8
+ using System;
9
+ using System.Collections.Generic;
10
+ using System.Diagnostics;
11
+ using System.IO;
12
+ using System.Net;
13
+ using System.Net.Sockets;
14
+ using System.Text;
15
+ using System.Threading;
16
+ using System.Web.Script.Serialization;
17
+ using System.Windows.Forms;
18
+
19
+ internal static class DshOpen
20
+ {
21
+ private const string DefaultBase = "http://127.0.0.1:3080";
22
+ private const int MaxPaths = 20;
23
+
24
+ [STAThread]
25
+ private static void Main(string[] args)
26
+ {
27
+ string baseUrl = ReadBase();
28
+ if (!ServerAlive(baseUrl))
29
+ {
30
+ MessageBox.Show(
31
+ "DSH Web UI 未运行(" + baseUrl + ")。\n请先启动 DSH,再使用「在 DSH 文件编辑中打开」。",
32
+ "DSH 文件编辑",
33
+ MessageBoxButtons.OK,
34
+ MessageBoxIcon.Information);
35
+ return;
36
+ }
37
+ List<string> paths = CollectPaths(args);
38
+ if (paths.Count == 0) return;
39
+ if (HandoffAndConfirm(baseUrl, paths)) return;
40
+ OpenUrl(BuildUrl(baseUrl, paths));
41
+ }
42
+
43
+ /// <summary>把参数解析为绝对路径清单(上限 MaxPaths,非法项跳过)。</summary>
44
+ private static List<string> CollectPaths(string[] args)
45
+ {
46
+ var paths = new List<string>();
47
+ foreach (string raw in args)
48
+ {
49
+ if (paths.Count >= MaxPaths) break;
50
+ string path;
51
+ try { path = Path.GetFullPath(raw); }
52
+ catch (Exception) { continue; }
53
+ if (!string.IsNullOrEmpty(path)) paths.Add(path);
54
+ }
55
+ return paths;
56
+ }
57
+
58
+ /// <summary>投递待打开请求;返回 true = 已确认由既有 DSH 页面执行(无需开新页)。</summary>
59
+ private static bool HandoffAndConfirm(string baseUrl, List<string> paths)
60
+ {
61
+ string resp = PostRpc(baseUrl, "edrv.external.handoff", "{\"paths\":" + JsonArrayOf(paths) + "}");
62
+ var data = ParseJson(resp);
63
+ if (data == null || ToInt(data, "clients") <= 0) return false;
64
+ Thread.Sleep(2000);
65
+ string token = data.ContainsKey("token") ? data["token"] as string : null;
66
+ string state = PostRpc(baseUrl, "edrv.external.pendingState",
67
+ "{\"token\":" + JsonString(token) + ",\"take\":true}");
68
+ var st = ParseJson(state);
69
+ return st != null && ToBool(st, "delivered");
70
+ }
71
+
72
+ private static void OpenUrl(string url)
73
+ {
74
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
75
+ }
76
+
77
+ /// <summary>POST /edrv/rpc;失败返回 null(回退开新页,不阻塞)。</summary>
78
+ private static string PostRpc(string baseUrl, string method, string argsJson)
79
+ {
80
+ try
81
+ {
82
+ HttpWebRequest req = (HttpWebRequest)WebRequest.Create(baseUrl.TrimEnd('/') + "/edrv/rpc");
83
+ req.Method = "POST";
84
+ req.ContentType = "application/json";
85
+ req.Timeout = 4000;
86
+ byte[] body = Encoding.UTF8.GetBytes("{\"method\":\"" + method + "\",\"args\":" + argsJson + "}");
87
+ req.ContentLength = body.Length;
88
+ using (Stream stream = req.GetRequestStream()) stream.Write(body, 0, body.Length);
89
+ using (WebResponse res = req.GetResponse())
90
+ using (StreamReader reader = new StreamReader(res.GetResponseStream(), Encoding.UTF8))
91
+ return reader.ReadToEnd();
92
+ }
93
+ catch (Exception) { return null; }
94
+ }
95
+
96
+ /// <summary>宽松 JSON 解析(失败返回 null)。</summary>
97
+ private static Dictionary<string, object> ParseJson(string json)
98
+ {
99
+ if (string.IsNullOrEmpty(json)) return null;
100
+ try { return new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(json); }
101
+ catch (Exception) { return null; }
102
+ }
103
+
104
+ private static int ToInt(Dictionary<string, object> data, string key)
105
+ {
106
+ object value;
107
+ return data.TryGetValue(key, out value) ? Convert.ToInt32(value) : 0;
108
+ }
109
+
110
+ private static bool ToBool(Dictionary<string, object> data, string key)
111
+ {
112
+ object value;
113
+ return data.TryGetValue(key, out value) && Convert.ToBoolean(value);
114
+ }
115
+
116
+ private static string JsonString(string value)
117
+ {
118
+ return "\"" + (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
119
+ }
120
+
121
+ private static string JsonArrayOf(List<string> items)
122
+ {
123
+ var parts = new List<string>();
124
+ foreach (string item in items) parts.Add(JsonString(item));
125
+ return "[" + string.Join(",", parts.ToArray()) + "]";
126
+ }
127
+
128
+ /// <summary>读取同目录 dsh-open.ini 的 base= 行;缺失或非法回退默认地址。</summary>
129
+ private static string ReadBase()
130
+ {
131
+ try
132
+ {
133
+ string ini = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "dsh-open.ini");
134
+ if (!File.Exists(ini)) return DefaultBase;
135
+ foreach (string line in File.ReadAllLines(ini, Encoding.UTF8))
136
+ {
137
+ string trimmed = line.Trim();
138
+ if (trimmed.StartsWith("base=", StringComparison.OrdinalIgnoreCase) && trimmed.Length > 5)
139
+ {
140
+ string value = trimmed.Substring(5).Trim();
141
+ if (value.Length > 0) return value;
142
+ }
143
+ }
144
+ }
145
+ catch (Exception) { /* ini 读取失败按默认地址 */ }
146
+ return DefaultBase;
147
+ }
148
+
149
+ /// <summary>TcpClient 探测 base 地址对应端口是否可达(1s 超时)。</summary>
150
+ private static bool ServerAlive(string baseUrl)
151
+ {
152
+ try
153
+ {
154
+ Uri uri = new Uri(baseUrl);
155
+ int port = uri.IsDefaultPort
156
+ ? (string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) ? 443 : 80)
157
+ : uri.Port;
158
+ using (TcpClient client = new TcpClient())
159
+ {
160
+ if (!client.ConnectAsync(uri.Host, port).Wait(1000)) return false;
161
+ return client.Connected;
162
+ }
163
+ }
164
+ catch (Exception) { return false; }
165
+ }
166
+
167
+ /// <summary>把路径清单编码合并为一个深链 URL(空清单 → null)。</summary>
168
+ private static string BuildUrl(string baseUrl, List<string> paths)
169
+ {
170
+ StringBuilder url = new StringBuilder(baseUrl.TrimEnd('/') + "/?edrvOpen=1");
171
+ int count = 0;
172
+ foreach (string path in paths)
173
+ {
174
+ if (count >= MaxPaths) break;
175
+ if (string.IsNullOrEmpty(path)) continue;
176
+ url.Append(count == 0 ? "&edrvPaths=" : ",");
177
+ url.Append(Uri.EscapeDataString(path));
178
+ count++;
179
+ }
180
+ return count == 0 ? null : url.ToString();
181
+ }
182
+ }
@@ -0,0 +1,52 @@
1
+ # dsh-open.ps1 — DSH 文件编辑 shell launcher(csc 不可用时的降级版,轻微控制台闪窗)。
2
+ # 行为与 dsh-open.cs 一致:读 dsh-open.ini base → 探测端口 → 投递待打开请求(有已打开
3
+ # DSH 页面时由其就地执行,2s 未领取取回并回退开新页)。
4
+ # 由 dsh-vscode-mode 设置页一键注册写入 HKCU\...\shell\DSHEditor\command。
5
+ # 作者 ddj 2026-09-08
6
+ param([string[]]$Path)
7
+ $ErrorActionPreference = 'Stop'
8
+ $base = 'http://127.0.0.1:3080'
9
+ $ini = Join-Path $PSScriptRoot 'dsh-open.ini'
10
+ if (Test-Path $ini) {
11
+ foreach ($line in Get-Content $ini) {
12
+ if ($line -match '^\s*base\s*=\s*(.+?)\s*$') { $base = $Matches[1]; break }
13
+ }
14
+ }
15
+
16
+ $alive = $false
17
+ try {
18
+ $uri = [Uri]$base
19
+ $port = if ($uri.IsDefaultPort) { if ($uri.Scheme -ieq 'https') { 443 } else { 80 } } else { $uri.Port }
20
+ $client = New-Object Net.Sockets.TcpClient
21
+ $alive = $client.ConnectAsync($uri.Host, $port).Wait(1000) -and $client.Connected
22
+ $client.Close()
23
+ } catch { $alive = $false }
24
+
25
+ if (-not $alive) {
26
+ Add-Type -AssemblyName System.Windows.Forms
27
+ [System.Windows.Forms.MessageBox]::Show(
28
+ "DSH Web UI 未运行($base)。`n请先启动 DSH,再使用「在 DSH 文件编辑中打开」。",
29
+ 'DSH 文件编辑') | Out-Null
30
+ exit 1
31
+ }
32
+
33
+ $paths = @($Path | Where-Object { $_ } | ForEach-Object { [IO.Path]::GetFullPath($_) })
34
+ if (-not $paths -or $paths.Count -eq 0) { exit 0 }
35
+
36
+ # 复用已有页面:投递待打开请求,clients>0 表示有页面在(2s 后确认领取,未领取取回回退)
37
+ $handoff = $null
38
+ try {
39
+ $rpcBase = $base.TrimEnd('/') + '/edrv/rpc'
40
+ $body = @{ method = 'edrv.external.handoff'; args = @{ paths = $paths } } | ConvertTo-Json -Depth 4 -Compress
41
+ $handoff = Invoke-RestMethod -Uri $rpcBase -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 4
42
+ if ($handoff.clients -gt 0) {
43
+ Start-Sleep -Milliseconds 2000
44
+ $stateBody = @{ method = 'edrv.external.pendingState'; args = @{ token = $handoff.token; take = $true } } | ConvertTo-Json -Depth 4 -Compress
45
+ $state = Invoke-RestMethod -Uri $rpcBase -Method Post -ContentType 'application/json' -Body $stateBody -TimeoutSec 4
46
+ if ($state.delivered) { exit 0 }
47
+ }
48
+ } catch { # 移交失败回退开新页
49
+ }
50
+
51
+ $encoded = @($paths | ForEach-Object { [Uri]::EscapeDataString($_) }) -join ','
52
+ Start-Process ($base.TrimEnd('/') + '/?edrvOpen=1&edrvPaths=' + $encoded)
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # dsh-open.sh — DSH 文件编辑 shell launcher(Linux/macOS 通用,bash 3.2+)。
3
+ # 由 dsh-vscode-mode 设置页注册写入:Nautilus 脚本 / Dolphin 服务菜单 / macOS 快速操作引用本脚本。
4
+ # 行为:读同目录 dsh-open.ini 的 base 地址 → curl 探测(可用时,1s 超时)→ 深链打开默认浏览器。
5
+ # 深链契约见插件 src/shared/externalOpen.ts(edrvOpen/edrvPaths/edrvLine/edrvColumn)。
6
+ # 作者 ddj 2026-09-07
7
+ set -u
8
+ export LC_ALL=C
9
+
10
+ case "$0" in */*) ini_dir="${0%/*}" ;; *) ini_dir="." ;; esac
11
+ base="http://127.0.0.1:3080"
12
+ ini="$ini_dir/dsh-open.ini"
13
+ if [ -f "$ini" ]; then
14
+ parsed=$(sed -n 's/^[[:space:]]*[Bb]ase[[:space:]]*=[[:space:]]*//p' "$ini" | head -n 1)
15
+ [ -n "$parsed" ] && base="$parsed"
16
+ fi
17
+
18
+ # 逐字节 percent-encode:LC_ALL=C 下 ${s:i:1} 按字节迭代,多字节字符(中文等)安全;
19
+ # unreserved 保留,其余全部 %XX(编码结果不含裸逗号/空白,逗号分隔契约安全)。
20
+ urlencode() {
21
+ local s="$1" out="" i c hex
22
+ for ((i = 0; i < ${#s}; i++)); do
23
+ c="${s:i:1}"
24
+ case "$c" in
25
+ [A-Za-z0-9.~_-]) out+="$c" ;;
26
+ *) printf -v hex '%%%02X' "'$c" ; out+="$hex" ;;
27
+ esac
28
+ done
29
+ printf '%s' "$out"
30
+ }
31
+
32
+ alert() {
33
+ local msg="$1"
34
+ if [ "$(uname)" = "Darwin" ]; then
35
+ osascript -e "display dialog \"$msg\" with title \"DSH 文件编辑\"" >/dev/null 2>&1 || true
36
+ elif command -v notify-send >/dev/null 2>&1; then
37
+ notify-send "DSH 文件编辑" "$msg" || true
38
+ elif command -v zenity >/dev/null 2>&1; then
39
+ zenity --info --title "DSH 文件编辑" --text "$msg" >/dev/null 2>&1 || true
40
+ fi
41
+ }
42
+
43
+ # 无参数(如 Nautilus 空选)静默退出
44
+ [ $# -gt 0 ] || exit 0
45
+
46
+ # DSH 未运行提示(curl 缺失时跳过探测直接打开)
47
+ if command -v curl >/dev/null 2>&1; then
48
+ if ! curl -s -o /dev/null --max-time 1 "$base"; then
49
+ alert "DSH Web UI 未运行($base)。请先启动 DSH。"
50
+ exit 1
51
+ fi
52
+ fi
53
+
54
+ # 全部路径编码后合并为一个深链(多选一次打开)
55
+ query=""
56
+ for p in "$@"; do
57
+ [ -n "$p" ] || continue
58
+ abs=$(cd "$(dirname "$p")" 2>/dev/null && pwd)/$(basename "$p")
59
+ [ -e "$abs" ] || abs="$p"
60
+ seg=$(urlencode "$abs")
61
+ [ -n "$seg" ] || continue
62
+ if [ -z "$query" ]; then query="$seg"; else query="$query,$seg"; fi
63
+ done
64
+ [ -n "$query" ] || exit 0
65
+
66
+ url="${base%/}/?edrvOpen=1&edrvPaths=$query"
67
+ case "$(uname)" in
68
+ Darwin) exec open "$url" ;;
69
+ *) exec xdg-open "$url" ;;
70
+ esac
Binary file