dsh-vscode-mode 0.12.0 → 0.13.1

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 (71) hide show
  1. package/README.md +148 -112
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +30185 -29711
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +3889 -2336
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/addToConversation.ts +4 -2
  14. package/src/client/ai/inlineProvider.ts +1 -8
  15. package/src/client/commandBridge.ts +6 -2
  16. package/src/client/externalOpen.ts +40 -19
  17. package/src/client/index.ts +17 -15
  18. package/src/client/keybindings.ts +18 -1
  19. package/src/client/monaco/loader.ts +3 -1
  20. package/src/client/openFlow.ts +36 -28
  21. package/src/client/openReceipt.ts +64 -0
  22. package/src/client/panelCommands.ts +101 -0
  23. package/src/client/pdf/pdfLoader.ts +4 -2
  24. package/src/client/receiptState.ts +120 -0
  25. package/src/client/rpc.ts +30 -4
  26. package/src/client/shortcutsOfficial.ts +39 -4
  27. package/src/client/sidebarBridge.ts +7 -0
  28. package/src/client/styles/mcp.css +1 -19
  29. package/src/client/ui/DiffBadge.ts +2 -1
  30. package/src/client/ui/EditorView.ts +237 -55
  31. package/src/client/ui/McpSettings.ts +32 -7
  32. package/src/client/ui/OfficialSideTab.ts +5 -2
  33. package/src/client/ui/QuickOpen.ts +10 -9
  34. package/src/client/ui/commandCatalog.ts +5 -4
  35. package/src/client/useOpenReceipt.ts +128 -0
  36. package/src/compat.ts +5 -3
  37. package/src/dap/manager.ts +46 -13
  38. package/src/devForm.ts +252 -92
  39. package/src/externalHandoff.ts +93 -52
  40. package/src/fileOpenSettings.ts +1 -24
  41. package/src/hostImport.ts +19 -0
  42. package/src/httpGuard.ts +101 -0
  43. package/src/index.ts +106 -97
  44. package/src/integrate.ts +56 -50
  45. package/src/lsp/transport.ts +84 -127
  46. package/src/mcp.ts +58 -9
  47. package/src/mcpIsolation.ts +211 -102
  48. package/src/mcpProject.ts +220 -133
  49. package/src/mcpRuntime.ts +343 -0
  50. package/src/openBridge.ts +69 -0
  51. package/src/openInbox.ts +207 -0
  52. package/src/paths.ts +8 -5
  53. package/src/processTree.ts +63 -0
  54. package/src/reveal.ts +5 -3
  55. package/src/routes.ts +156 -154
  56. package/src/rpc.ts +14 -13
  57. package/src/shared/appUrl.ts +19 -0
  58. package/src/shared/externalOpen.ts +71 -0
  59. package/src/shared/integration.ts +2 -0
  60. package/src/shared/keybindings.ts +8 -5
  61. package/src/shared/mcp.ts +4 -1
  62. package/src/shared/rpc.ts +7 -4
  63. package/src/shared/svn.ts +6 -8
  64. package/src/svnPlanTool.ts +51 -0
  65. package/src/unityBridge.ts +26 -3
  66. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  67. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  68. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  69. package/unity/com.dsh.editor/README.md +16 -25
  70. package/unity/com.dsh.editor/package.json +2 -2
  71. package/src/client/ui/ShortcutSettings.ts +0 -294
package/README.md CHANGED
@@ -44,8 +44,8 @@
44
44
  保存·全部关闭(`Ctrl+F4` 关当前)/ 复制路径·复制相对路径 / 在文件资源管理器
45
45
  (视图)中显示。关闭前未保存页签静默落盘;浏览器无法实现的条目(向右拆分 /
46
46
  新窗口)不提供。
47
- - **工作区搜索填充**:编辑器有选中时按 `Ctrl+Shift+F`,选中文本自动填入搜索框并
48
- 立即搜索(多行选区取首行;填入后全选,便于直接改写)。
47
+ - **工作区搜索填充**:编辑器有选中时执行「工作区搜索」(当前键位见官方快捷键目录),
48
+ 选中文本自动填入搜索框并立即搜索(多行选区取首行;填入后全选,便于直接改写)。
49
49
  - **Markdown 预览**(v0.5.3):打开 `.md` / `.markdown` 后,工具栏「预览」按钮、
50
50
  `Ctrl+Shift+V` 或命令栏「切换 Markdown 预览」切入 GFM 渲染(标题/列表/表格/代码块/
51
51
  引用/KaTeX),「以源码打开」切回编辑。渲染走官方 `MarkdownText` 原语,零新增依赖、
@@ -65,8 +65,9 @@
65
65
  分类 / 当前键位;需要打开文件的命令在空编辑器时自动隐藏。
66
66
 
67
67
  - **单一数据源**:命令目录(`ui/commandCatalog`)驱动命令栏和官方快捷键目录;
68
- 设置 →「快捷键」可查看官方目录与编辑插件命令,绑定经 DSH 官方服务保存。旧版 DSH
69
- 没有该服务时继续使用原有窗口按键派发,旧 `keybindings` 设置仍可生效。
68
+ 键位查看/改绑统一走 DSH 官方快捷键配置(设置 → VSCodeMode → 通用 →
69
+ 「打开官方快捷键配置」,或 `Ctrl+/`),插件不再自建录键界面,绑定由官方服务持久化。
70
+ 旧版 DSH 没有该服务时继续使用原有窗口按键派发,旧 `keybindings` 设置仍可生效。
70
71
  - **开放注册表**:`window.__edrvCommands__` 暴露 `register()` 注册命令;插件
71
72
  不再提供独立的运行时键位注册,改用 DSH 官方 shortcuts 配置。
72
73
  - **开箱 20 条命令**:保存 / 快速打开 / 在文件浏览器中打开 / 显示所有命令 /
@@ -166,8 +167,8 @@ cmaps 支持中文渲染);工具条进入注释编辑:✎ 文本框 / 🖌
166
167
  ### 9. 文件管理侧边栏(类 VSCode 活动栏 + 面板)
167
168
 
168
169
  编辑器内嵌活动栏 + 可拖拽调宽面板区;首期「资源管理器」= 懒加载目录树
169
- (展开实时读取、点文件打开、差异角标、活动文件高亮、`edrv:refresh` / 手动 ⟳、
170
- `Ctrl+B` 显隐并持久化),侧栏形态默认收起以节省面板宽度。
170
+ (展开实时读取、点文件打开、差异角标、活动文件高亮、`edrv:refresh` / 手动 ⟳,
171
+ 可通过命令栏切换显隐并持久化),侧栏形态默认收起以节省面板宽度。
171
172
 
172
173
  - 拖拽调宽下限 = 最小宽度(默认 300,设置 → VSCodeMode → 通用「编辑器」
173
174
  可调 180–560),拖拽低于最小宽度自动收起。
@@ -209,8 +210,19 @@ cmaps 支持中文渲染);工具条进入注释编辑:✎ 文本框 / 🖌
209
210
  ### 11. MCP 可视化管理(设置 → VSCodeMode)
210
211
 
211
212
  子 Tab「我的 MCP」(profile 全局)+「项目 MCP」(各项目根 `.mcp.json`,
212
- 对齐 Claude Code / Cursor,随仓库共享):查看各项目连接状态 / 工具、添加
213
- (stdio / streamable-http)、刷新、启用 / 禁用、删除。工具全局生效。
213
+ 对齐 Claude Code / Cursor,随仓库共享):支持 stdio / streamable-http 的新增、
214
+ 刷新、启停、删除和运行状态查看。
215
+
216
+ - **项目作用域**(v0.13.0):工具、资源与服务器指令均按 agent 所属工作区隔离;
217
+ 跨工作区子会话不会因继承父 scope 而获得外项目 MCP。全局 MCP 保持全局可用。
218
+ - **实例与状态**:每个活动 agent、每个启用项目服务各有一个插件实例;没有活动
219
+ agent 时只保存配置(`configured`、实例数 0)。`instanceCount` 是挂载实例数,
220
+ `unverified` 表示没有公开连接状态证据,不能把 fiber ACTIVE 当成已连接。
221
+ 零工具可能是合法的纯资源服务器。
222
+ - **配置保护**:reconcile 尊重 `disabled`;读取/解析错误保留最后有效连接,
223
+ 不把不可读文件当成空配置。保存保留未知字段和未改动的脱敏凭据。
224
+
225
+ 完整配置、迁移与排错见随包 [MCP 技能](skills/dsh-vscodemode-mcp/SKILL.md)。
214
226
 
215
227
  ### 12. 插件自带技能组(v0.3.3)
216
228
 
@@ -274,85 +286,67 @@ DSH 启动回放 `~/.dsh/sessions` 全部会话(V8 展开约 10×,可能 OOM
274
286
  选择重新加载或用编辑器内容覆盖;冲突未处理期间自动保存被抑制。
275
287
  - 老 host(无该 RPC)自动降级为无同步、无护栏,不报错。
276
288
 
277
- ### 15. 系统集成(v0.1.53)
278
-
279
- 设置 → VSCodeMode → 通用 →「系统集成」卡片,两项能力:
289
+ ### 15. 系统集成(v0.1.53;v0.13.0 更新打开链路)
280
290
 
281
- **① 文件管理器右键菜单**——一键把「在 DSH 文件编辑中打开」注册进系统:
282
- Windows 资源管理器三类入口 / Linux Nautilus + Dolphin / macOS Automator 配方;
283
- 插件卸载自动清理、更新自动恢复;点击经 launcher 打开默认浏览器深链,编辑器
284
- 直接打开所选文件;DSH 未运行时弹提示。
291
+ 设置 → VSCodeMode → 通用 →「系统集成」卡片提供文件管理器右键菜单与 Unity
292
+ 外部脚本编辑器。两者现在共用按 profile 隔离的本机私有 OPEN 请求/ACK 通道,
293
+ 无需向 launcher 或 Unity 提供 Host Cookie,也不依赖固定 HTTP 端口。
285
294
 
286
- **② Unity 外部脚本编辑器**——内置 UPM 包 `com.dsh.editor`(参照
287
- com.unity.ide.traeCN 机制实现 `IExternalEditor` 虚拟安装,无需本地 exe),
288
- 设置页一键安装 / 更新为 Unity 内嵌包(复制到 `<项目>/Packages/com.dsh.editor`,
289
- 自动发现、不改 manifest.json);Unity Preferences → External Tools 选
290
- 「DSH 文件编辑」后,双击脚本 / Console 报错跳转即在浏览器打开对应文件与行列。
291
-
292
- **深链契约**(launcher / Unity 包 / client 三端共用):
293
-
294
- ```
295
- http://127.0.0.1:3080/?edrvOpen=1&edrvPaths=<enc1>[,<enc2>…][&edrvLine=N][&edrvColumn=M]
296
- ```
295
+ **① 文件管理器右键菜单**
297
296
 
298
- 每段路径独立 `encodeURIComponent`、逗号连接;client 解析后按下方「打开规则」
299
- 路由,处理完剥离参数防刷新重开,跨源 referrer 守卫防外部网页诱导。
297
+ - Windows 提供文件、目录、目录空白处三类入口;Linux 提供 Nautilus 脚本与
298
+ Dolphin 服务菜单;macOS 提供 Finder Automator 快速操作配方。
299
+ - **注册和移除必须由用户在设置页显式操作**。插件 reload/更新只升级已注册的
300
+ launcher 文件及配置,保留所选 profile;不会自动重注册或撤销系统菜单。
301
+ 卸载插件前如需清理菜单,请先点「移除注册」。
302
+ - Windows 优先使用编译后的 C# launcher,缺少编译器时回退 PowerShell;
303
+ POSIX 使用 Bash launcher。三个入口统一调用随包 `dsh-open.mjs` producer,
304
+ 从配置读取所选 profile、私有收件箱和运行程序。
305
+ - 已打开的客户端可以领取请求并就地打开。需要唤起时,Desktop 使用官方
306
+ `dsh://open` 入口,Web 使用用户选定的完整应用地址(含实际端口和部署子路径)。
307
+ **唤起窗口或领取请求本身不代表打开成功**。
300
308
 
301
- **打开规则(智能路由)**——按首路径类型分派(其余路径:文件进编辑器、
302
- 文件夹补引用):
309
+ **打开与回执规则**
303
310
 
304
- | 首路径 | 行为 |
311
+ | 阶段 | 行为 |
305
312
  |---|---|
306
- | 文件夹,在已注册工作区内 | 弹窗二选一:**使用最近的工作区**(跳转 + 新增对话 + 文件夹引用 + 编辑页)/ **新建工作区**;取消则中止 |
307
- | 文件夹,不在任何工作区 | 不弹窗,以该文件夹为根注册新工作区 + 新增对话 + 引用 + 编辑页 |
308
- | 文件,在已注册工作区内 | 有会话 → 打开最近对话 + 编辑器展开(行列透传);无会话 → 新建对话 + 文件引用 + 展开 |
309
- | 文件,不在任何工作区 | 打开最近一次对话 + 编辑器展开 |
310
- | 文件,无任何工作区 / 对话 | 以文件父目录注册工作区 + 新建对话 + 引用 + 编辑器打开 |
311
-
312
- **页面复用**:launcher 先向 host 投递待打开请求(`edrv.external.handoff`)——
313
- 已打开的 DSH 页面(3s 移交轮询)就地执行打开规则,**不重复开新页**;2s 未领取
314
- (页面刚关 / 后台节流)或无活跃页面时回退打开新页(URL 深链)。时序:等待会话 /
315
- 工作区列表就绪(≤15s);`sessions.create / workspaces.create` 为 DSH 官方服务
316
- 方法;引用插入轮询输入门面就绪(忙态自动降级纯文本)。
317
-
318
- **各平台注册细节**:
319
-
320
- - **Windows**:`assets/shell/dsh-open.cs` 复制到 `~/.dsh/dsh-vscode-mode/shell/`
321
- 并用 .NET Framework 4.x `csc.exe` 编译为 `dsh-open.exe`(WinExe 无闪窗;
322
- csc 缺失自动降级 `dsh-open.ps1`);写三类 HKCU 键
323
- `HKCU\Software\Classes\{\*,Directory,Directory\Background}\shell\DSHEditor`
324
- (显示名 + Icon `dsh-whale.ico` + command,首次注册前 `reg export` 备份)。
325
- 全部 reg/csc 调用走 host `subprocess.spawn` argv 数组(stdio `inherit`,
326
- 受管环境禁管道),无 shell 插值。
327
- - **Linux**(纯文件写入,无需管理员):Nautilus 右键脚本(`NAUTILUS_SCRIPT_SELECTED_FILE_PATHS`
328
- 多选)+ KDE Dolphin 服务菜单(`%F` 多选,兼容旧 `kservices5/ServiceMenus/`)。
329
- - **macOS**:Finder 菜单不做自动生成——设置页「复制 Automator 配方」按步骤创建
330
- 快速操作(约 1 分钟,脚本指向 `shell/dsh-open.sh`),创建后面板自动检测显示 ✓;
331
- 「移除注册」仅删除引用本插件 launcher 的 workflow。
332
- - **行为**:launcher 读同目录 `dsh-open.ini`(`base=` 深链基址,注册时按设置写入)
333
- → 探测端口(Windows TcpClient / POSIX curl,1s)→ 未运行弹提示(MessageBox /
334
- osascript / notify-send / zenity)→ 路径编码合并为一个 URL 交给默认浏览器
335
- (POSIX 侧 `LC_ALL=C` 逐字节 percent-encode,中文 / 空格 / 任意字符安全);
336
- 改「DSH 服务地址」后点「注册」刷新 ini。
337
- - **生命周期**:注册成功写 marker(`shell/registered.json`)→ 插件卸载 / reload
338
- 自动清理注册痕迹,重启 / 更新后自动恢复(开发态反复 reload 不丢注册);点
339
- 「移除注册」删除 marker,此后不再自动恢复;强杀进程跳过清理时残留键由下一次
340
- 启动的幂等重写与「移除注册」兜底。
341
- - RPC:`edrv.integration.status / register / unregister`。
342
-
343
- **Unity 包细节**:包源随插件分发于 `unity/com.dsh.editor/`(`package.json` +
344
- `Editor/DshCodeEditor.cs` + asmdef,`"unity": "2019.4"` 基线;虚拟安装
345
- `dsh-editor://vscode-mode`,`OpenProject` → `Application.OpenURL(深链)`,不生成
346
- csproj);**打开过滤**:仅文本 / 代码类扩展名(白名单 + `EditorSettings.projectGenerationUserExtensions`
347
- 用户自定义扩展)交 DSH 打开,其余返回 `false` 交还 Unity 原生(双击预制体进
348
- 预制体模式、双击场景开场景),对齐官方 `DefaultExternalCodeEditor` 行为;
349
- 安装 / 更新 = 整目录复制 / 替换为 `<项目>/Packages/com.dsh.editor`(目标已存在
350
- 且 name 不是 `com.dsh.editor` 时拒绝覆盖),Unity 已打开时切回窗口自动刷新;
351
- 卸载 = 删除该目录;手动兜底:Package Manager → Add package from disk 选
352
- `unity/com.dsh.editor`;登记清单存 `~/.dsh/dsh-vscode-mode/unity-projects.json`;
353
- RPC:`edrv.unity.list / add / remove / install`。
354
- 安全说明:深链可在编辑器中查看任意绝对路径文件(与用户手动打开等价);保存仍
355
- 受会话沙箱 `policyOf` 约束,工作区外保存被拒是预期行为。
313
+ | 投递 | producer 将带请求 id、目标 profile、路径和可选行列的 OPEN 请求写入当前用户私有队列 |
314
+ | 领取 | 客户端领取带 lease 的请求,按工作区/会话路由到文件编辑器 |
315
+ | 确认 | 只有目标路径对应的实际 Monaco model 或预览内容成功就绪,才提交匹配请求与 lease 的成功 ACK |
316
+ | 失败/超时 | 返回失败或未确认;不会把一个已展开的空页签视为成功 |
317
+
318
+ 请求按 profile 与请求身份匹配,队列内容受类型、大小、时效和本机权限检查约束;
319
+ producer 只承担「打开」请求,不是通用 RPC 代理。旧 `edrvOpen/edrvPaths` URL
320
+ 参数仍作为兼容入口保留,已不是 launcher 和 Unity 的当前主链。
321
+
322
+ 文件与目录仍按现有工作区路由:工作区内文件进入相应会话;无会话时可创建会话;
323
+ 目录根据是否位于已注册工作区内选择复用或新建。选择取消会结束该请求。行列定位
324
+ 与真正加载的目标文件绑定,避免切换文件时把其他 model 的内容当成完成证据。
325
+
326
+ **② Unity 外部脚本编辑器(内嵌包 `com.dsh.editor` 0.3.0)**
327
+
328
+ 1. 在设置卡片中选择 Unity 项目,执行安装/更新,将随包 UPM 目录复制到
329
+ `<项目>/Packages/com.dsh.editor`;无需修改项目 manifest。
330
+ 2. 首次安装会自动准备当前 profile 的独立 bridge,并写入本机项目提示
331
+ `<项目>/UserSettings/dsh-editor.ini`;**无需先注册系统右键菜单**。
332
+ 3. 在 Unity Preferences → External Tools 选择「DSH 文件编辑」。双击文本脚本、
333
+ Console 报错行列或使用打开项目入口,会通过统一 producer 等待 ACK。
334
+ 4. Unity 的「DSH 桥接配置」可填写绝对路径覆盖自动选择。优先级为:UI 显式覆盖 →
335
+ 项目 UserSettings 提示 → 提示缺失时的旧 shell 配置回退;损坏提示会报错,
336
+ 不会静默切换到另一个 profile。
337
+
338
+ UserSettings 提示包含本机 profile 路径,不适合作为团队共享配置。换机器或切换
339
+ profile 后应重新安装/更新桥接,或在 Unity UI 中显式指定对应 bridge 配置。
340
+
341
+ Unity 包保留文本/代码扩展名白名单与用户自定义扩展;Prefab、场景等资产仍交还
342
+ Unity 原生处理,不生成 csproj。Unity API 与 EditorPrefs 在主线程使用;后台任务
343
+ 负责 producer 与 IO。安装目标已有同名目录但不是本包时拒绝覆盖,卸载仅删除该包目录。
344
+
345
+ - 管理 RPC:`edrv.integration.status/register/unregister`、`edrv.unity.list/add/remove/install`。
346
+ - 保存文件仍遵循当前会话沙箱策略;外部打开成功不扩大文件写入权限。
347
+ - 当前验证边界:Windows 官方 Desktop 已完成隔离启动与侧栏挂载等检查;Unity
348
+ 仅有 C# 编译桩与 9 项 hint 探针,尚未执行真实 Unity EditMode。Linux/macOS
349
+ 尚无本轮现场验证。具体完成项与待验收项见 [0.13 发布说明](docs/release-0.13.0.md)。
356
350
 
357
351
  ### 16. SVN 集成(v0.5.0)
358
352
 
@@ -432,17 +426,19 @@ TortoiseSVN(`TortoiseProc.exe`)仅作 Windows 过渡增强:自研能力覆
432
426
 
433
427
  ## 安装
434
428
 
435
- 官方 `dsh plugin` 方式,三选一:
429
+ 官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
430
+ profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
431
+ [0.13.0 发布与迁移说明](docs/release-0.13.0.md)。
436
432
 
437
433
  ```bash
438
434
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
439
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.12.0
435
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.13.1
440
436
 
441
437
  # ② npm 注册表(发布到 npm 后)
442
438
  dsh plugin --profile web add dsh-vscode-mode
443
439
 
444
440
  # ③ GitHub Release tgz 直装
445
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.12.0/dsh-vscode-mode-0.12.0.tgz
441
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.13.1/dsh-vscode-mode-0.13.1.tgz
446
442
  ```
447
443
 
448
444
  > `dsh plugin ...` 是 pnpm 转发器:git 安装会克隆仓库、执行该包 `prepare` 脚本
@@ -479,14 +475,16 @@ bundles 层后重启 DSH 即自动把插件行挂进 loader 树。**不要**再
479
475
 
480
476
  - **文件树**:资源管理器面板点击打开;**文件链接**(DSH 0.1.5+):聊天 / 文件树
481
477
  里的文件链接直达右侧栏编辑器(含行号定位)。
482
- - **快速打开**:`Ctrl+P`(QuickOpen)——`↑↓` 选择、`Enter` 打开高亮项、
483
- `Esc` 关闭,鼠标悬停与高亮同步。
478
+ - **快速打开**:执行插件 QuickOpen 命令(键位见「设置 → 快捷键」)——`↑↓` 选择、
479
+ `Enter` 打开高亮项、`Esc` 关闭,鼠标悬停与高亮同步;`Ctrl+P` 由官方文件切换使用。
484
480
  - **差异入口**:对话输入框上方 dock 的「差异 N 个文件 · 查看下一个」按钮。
485
481
 
486
482
  ### 快捷键速查
487
483
 
488
484
  以下是桌面端常见默认键位;Web / Linux 的浏览器或系统保留键可能不绑定。
489
- 实际生效键位以「设置 → 快捷键」为准;旧版 DSH 无官方 shortcuts 时沿用原有键位。
485
+ 实际生效键位以 **DSH 官方快捷键配置**为准:设置 → VSCodeMode → 通用 →「打开官方
486
+ 快捷键配置」(或 `Ctrl+/`)。插件命令与官方命令同表展示、由官方统一校验冲突;
487
+ 旧版 DSH 无官方 shortcuts 时沿用原有键位。
490
488
 
491
489
  | 快捷键 | 功能 |
492
490
  |---|---|
@@ -498,7 +496,7 @@ bundles 层后重启 DSH 即自动把插件行挂进 loader 树。**不要**再
498
496
  | `F12` / `Shift+F12` | 转到定义 / 查找所有引用 |
499
497
  | `Ctrl+点击` / `Ctrl+hover` | 引用导航 / 可导航标识符提示 |
500
498
  | 自定义键位 | 切换文件管理侧边栏(`Ctrl+B` 与 DSH 自身侧栏命令冲突,插件默认不绑定) |
501
- | `Ctrl+Shift+F` | 工作区搜索(有选中则把选中文本填入搜索框) |
499
+ | 以当前官方目录为准(Web 采用平台允许的组合) | 工作区搜索(有选中则填入搜索框);不保证所有平台都使用 `Ctrl+Shift+F` |
502
500
  | `Ctrl+Shift+V` | 切换 Markdown 预览(仅活动文件为 .md 时生效) |
503
501
  | `Ctrl+F4` | 关闭当前页签 |
504
502
  | `Alt+←` / `Alt+→` | 后退 / 前进(跨文件恢复焦点位置) |
@@ -531,17 +529,25 @@ bundles 层后重启 DSH 即自动把插件行挂进 loader 树。**不要**再
531
529
 
532
530
  ## 配置
533
531
 
534
- 设置 →「快捷键」页:官方目录总览和插件快捷键录制、逐项恢复;编辑通过官方
535
- shortcuts 服务持久化(Desktop:`userData/keybindings.json`;Web:
536
- `localStorage` 的 `dsh.keybindings.v1`)。旧版插件 `keybindings` 配置的单候选自定义值
537
- 按当前平台一次性尝试迁入官方存储;旧配置始终保留供其他平台及人工恢复,
538
- 多候选、冲突、不支持或未绑定的旧值不会被部分迁入或删除。
532
+ 键位配置统一走 DSH 官方快捷键:设置 → VSCodeMode → 通用 →「打开官方快捷键配置」
533
+ (等价 `Ctrl+/`;官方入口不可用时按钮就地给出手动打开方式)。录键、冲突校验、
534
+ 按平台默认值、恢复默认都在官方弹窗内完成,插件不再自建录键页面。键位持久化由官方
535
+ 服务负责(Desktop:`userData/keybindings.json`;Web:`localStorage` 的
536
+ `dsh.keybindings.v1`)。旧版插件 `keybindings` 配置的单候选自定义值按当前平台
537
+ 一次性尝试迁入官方存储;旧配置始终保留供其他平台及人工恢复,多候选、冲突、
538
+ 不支持或未绑定的旧值不会被部分迁入或删除。
539
+
540
+ v0.13.0 在 0.12 官方接管的基础上修正真实 `normalized` 绑定与物理 Control/Meta
541
+ 的转换;首次打开命令面板/QuickOpen 等入口按当前会话 scope 路由,不要求编辑器先挂载。
542
+ Web 工作区搜索使用官方当前平台允许的三修饰符组合,最终显示与冲突判断以官方目录为准。
543
+ v0.13.1 起插件不再自建录键界面(原独立「快捷键」设置页与共享录制模块已移除),
544
+ 改由通用页的「打开官方快捷键配置」入口直达官方弹窗。
539
545
 
540
546
  设置 →「VSCodeMode」页:
541
547
 
542
548
  | 分区 | 说明 |
543
549
  |---|---|
544
- | 通用 | 编辑器宽度(180–560)等 / 系统集成卡片 / 文件链接使用工具 / 开发形态开关 |
550
+ | 通用 | 编辑器宽度(180–560)等 / 系统集成卡片 / 文件链接使用工具 / 开发形态开关 / 官方快捷键配置入口 |
545
551
  | 我的 MCP / 项目 MCP | MCP 服务器可视化管理(stdio / streamable-http) |
546
552
  | 性能优化 | 会话盘点 / 移出归档 / 压缩调优 / 侧车摘要指引 |
547
553
  | 兼容性 | 兼容性报告(外部插件探测、路由 / 重复装配护栏)、插件技能组装载状态、旧版形态引导 |
@@ -575,21 +581,27 @@ npm pack # 产物 tgz(含 lib/assets/src/cordis.patch.yml)
575
581
 
576
582
  ### 开发形态(devForm)
577
583
 
578
- 开发 = profile 中以 `link:` 依赖 + junction 指向工作区的安装。设置 → 通用在开发
579
- 形态开启时显示「关闭开发形态」开关(切回正式版安装,重启生效);AI 后续开发时
580
- 可经 RPC 自动开启 / 关闭:
584
+ 开发形态 = 当前运行 profile 以 `link:` 依赖和链接入口指向源码目录。设置 → 通用
585
+ 可查看和切换形态;切换成功后需要重启 DSH 才能加载新的 Host 代码。
581
586
 
582
- ```bash
583
- # 读取当前形态(compat 报告亦含 devForm 字段)
584
- curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' \
585
- -d '{"method":"vscode.devFormGet","args":{}}'
586
- # 开启开发形态(link 到工作区;path 必填,重启后生效)
587
- curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' \
588
- -d '{"method":"vscode.devFormSet","args":{"enabled":true,"path":"D:/Work/ToolsDev/DeepSeekHarnessPlugin/packages/dsh-edit-review"}}'
589
- # 关闭开发形态(改回 ^<version> + 删 junction + pnpm install,重启后生效)
590
- curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' \
591
- -d '{"method":"vscode.devFormSet","args":{"enabled":false}}'
592
- ```
587
+ - **定位当前 profile**:优先使用官方 `profileContext.dir`;旧宿主仅在依赖本插件的
588
+ profile 候选唯一时回退。服务信息无效或候选有歧义时拒绝切换,不猜测 `web` 目录。
589
+ - **失败恢复**:写入前备份 manifest、lockfile 和原安装入口;安装失败时恢复原内容
590
+ 与链接身份。若恢复本身失败,会保留备份位置供处理,不能宣称已经恢复成功。
591
+ - **开发目录校验**:要求绝对路径、正确包身份,且不位于被替换的 profile
592
+ `node_modules` 内;移除链接只处理链接本身,不递归删除源码。
593
+ - **运行程序**:有官方 `profileContext.packageManager` 时使用它提供的程序、参数和
594
+ 显式环境(含 Desktop 自带运行方式);旧宿主再回退 PATH。
595
+
596
+ 程序化调用继续使用插件已有 RPC,但须经当前已鉴权应用的同源通道;未携带有效
597
+ Host 身份的裸 HTTP/curl 请求不再是支持的接入方式。请求地址应保留应用子路径,
598
+ Desktop 应沿用当前 `dsh-app:` 应用上下文,不拼接固定 `localhost` 端口。
599
+
600
+ | 方法 | `args` | 行为 |
601
+ |---|---|---|
602
+ | `vscode.devFormGet` | `{}` | 读取当前形态,兼容性报告也包含 `devForm` |
603
+ | `vscode.devFormSet` | `{ "enabled": true, "path": "<源码绝对路径>" }` | 开启开发形态;按返回的 `restart` 重启 |
604
+ | `vscode.devFormSet` | `{ "enabled": false }` | 切回当前插件版本的正式依赖;按返回的 `restart` 重启 |
593
605
 
594
606
  ### 源码结构
595
607
 
@@ -637,8 +649,11 @@ pdf.js vendor + 图标 + launcher)、`skills/`(插件技能组)、`src/`
637
649
  - **持久化**:工作区旁车 `.dsh-edit-review.json`(version 2,按 cwd 分桶,写前
638
650
  合并,v1 自动迁移);归档 `.dsh-edit-review-archive.json`(按 path+batch
639
651
  合并批次,含每 hunk 决策与 before)。
640
- - **RPC**:静态包经 webServer 精确路由 `/edrv/rpc`,Client 同源 fetch,载荷形状
641
- 由 `shared/rpc` 类型化。
652
+ - **RPC 与资源路由**:`/edrv` 接口先经官方 `connection.requestRejection(req)`
653
+ 校验 Host 身份,认证能力缺失时拒绝请求。RPC 校验方法、JSON content-type、
654
+ envelope 和对象参数,同时检查 Content-Length 与实际读取字节上限;响应不缓存。
655
+ Client 按应用 document base 解析 RPC、资源与 vendor URL,保留反向代理子路径,
656
+ 支持 HTTP(S) 和 Desktop 的 `dsh-app:` 来源。
642
657
  - **批次 / 融合 / 归档**:每次新 edit/write 递增文件 batch,早于最新批次的未归档
643
658
  差异自动"融合"归档;每条差异处理完成立即单条归档;DiffLauncher「归档」页按
644
659
  批次浏览 + 回滚。
@@ -659,8 +674,15 @@ pdf.js vendor + 图标 + launcher)、`skills/`(插件技能组)、`src/`
659
674
  `client/` 加组件,其余模块零改动;`monaco/*` 是可复用的编辑器服务(资源树 /
660
675
  对比 / 诊断面板共用);`edrvSidebarPanels` 注册表承载后续面板(搜索 / 差异 /
661
676
  时间线),`edrv.listDir` 为通用目录树 API。
662
- - ⚠️ **Host 改动需重启 DSH 应用**(Node ESM 模块缓存);Client 经
663
- `dsh-client-hmr` 热重载。
677
+ - **Host 模块来源**:动态加载优先锚定运行中 DSH 的入口安装树;已解析模块的执行
678
+ 错误不会被旧开发依赖副本掩盖。本轮官方 Host 审计与现场基线是 `0.1.7-rc.2`,
679
+ 依赖声明、源码能力探测和现场验证是不同层次,详见 [版本适配矩阵](docs/version-adaptation.md)。
680
+ - **子进程环境与回收**:LSP/DAP 等共用环境清理,按变量名排除父环境中的
681
+ KEY/PASSWORD/SECRET/TOKEN 与 `DSH_*` 隐式信息;用户显式配置的环境仍会传递。
682
+ Windows 使用进程树终止,POSIX 使用独立进程组;回收是 best effort,不能保证
683
+ 清除主动脱离进程组或受系统权限限制的后代进程。
684
+ - **代码更新生效**:Host 改动需要重启 DSH;Client 需要重建产物并刷新原应用页面。
685
+ 只有对应开发构建 watcher 和 HMR 接收端同时工作时,才可依赖自动更新。
664
686
 
665
687
  ### CI(GitHub Actions)
666
688
 
@@ -746,6 +768,20 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
746
768
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
747
769
  近期关键版本:
748
770
 
771
+ - **v0.13.1**:**快捷键入口收敛到官方弹窗**——移除插件自建的独立「快捷键」设置页
772
+ (含行内录键编辑器与共享录制模块 `shortcutRecorder.ts`),改在 VSCodeMode → 通用页
773
+ 提供「打开官方快捷键配置」一键跳转(`openOfficialShortcuts()`;官方入口不可用时
774
+ 就地给出手动打开方式)。键位查看、录键、冲突校验与恢复默认全部由 DSH 官方弹窗承担,
775
+ 避免同一套录键逻辑两处维护;0.13.0 的绑定转换修复与 Web 三修饰符默认值不变。
776
+ - **v0.13.0**:**Host 鉴权与 Desktop 适配、项目 MCP 三面隔离、外部打开回执**——
777
+ `/edrv` 路由接入官方认证与有界请求体读取,URL 保留应用子路径并支持 `dsh-app:`;
778
+ 项目 MCP 改为每 agent 挂载,隔离工具/资源/指令,新增配置态、未验证状态与实例数;
779
+ 开发形态切换使用实际 `profileContext` 并支持失败恢复;子进程环境清理与进程树
780
+ 回收收敛;在 **0.12 已有官方快捷键接管**上修复 normalized 绑定、录制收尾及
781
+ 按会话 scope 首开。C#/PowerShell/Bash launcher 与 Unity 0.3.0 共用私有
782
+ OPEN/lease ACK 链路,真正加载目标 model/预览后才确认;Unity 首装自动配置
783
+ profile bridge,无需注册 OS 菜单。迁移、部署、回滚和验证边界见
784
+ [0.13.0 发布说明](docs/release-0.13.0.md)。
749
785
  - **v0.12.0**:**用户规则双目录兼容 + 官方快捷键接管**——用户规则同时读取
750
786
  `DSH_HOME/rules/` 和 `~/.dsh/rules/`,同名以当前目录为准,旧规则原地管理,
751
787
  列表与提示词注入采用同一优先级;快捷键迁移到 DSH 官方 shortcuts 目录及独立设置页,
@@ -1,182 +1,98 @@
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
1
+ // Windows C# 5 launcher; shared producer owns private queue validation and ACK handling.
2
+ // @author ddj 2026-09-28
8
3
  using System;
9
4
  using System.Collections.Generic;
10
5
  using System.Diagnostics;
11
6
  using System.IO;
12
- using System.Net;
13
- using System.Net.Sockets;
14
7
  using System.Text;
15
- using System.Threading;
16
- using System.Web.Script.Serialization;
17
8
  using System.Windows.Forms;
18
9
 
19
10
  internal static class DshOpen
20
11
  {
21
- private const string DefaultBase = "http://127.0.0.1:3080";
22
- private const int MaxPaths = 20;
23
-
12
+ /// <summary>Run the configured producer and report unconfirmed opens. @author ddj 2026-09-28</summary>
13
+ /// <param name="args">Paths supplied by Explorer.</param>
14
+ /// <returns>Zero only when the producer confirms an ACK (or no paths were supplied).</returns>
24
15
  [STAThread]
25
- private static void Main(string[] args)
16
+ private static int Main(string[] args)
26
17
  {
27
- string baseUrl = ReadBase();
28
- if (!ServerAlive(baseUrl))
18
+ if (args.Length == 0) return 0;
19
+ try { return Run(args); }
20
+ catch (Exception error)
29
21
  {
30
- MessageBox.Show(
31
- "DSH Web UI 未运行(" + baseUrl + ")。\n请先启动 DSH,再使用「在 DSH 文件编辑中打开」。",
32
- "DSH 文件编辑",
33
- MessageBoxButtons.OK,
34
- MessageBoxIcon.Information);
35
- return;
22
+ MessageBox.Show(error.Message, "DSH 文件编辑", MessageBoxButtons.OK, MessageBoxIcon.Warning);
23
+ return 1;
36
24
  }
37
- List<string> paths = CollectPaths(args);
38
- if (paths.Count == 0) return;
39
- if (HandoffAndConfirm(baseUrl, paths)) return;
40
- OpenUrl(BuildUrl(baseUrl, paths));
41
25
  }
42
26
 
43
- /// <summary>把参数解析为绝对路径清单(上限 MaxPaths,非法项跳过)。</summary>
44
- private static List<string> CollectPaths(string[] args)
27
+ /// <summary>Read installed metadata and wait for the queue producer. @author ddj 2026-09-28</summary>
28
+ /// <param name="paths">Paths to open.</param><returns>Confirmed producer exit status.</returns>
29
+ private static int Run(string[] paths)
45
30
  {
46
- var paths = new List<string>();
47
- foreach (string raw in args)
31
+ string folder = AppDomain.CurrentDomain.BaseDirectory;
32
+ string ini = Path.Combine(folder, "dsh-open.ini");
33
+ Dictionary<string, string> config = ReadIni(ini);
34
+ string executable = RequiredPath(config, "node");
35
+ string helper = RequiredPath(config, "helper");
36
+ string mode;
37
+ if (!config.TryGetValue("nodeMode", out mode) || (mode != "node" && mode != "electron"))
38
+ throw new InvalidDataException("Missing nodeMode in DSH bridge configuration.");
39
+ var arguments = new StringBuilder(Quote(helper) + " --config " + Quote(ini) + " --");
40
+ foreach (string path in paths) arguments.Append(" " + Quote(Path.GetFullPath(path)));
41
+ var start = new ProcessStartInfo(executable, arguments.ToString());
42
+ start.UseShellExecute = false;
43
+ start.CreateNoWindow = true;
44
+ start.RedirectStandardError = true;
45
+ if (mode == "electron") start.EnvironmentVariables["ELECTRON_RUN_AS_NODE"] = "1";
46
+ else start.EnvironmentVariables.Remove("ELECTRON_RUN_AS_NODE");
47
+ using (Process child = Process.Start(start))
48
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);
49
+ string error = child.StandardError.ReadToEnd();
50
+ child.WaitForExit();
51
+ if (child.ExitCode != 0) throw new IOException(string.IsNullOrWhiteSpace(error) ? "DSH OPEN was not confirmed." : error);
52
+ return 0;
54
53
  }
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
54
  }
71
55
 
72
- private static void OpenUrl(string url)
56
+ /// <summary>Read an existing INI; missing metadata fails closed. @author ddj 2026-09-28</summary>
57
+ /// <param name="file">Installed config path.</param><returns>Case-insensitive values.</returns>
58
+ private static Dictionary<string, string> ReadIni(string file)
73
59
  {
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
60
+ var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
61
+ foreach (string line in File.ReadAllLines(file, Encoding.UTF8))
81
62
  {
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();
63
+ int equal = line.IndexOf('=');
64
+ if (equal > 0) values[line.Substring(0, equal).Trim()] = line.Substring(equal + 1).Trim();
92
65
  }
93
- catch (Exception) { return null; }
66
+ return values;
94
67
  }
95
68
 
96
- /// <summary>宽松 JSON 解析(失败返回 null)。</summary>
97
- private static Dictionary<string, object> ParseJson(string json)
69
+ /// <summary>Require a fully qualified existing file. @author ddj 2026-09-28</summary>
70
+ /// <param name="config">INI values.</param><param name="key">Metadata key.</param><returns>Validated path.</returns>
71
+ private static string RequiredPath(Dictionary<string, string> config, string key)
98
72
  {
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; }
73
+ string value;
74
+ if (!config.TryGetValue(key, out value) || !Path.IsPathRooted(value) ||
75
+ !string.Equals(Path.GetFullPath(value), value, StringComparison.OrdinalIgnoreCase) || !File.Exists(value))
76
+ throw new InvalidDataException("Missing absolute " + key + " in DSH bridge configuration.");
77
+ if (key == "node" && !string.Equals(Path.GetExtension(value), ".exe", StringComparison.OrdinalIgnoreCase))
78
+ throw new InvalidDataException("DSH requires a stored Node or Electron executable, not a command shim.");
79
+ return value;
165
80
  }
166
81
 
167
- /// <summary>把路径清单编码合并为一个深链 URL(空清单 → null)。</summary>
168
- private static string BuildUrl(string baseUrl, List<string> paths)
82
+ /// <summary>Quote Windows argv with backslash-before-quote handling. @author ddj 2026-09-28</summary>
83
+ /// <param name="value">Literal argument.</param><returns>One command-line argument.</returns>
84
+ private static string Quote(string value)
169
85
  {
170
- StringBuilder url = new StringBuilder(baseUrl.TrimEnd('/') + "/?edrvOpen=1");
171
- int count = 0;
172
- foreach (string path in paths)
86
+ var result = new StringBuilder("\"");
87
+ int slashes = 0;
88
+ foreach (char ch in value)
173
89
  {
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++;
90
+ if (ch == '\\') { slashes++; continue; }
91
+ result.Append('\\', ch == '"' ? slashes * 2 + 1 : slashes);
92
+ result.Append(ch);
93
+ slashes = 0;
179
94
  }
180
- return count == 0 ? null : url.ToString();
95
+ result.Append('\\', slashes * 2);
96
+ return result.Append('"').ToString();
181
97
  }
182
98
  }