@wszhoho/dsh-file-attachment 0.4.0 → 0.4.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 (3) hide show
  1. package/README.md +6 -2
  2. package/lib/client.js +47 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V
4
4
 
5
5
  图片文件由插件接管后走 dsh web 既有草稿图片流程(`addImages`):不落盘、不进文件条、不缩放;仅当输入框未就绪且为纯图片时,才原样交回原生。
6
6
 
7
- 另外,输入框工具栏(加号之后)提供了一个**上传按钮(📎 回形针图标)**,点击后弹出系统文件选择框,可多选文档/图片,复用与拖入/粘贴完全一致的落盘 + `@路径` 插入管线。PC 与移动端浏览器均适用(移动端走原生文件/拍照选择器)。
7
+ 另外,输入框工具栏(加号之后)提供了一个**上传按钮(📎 回形针图标)**,点击后弹出系统文件选择框,可多选文档/图片,复用与拖入/粘贴完全一致的落盘 + `@路径` 插入管线。PC 与移动端浏览器均适用(移动端走原生文件选择器)。
8
8
 
9
9
  ## 界面
10
10
 
@@ -14,11 +14,15 @@ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V
14
14
 
15
15
  **上传按钮**:输入框工具栏 `+` 之后的 📎 回形针图标,点击弹出系统文件选择框(可多选文档/图片)。
16
16
 
17
+ **可上传类型设置**:「设置 → 文件附件」页可按 文档 / 代码 / 配置文件 三类增删扩展名(小写、不带点),图片恒可发送。
18
+
19
+ ![可上传类型设置](docs/settings-file-types.png)
20
+
17
21
  ## 行为一览
18
22
 
19
23
  | 操作 | 行为 |
20
24
  | --- | --- |
21
- | 点击**上传按钮(📎)**,在系统文件选择框中**多选**文档/图片 | 与拖入/粘贴同一管线:文档 → 落盘 + 光标插入全部 `@` 路径;图片 → 走既有草稿图片流程;`accept` 限定图片 + 常见文档扩展名,移动端自动弹出文件/拍照选择器 |
25
+ | 点击**上传按钮(📎)**,在系统文件选择框中**多选**文档/图片 | 与拖入/粘贴同一管线:文档 → 落盘 + 光标插入全部 `@` 路径;图片 → 走既有草稿图片流程;`accept` 限定图片 + 常见文档扩展名,移动端自动弹出文件选择器 |
22
26
  | 拖入/粘贴 **文档/代码/配置文件**(非图片,默认 doc·code·config 三类扩展名,如 docx/xlsx/pdf/md/txt/js/json) | 浏览器读全文 → base64 → 宿主落盘 `<会话工作区>/.dsh-file-attachment/<日期>/<文件名>` → 光标插入 `@绝对路径`,成功不提示、失败简短 toast |
23
27
  | 一次拖入/粘贴 **多个文件**(支持多文件) | 批量处理:逐个读取全文 → base64 → 落盘 → 在光标处一次性插入全部 `@` 引用;全部成功不提示,部分失败仅简短提示「N 个已跳过」 |
24
28
  | 拖入 **目录** | 拒绝,toast 提示「不支持拖入目录」,不插入引用 |
package/lib/client.js CHANGED
@@ -146,7 +146,7 @@ window.__ModuleLoader__.load({
146
146
  const isTarget = (oc) => oc.ref === plain || oc.ref === quoted
147
147
  || (oc.clipboardText !== void 0 && (oc.clipboardText === plain || oc.clipboardText === quoted))
148
148
  for (let n = 0; n < 64; n++) {
149
- const snap = shell.snapshot
149
+ const snap = readShellSnapshot(shell)
150
150
  if (snap === null || snap === void 0 || typeof snap.draft !== 'string') break
151
151
  // 发送进行中不动草稿(此时 occurrence 会随发送完成清空)
152
152
  if (snap.phase === 'adjudicating' || snap.phase === 'submitting') break
@@ -300,10 +300,25 @@ window.__ModuleLoader__.load({
300
300
  return seg
301
301
  }
302
302
 
303
- // 找可写的 composer textarea:可见、未禁用、未只读、phase 为 plain;
304
- // 多个候选时优先 value 与桥内草稿一致的那一个(排除审批弹窗里的同名元素)
305
- function findComposerTextarea() {
306
- const list = Array.from(document.querySelectorAll('[data-composer-card] textarea'))
303
+ // 输入机快照读取:兼容两代 dsh。
304
+ // 旧版(纯文本机,alpha.2 之前):shell.snapshot 直接是 InputState;
305
+ // 新版(Lexical composer,alpha.2 起):shell.state 是 SnapshotStore,经
306
+ // getSnapshot() 取值(字段不变:draft/draftRev/phase/occurrences)。
307
+ function readShellSnapshot(shell) {
308
+ if (shell === null || shell === void 0) return undefined
309
+ const st = shell.state
310
+ if (st !== null && st !== void 0 && typeof st.getSnapshot === 'function') {
311
+ return st.getSnapshot()
312
+ }
313
+ return shell.snapshot
314
+ }
315
+
316
+ // 找可写的 composer 输入表面:可见、未禁用、未只读、phase 为 plain;
317
+ // 多个候选时优先文本与桥内草稿一致的那一个(排除审批弹窗里的同名元素)。
318
+ // 双代兼容:旧版 textarea([data-composer-card] textarea);
319
+ // 新版 Lexical contenteditable([data-composer-card] [data-composer-input])。
320
+ function findComposerInput() {
321
+ const list = Array.from(document.querySelectorAll('[data-composer-card] textarea, [data-composer-card] [data-composer-input]'))
307
322
  const live = list.filter((ta) => {
308
323
  if (ta.disabled || ta.readOnly) return false
309
324
  const phase = ta.getAttribute('data-phase')
@@ -312,9 +327,10 @@ window.__ModuleLoader__.load({
312
327
  })
313
328
  if (live.length === 0) return null
314
329
  const draft = bridge.input !== void 0 ? bridge.input.draft : undefined
330
+ const textOf = (el) => (el.tagName === 'TEXTAREA' ? el.value : (el.textContent || ''))
315
331
  if (typeof draft === 'string') {
316
332
  for (let i = 0; i < live.length; i++) {
317
- if (live[i].value === draft) return live[i]
333
+ if (textOf(live[i]) === draft) return live[i]
318
334
  }
319
335
  }
320
336
  return live[0]
@@ -430,7 +446,7 @@ window.__ModuleLoader__.load({
430
446
  announce(`输入框不可用 [mounted:${bridge.mounted} shell:${shell === null ? 'null' : 'ok'} addImg:${typeof bridge.addImages}]`)
431
447
  return
432
448
  }
433
- const snap0 = shell.snapshot
449
+ const snap0 = readShellSnapshot(shell)
434
450
  const phase = snap0 ? snap0.phase : 'plain'
435
451
  if (phase !== 'plain' && phase !== 'command' && phase !== 'claimed') {
436
452
  announce('输入框正忙')
@@ -484,8 +500,24 @@ window.__ModuleLoader__.load({
484
500
  const m = formatMention(res.path, false)
485
501
  const clipboardText = m !== undefined ? m : '@' + res.path
486
502
  // 每次插入前重取 snapshot(draftRev 随插入递增,span 取当前末尾)
487
- const snap = shell.snapshot
488
- const span = { draftRev: snap.draftRev, start: snap.draft.length, end: snap.draft.length }
503
+ const snap = readShellSnapshot(shell)
504
+ // 前导分隔空格:聊天记录对 @引用 file-chip 渲染(projectUserText)
505
+ // 要求 @ 前是行首或空白,而 insertReference 只在 chip 后补空格。
506
+ // 草稿末尾紧贴文字时先补一格,否则发送后聊天记录显示裸 @路径
507
+ //(旧版 insertTextAtCaret 的 padStart 同款保底)。
508
+ if (snap !== void 0 && typeof snap.draft === 'string' && snap.draft !== ''
509
+ && !isSpace(snap.draft.charCodeAt(snap.draft.length - 1))) {
510
+ const leadSpan = { draftRev: snap.draftRev, start: snap.draft.length, end: snap.draft.length }
511
+ if (typeof shell.insertText === 'function') {
512
+ try { shell.insertText(' ', leadSpan) } catch (err) { /* 忽略:补空格失败不阻塞插入 */ }
513
+ } else if (typeof shell.setDraft === 'function') {
514
+ // 含 chip 的草稿末尾必为空白(chip 自动尾随空格),故此处必为纯文本,拼接安全
515
+ try { shell.setDraft(snap.draft + ' ') } catch (err) { /* 忽略 */ }
516
+ }
517
+ }
518
+ // 补空格会递增 draftRev,重新取快照后再定插入 span
519
+ const snap2 = readShellSnapshot(shell)
520
+ const span = { draftRev: snap2.draftRev, start: snap2.draft.length, end: snap2.draft.length }
489
521
  const okInsert = shell.insertReference(
490
522
  {
491
523
  source: 'reference', // 复用内置已注册的 @引用 source(带 codec.serialize),否则报 no serializer
@@ -523,12 +555,12 @@ window.__ModuleLoader__.load({
523
555
  handleFilesEvent(e, files, dt.items, 'drop')
524
556
  }
525
557
 
526
- // document 级 paste(capture):仅当粘贴目标就是 composer textarea 时接管文件;
558
+ // document 级 paste(capture):仅当粘贴目标就是 composer 输入表面时接管文件;
527
559
  // 纯文本(含目录路径字符串)完全原生,不改写。
528
560
  function onPaste(e) {
529
561
  const t = e.target
530
- const ta = findComposerTextarea()
531
- if (ta === null || t !== ta) return
562
+ const ta = findComposerInput()
563
+ if (ta === null || (t !== ta && !(ta.contains && ta.contains(t)))) return
532
564
  const cd = e.clipboardData
533
565
  if (cd === null || cd === void 0) return
534
566
  const files = Array.prototype.slice.call(cd.files)
@@ -599,7 +631,7 @@ window.__ModuleLoader__.load({
599
631
  }
600
632
  }
601
633
  } catch (err) { shell = null; addImages = null }
602
- const input = shell !== null && shell.snapshot !== void 0 ? shell.snapshot : undefined
634
+ const input = readShellSnapshot(shell)
603
635
  react.useEffect(() => {
604
636
  bridge.shell = shell
605
637
  bridge.addImages = addImages
@@ -635,12 +667,12 @@ window.__ModuleLoader__.load({
635
667
  attachedListeners.add(fn)
636
668
  return () => { attachedListeners.delete(fn) }
637
669
  }, [])
638
- // 发送后清空:轮询 shell.snapshot,occurrences 从有→无 即一次发送完成
670
+ // 发送后清空:轮询输入机快照,occurrences 从有→无 即一次发送完成
639
671
  const hadOcc = react.useRef(false)
640
672
  react.useEffect(() => {
641
673
  const timer = setInterval(() => {
642
674
  const shell = bridge.shell
643
- const snap = shell ? shell.snapshot : null
675
+ const snap = readShellSnapshot(shell)
644
676
  const occ = snap ? snap.occurrences : null
645
677
  const has = !!(occ && occ.length > 0)
646
678
  const s = bridge.sessionId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wszhoho/dsh-file-attachment",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "文件附件:输入框上传按钮 + 拖拽/粘贴文件(支持多文件);图片走 dsh 原生图片草稿机制(addImages,不缩放/不落盘),文档落盘到 .dsh-file-attachment 并以芯片 @短名 引用(发送时还原 @绝对路径);文档/代码/配置文件可上传类型可在设置页配置;支持 PC 与移动端浏览器",
5
5
  "keywords": [
6
6
  "dsh",