dsh-vscode-mode 0.23.5 → 0.24.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/README.md CHANGED
@@ -1,13 +1,34 @@
1
1
  # dsh-vscode-mode
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/dsh-vscode-mode?logo=npm&label=npm)](https://www.npmjs.com/package/dsh-vscode-mode)
4
+ [![GitHub release](https://img.shields.io/github/v/release/Lenonss/DSH_VsCodeMode?label=release)](https://github.com/Lenonss/DSH_VsCodeMode/releases)
5
+ [![CI](https://github.com/Lenonss/DSH_VsCodeMode/actions/workflows/ci.yml/badge.svg)](https://github.com/Lenonss/DSH_VsCodeMode/actions/workflows/ci.yml)
6
+ [![license](https://img.shields.io/github/license/Lenonss/DSH_VsCodeMode)](LICENSE)
7
+
3
8
  > DSH 上的**类 VSCode 编码体验**:Monaco 文件编辑器 + Agent 差异审查 + LSP 智能。
4
9
  > 由 `@dsh-external/dsh-edit-review` 重构而来。
5
10
 
11
+ ![同屏工作台:中央 AI 对话、右侧 Monaco 编辑器与底部差异操作条](docs/screenshots/workbench.png)
12
+
6
13
  - 📝 **同屏编辑**:DSH 0.1.5+ 官方右侧 Sidebar,AI 对话与文件编辑同屏
7
14
  - 🔍 **差异审查**:Keep / Undo / 回滚 / 归档,状态持久化重启不丢
8
15
  - 🧠 **LSP 智能**:转到定义 / 查找引用 / Peek 多结果选择 / 智能补全(注释索引)/ 签名帮助
16
+ - 🐞 **断点调试**:DAP 桥通用化,行内断点、`.dsh/launch.json`、四段调试面板
9
17
  - ⌨️ **命令面板**:`Ctrl+Shift+P`,19 条命令开箱即用,可编程扩展
10
18
 
19
+ **安装**:`npm i dsh-vscode-mode`(需 DSH 0.1.5+,装完重启 DSH)→ 详见[安装](#安装)。
20
+
21
+ **能力速览**
22
+
23
+ | 场景 | 你能得到什么 |
24
+ | --- | --- |
25
+ | 同屏编码 | 中央对话 + 右侧 Monaco,官方 Sidebar 多标签 / 分栏 / 浮出 / 全屏 |
26
+ | 审改动 | 行内红绿差异、累计基线待审、Keep / Undo / 回滚 / 归档,重启不丢 |
27
+ | 懂代码 | LSP:定义 / 引用 / Peek / 补全 / 签名帮助,按文件语言自动选后端 |
28
+ | 调问题 | DAP:断点、调用堆栈、变量、监视、调试控制台 REPL(Lua attach / .NET) |
29
+ | 管文件 | 活动栏 + 资源管理器 + 右键菜单 + 工作区搜索 + SVN 状态徽标 |
30
+ | 看文档 | Markdown 预览、PDF 注释编辑、Excel 基础编辑与只读数据预览 |
31
+
11
32
  ## 目录
12
33
 
13
34
  - [特性](#特性)
@@ -336,7 +357,7 @@ DSH 启动回放 `~/.dsh/sessions` 全部会话(V8 展开约 10×,可能 OOM
336
357
  - 治理原则:存量靠归档、增量靠压缩调优 + 短会话习惯 + 体积指示器;packChunks
337
358
  (chunk 打包存储)为 DSH 默认开启,无需再配置。
338
359
 
339
- ### 14. 外部改动自动同步(即将发布)
360
+ ### 14. 外部改动自动同步(v0.4.4)
340
361
 
341
362
  磁盘文件被外部改动(其他编辑器 / Unity / 脚本 / agent 工具 / 同步盘)后自动
342
363
  跟进,不必手点 ⟳:
@@ -453,7 +474,7 @@ TortoiseSVN(`TortoiseProc.exe`)仅作 Windows 过渡增强:自研能力覆
453
474
 
454
475
  > 说明:`plans/`(含对标蓝图与实测结论)为本地开发资料,不入库。
455
476
 
456
- ### 17. 断点调试(DAP,开发中)
477
+ ### 17. 断点调试(DAP,v0.6.0)
457
478
 
458
479
  编辑器内断点调试(DAP 桥通用化:适配器由扩展清单 `contributes.debuggers` 驱动,
459
480
  不限定语言;Lua 链路复用已装 emmylua 扩展适配器,**零游戏侧改动**):
@@ -517,29 +538,47 @@ DSH 每次启动监听随机端口,`http://127.0.0.1:<port>` 就是浏览器 o
517
538
  `DSH_HOME/dsh-vscode-mode/logs/debug.<hashOf(cwd)>.log`(无会话期诊断落在
518
539
  `debug.<hashOf('ui-state')>.log`)。
519
540
 
541
+ ### 19. 远程访问兼容(经第三方远程访问插件访问)
542
+
543
+ 宿主 `/edrv/*` 的准入是 `connection.requestRejection()`,**Host 围栏优先于登录态**:
544
+ 只认 loopback、监听 bind IP 与 `trustedHosts`。远程设备访问时 `Host` 不符即 403,
545
+ 与是否已经登录无关(401 才是登录失效)。第三方远程访问插件
546
+ (`@linxin666/dsh-remote-web-ui` 等)把 `/api/`、`/sidebar/`、`/git/`、`/pet/` 改写到它的
547
+ `/remote` 通道,`/edrv/` 不在白名单内,所以文件管理器 RPC 与 Monaco 静态资源全部直连
548
+ 宿主被拒。
549
+
550
+ 本插件按「通道前缀」适配(`src/shared/remoteChannel.ts`):
551
+
552
+ - **判定**:远程通道在 `<head>` 解析期安装座位对象 `__DSH_REMOTE_CHANNEL_BOOT__`;
553
+ 存在即认为本页经远程通道访问,前缀取 `/remote`,否则为空串。
554
+ - **覆盖**:`/edrv/rpc`(业务 RPC 与二进制预览)、Monaco 的 `loader.js` / AMD 模块 /
555
+ `editor.main.css` / codicon 字体 / 语言 Worker(随 `paths.vs` 继承)、pdf.js 的
556
+ worker 与 cmaps、`/edrv/assets/*` 图片全部走 `transportUrl()`,`appUrl()` 继续负责
557
+ 应用 base 与桌面 `dsh-app://` 协议,不硬编码主机或端口。
558
+ - **为什么不是 `/api/edrv`**:远程插件的补丁只改 fetch / EventSource / WebSocket 与
559
+ `<img|script|iframe>.src`,`<link rel=stylesheet>`、CSS `url()`、`new Worker`、
560
+ ESM `import` 都不经过补丁;服务端 `/remote/<内部路径>` 才是覆盖全资源类型的正式通道。
561
+ - **安全与兼容**:不新增路由、不改 `admitRoute`、不公开任何静态资源;`/remote` 门禁
562
+ 仍要求已配对设备(其设备 cookie 同源携带),转发到回环后本插件的权限校验照常执行;
563
+ 本地与桌面入口保持 `/edrv/*` 原样,未装远程插件时行为不变。
564
+
520
565
  ## 安装
521
566
 
522
567
  官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
523
- profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
524
- **v0.23.2** 修复 Keep 在非活跃端(桌面端)点了没反应,共两处成因。①权限:`edrv.review.decide` 原先一律要求本端活跃会话,而同端详情接口的 `capability.keep` 只按审查库推导,于是会话在另一实例时按钮亮着、点击后抛错并被兜底成 HTTP 500「DSH 请求失败(HTTP 500)」。现按写入副作用区分——官方记录 Keep 只写共享审查库(`BEGIN IMMEDIATE` + operationId 幂等 + `id/seq/revision/current/decision` CAS,库为每工作区 SQLite 且 `WAL + busy_timeout=2000`),冷端放行;官方 Undo 需校验会话证据并写回源文件、待审投影条目写本机待审 JSON(无跨进程锁),两者仍限本端活跃会话,并返回可读的跨端原因而非 500。详情响应新增 `capability.live`,冷端差异栏禁用 Undo 并给出跨端说明、Keep 提示补一句「写入共享审查库,仍可执行」,批量预检对冷端待审条目给出准确原因。②选中目标:已结算(`oldText === newText`,累计净变化为零)的待审投影 `capability` 恒为不可 Keep,但单文件通道把它排在官方记录之前、客户端取 `items[0]`,同路径官方待审行又被历史过滤掉,于是差异栏「待审 0」、Keep 禁用、点击无反应。现 `pendingPageOf()` 在交给官方审查通道前剔除已结算投影,官方待审记录重新成为审查目标。另外详情未加载时点 Keep 不再静默返回,改为可读提示;打开文件时若某条官方记录的改动已全部不在当前文件中(无可定位差异且缓冲区干净),自动按 Keep 结案(只标记不写盘),差异栏不再显示「本记录 N 处改动均已不在当前文件中」这类失效叙事;官方元数据首次载入期间差异栏显示 `—` 而不是按历史记录报数,避免重启瞬间闪出几十条假待审。验证:typecheck 0 error、目标单测 57/57、`pnpm build` 通过,并用真实工作区审查库探针确认差异栏首条已回到官方记录且 `capability={keep:true,undo:true,live:true}`;桌面端 GUI 点按验收因 App 重启后恢复的是另一个会话而未完成,缺口保留,见 [0.23.2 发布说明](docs/release-0.23.2.md)。以下保留历史版本事实:
525
- **v0.23.1** 修复「Keep All / Undo All」在工作区待审超过一页时必定中止:批量按钮原先复用官方审查的分页接口逐页取待审条目,并把每页返回的展示用计数当成全量总数校验,因此官方待审超过 50 条(本仓库常态约 150 条)时会先报「待审记录在分页期间变化」或最终报「待审记录数量不完整」,实际一条都不会被决策(已完成 0 项)。现在批量操作改走一次性完整快照——`edrv.review.list` 新增 `all: true` 参数,服务端循环分页取全官方待审行,与工作区历史改动投影按路径去重后返回 `nextCursor: null` 与真实总数(`pending`),客户端单次取快照、校验条目唯一且仍为待审后才逐项决策;快照总量超过 2000 条时明确拒绝并提示分批处理,条目在决策途中变化则继续整批中止、不做部分写入。差异栏的日常浏览仍走原分页接口,展示行为不变。本版修复只经单测与真实审查库只读核对,未在重启后的 GUI 上点按批量按钮验证,缺口保留,见 [0.23.1 发布说明](docs/release-0.23.1.md)。以下保留历史版本事实:
526
- **v0.23.0** 的快捷菜单:编辑器内一个可拖动悬浮按钮,点开是自定义菜单树(组 / 按钮,最多 5 层),按钮可携带提示词并直接发送给 AI 或只填入输入框;配置存于 `<DSH_HOME>/dsh-vscode-mode/data/quick-menu.json`,带 `revision` 乐观锁与损坏降级;节点可限定生效工作区(默认全部),按钮可绑定技能与规则,发送时按当前会话可见性校验后只拼装通过的绑定。新增 `skills.list` 只读 RPC 枚举「全局技能 + 当前工作区技能」作为候选,配置界面的工作区、技能、规则全部改为下拉弹层/多选下拉,菜单设置重排为四段并统一按钮盒模型(修掉「导入 JSON」下移不居中)。本版**未做 GUI 真机验收**(本机 pnpm store 损坏、插件未部署),缺口已获用户接受,见 [0.23.0 发布说明](docs/release-0.23.0.md)。以下保留历史版本事实:
527
- **v0.22.0** 的累计基线待审(非 UI 能力对齐 dsh-diff-approval):同一文件的多次编辑折叠成一条待审、基线保留最早内容,A→B→C 显示 A→C、A→B→A 自然归零;Keep 只推进基线不改磁盘,Revert 走版本守卫写回并读回校验,撤销/重做用游标模型跨重启保留;新增 `str_replace_editor` 预像配对、`fs/observed` 终像、已入列路径的外部改动吸收与缺失/不可读生命周期、git/svn/Perforce 基线导入,以及旧 sidecar 写入口对已接管路径的服务端拒绝。差异仍在编辑器内行内展示、块级 Keep/Undo 用药丸,文件级在差异栏;详情面板与批注(含「让 AI 应答」)本版移除。验收边界:p4 仅有假 shell 单测(本机无 Perforce 客户端),「让 AI 应答」随批注一并下线故不再验证;本版未在宿主上重启验收 GUI 全流程,见 [0.22.0 发布说明](docs/release-0.22.0.md)。以下保留历史版本事实:
528
- **v0.21.4** 的任务发送结果分类与诚实草稿降级:只有明确 accepted 才等待分析方案;unknown/submitted/cancelled 不另填草稿或重发,安全降级只填实际当前会话草稿并提示手动发送。旧 boolean 接口保留,legacy true 不等于受理。开发 link 客户端 rev458288ddb012 的真实公开入口与目标受理已验;完整 SVN 分析/方案投递因 `SetNamedSecurityInfoW failed (Win32 5)` 未完成,用户仅本批接受此缺口,不代表 Host 整体升级或当前实例已安装 npm 0.21.4。见 [0.21.4 发布说明](docs/release-0.21.4.md)。保留以下历史版本事实:
529
- **v0.21.3** 的 MCP 初始化失败结果与安全收尾解耦:已进入 startup 的调用者在失败或截止时间到达时及时收到拒绝,不再等待旧资源收尾;旧 work/close 未确认完成前仍占冷启动名额、阻止同 key 重建,确认收尾后才开始冷却。首次偶发超时根因仍未知,排队和其他屏障仍可能无界;本版当前 Host/GUI 未部署未验证,用户仅本批接受官方实装隔离模块与 JS 套件替代验收,见 [0.21.3 发布说明](docs/release-0.21.3.md)。官方 bridge 隔离 fixture 同步迁移到当前公开 native 执行器及批调用契约,不代表生产执行器故障修复。以下为历史版本事实,不代表本版现场验收:
530
- **v0.21.2** 的新版 DSH 会话 Controller 与 MCP 公开工具目录适配:任务发送按目标会话持有生命周期,受理不明时不跨通道重发;文件范围引用的剪贴板与降级文本保留行区间;MCP 初始化失败增加脱敏持久化诊断。当前 Web Host 已完成真实 CodeGraph 调用,但首次连接偶发超时根因尚未证实,不宣称已修复,见 [0.21.2 发布说明](docs/release-0.21.2.md)。保留以下历史版本能力与验收边界:
531
- **v0.21.1** 修复缺失历史文件:按完整快照显示整文件删除,Keep 保留删除、Undo 安全恢复快照,预览只读且拒绝覆盖重新出现的文件;真实 GUI 验收缺口已明确保留,见 [0.21.1 发布说明](docs/release-0.21.1.md)。保留 **v0.21.0** 新增 XLSX 基础编辑与浏览器导出副本,原件不自动写回,高级应用通过官方 Host 打开;官方差异定位统一 BOM 与 LF/CRLF/混合行尾口径,并区分内容缺失和多处命中。定位修复不扩展 CRLF patch Undo 能力,目标官方记录 ↑/↓ 的真实点击验收仍保留缺口。保留 **v0.20.5** 跨 DSH 实例只读互通——桌面端与 Web 端各自运行独立宿主时,官方审查列表/详情与工作区只读接口改按持久化会话头的 cwd 解析,另一实例创建的会话也能只读打开官方差异栏,写入类接口仍要求本端活跃会话。保留 **v0.20.4** 统一两套差异栏外观——非官方记录栏角标由内部术语「侧车记录」改为「历史改动」,悬停说明 Keep 只标记已看过、不改写磁盘,Undo 逐项校验后写回,两者都不影响官方审查;官方元数据不可用时给出可读原因与重试按钮,不再静默降级;「会话没在当前端激活」由红色告警改为中性说明。保留 v0.20.3 修复官方差异全部无法定位时差异栏无提示、上下箭头静默禁用(显示「无可跳转」与失效原因,Keep 说明只标记已采纳、不改写磁盘,会话缺失等宿主错误改为可读提示)、v0.20.2 的 MCP 状态告警收敛与原生确认弹窗,以及 v0.20.1 的文件内差异展示、块级/文件级安全审查和跨分页批量操作。升级后重启 DSH 并刷新页面;持久化与回退边界见 [0.20.0 发布说明](docs/release-0.20.0.md);从 0.14.2 或更旧版本升级还须阅读
532
- [0.14.3 数据迁移与回退说明](docs/release-0.14.3.md)。
568
+ profile 请替换为实际目标,不要把示例目录当成当前运行 profile。
569
+
570
+ > **升级须知**:近期版本的修复要点、验收缺口与回退边界见[更新日志](#更新日志);
571
+ > 从 0.14.2 或更旧版本升级还须阅读[0.14.3 数据迁移与回退说明](docs/release-0.14.3.md)。
533
572
 
534
573
  ```bash
535
574
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
536
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.23.5
575
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.24.0
537
576
 
538
577
  # ② npm 注册表(发布到 npm 后)
539
- dsh plugin --profile web add dsh-vscode-mode@0.23.5
578
+ dsh plugin --profile web add dsh-vscode-mode@0.24.0
540
579
 
541
580
  # ③ GitHub Release tgz 直装(Release asset 可用后)
542
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.23.5/dsh-vscode-mode-0.23.5.tgz
581
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.24.0/dsh-vscode-mode-0.24.0.tgz
543
582
  ```
544
583
 
545
584
  > **DSH 版本政策(0.17.0 起)**:六项 DSH peer 仍统一声明为 `*`,另以
@@ -666,15 +705,29 @@ v0.13.1 起插件不再自建录键界面(原独立「快捷键」设置页与
666
705
 
667
706
  ## 截图
668
707
 
669
- ![侧边栏编辑形态:AI 对话与文件编辑同屏](docs/screenshots/img1.png)
708
+ > 以下截图取自 v0.23.5 的真实运行界面(DSH Web GUI + dsh-vscode-mode 右侧栏)。
709
+
710
+ ### 同屏工作台
711
+
712
+ ![同屏工作台:中央 AI 对话、右侧 Monaco 编辑器与底部差异操作条](docs/screenshots/workbench.png)
670
713
 
671
- > 右侧栏(DSH 0.1.5+ 官方 Sidebar / 旧版 betterSidebar)内的 Monaco 文件编辑器
672
- > 与中央 AI 对话同屏;差异条统一挂在对话输入框上方的原生 dock(编辑器未打开 =
673
- > 「差异 N 个文件 · 查看下一个」,打开后 = 完整 Keep / Undo 操作条)。
714
+ > AI 对话(中央)与 Monaco 文件编辑器(右侧官方 Sidebar)同屏;页签带差异角标,
715
+ > 底部原生 dock 汇总「差异 N 个文件」并提供 Keep All / Undo All。编辑器打开时
716
+ > 面板区可收起(`Ctrl+B` 切换,图标列常显),把宽度全部让给代码。
674
717
 
675
- ![文件编辑与差异审查界面(二)](docs/screenshots/img2.png)
718
+ ### 命令面板
676
719
 
677
- ![文件编辑与差异审查界面(三)](docs/screenshots/img3.png)
720
+ ![命令面板:Ctrl+Shift+P 列出视图 / 文件 / 导航 / 语言智能 / 代码片段 / 编辑 / 调试命令](docs/screenshots/command-palette.png)
721
+
722
+ > `Ctrl+Shift+P` 打开命令面板,命令按类别分组并支持模糊搜索;命令既可来自扩展清单
723
+ > 注册,也可由插件通过 `edrv.executeCommand` 编程调用。
724
+
725
+ ### 差异审查
726
+
727
+ ![差异审查:资源管理器文件树、编辑器行内差异与差异操作条](docs/screenshots/diff-review.png)
728
+
729
+ > 资源管理器展开工作区文件树(带差异角标),编辑器用行内红绿高亮展示 Agent 改动,
730
+ > 底部 dock 可逐文件切换、Keep / Undo,并提示当前记录还有几个待审文件。
678
731
 
679
732
  ## 开发
680
733
 
@@ -720,7 +773,7 @@ Desktop 应沿用当前 `dsh-app:` 应用上下文,不拼接固定 `localhost`
720
773
  ```
721
774
  src/
722
775
  ├── index.ts Host 入口:name/inject/apply(薄装配,含兼容性报告启动日志)
723
- ├── shared/ ★ 双面契约(禁 node/react):types/rpc/compat/mcp
776
+ ├── shared/ ★ 双面契约(禁 node/react):types/rpc/compat/mcp + remoteChannel(远程通道前缀)
724
777
  ├── compat.ts ★ Host 兼容层:外部插件探测、路由/重复装配护栏、兼容性报告
725
778
  ├── devForm.ts Host 开发形态管理:link:/junction 切换(RPC vscode.devForm*)
726
779
  ├── model.ts Host 纯域逻辑(可单测):normalize/markDecision/summary/批次/归档
@@ -875,12 +928,51 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
875
928
  技能名必须 kebab-case + `dsh-vscodemode-` 前缀(下划线非法),frontmatter 用
876
929
  官方字段名(见[特性 12](#12-插件自带技能组v033))。
877
930
 
931
+ **8. 远程访问时文件管理器报「DSH 拒绝了当前页面的访问」、编辑器报「Monaco loader 加载失败」**
932
+
933
+ 根因:宿主的 `/edrv/*` 路由准入走 `connection.requestRejection()`,其中 **Host 围栏
934
+ (`isTrustedApiRequest`)先于任何登录态校验**——只放行 loopback / 监听 bind IP /
935
+ 声明过的 `trustedHosts`。手机、平板或隧道域名访问时 `Host` 既不是 loopback 也不在
936
+ 白名单,因此**登录态再有效也一律 403**(`401` 才是登录失效)。远程访问插件
937
+ (`@linxin666/dsh-remote-web-ui`)只把 `/api/`、`/sidebar/`、`/git/`、`/pet/` 改写到
938
+ 它自己的 `/remote` 通道,`/edrv/` 不在其中,于是 RPC、Monaco 的 loader/AMD 模块/CSS/
939
+ 字体/Worker、pdf.js、预览图片全部直连宿主被拒。
940
+
941
+ 修复(**纯客户端**,宿主路由与鉴权零改动):远程通道在 `<head>` 解析期会
942
+ 安装 `window.__DSH_REMOTE_CHANNEL_BOOT__` 座位对象;检测到它时,插件把所有内部地址
943
+ (`/edrv/rpc`、`/edrv/vendor/*`、`/edrv/assets/*`)**统一前缀成 `/remote/edrv/...`**,
944
+ 由远程插件的服务端 catch-all 代理回环转发——该代理带同源设备 cookie 并换用进程内凭据
945
+ 重发到 `127.0.0.1`,于是我们的 Host 围栏与登录校验**照常执行且通过**;任何子资源类型
946
+ (`<link>`、CSS `url()`、`new Worker`、ESM `import`)都覆盖,不依赖主线程 fetch 补丁。
947
+ 本地页面、桌面 `dsh-app://`、未装远程插件时前缀为空,行为与之前完全一致。
948
+
949
+ - 未做也不建议:把所有 `/edrv/` 硬改成 `/api/edrv/`(补丁不覆盖 `link.href`/Worker/模块
950
+ import,会留下半成品);公开 vendor 路由;要求手工配 `trustedHosts`。
951
+ - 诊断:加载失败会先 HEAD 探测真实状态码再给结论——403 指「宿主拒绝了当前页面」、
952
+ 401 指登录态失效、404 指资源缺失、0 指请求没到宿主;远程通道 403 若是设备未配对,
953
+ 会直接提示「远程访问设备尚未配对」。
954
+
878
955
  ## 更新日志
879
956
 
880
957
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
881
- 近期关键版本:
882
958
 
883
- - **v0.23.5**:修复差异栏把同一文件算成两条(差异文件数偏大)。待审条目存的是写入时的相对路径,官方导航用绝对身份,而 `mergedPage()` 与 `diffSummary` 的去重都只按 path 建覆盖集,相对路径的待审行压不掉绝对身份的官方行,于是同一文件同时留下待审行与官方汇总行;现两侧覆盖集都补入条目身份(与官方 identity 同源)。另修复本会话新建的文件被渲染成一堆修改块:累计基线保留「最早内容」,新建文件首次采集时磁盘已有会话自己的中间产物,基线因此非空,「新增 516 行」被切成若干 `-`/`+` 混合块,与官方整文件新增(`before=false`、`oldLines=0`)口径相反;现按每个文件在该会话内最早的官方记录判定整文件新增,命中时待审侧基线置空,列表与详情只出一块全加差异、增删计数等于整文件行数,`addedFiles()` 查询失败则回退旧口径。验证:typecheck 0 error;全量单测通过;`npm run build` 通过;本版未做 GUI 点按验收(缺口保留)。见 [0.23.5 发布说明](docs/release-0.23.5.md)。- **v0.23.4**:修复桌面端点计划卡「查看全文」会先闪开一页官方计划预览、随即自动关闭。根因:官方卡片走 `ctx.sidebarRight.openResource(planAddress(callId))`,而本插件以 extension 优先级认领了 `dsh-resource://plan/**` 与 `dsh-resource://plan-review/**`,于是侧栏先按本插件类型建出一个页签(正文「正在打开计划预览…」),挂载后才把计划转进「文件编辑」、再把这个页签关掉——用户看到的就是这次建而即关。现由新增 `src/client/planDirect.ts` 在装配期包装 `sidebarRight.openResource`:只接管「2 段计划地址 + 会话为在屏会话」,正文依次取回顾卡片的 `params.planReview`、插件侧地址→正文缓存、可选 `resources.source()` 资源流(上限 1.5s),拿到正文后经 `requestOpen(..., { preview: true })` 直通本插件编辑器的只读计划页签,不再创建侧栏页签;非计划地址、跨会话与 subagent 5 段地址、正文不可得、编辑器 5s 未回执、以及服务对象只读导致包装赋值失败,一律回落原生 `openResource`,原「认领 + 挂载后转移」链路保留为后备。`openReceipt` 新增可选 `timeoutMs`,`planState` 新增有界(64 条)地址→正文缓存并由 live `exit_plan_mode` 写入。验证:typecheck 0 error;`tests/planDirect.test.ts`(新增 9 例)、`tests/planBridge.test.ts`(13)、`tests/planState.test.ts`(10)、`tests/openReceipt.test.ts`(9)四套 41/41;`npm run build` 双 bundle 通过(含客户端产物完整性校验);浏览器确认新 bundle 已加载且 Console 无「计划直通装配失败」;GUI 点按由用户在本机 Web 端手动复验后发布。见 [发布说明](docs/release-0.23.4.md)。
959
+ | 版本 | 要点 |
960
+ | --- | --- |
961
+ | v0.24.0 | 经第三方远程访问插件时,文件管理器与 Monaco 恢复可用;加载失败提示按状态区分 |
962
+ | v0.23.5 | 同一文件不再算成两条差异;新建文件按整文件新增渲染 |
963
+ | v0.23.4 | 计划卡「查看全文」直通编辑器,不再闪一下再关 |
964
+ | v0.23.3 | 已删除文件的差异条目不再卡死;重启后不再成串「采集不完整」 |
965
+ | v0.23.2 | 桌面端 Keep 点了没反应的问题修复 |
966
+ | v0.23.1 | Keep All / Undo All 在待审超过一页时不再中止 |
967
+ | v0.23.0 | 快捷菜单:可拖动悬浮按钮 + 自定义菜单树 |
968
+
969
+ <details>
970
+ <summary>完整版本记录(v0.24.0 → v0.1.5x,点击展开)</summary>
971
+
972
+
973
+ - **v0.24.0**:修复经第三方远程访问插件(如 `@linxin666/dsh-remote-web-ui`)访问 DSH Web GUI 时,文件管理器报「DSH 拒绝了当前页面的访问」、编辑器报「Monaco loader 加载失败」。根因在宿主准入顺序:`connection.requestRejection()` 先用 `isTrustedApiRequest` 判 Host/Origin,不属于回环、监听地址或 `trustedHosts` 就**直接 403**,根本不看登录态(401 才是认证失败),而本插件的 `/edrv/rpc` 与 `/edrv/vendor/**` 都不在远程插件的改写白名单里,于是 RPC、Monaco 的脚本/CSS/字体/Worker、pdf.js 模块 import 与角标图片全部直连宿主被拒。现按远程通道选路径前缀:`window.__DSH_REMOTE_CHANNEL_BOOT__` 存在(远程插件在 `<head>` 解析期安装的座位对象)时,所有插件内部路径改挂其已接管的 `/remote` catch-all(`/remote/edrv/...`),本地与桌面入口保持 `/edrv/...` 原样。新增 `src/shared/remoteChannel.ts`(`channelPrefix` / `transportPath` / `transportUrl`,仍经 `appUrl` 继承应用基址与 `dsh-app:`),RPC、二进制预览、Monaco 基址、pdf.js 基址与角标图片统一走它;**服务端零改动**,不新增路由、不取消鉴权、不读也不写任何凭据,`/remote` 门禁仍要求已配对设备。另新增 `src/client/loadFailure.ts`:加载失败先 HEAD 探测真实状态,403(Host 围栏)/401(登录失效)/404(资源或路由缺失)/0(请求未到达宿主)分别给不同提示,且只带 pathname 与状态码;设备未配对(403 `code:'unpaired'`)直接提示去配对,不再把所有 Monaco 失败都说成「检查资源是否完整」。验证:typecheck 0 error;全量 284 文件、3691 通过 + 22 跳过;`node --test tests/nativeOpen.test.mjs` 9/9;build 四 bundle 通过;本机 GUI 全链 200 并确认无 cookie 仍 401;远程通道用本地回环代理复刻转发语义后,138/138 请求落 `/remote/edrv/…`、目录/读/写/保存与 Monaco+Worker 全部通;**真机 + 真插件的配对门禁与 TLS 未验收(缺口保留)**。见 [0.24.0 发布说明](docs/release-0.24.0.md)。
974
+ - **v0.23.5**:修复差异栏把同一文件算成两条(差异文件数偏大)。待审条目存的是写入时的相对路径,官方导航用绝对身份,而 `mergedPage()` 与 `diffSummary` 的去重都只按 path 建覆盖集,相对路径的待审行压不掉绝对身份的官方行,于是同一文件同时留下待审行与官方汇总行;现两侧覆盖集都补入条目身份(与官方 identity 同源)。另修复本会话新建的文件被渲染成一堆修改块:累计基线保留「最早内容」,新建文件首次采集时磁盘已有会话自己的中间产物,基线因此非空,「新增 516 行」被切成若干 `-`/`+` 混合块,与官方整文件新增(`before=false`、`oldLines=0`)口径相反;现按每个文件在该会话内最早的官方记录判定整文件新增,命中时待审侧基线置空,列表与详情只出一块全加差异、增删计数等于整文件行数,`addedFiles()` 查询失败则回退旧口径。验证:typecheck 0 error;全量单测通过;`npm run build` 通过;本版未做 GUI 点按验收(缺口保留)。见 [0.23.5 发布说明](docs/release-0.23.5.md)。
975
+ - **v0.23.4**:修复桌面端点计划卡「查看全文」会先闪开一页官方计划预览、随即自动关闭。根因:官方卡片走 `ctx.sidebarRight.openResource(planAddress(callId))`,而本插件以 extension 优先级认领了 `dsh-resource://plan/**` 与 `dsh-resource://plan-review/**`,于是侧栏先按本插件类型建出一个页签(正文「正在打开计划预览…」),挂载后才把计划转进「文件编辑」、再把这个页签关掉——用户看到的就是这次建而即关。现由新增 `src/client/planDirect.ts` 在装配期包装 `sidebarRight.openResource`:只接管「2 段计划地址 + 会话为在屏会话」,正文依次取回顾卡片的 `params.planReview`、插件侧地址→正文缓存、可选 `resources.source()` 资源流(上限 1.5s),拿到正文后经 `requestOpen(..., { preview: true })` 直通本插件编辑器的只读计划页签,不再创建侧栏页签;非计划地址、跨会话与 subagent 5 段地址、正文不可得、编辑器 5s 未回执、以及服务对象只读导致包装赋值失败,一律回落原生 `openResource`,原「认领 + 挂载后转移」链路保留为后备。`openReceipt` 新增可选 `timeoutMs`,`planState` 新增有界(64 条)地址→正文缓存并由 live `exit_plan_mode` 写入。验证:typecheck 0 error;`tests/planDirect.test.ts`(新增 9 例)、`tests/planBridge.test.ts`(13)、`tests/planState.test.ts`(10)、`tests/openReceipt.test.ts`(9)四套 41/41;`npm run build` 双 bundle 通过(含客户端产物完整性校验);浏览器确认新 bundle 已加载且 Console 无「计划直通装配失败」;GUI 点按由用户在本机 Web 端手动复验后发布。见 [发布说明](docs/release-0.23.4.md)。
884
976
  - **v0.23.3**:修复 issue #13(已删除文件的差异条目把人卡死)与 #12(重启后成串「采集不完整」)。①文件在磁盘上被删掉时,客户端申请整文件删除预览被「已有官方审查记录」拒绝,而拒绝只是一条普通 `Error`,于是落到通用加载失败分支——「文件加载失败:…」+ 进度钉在 0%、重试同错、标签页关不掉;`edrv.review.detail` 还把 Undo 不可用解释成「缺少可验证的原始字节」,而真实原因是文件已不在。现缺失文件预览的可预期拒绝带稳定码(`official-covered` / `official-decided` / `no-records` / `no-evidence` / `too-large`),客户端据此显示「文件已在磁盘删除(· 有官方待审改动)」终态:标题 + 原因 + 重试 / 关闭标签页,不再画 0% 进度条;官方待审记录在磁盘已删除时给出「Keep 按删除结案、Undo 需要读写该文件」的准确原因(Keep 语义不变,仍只写共享审查库),记录都已结案时提示可直接关闭;`!review` 的磁盘状态抖动改为最多重读 2 次后给终态,消除缺失↔重现的无限递归。v0.22.0 起 `hasOfficial()` 已用 `typeof path !== 'string'` 守卫堵掉把不透明 FsTarget 绑进 SQL 导致的 `Unknown named parameter 'targetKey'`,本版不再复现。②官方 `workspace/changes` 的 summary 只存在宿主内存里,插件启动补采原先无条件把历史事件全部入队,重启后每一轮都必然抛「summary 已失效或尚不可用」并写成不可修复的 `gap`(面板成串「轮次 N 采集不完整」);现补采先 `summary()` 探测再入队,失效事件只记一条汇总日志。验证:typecheck 与基线同为 37 个既有错误(`fflate/browser` 依赖缺失)、零新增;定向单测 111/111(含新增 `tests/deletedFileReview.test.ts` 5 例与 `tests/officialBackfill.test.ts` 2 例)、EditorView 相关 82/82;`npm run build` 四个 bundle 通过。见 [发布说明](docs/release-0.23.3.md)。
885
977
  - **v0.23.2**:修复 Keep 在非活跃端(桌面端)点了没反应,两处成因。①`edrv.review.decide` 原先一律要求本端活跃会话,而能力位只按审查库推导,会话在另一实例时按钮亮着、点击后抛错被兜底成 HTTP 500。现按写入副作用区分:官方记录 Keep 只写共享审查库(`BEGIN IMMEDIATE` + 幂等键 + 版本 CAS),冷端放行;官方 Undo(校验证据并写回源文件)与待审投影条目(本机 JSON 无跨进程锁)仍限活跃端,返回可读跨端原因而非 500;详情新增 `capability.live`,冷端禁用 Undo 并说明、Keep 提示补「写入共享审查库,仍可执行」。②已结算待审投影的 `capability.keep` 恒为假,却因排在 `items[0]`、同路径官方待审行被历史过滤,把官方记录挤出差异栏(表现为「待审 0」+Keep 禁用+点击无反应);现 `pendingPageOf()` 剔除已结算投影,官方记录重新成为审查目标。另详情未加载时点 Keep 不再静默失败;打开文件时若某条官方记录的改动已全部不在当前文件中(无可定位差异且缓冲区干净),自动按 Keep 结案(只标记不写盘),差异栏不再显示「本记录 N 处改动均已不在当前文件中」;官方元数据首次载入期间差异栏显示 `—` 而非按历史记录报数,重启瞬间不再闪出几十条假待审。验证:typecheck 0 error、目标单测 57/57、build 通过、真实审查库探针确认首条回到官方记录且 `capability={keep:true,undo:true,live:true}`;桌面端 GUI 点按验收因重启后恢复的是另一个会话未完成,缺口保留。发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.2.md)。
886
978
  - **v0.23.1**:修复 Keep All / Undo All 在待审超过一页时必定中止——批量按钮原先复用分页接口并把每页的展示用计数当全量总数校验,官方待审 150 条时先报「待审记录在分页期间变化」或最终报「待审记录数量不完整」,实际 0 项被决策。现改为一次性完整快照(`edrv.review.list` 新增 `all: true`,服务端循环取全并与历史改动投影去重,`nextCursor: null`、`pending` 为真实总数),客户端单次取快照、校验唯一且仍待审后逐项决策;超过 2000 条明确拒绝并提示分批,途中变化仍整批中止不部分写入。差异栏浏览仍走原分页接口。本版未在重启后的 GUI 点按验证,缺口保留;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.1.md)。
@@ -1057,8 +1149,10 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
1057
1149
  多选比较 / 区间 / Stop on copy / Include merged revisions / Statistics)、
1058
1150
  并排差异(基线 / 版本间 / 与工作副本)、诊断日志查看器。
1059
1151
  - **v0.4.x**:LSP 与 Monaco 系列修复(见[常见问题](#常见问题));外部改动自动
1060
- 同步即将发布。
1152
+ 同步(v0.4.4)。
1061
1153
  - **v0.3.x**:命令栏(v0.2.0)→ 代码片段 / 快捷引用(v0.3.0)→ 插件技能组
1062
1154
  (v0.3.3)。
1063
1155
  - **v0.1.5x**:官方侧边栏形态(v0.1.60 推荐)、PDF 浏览编辑(v0.1.55)、
1064
1156
  系统集成(v0.1.53)、规则管理(v0.1.49 / 50)。
1157
+
1158
+ </details>
package/lib/client.js CHANGED
@@ -3040,6 +3040,61 @@ window.__ModuleLoader__.load({
3040
3040
  return new URL(path.slice(1), root).href;
3041
3041
  }
3042
3042
  //#endregion
3043
+ //#region src/shared/remoteChannel.ts
3044
+ /**
3045
+ * 远程访问通道适配:插件内部路径 → 远程传输可达路径。
3046
+ *
3047
+ * 背景:宿主 connection 插件对 Host 不是 loopback / bind 地址 / trustedHosts 的请求一律回 403
3048
+ * (Host 围栏先于任何 cookie 校验),因此 /edrv/rpc 与 /edrv/vendor/** 在局域网 IP 或隧道域名
3049
+ * 页面上必然被拒。第三方远程访问插件(@linxin666/dsh-remote-web-ui)提供 /remote/<内部路径>
3050
+ * 的 catch-all 代理:校验配对设备后回环重发到本进程,本插件自身的准入照旧执行。
3051
+ *
3052
+ * 该插件在 <head> 首部内联一段 classic script(解析期同步执行,早于任何 module script),并在
3053
+ * window 上安装 seat 对象作为「本页已接入通道」的唯一标记。本模块据此选前缀:
3054
+ * 有标记 ⇒ '/remote' + 原路径;无标记 ⇒ 原路径(本机、桌面 dsh-app:、未装该插件均不受影响)。
3055
+ * 只决定 URL 前缀,不读取、不写入、不转发任何 cookie 或凭据。
3056
+ *
3057
+ * Purity rule: no node/react imports(与 appUrl.ts 一致)。
3058
+ * @author ddj 2026年11月02号
3059
+ */
3060
+ /** 远程访问插件在解析期安装的通道 seat 全局名(其 REMOTE_CHANNEL_BOOT_GLOBAL)。 */
3061
+ const CHANNEL_SEAT = "__DSH_REMOTE_CHANNEL_BOOT__";
3062
+ /** 远程访问插件接管的前缀:/remote/<内部路径> 由服务端回环转发。 */
3063
+ const CHANNEL_PREFIX = "/remote";
3064
+ /**
3065
+ * 当前页面的通道前缀。
3066
+ * @author ddj 2026年11月02号
3067
+ * @returns 本页已接入远程通道时为 '/remote',否则空串(保持既有本地路径)。
3068
+ */
3069
+ function channelPrefix() {
3070
+ if (typeof window === "undefined") return "";
3071
+ try {
3072
+ const seat = window[CHANNEL_SEAT];
3073
+ return seat && typeof seat === "object" ? CHANNEL_PREFIX : "";
3074
+ } catch (e) {
3075
+ return "";
3076
+ }
3077
+ }
3078
+ /**
3079
+ * 给插件内部路径加通道前缀。
3080
+ * @author ddj 2026年11月02号
3081
+ * @param path 以 '/' 开头的插件内部路径(合法性由 {@link transportUrl} 内的 appUrl 兜底校验)
3082
+ * @returns 带通道前缀的路径;本机页面原样返回
3083
+ */
3084
+ function transportPath(path) {
3085
+ return channelPrefix() + path;
3086
+ }
3087
+ /**
3088
+ * 把插件内部路径解析为当前页面可请求的地址(继承通道前缀、反向代理前缀与桌面协议)。
3089
+ * @author ddj 2026年11月02号
3090
+ * @param path 以 '/' 开头的插件内部路径
3091
+ * @param base 测试用显式 document base;缺省同 appUrl(document.baseURI → location.href)
3092
+ * @returns 应用地址;无浏览器环境时返回原根路径
3093
+ */
3094
+ function transportUrl(path, base) {
3095
+ return appUrl(transportPath(path), base);
3096
+ }
3097
+ //#endregion
3043
3098
  //#region src/shared/rpc.ts
3044
3099
  /** webServer 精确路由。 */
3045
3100
  const RPC_PATH = "/edrv/rpc";
@@ -3127,7 +3182,7 @@ window.__ModuleLoader__.load({
3127
3182
  */
3128
3183
  async function tryBinaryRead(args) {
3129
3184
  try {
3130
- const res = await fetch(appUrl(RPC_PATH), {
3185
+ const res = await fetch(transportUrl(RPC_PATH), {
3131
3186
  method: "POST",
3132
3187
  headers: { "content-type": "application/json" },
3133
3188
  body: JSON.stringify({
@@ -3271,7 +3326,7 @@ window.__ModuleLoader__.load({
3271
3326
  }
3272
3327
  /** @private @author ddj 2026年10月10号 @param method Typed method. @param args Request. @param signal Fetch lifetime. @returns Validated authenticated business response. @throws Transport/format errors. */
3273
3328
  async function rpcFetch(method, args, signal) {
3274
- const res = await fetch(appUrl(RPC_PATH), {
3329
+ const res = await fetch(transportUrl(RPC_PATH), {
3275
3330
  method: "POST",
3276
3331
  credentials: "same-origin",
3277
3332
  headers: { "content-type": "application/json" },
@@ -3281,16 +3336,60 @@ window.__ModuleLoader__.load({
3281
3336
  }),
3282
3337
  signal
3283
3338
  });
3284
- if (!res.ok) {
3285
- const message = res.status === 401 ? "DSH 登录已失效,请重新打开已登录的应用页面" : res.status === 403 ? "DSH 拒绝了当前页面的访问" : res.status === 413 ? "请求超过支持的文件大小" : res.status === 503 ? "DSH 认证服务尚未就绪" : "DSH 请求失败(HTTP " + res.status + ")";
3286
- throw new Error(message);
3287
- }
3339
+ if (!res.ok) throw new Error(await failureMessage(res));
3288
3340
  if (!/^application\/json(?:\s*;|\s*$)/i.test(res.headers.get("content-type") ?? "")) throw new Error("DSH 返回了非 JSON 响应,请检查应用地址和代理配置");
3289
3341
  const value = await res.json();
3290
3342
  if (!value || typeof value !== "object" || typeof value.ok !== "boolean") throw new Error("DSH RPC 响应格式不正确");
3291
3343
  return value;
3292
3344
  }
3293
3345
  /**
3346
+ * 非 2xx 响应 → 可读失败原因:403 再区分「远程设备未配对」与「Host 围栏拒绝」。
3347
+ * @author ddj 2026年11月02号
3348
+ * @param res 失败响应
3349
+ * @returns 用户可读原因
3350
+ */
3351
+ async function failureMessage(res) {
3352
+ if (res.status === 403) {
3353
+ if (await unpairedDenied(res)) return "远程访问设备尚未配对:请在远程访问面板完成配对后重试(HTTP 403)";
3354
+ return "DSH 拒绝了当前页面的访问(HTTP 403,Host 校验未通过)" + channelNote();
3355
+ }
3356
+ if (res.status === 401) return "DSH 登录已失效,请重新打开已登录的应用页面";
3357
+ if (res.status === 404) return "插件路由不存在(HTTP 404),请确认 dsh-vscode-mode 与宿主版本匹配";
3358
+ if (res.status === 413) return "请求超过支持的文件大小";
3359
+ if (res.status === 503) return "DSH 认证服务尚未就绪";
3360
+ return "DSH 请求失败(HTTP " + res.status + ")";
3361
+ }
3362
+ /**
3363
+ * 403 是否为远程访问通道的「未配对」拒绝(远程插件以 `{result:{ok:false,error:{code}}}` 回)。
3364
+ * @author ddj 2026年11月02号
3365
+ * @param res 403 响应
3366
+ * @returns 是否为未配对拒绝
3367
+ */
3368
+ async function unpairedDenied(res) {
3369
+ try {
3370
+ const body = await res.clone().json();
3371
+ return (body?.result?.error?.code ?? body?.error?.code) === "unpaired";
3372
+ } catch (e) {
3373
+ return false;
3374
+ }
3375
+ }
3376
+ /**
3377
+ * 网络来源页面却未接入远程通道时的补充提示(本机页面不追加)。
3378
+ * @author ddj 2026年11月02号
3379
+ * @returns 提示后缀;无需提示时为空串
3380
+ */
3381
+ function channelNote() {
3382
+ if (channelPrefix() !== "") return "";
3383
+ try {
3384
+ if (typeof location === "undefined") return "";
3385
+ const host = location.hostname;
3386
+ if (host === "localhost" || host === "::1" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host)) return "";
3387
+ return ";本页未接入远程访问通道(远程访问插件未安装或未完成配对)";
3388
+ } catch (e) {
3389
+ return "";
3390
+ }
3391
+ }
3392
+ /**
3294
3393
  * 诊断日志:开关关闭时直接返回(零 RPC 零落盘);开启时走 host edrv.debug 落盘。
3295
3394
  * @author ddj 2026年08月26号 / 2026年09月17号
3296
3395
  * @param sessionId 会话 id
@@ -5054,13 +5153,95 @@ window.__ModuleLoader__.load({
5054
5153
  return scheme === "light" || scheme === "dark" ? scheme : void 0;
5055
5154
  }
5056
5155
  //#endregion
5156
+ //#region src/client/loadFailure.ts
5157
+ /**
5158
+ * dsh-vscode-mode client — 加载失败诊断(把「加载失败」拆成可定位的四类原因)。
5159
+ *
5160
+ * 为什么需要:宿主对非 loopback/bind/trustedHosts 的 Host 一律回 403(Host 围栏),
5161
+ * 认证失效回 401,路由或资源缺失回 404,另有纯网络失败。此前 Monaco/pdf.js 的失败面板
5162
+ * 一律提示「检查资源是否完整」,远程访问时会把 403 误报成资源缺失,误导排查方向。
5163
+ *
5164
+ * 文案只输出 pathname 与状态码,不含 origin、查询串、cookie 或任何凭据。
5165
+ * @author ddj 2026年11月02号
5166
+ */
5167
+ /** 状态码 0:请求未完成(网络错误 / 被拦截 / 离线)。 */
5168
+ const NETWORK_FAILED = 0;
5169
+ /**
5170
+ * 探测资源地址的真实响应状态。
5171
+ * @author ddj 2026年11月02号
5172
+ * @param url 已解析的绝对或根路径地址
5173
+ * @returns HTTP 状态码;请求未完成时返回 0
5174
+ */
5175
+ async function probeStatus(url) {
5176
+ try {
5177
+ return (await fetch(url, {
5178
+ method: "HEAD",
5179
+ credentials: "same-origin"
5180
+ })).status;
5181
+ } catch (e) {
5182
+ return NETWORK_FAILED;
5183
+ }
5184
+ }
5185
+ /**
5186
+ * 取地址的 pathname(只用于诊断文案,避免暴露 origin 与查询串)。
5187
+ * @author ddj 2026年11月02号
5188
+ * @param url 绝对或根路径地址
5189
+ * @returns pathname;无法解析时原样返回
5190
+ */
5191
+ function pathOf$1(url) {
5192
+ try {
5193
+ return new URL(url, "http://localhost").pathname;
5194
+ } catch (e) {
5195
+ return url;
5196
+ }
5197
+ }
5198
+ /**
5199
+ * 状态码 → 可操作的原因说明(四类互不相同)。
5200
+ * @author ddj 2026年11月02号
5201
+ * @param status HTTP 状态码,0 表示请求未完成
5202
+ * @returns 原因说明
5203
+ */
5204
+ function statusHint(status) {
5205
+ if (status === NETWORK_FAILED) return "请求未到达宿主(网络不可达、被浏览器或代理拦截、或宿主已停止)";
5206
+ if (status === 403) return "宿主拒绝了当前页面(Host/Origin 校验未通过;远程访问需经远程访问插件的通道)";
5207
+ if (status === 401) return "宿主登录态失效,请重新打开已登录的应用页面";
5208
+ if (status === 404) return "路由或资源不存在(插件资源缺失或插件版本与宿主不匹配)";
5209
+ return "宿主返回了非预期状态";
5210
+ }
5211
+ /**
5212
+ * 组装加载失败文案:标签 + 资源路径 + 状态 + 原因。
5213
+ * @author ddj 2026年11月02号
5214
+ * @param label 失败对象(如 'Monaco loader 加载失败')
5215
+ * @param url 加载地址
5216
+ * @param status 探测到的状态码(0=网络错误)
5217
+ * @returns 用户可读的诊断文案
5218
+ */
5219
+ function loadFailureText(label, url, status) {
5220
+ const outcome = status === NETWORK_FAILED ? "请求未完成" : "返回 HTTP " + status;
5221
+ return label + ":" + pathOf$1(url) + " " + outcome + ";" + statusHint(status);
5222
+ }
5223
+ /**
5224
+ * Monaco 失败面板提示:按错误内容给出对应排查方向(不再一律提示「检查资源是否完整」)。
5225
+ * @author ddj 2026年11月02号
5226
+ * @param message 编辑器记录的失败原因
5227
+ * @returns 排查建议
5228
+ */
5229
+ function monacoHint(message) {
5230
+ const text = String(message ?? "");
5231
+ if (/403/.test(text)) return "宿主拒绝了本次请求:远程访问需经远程访问插件的通道(本页已自动接入);本机访问请用 127.0.0.1 或 localhost 打开。";
5232
+ if (/401/.test(text)) return "宿主登录态失效:请重新打开已登录的应用页面后再试。";
5233
+ if (/404/.test(text)) return "Monaco 静态资源未找到:插件包 assets/vendor/monaco 可能不完整,或插件版本与宿主不匹配。";
5234
+ if (/请求未完成|Failed to fetch|NetworkError/i.test(text)) return "请求未能到达宿主:请检查网络、代理与宿主进程状态。";
5235
+ return "请展开下方详情,并在浏览器控制台确认失败请求的地址与状态码。";
5236
+ }
5237
+ //#endregion
5057
5238
  //#region src/client/monaco/loader.ts
5058
5239
  /**
5059
5240
  * dsh-vscode-mode client — Monaco 加载与语言映射(AMD 构建,离线随包分发)。
5060
5241
  * 迁移自原 src/client/index.ts 的 MONACO_BASE/LANG_BY_EXT/langOf/loadMonaco,语义不改。
5061
5242
  * 作者 ddj 2026-08-20
5062
5243
  */
5063
- const MONACO_BASE = appUrl("/edrv/vendor/monaco/vs");
5244
+ const MONACO_BASE = transportUrl("/edrv/vendor/monaco/vs");
5064
5245
  let monacoPromise = null;
5065
5246
  let monacoStage = {
5066
5247
  phase: "idle",
@@ -5274,9 +5455,10 @@ window.__ModuleLoader__.load({
5274
5455
  s.src = MONACO_BASE + "/loader.js";
5275
5456
  s.dataset.edrvMonacoLoader = "1";
5276
5457
  s.onload = boot;
5277
- s.onerror = (event) => {
5278
- const hint = event && event.type ? "(" + event.type + ")" : "";
5279
- fail(/* @__PURE__ */ new Error("Monaco loader 加载失败" + hint));
5458
+ s.onerror = async () => {
5459
+ const loaderUrl = MONACO_BASE + "/loader.js";
5460
+ const status = await probeStatus(loaderUrl);
5461
+ fail(new Error(loadFailureText("Monaco loader 加载失败", loaderUrl, status)));
5280
5462
  };
5281
5463
  hideNodeGlobals();
5282
5464
  document.head.appendChild(s);
@@ -376673,8 +376855,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376673
376855
  * 用 script 注入而非打包内 import():客户端包是 CJS,bundler 会改写动态 import 破坏 URL 语义。
376674
376856
  * 作者 ddj 2026-09-22
376675
376857
  */
376676
- /** vendor 根,保留官方桌面协议及反向代理前缀。 */
376677
- const PDFJS_BASE = appUrl("/edrv/vendor/pdfjs");
376858
+ /** vendor 根,保留官方桌面协议、反向代理前缀与远程访问通道前缀。 */
376859
+ const PDFJS_BASE = transportUrl("/edrv/vendor/pdfjs");
376678
376860
  /** pdf.js Worker 地址(GlobalWorkerOptions.workerSrc)。 */
376679
376861
  const PDFJS_WORKER_SRC = PDFJS_BASE + "/build/pdf.worker.mjs";
376680
376862
  /** CJK 字符映射目录(中文 PDF 渲染必需,尾斜杠固定)。 */
@@ -376731,7 +376913,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376731
376913
  s.textContent = "import * as lib from \"" + PDFJS_BASE + "/build/pdf.mjs\";\nimport * as viewer from \"" + PDFJS_BASE + "/web/pdf_viewer.mjs\";\nwindow.__edrvPdfjs = { lib, viewer };\ndocument.querySelector(\"script[data-edrv-pdfjs-loader]\")?.dispatchEvent(new Event(\"edrv-pdfjs-ready\"));";
376732
376914
  s.dataset.edrvPdfjsLoader = "1";
376733
376915
  s.onload = boot;
376734
- s.onerror = () => fail(/* @__PURE__ */ new Error("pdf.js loader 脚本加载失败(检查 assets/vendor/pdfjs 完整 / /edrv/vendor 路由可达)"));
376916
+ s.onerror = async () => {
376917
+ const moduleUrl = PDFJS_BASE + "/build/pdf.mjs";
376918
+ const status = await probeStatus(moduleUrl);
376919
+ fail(new Error(loadFailureText("pdf.js 模块加载失败", moduleUrl, status)));
376920
+ };
376735
376921
  document.head.appendChild(s);
376736
376922
  });
376737
376923
  return pdfjsPromise;
@@ -398687,7 +398873,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398687
398873
  loadPage: loadTextPage
398688
398874
  });
398689
398875
  else if (!monaco && !monacoErr) body = loadingBody(loadStage.message, loadStage.progress);
398690
- else if (monacoErr) body = react$1.default.createElement("div", { className: "edrv-empty" }, react$1.default.createElement("div", null, "Monaco 编辑器加载失败:" + String(monacoErr)), react$1.default.createElement("div", { style: { fontSize: 11 } }, "请确认插件包 assets/vendor/monaco 完整(/edrv/vendor 路由可达)"), react$1.default.createElement("button", {
398876
+ else if (monacoErr) body = react$1.default.createElement("div", { className: "edrv-empty" }, react$1.default.createElement("div", null, "Monaco 编辑器加载失败:" + String(monacoErr)), react$1.default.createElement("div", { style: { fontSize: 11 } }, monacoHint(String(monacoErr))), react$1.default.createElement("button", {
398691
398877
  className: "edrv-pill edrv-pill-ghost",
398692
398878
  onClick: () => {
398693
398879
  setMonacoErr(null);
@@ -399540,7 +399726,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
399540
399726
  cursor: "pointer"
399541
399727
  }
399542
399728
  }, react$1.default.createElement("img", {
399543
- src: appUrl("/edrv/assets/compare-select.png"),
399729
+ src: transportUrl("/edrv/assets/compare-select.png"),
399544
399730
  alt: "差异",
399545
399731
  style: {
399546
399732
  width: 22,
package/lib/client.js.map CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/lib/client.js.map
3
- size: 35694514 bytes
4
- sha256: 7fa559b003ee01e2e53a8698796d4a4e567f8e63d0b6d00548272a0057026c6d
3
+ size: 35705679 bytes
4
+ sha256: 99ccafcfabf9448fefe75878b567b71a795cb2d17af9ac3077189a81c6a0eb09