dsh-vscode-mode 0.1.53 → 0.1.55

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 (210) hide show
  1. package/README.md +11 -0
  2. package/assets/vendor/pdfjs/build/pdf.mjs +26370 -0
  3. package/assets/vendor/pdfjs/build/pdf.worker.mjs +61080 -0
  4. package/assets/vendor/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  5. package/assets/vendor/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  6. package/assets/vendor/pdfjs/cmaps/78-H.bcmap +0 -0
  7. package/assets/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  8. package/assets/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  9. package/assets/vendor/pdfjs/cmaps/78-V.bcmap +0 -0
  10. package/assets/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  11. package/assets/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  12. package/assets/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  13. package/assets/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  14. package/assets/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  15. package/assets/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  16. package/assets/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  17. package/assets/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  18. package/assets/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  19. package/assets/vendor/pdfjs/cmaps/Add-H.bcmap +0 -0
  20. package/assets/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  21. package/assets/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  22. package/assets/vendor/pdfjs/cmaps/Add-V.bcmap +0 -0
  23. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  24. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  25. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  26. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  27. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  28. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  29. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  30. package/assets/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  31. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  32. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  33. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  34. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  35. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  36. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  37. package/assets/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  38. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  39. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  40. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  41. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  42. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  43. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  44. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  45. package/assets/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  46. package/assets/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  47. package/assets/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  48. package/assets/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  49. package/assets/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  50. package/assets/vendor/pdfjs/cmaps/B5-H.bcmap +0 -0
  51. package/assets/vendor/pdfjs/cmaps/B5-V.bcmap +0 -0
  52. package/assets/vendor/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  53. package/assets/vendor/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  54. package/assets/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  55. package/assets/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  56. package/assets/vendor/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  57. package/assets/vendor/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  58. package/assets/vendor/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  59. package/assets/vendor/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  60. package/assets/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  61. package/assets/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  62. package/assets/vendor/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  63. package/assets/vendor/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  64. package/assets/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  65. package/assets/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  66. package/assets/vendor/pdfjs/cmaps/EUC-H.bcmap +0 -0
  67. package/assets/vendor/pdfjs/cmaps/EUC-V.bcmap +0 -0
  68. package/assets/vendor/pdfjs/cmaps/Ext-H.bcmap +0 -0
  69. package/assets/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  70. package/assets/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  71. package/assets/vendor/pdfjs/cmaps/Ext-V.bcmap +0 -0
  72. package/assets/vendor/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  73. package/assets/vendor/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  74. package/assets/vendor/pdfjs/cmaps/GB-H.bcmap +4 -0
  75. package/assets/vendor/pdfjs/cmaps/GB-V.bcmap +0 -0
  76. package/assets/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  77. package/assets/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  78. package/assets/vendor/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  79. package/assets/vendor/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  80. package/assets/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  81. package/assets/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  82. package/assets/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  83. package/assets/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  84. package/assets/vendor/pdfjs/cmaps/GBT-H.bcmap +0 -0
  85. package/assets/vendor/pdfjs/cmaps/GBT-V.bcmap +0 -0
  86. package/assets/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  87. package/assets/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  88. package/assets/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  89. package/assets/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  90. package/assets/vendor/pdfjs/cmaps/H.bcmap +0 -0
  91. package/assets/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  92. package/assets/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  93. package/assets/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  94. package/assets/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  95. package/assets/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  96. package/assets/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  97. package/assets/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  98. package/assets/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  99. package/assets/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  100. package/assets/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  101. package/assets/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  102. package/assets/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  103. package/assets/vendor/pdfjs/cmaps/Hankaku.bcmap +0 -0
  104. package/assets/vendor/pdfjs/cmaps/Hiragana.bcmap +0 -0
  105. package/assets/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  106. package/assets/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  107. package/assets/vendor/pdfjs/cmaps/KSC-H.bcmap +0 -0
  108. package/assets/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  109. package/assets/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  110. package/assets/vendor/pdfjs/cmaps/KSC-V.bcmap +0 -0
  111. package/assets/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  112. package/assets/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  113. package/assets/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  114. package/assets/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  115. package/assets/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  116. package/assets/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  117. package/assets/vendor/pdfjs/cmaps/Katakana.bcmap +0 -0
  118. package/assets/vendor/pdfjs/cmaps/LICENSE +36 -0
  119. package/assets/vendor/pdfjs/cmaps/NWP-H.bcmap +0 -0
  120. package/assets/vendor/pdfjs/cmaps/NWP-V.bcmap +0 -0
  121. package/assets/vendor/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  122. package/assets/vendor/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  123. package/assets/vendor/pdfjs/cmaps/Roman.bcmap +0 -0
  124. package/assets/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  125. package/assets/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  126. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  127. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  128. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  129. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  130. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  131. package/assets/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  132. package/assets/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  133. package/assets/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  134. package/assets/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  135. package/assets/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  136. package/assets/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  137. package/assets/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  138. package/assets/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  139. package/assets/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  140. package/assets/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  141. package/assets/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  142. package/assets/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  143. package/assets/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  144. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  145. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  146. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  147. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  148. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  149. package/assets/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  150. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  151. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  152. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  153. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  154. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  155. package/assets/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  156. package/assets/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  157. package/assets/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  158. package/assets/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  159. package/assets/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  160. package/assets/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  161. package/assets/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  162. package/assets/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  163. package/assets/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  164. package/assets/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  165. package/assets/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  166. package/assets/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  167. package/assets/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  168. package/assets/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  169. package/assets/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  170. package/assets/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  171. package/assets/vendor/pdfjs/cmaps/V.bcmap +0 -0
  172. package/assets/vendor/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  173. package/assets/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
  174. package/assets/vendor/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
  175. package/assets/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
  176. package/assets/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  177. package/assets/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
  178. package/assets/vendor/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
  179. package/assets/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
  180. package/assets/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  181. package/assets/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
  182. package/assets/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
  183. package/assets/vendor/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
  184. package/assets/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION +102 -0
  185. package/assets/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
  186. package/assets/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  187. package/assets/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
  188. package/assets/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
  189. package/assets/vendor/pdfjs/web/pdf_viewer.css +6989 -0
  190. package/assets/vendor/pdfjs/web/pdf_viewer.mjs +9459 -0
  191. package/lib/client.js +852 -13
  192. package/lib/client.js.map +1 -1
  193. package/lib/index.js +137 -17
  194. package/lib/index.js.map +1 -1
  195. package/package.json +1 -1
  196. package/src/client/compat.ts +31 -0
  197. package/src/client/imagePreview.ts +45 -0
  198. package/src/client/index.ts +46 -8
  199. package/src/client/pdf/pdfLoader.ts +82 -0
  200. package/src/client/pdf/pdfPanel.ts +239 -0
  201. package/src/client/pdfPreview.ts +48 -0
  202. package/src/client/remoteOpenRouter.ts +78 -0
  203. package/src/client/styles/editor.css +17 -0
  204. package/src/client/ui/EditorView.ts +221 -2
  205. package/src/compat.ts +1 -1
  206. package/src/dshVersion.ts +1 -0
  207. package/src/routes.ts +3 -0
  208. package/src/rpc.ts +65 -13
  209. package/src/shared/rpc.ts +50 -2
  210. package/src/store.ts +2 -0
@@ -9,6 +9,9 @@ import React from 'react'
9
9
  import { dbg, rpc } from '../rpc.js'
10
10
  import { emitRefresh } from '../events.js'
11
11
  import { langOf, loadMonaco } from '../monaco/loader.js'
12
+ import { dataUrlOf, isImagePath, isSvgPath } from '../imagePreview.js'
13
+ import { base64ToBytes, isPdfPath } from '../pdfPreview.js'
14
+ import { createPdfPanel } from '../pdf/pdfPanel.js'
12
15
  import { applyTheme, registerThemes, themeNameOf } from '../monaco/theme.js'
13
16
  import { createDiffRenderer } from '../monaco/diffRender.js'
14
17
  import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
@@ -53,6 +56,14 @@ export function EditorView(props) {
53
56
  const [dirtyMap, setDirtyMap] = React.useState({})
54
57
  const [content, setContent] = React.useState(null)
55
58
  const [contentPath, setContentPath] = React.useState(null)
59
+ const [imageSrc, setImageSrc] = React.useState(null) // 图片预览 data URL(图片 tab 专用,文本 tab 恒为 null)
60
+ const [imgSize, setImgSize] = React.useState(null) // 图片自然尺寸 { w, h }(路径栏 meta)
61
+ const [imgBroken, setImgBroken] = React.useState(false) // 图片解码失败(onError),显示占位与重试
62
+ const svgTextRef = React.useRef(new Set()) // 强制以文本打开的 SVG 路径集合(toggleSvgText 维护)
63
+ const [pdfBytes, setPdfBytes] = React.useState(null) // 当前 PDF tab 的原始字节(null=加载中/非 PDF)
64
+ const pdfB64CacheRef = React.useRef(new Map()) // path → base64(tab 切回免重读;FIFO 上限防内存膨胀)
65
+ const pdfCtlRef = React.useRef(new Map()) // path → PDF 面板控制器(mount 产出,关闭/卸载销毁)
66
+ const pdfHostRef = React.useRef(null) // PDF 面板外壳 div(命令式控制器接管)
56
67
  const [status, setStatus] = React.useState('')
57
68
  const [lspServers, setLspServers] = React.useState([])
58
69
  const [error, setError] = React.useState(null)
@@ -117,6 +128,10 @@ export function EditorView(props) {
117
128
  }, [records, active])
118
129
 
119
130
  const contentReady = content !== null && contentPath === active
131
+ // 当前 tab 是否处于图片预览模式(与 loadContent 的分派条件同源;SVG 文本模式时为 false)
132
+ const isImageActive = !!active && isImagePath(active) && !svgTextRef.current.has(active)
133
+ // 当前 tab 是否为 PDF 面板(与 loadContent 分派条件同源)
134
+ const isPdfActive = !!active && isPdfPath(active)
120
135
  const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
121
136
  // useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
122
137
  const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
@@ -267,6 +282,10 @@ export function EditorView(props) {
267
282
  const closeTab = (path) => {
268
283
  flushSave()
269
284
  if (path === active) { saveViewState(path); recordNav() }
285
+ // PDF tab 关闭:销毁面板控制器并清缓存(未保存注释随之丢弃,脏点已提示)
286
+ const pdfCtl = pdfCtlRef.current.get(path)
287
+ if (pdfCtl) { pdfCtl.destroy(); pdfCtlRef.current.delete(path) }
288
+ pdfB64CacheRef.current.delete(path)
270
289
  setTabs((prev) => {
271
290
  const idx = prev.findIndex((t) => t.path === path)
272
291
  if (idx < 0) return prev
@@ -318,6 +337,16 @@ export function EditorView(props) {
318
337
  */
319
338
  const loadContent = (path, sid, force) => {
320
339
  const seq = ++loadSeqRef.current
340
+ // 图片文件走专用通道:base64 → data URL 预览,不建 Monaco model、不进文本/差异流程
341
+ if (isImagePath(path) && !svgTextRef.current.has(path)) {
342
+ loadImage(path, sid, seq)
343
+ return
344
+ }
345
+ // PDF 文件走专用通道:base64 → PDF 面板(浏览 + 注释编辑),不建 Monaco model
346
+ if (isPdfPath(path)) {
347
+ loadPdf(path, sid, seq, force === true)
348
+ return
349
+ }
321
350
  const cachedModel = force ? null : modelsRef.current.get(path)
322
351
  if (cachedModel) {
323
352
  // model 命中:直接显示其内容(切 tab 零等待),后台静默校验
@@ -361,6 +390,94 @@ export function EditorView(props) {
361
390
  })
362
391
  }
363
392
 
393
+ /**
394
+ * 加载图片文件为 data URL(编辑区只读预览);失败走 loadError 面板(重试经 loadContent 分派回此)。
395
+ * @author ddj 2026年09月08号
396
+ * @param path 图片文件路径
397
+ * @param sid 会话 id
398
+ * @param seq 加载序号(过期响应丢弃)
399
+ */
400
+ const loadImage = (path, sid, seq) => {
401
+ setImageSrc(null)
402
+ setImgSize(null)
403
+ setImgBroken(false)
404
+ setLoadStage({ progress: monaco ? 72 : 12, message: '读取图片…' })
405
+ rpc('edrv.read', { sessionId: sid, path, encoding: 'base64' }).then((res) => {
406
+ if (seq !== loadSeqRef.current || path !== active) return
407
+ if (res && res.ok && res.encoding === 'base64') {
408
+ if (!res.mime) {
409
+ setLoadError('host 版本过旧:图片响应缺少 mime')
410
+ setError('host 版本过旧:图片响应缺少 mime')
411
+ setStatus('读取失败')
412
+ return
413
+ }
414
+ setImageSrc(dataUrlOf(res.content, res.mime))
415
+ setLoadStage({ progress: 100, message: '图片已就绪' })
416
+ setStatus('已加载')
417
+ return
418
+ }
419
+ const base = res?.error ? String(res.error) : '读取失败'
420
+ // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
421
+ const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
422
+ setLoadError(message)
423
+ setError(message)
424
+ setStatus('读取失败')
425
+ }).catch((e) => {
426
+ if (seq !== loadSeqRef.current) return
427
+ const message = 'read异常:' + String(e)
428
+ setLoadError(message)
429
+ setError(message)
430
+ setStatus('读取失败')
431
+ })
432
+ }
433
+
434
+ /**
435
+ * 加载 PDF 文件为原始字节(编辑区 PDF 面板);命中 base64 缓存秒切,force 绕过缓存重读。
436
+ * 面板控制器由 pdf host mount effect 产出(per-path,关闭/卸载销毁)。
437
+ * @author ddj 2026年09月22号
438
+ * @param path PDF 文件路径
439
+ * @param sid 会话 id
440
+ * @param seq 加载序号(过期响应丢弃)
441
+ * @param force true=绕过缓存强制 RPC 重读(刷新按钮)
442
+ */
443
+ const loadPdf = (path, sid, seq, force) => {
444
+ setPdfBytes(null)
445
+ setLoadStage({ progress: monaco ? 72 : 12, message: '读取 PDF…' })
446
+ const cache = pdfB64CacheRef.current
447
+ const hit = !force ? cache.get(path) : undefined
448
+ if (typeof hit === 'string' && hit) {
449
+ // 刷新插入序(Map 按插入序 FIFO 驱逐)后秒切
450
+ cache.delete(path)
451
+ cache.set(path, hit)
452
+ setPdfBytes(base64ToBytes(hit))
453
+ setLoadStage({ progress: 100, message: 'PDF 已就绪' })
454
+ setStatus('已加载')
455
+ return
456
+ }
457
+ rpc('edrv.read', { sessionId: sid, path, encoding: 'base64' }).then((res) => {
458
+ if (seq !== loadSeqRef.current || path !== active) return
459
+ if (res && res.ok && res.encoding === 'base64') {
460
+ cache.set(path, res.content)
461
+ while (cache.size > 6) cache.delete(cache.keys().next().value)
462
+ setPdfBytes(base64ToBytes(res.content))
463
+ setLoadStage({ progress: 100, message: 'PDF 已就绪' })
464
+ setStatus('已加载')
465
+ return
466
+ }
467
+ const base = res?.error ? String(res.error) : '读取失败'
468
+ const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
469
+ setLoadError(message)
470
+ setError(message)
471
+ setStatus('读取失败')
472
+ }).catch((e) => {
473
+ if (seq !== loadSeqRef.current) return
474
+ const message = 'read异常:' + String(e)
475
+ setLoadError(message)
476
+ setError(message)
477
+ setStatus('读取失败')
478
+ })
479
+ }
480
+
364
481
  // 挂载轮询 + 事件订阅
365
482
  React.useEffect(() => {
366
483
  if (!sessionId) return
@@ -633,6 +750,10 @@ export function EditorView(props) {
633
750
  if (!active) return
634
751
  setContent(null)
635
752
  setContentPath(null)
753
+ setImageSrc(null)
754
+ setImgSize(null)
755
+ setImgBroken(false)
756
+ setPdfBytes(null)
636
757
  setLoadError(null)
637
758
  setLoadStage({ progress: 10, message: '准备读取文件…' })
638
759
  setStatus('加载中…')
@@ -701,8 +822,18 @@ export function EditorView(props) {
701
822
  }
702
823
 
703
824
  const doSave = (silent) => {
825
+ if (!active) return
826
+ // PDF tab:保存委托给面板控制器(saveDocument → base64 → edrv.saveBinary)
827
+ if (isPdfPath(active)) {
828
+ const ctl = pdfCtlRef.current.get(active)
829
+ if (!ctl) { if (!silent) setStatus('PDF 未就绪'); return }
830
+ if (!ctl.isDirty()) { if (!silent) setStatus('PDF 无未保存修改'); return }
831
+ void ctl.savePdf()
832
+ return
833
+ }
704
834
  const ed = editorRef.current
705
- if (!active || !ed) return
835
+ if (!ed) return
836
+ if (isImageActive) { setStatus('图片只读预览'); return }
706
837
  const text = ed.getValue()
707
838
  if (!silent) setStatus('保存中…')
708
839
  rpc('edrv.save', { sessionId, path: active, content: text }).then((res) => {
@@ -737,6 +868,30 @@ export function EditorView(props) {
737
868
  setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
738
869
  }, [monaco, active, content])
739
870
 
871
+ // PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
872
+ const ensurePdfHost = (node) => { pdfHostRef.current = node }
873
+
874
+ // PDF 面板装配:控制器挂进外壳 div,切换 tab/卸载即销毁(重开重挂,base64 缓存兜底秒切)
875
+ React.useEffect(() => {
876
+ const host = pdfHostRef.current
877
+ if (!isPdfActive || !pdfBytes || !host) return
878
+ const path = active
879
+ const ctl = createPdfPanel({
880
+ sessionId,
881
+ path,
882
+ setStatus,
883
+ setError,
884
+ onDirtyChange: (d) => setDirtyMap((prev) => Object.assign({}, prev, { [path]: d })),
885
+ onReload: () => loadContent(path, sessionId, true),
886
+ })
887
+ pdfCtlRef.current.set(path, ctl)
888
+ void ctl.mount(host, pdfBytes)
889
+ return () => {
890
+ ctl.destroy()
891
+ if (pdfCtlRef.current.get(path) === ctl) pdfCtlRef.current.delete(path)
892
+ }
893
+ }, [isPdfActive, pdfBytes, active])
894
+
740
895
  // 行内差异自绘(decorations / view zones / minus overlay)→ diffRenderer
741
896
  React.useEffect(() => {
742
897
  if (!monaco || !editorRef.current || !active || content === null) return
@@ -752,6 +907,8 @@ export function EditorView(props) {
752
907
  if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
753
908
  for (const m of modelsRef.current.values()) m.dispose()
754
909
  modelsRef.current.clear()
910
+ for (const ctl of pdfCtlRef.current.values()) ctl.destroy()
911
+ pdfCtlRef.current.clear()
755
912
  const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null
756
913
  if (root) {
757
914
  const ov = root.querySelector('.edrv-minus-overlay')
@@ -975,6 +1132,53 @@ export function EditorView(props) {
975
1132
  refreshRecords(skipStale === true)
976
1133
  }
977
1134
 
1135
+ /**
1136
+ * SVG 在图片预览与文本编辑间切换(按路径记忆;重载分派随集合状态自动路由)。
1137
+ * @author ddj 2026年09月08号
1138
+ * @param path SVG 文件路径
1139
+ */
1140
+ const toggleSvgText = (path) => {
1141
+ const next = new Set(svgTextRef.current)
1142
+ if (next.has(path)) next.delete(path)
1143
+ else next.add(path)
1144
+ svgTextRef.current = next
1145
+ setContent(null)
1146
+ setContentPath(null)
1147
+ setImageSrc(null)
1148
+ setImgSize(null)
1149
+ setImgBroken(false)
1150
+ setLoadError(null)
1151
+ setStatus(next.has(path) ? '已切换为文本模式' : '已切换为图片预览')
1152
+ loadContent(path, sessionId, true)
1153
+ }
1154
+
1155
+ /**
1156
+ * 图片预览面板:工具条(尺寸 meta / SVG 文本切换 / 刷新)+ 棋盘底自适应图片。
1157
+ * @author ddj 2026年09月08号
1158
+ */
1159
+ const imagePanel = () => React.createElement('div', { className: 'edrv-imgview' },
1160
+ React.createElement('div', { className: 'edrv-imgview-bar' },
1161
+ React.createElement('span', { className: 'edrv-imgview-meta' },
1162
+ imgBroken ? '图片无法显示' : (imgSize ? imgSize.w + '×' + imgSize.h : '…')),
1163
+ React.createElement('span', { style: { flex: 1 } }),
1164
+ (isSvgPath(active) ? React.createElement('button', {
1165
+ className: 'edrv-pill edrv-pill-ghost', title: '以文本方式查看/编辑(SVG 为文本格式)',
1166
+ onClick: () => toggleSvgText(active),
1167
+ }, '以文本打开') : null),
1168
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '重新加载图片', onClick: () => reloadFile() }, '⟳ 刷新')),
1169
+ React.createElement('div', { className: 'edrv-imgview-stage' },
1170
+ (imgBroken
1171
+ ? React.createElement('div', { className: 'edrv-imgview-broken' },
1172
+ React.createElement('div', null, '图片解码失败,文件可能已损坏'),
1173
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => reloadFile() }, '重试'))
1174
+ : React.createElement('img', {
1175
+ className: 'edrv-imgview-img',
1176
+ src: imageSrc,
1177
+ alt: active || '',
1178
+ onLoad: (e) => { setImgBroken(false); setImgSize({ w: e.target.naturalWidth, h: e.target.naturalHeight }) },
1179
+ onError: () => setImgBroken(true),
1180
+ }))))
1181
+
978
1182
  /**
979
1183
  * 关闭当前差异浮窗并清理 hover 锚点。
980
1184
  * @author ddj 2026年08月21号
@@ -1183,6 +1387,12 @@ export function EditorView(props) {
1183
1387
  const openPath = () => {
1184
1388
  const p = (pathDraft || '').trim()
1185
1389
  if (!p) return
1390
+ // 图片/PDF 文件跳过文本读探测(二进制会被 host 拒绝),直接进 tab 由专用预览接管
1391
+ if (isImagePath(p) || isPdfPath(p)) {
1392
+ openFile(p, false)
1393
+ setOpenInput(false); setPathDraft(''); setStatus('已打开'); setError(null)
1394
+ return
1395
+ }
1186
1396
  setStatus('打开中…')
1187
1397
  rpc('edrv.read', { sessionId, path: p }).then((res) => {
1188
1398
  if (res && res.ok) {
@@ -1214,7 +1424,8 @@ export function EditorView(props) {
1214
1424
  const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
1215
1425
  React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
1216
1426
  React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开文件'),
1217
- (active && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
1427
+ (active && !isImageActive && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
1428
+ (isImageActive && imgSize ? React.createElement('span', { className: 'edrv-pb-meta' }, imgSize.w + '×' + imgSize.h) : null),
1218
1429
  (cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
1219
1430
  (status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
1220
1431
 
@@ -1304,6 +1515,14 @@ export function EditorView(props) {
1304
1515
  setLoadStage({ progress: 10, message: '重新读取文件…' })
1305
1516
  loadContent(active, sessionId)
1306
1517
  })
1518
+ } else if (isImageActive && imageSrc) {
1519
+ body = imagePanel()
1520
+ } else if (isImageActive) {
1521
+ body = loadingBody(loadStage.message || '读取图片…', loadStage.progress)
1522
+ } else if (isPdfActive && pdfBytes) {
1523
+ body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active })
1524
+ } else if (isPdfActive) {
1525
+ body = loadingBody(loadStage.message || '读取 PDF…', loadStage.progress)
1307
1526
  } else if (content === null) {
1308
1527
  body = loadingBody(loadStage.message || '读取文件内容…', loadStage.progress)
1309
1528
  } else {
package/src/compat.ts CHANGED
@@ -129,7 +129,7 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
129
129
  }
130
130
 
131
131
  /** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
132
- const TESTED_DSH_MAX = '0.1.2-alpha.4'
132
+ const TESTED_DSH_MAX = '0.1.3-alpha.2'
133
133
 
134
134
  /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
135
135
  export function versionAdapters(dshVersion: string): CompatAdapter[] {
package/src/dshVersion.ts CHANGED
@@ -86,6 +86,7 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
86
86
  export function familyLabel(input: string): string {
87
87
  const version = parseDshVersion(input)
88
88
  if (!version) return '未知'
89
+ if (inDshRange(version, { from: '0.1.3-alpha.1' })) return '0.1.3-alpha 及更新(对话文件链接=remote.session.openWorkspacePath)'
89
90
  if (inDshRange(version, { from: '0.1.2-alpha.1' })) return '0.1.2-alpha 及更新(设置 API=settings.installSection)'
90
91
  return '0.1.0/0.1.1 rc 线(设置 API=installSettingsSection)'
91
92
  }
package/src/routes.ts CHANGED
@@ -19,6 +19,7 @@ const IMG_ROUTES = [
19
19
  /** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
20
20
  const VENDOR_MIME: Record<string, string> = {
21
21
  '.js': 'text/javascript; charset=utf-8',
22
+ '.mjs': 'text/javascript; charset=utf-8',
22
23
  '.css': 'text/css; charset=utf-8',
23
24
  '.json': 'application/json',
24
25
  '.map': 'application/json',
@@ -32,6 +33,8 @@ const VENDOR_MIME: Record<string, string> = {
32
33
  '.ttf': 'font/ttf',
33
34
  '.html': 'text/html; charset=utf-8',
34
35
  '.md': 'text/markdown',
36
+ '.bcmap': 'application/octet-stream',
37
+ '.pfb': 'application/octet-stream',
35
38
  }
36
39
 
37
40
  /** 图标目录(paths.ts 统一解析:config.imageDir 覆盖优先,否则插件包 assets/)。 */
package/src/rpc.ts CHANGED
@@ -5,9 +5,11 @@
5
5
  * 作者 ddj 2026-08-20
6
6
  */
7
7
  import type { DecideItem, DecideResult, RpcHandlerMap, RpcMethod, RpcRequestMap, RpcResult, RpcScope } from './shared/rpc.js'
8
+ import { binaryMimeOf } from './shared/rpc.js'
8
9
  import type { DiffRecord, RecordView } from './shared/types.js'
9
10
  import type { Ctx, Session } from './store.js'
10
11
  import {
12
+ BINARY_READ_CAP,
11
13
  READ_CAP,
12
14
  appendArchiveEntries,
13
15
  archiveRecords,
@@ -152,6 +154,36 @@ async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
152
154
  return { session, cwd }
153
155
  }
154
156
 
157
+ /**
158
+ * 手动保存收尾(edrv.save / edrv.saveBinary 共用):目录树失效 + 该路径 pending
159
+ * 文本差异记录标记 superseded 并归档(旧文本 diff 不得再应用到新内容上)。
160
+ * @author ddj 2026年09月22号
161
+ * @param ctx DSH 上下文
162
+ * @param registry 差异记录桶注册表
163
+ * @param sc requireSession 成功结果(session + cwd)
164
+ * @param path 保存的文件路径
165
+ */
166
+ async function afterManualSave(
167
+ ctx: Ctx,
168
+ registry: Registry,
169
+ sc: { session: Session; cwd: string },
170
+ path: string,
171
+ ): Promise<void> {
172
+ // 手动保存后目录树可能变化(新建/删除文件):父目录+祖先进失效,后台自愈。
173
+ invalidateIndex(ctx, sc.cwd, path)
174
+ const bucket = await bucketOf(registry, ctx, sc.cwd)
175
+ let changed = false
176
+ for (const rec of bucket.values()) {
177
+ if (rec.path === path && rec.superseded !== true) { rec.superseded = true; rec.at = new Date().toISOString(); changed = true }
178
+ }
179
+ if (changed) {
180
+ const done: DiffRecord[] = []
181
+ for (const rec of bucket.values()) if (rec.path === path && rec.superseded) done.push(rec)
182
+ if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, '被手动编辑覆盖')
183
+ else await saveBucket(ctx, sc.cwd, bucket, sc.session)
184
+ }
185
+ }
186
+
155
187
  /**
156
188
  * 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
157
189
  * rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
@@ -302,6 +334,13 @@ export function buildHandlers(
302
334
  // 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
303
335
  return { ok: false, error: '文件不存在', resolvedPath: fs.processPath(target) }
304
336
  }
337
+ if ((info.size ?? 0) > BINARY_READ_CAP) return { ok: false, error: '文件过大(>32MB),不支持整文件预览' }
338
+ if (args.encoding === 'base64') {
339
+ // 图片/PDF 等二进制预览:readBytes 无解码、无二进制拒绝;超上限已由上方 stat 拦截
340
+ const bytes = await fs.readBytes(target, undefined, BINARY_READ_CAP)
341
+ const content = Buffer.from(bytes).toString('base64')
342
+ return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path) }
343
+ }
305
344
  if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
306
345
  const content = await fs.readText(target)
307
346
  return { ok: true, content, size: content.length }
@@ -342,24 +381,37 @@ export function buildHandlers(
342
381
  try {
343
382
  const target = await resolveTarget(ctx, sc.session, args.path)
344
383
  await fs.writeText(target, args.content, void 0, void 0, policyOf(ctx, sc.session))
345
- // 手动保存后目录树可能变化(新建/删除文件):父目录+祖先进失效,后台自愈。
346
- invalidateIndex(ctx, sc.cwd, args.path)
347
- const bucket = await bucketOf(registry, ctx, sc.cwd)
348
- let changed = false
349
- for (const rec of bucket.values()) {
350
- if (rec.path === args.path && rec.superseded !== true) { rec.superseded = true; rec.at = new Date().toISOString(); changed = true }
351
- }
352
- if (changed) {
353
- const done: DiffRecord[] = []
354
- for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded) done.push(rec)
355
- if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, '被手动编辑覆盖')
356
- else await saveBucket(ctx, sc.cwd, bucket, sc.session)
357
- }
384
+ await afterManualSave(ctx, registry, sc, args.path)
358
385
  return { ok: true }
359
386
  } catch (error) {
360
387
  return { ok: false, error: '保存失败:' + String(error) }
361
388
  }
362
389
  },
390
+ 'edrv.saveBinary': async (args) => {
391
+ if (args.encoding !== 'base64') return { ok: false, error: '仅支持 base64 编码' }
392
+ const sc = await requireSession(ctx, args.sessionId)
393
+ if ('err' in sc) return { ok: false, error: sc.err }
394
+ const fs = ctx.get('fs')
395
+ if (!fs) return { ok: false, error: '缺少 fs' }
396
+ try {
397
+ const target = await resolveTarget(ctx, sc.session, args.path)
398
+ const info = await fs.stat(target)
399
+ if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
400
+ // fs 服务仅提供文本写(writeText/editText),二进制回写经 node:fs 直写解析后的
401
+ // 真实路径;以工作区边界 + 大小上限 + 用户显式保存动作三重约束兜底(对齐
402
+ // revert.ts deleteCreated 的 contains 用法)。
403
+ const rootTarget = await fs.resolve(policyOf(ctx, sc.session)?.workspaceRoot ?? '.', {})
404
+ if (!fs.contains(rootTarget, target)) return { ok: false, error: '拒绝保存:目标不在会话工作区内' }
405
+ const bytes = Buffer.from(args.content, 'base64')
406
+ if (bytes.byteLength === 0) return { ok: false, error: '内容为空,拒绝写回' }
407
+ if (bytes.byteLength > BINARY_READ_CAP) return { ok: false, error: '内容过大(>32MB),拒绝写回' }
408
+ await writeFile(fs.processPath(target), bytes)
409
+ await afterManualSave(ctx, registry, sc, args.path)
410
+ return { ok: true }
411
+ } catch (error) {
412
+ return { ok: false, error: '二进制保存失败:' + String(error) }
413
+ }
414
+ },
363
415
  'edrv.archiveList': async (args) => {
364
416
  const sc = await requireSession(ctx, args.sessionId)
365
417
  if ('err' in sc) return { ok: false, error: sc.err }
package/src/shared/rpc.ts CHANGED
@@ -21,6 +21,52 @@ import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocatio
21
21
  /** webServer 精确路由。 */
22
22
  export const RPC_PATH = '/edrv/rpc'
23
23
 
24
+ /**
25
+ * 图片扩展名(小写、无点)→ MIME。edrv.read base64 响应与 client 图片预览共用,
26
+ * 收敛在此避免 host/client 两处漂移。
27
+ * @author ddj 2026年09月08号
28
+ */
29
+ export const IMAGE_MIME: Record<string, string> = {
30
+ png: 'image/png',
31
+ jpg: 'image/jpeg',
32
+ jpeg: 'image/jpeg',
33
+ gif: 'image/gif',
34
+ webp: 'image/webp',
35
+ bmp: 'image/bmp',
36
+ ico: 'image/x-icon',
37
+ avif: 'image/avif',
38
+ svg: 'image/svg+xml',
39
+ }
40
+
41
+ /**
42
+ * 路径 → 图片 MIME:按 basename 扩展名小写查 {@link IMAGE_MIME},未命中回退 application/octet-stream。
43
+ * @author ddj 2026年09月08号
44
+ * @param path 文件路径(`/` 或 `\` 分隔均可)
45
+ * @returns MIME 字符串
46
+ */
47
+ export function imageMimeOf(path: string): string {
48
+ const base = String(path || '').split(/[\\/]/).pop() || ''
49
+ const dot = base.lastIndexOf('.')
50
+ const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : ''
51
+ return IMAGE_MIME[ext] ?? 'application/octet-stream'
52
+ }
53
+
54
+ /** PDF 统一 MIME(host base64 响应与 client PDF 面板同源,避免两处漂移)。 */
55
+ export const PDF_MIME = 'application/pdf'
56
+
57
+ /**
58
+ * 路径 → 二进制预览 MIME:PDF 特判 {@link PDF_MIME},其余走图片表回退 octet-stream。
59
+ * @author ddj 2026年09月22号
60
+ * @param path 文件路径(`/` 或 `\` 分隔均可)
61
+ * @returns MIME 字符串
62
+ */
63
+ export function binaryMimeOf(path: string): string {
64
+ const base = String(path || '').split(/[\\/]/).pop() || ''
65
+ const dot = base.lastIndexOf('.')
66
+ if (dot > 0 && base.slice(dot + 1).toLowerCase() === 'pdf') return PDF_MIME
67
+ return imageMimeOf(path)
68
+ }
69
+
24
70
  /** 决策作用域:call=整条记录,hunk=单个差异块。 */
25
71
  export type RpcScope = 'call' | 'hunk'
26
72
 
@@ -111,9 +157,10 @@ export interface RpcRequestMap {
111
157
  'edrv.accept': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
112
158
  'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
113
159
  'edrv.decideBatch': { sessionId?: string; items: DecideItem[] }
114
- 'edrv.read': { sessionId?: string; path: string }
160
+ 'edrv.read': { sessionId?: string; path: string; encoding?: 'base64' }
115
161
  'edrv.original': { sessionId?: string; path: string }
116
162
  'edrv.save': { sessionId?: string; path: string; content: string }
163
+ 'edrv.saveBinary': { sessionId?: string; path: string; content: string; encoding: 'base64' }
117
164
  'edrv.archiveList': { sessionId?: string }
118
165
  'edrv.archiveRead': { sessionId?: string; path?: string }
119
166
  'edrv.rollback': { sessionId?: string; path: string; batch?: number }
@@ -194,9 +241,10 @@ export interface RpcOkMap {
194
241
  'edrv.accept': { record: RecordView }
195
242
  'edrv.reject': { record: RecordView }
196
243
  'edrv.decideBatch': { results: DecideResult[] }
197
- 'edrv.read': { content: string; size: number }
244
+ 'edrv.read': { content: string; size: number; encoding?: 'base64'; mime?: string }
198
245
  'edrv.original': { content: string; size: number; stale: StaleHunk[]; fallback: boolean }
199
246
  'edrv.save': object
247
+ 'edrv.saveBinary': object
200
248
  'edrv.archiveList': { entries: ArchiveEntry[] }
201
249
  'edrv.archiveRead': { batches: ArchiveBatch[] }
202
250
  'edrv.rollback': { path: string; batch: number | null }
package/src/store.ts CHANGED
@@ -11,6 +11,8 @@ import { SIDECAR, SIDECAR_ARCHIVE } from './paths.js'
11
11
 
12
12
  export { SIDECAR, SIDECAR_ARCHIVE }
13
13
  export const READ_CAP = 8 * 1024 * 1024
14
+ /** 二进制预览/写回上限(PDF 等整文件 base64 传输;文本侧仍用 READ_CAP)。 */
15
+ export const BINARY_READ_CAP = 32 * 1024 * 1024
14
16
 
15
17
  /** DSH 上下文与会话类型较宽松:本地无 dsh 类型声明,显式 any + 文档约束。 */
16
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any