@wszhoho/dsh-file-attachment 0.1.0 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wszhoho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # dsh-file-attachment
2
+
3
+ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V 粘贴文档(支持多文件,一次可拖入/粘贴多个文件)**时,浏览器端批量读取全文,保存到**当前会话工作区根目录下的 `.dsh-file-attachment/`**,并在输入框光标处插入 `@<保存副本绝对路径>` 引用。agent 读取 `@路径` 时直接读到落盘副本,不再依赖原始文件位置。
4
+
5
+ 图片文件完全沿用 dsh web 既有草稿图片流程,不拦截、不改动。
6
+
7
+ ## 行为一览
8
+
9
+ | 操作 | 行为 |
10
+ | --- | --- |
11
+ | 拖入/粘贴 **文档**(docx/xlsx/pdf/md/txt/rpk 等非图片文件) | 浏览器读全文 → base64 → 宿主落盘 `<会话工作区>/.dsh-file-attachment/<时间戳>-<文件名>` → 光标插入 `@绝对路径`,底部 toast 提示保存目录 |
12
+ | 一次拖入/粘贴 **多个文件**(支持多文件) | 批量处理:逐个读取全文 → base64 → 落盘 → 在光标处一次性插入全部 `@` 引用,toast 汇总保存数量与目录 |
13
+ | 拖入 **目录** | 拒绝,toast 提示「不支持拖入目录」,不插入引用 |
14
+ | 拖入/粘贴 **图片** | 不拦截,原样走 dsh web 既有草稿图片流程(零变化) |
15
+ | 粘贴 **纯文本**(含从地址栏复制的目录路径字符串) | 不改写,完全原生行为 |
16
+ | 重复拖入同名文件(同一秒内) | 追加 `-1`、`-2` 序号,绝不覆盖 |
17
+ | 单文件 > 50MB | 跳过并提示 |
18
+
19
+ ### 文件命名
20
+
21
+ `<时间戳>-<清洗后文件名>`,例如 `2026-02-11T15-04-05-说明.docx`;时间戳为 `2026-02-11T15-04-05` 格式(去掉冒号/毫秒/Z,各平台文件名安全且可排序)。同名冲突在扩展名前追加 `-N`。
22
+
23
+ ### 项目根解析
24
+
25
+ 保存目录的根 = **当前会话工作区**(`session.header.cwd`),回退顺序:会话 cwd → `sandboxPolicy.workspaceRoot` → `process.cwd()`。每个项目(工作区)都各自维护自己的 `.dsh-file-attachment/` 目录。
26
+
27
+ ### 跨平台
28
+
29
+ - 保存路径用 Node `path.join`(平台分隔符自适应)
30
+ - 时间戳/文件名清洗同时对 Windows 与 POSIX 命名规则安全
31
+ - Host 半直接 `node:fs/promises` 写字节,无 shell 依赖
32
+
33
+ ## 安装(官方 `dsh plugin` 方式)
34
+
35
+ 本仓库是一个标准 dsh 插件包(声明了 `dsh.bundle.patch`),并已发布为 npm 包 `@wszhoho/dsh-file-attachment`。用官方 CLI 安装,pnpm 会自动处理依赖、并把本包加入 profile 的 `dsh.profile.bundles` 层。`dsh plugin add` 的参数会转发给 profile 目录的 pnpm 执行,三种来源任选其一:
36
+
37
+ ### 从 npm 安装(普通用户推荐)
38
+
39
+ ```powershell
40
+ dsh plugin --profile web add @wszhoho/dsh-file-attachment
41
+ # 重启 web GUI:dsh web(或从托盘重启)
42
+ ```
43
+
44
+ ### 本地源码安装(开发用,边改边验)
45
+
46
+ ```powershell
47
+ # 在仓库父目录执行(<parent> 换成放置仓库的实际目录,如 E:\wszhoho)
48
+ cd <parent>
49
+ dsh plugin --profile web add ./dsh-file-attachment
50
+ # ./dsh-file-attachment 相对路径等价 pnpm link;改完 lib/*.js 后重启 dsh web 即生效
51
+ ```
52
+
53
+ ### 从 GitHub 安装
54
+
55
+ ```powershell
56
+ dsh plugin --profile web add github:<你的账号>/dsh-file-attachment
57
+ ```
58
+
59
+ - 无论哪种来源,安装后都自动进入 `dsh.profile.bundles`,无需手改 profile `package.json`;
60
+ - 升级:`dsh plugin --profile web update dsh-file-attachment`。
61
+
62
+ ## 架构
63
+
64
+ ```
65
+ packages/dsh-file-attachment/
66
+ ├── package.json # dsh.client 声明 + bundle patch 指向
67
+ ├── cordis.patch.yml # bundle 补丁:插入本插件行
68
+ └── lib/
69
+ ├── index.js # Host 半:webServer 路由(POST /dsh-file-attachment/save),node:fs/promises 写盘
70
+ └── client.js # Client 半:document 级 capture drop/paste/dragenter 监听 + 槽组件
71
+ ```
72
+
73
+ - **Host 半**:`webServer.register` 前缀路由 `/dsh-file-attachment`,POST `/dsh-file-attachment/save` 接收 `{name, data(base64), sessionId}`,base64 解码后用 `node:fs/promises` 写盘到会话工作区 `.dsh-file-attachment/`,返回 `{ok, value:{path,dir,name,size}}`。
74
+ - **Client 半**:`conversation.composer.dock`(list 槽,同步桥状态,不渲染文本)+ `shell.overlay`(list 槽,toast 通知展示),document 级 capture 监听先于应用 bubble 监听执行。
75
+ - **通知**:`shell.overlay` frame-wide 浮动 toast(不参与文档流,不破坏布局),4 秒自动消失。
76
+ - **浮层**:拖入任何文件时 capture 阶段拦截应用自带「拖入图片…」DropOverlay(文案面向图片,对文档是误导)。
77
+
78
+ ## 开发说明
79
+
80
+ - `lib/*.js` 使用 **UTF-8 with BOM**(项目约定);`package.json`、`cordis.patch.yml` 使用 **UTF-8 无 BOM**。
81
+ - Host 半用 `webServer.register({ kind: 'prefix', path: '/dsh-file-attachment', handler })` 收文件(参考 `dsh-upload-file` 同架构的 `/dsh-upload` 路由),纯 ESM 无构建,不依赖装饰器/远程反射。
82
+ - Client 半保存用 `fetch('/dsh-file-attachment/save', { method: 'POST', body: JSON.stringify({ name, data, sessionId }) })`,信封为 `{ ok, value | error }`。
83
+ - 项目根 = 会话工作区(`session.header.cwd` → `sandboxPolicy.workspaceRoot` → `process.cwd()` 兜底)。
84
+ - 50MB 上限两侧一致(client 跳过 + host 校验)。
85
+
86
+ ## License
87
+
88
+ MIT
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-fileAttachment 补丁行:挂载本插件(Host 半提供 fileAttachment remote 服务写临时目录;Client 半监听拖拽/粘贴)
1
+ # dsh-file-attachment 补丁行:挂载本插件(Host 半提供 file-attachment 服务写临时目录;Client 半监听拖拽/粘贴)
2
2
  - insert:
3
- - id: dsh-fileAttachment
3
+ - id: dsh-file-attachment
4
4
  name: '@wszhoho/dsh-file-attachment'
package/lib/client.js CHANGED
@@ -1,15 +1,15 @@
1
- // @local/dsh-fileAttachment 浏览器半(client 半,全部功能在此)。
1
+ // @wszhoho/dsh-file-attachment 浏览器半(client 半,全部功能在此)。
2
2
  // 拖放/粘贴文件进输入框:
3
3
  // 1) 纯图片 —— 不拦截事件,原样交给既有图片拖放路径(行为零变化);
4
4
  // 2) 文档(非图片文件)—— capture 阶段接管:浏览器端读取全文 → base64 →
5
- // remote.fileAttachment.save 落盘到 <会话工作区>/.dsh-fileAttachment/ →
5
+ // fetch POST /dsh-file-attachment/save 落盘到 <会话工作区>/.dsh-file-attachment/ →
6
6
  // 在草稿光标处插入 @绝对路径 引用;粘贴文件走同一核心;
7
7
  // 3) 目录 —— 拒绝并 toast 提示「不支持拖入目录」(需求确认:不插引用);
8
8
  // 4) 纯文本粘贴(含目录路径字符串)—— 完全原生,不做任何改写。
9
9
  // 5) 拖入任何文件时在 capture 阶段拦截应用自带「拖入图片…」DropOverlay 浮层。
10
10
  // 通知:shell.overlay 浮动 toast(frame-wide,fixed 定位,点击穿透,不破坏布局)。
11
11
  window.__ModuleLoader__.load({
12
- id: '@local/dsh-fileAttachment',
12
+ id: '@wszhoho/dsh-file-attachment',
13
13
  factory: (require) => {
14
14
  const module = { exports: {} }
15
15
  const exports = module.exports
@@ -17,7 +17,7 @@ window.__ModuleLoader__.load({
17
17
  const react = require('react')
18
18
 
19
19
  // Client 服务依赖:槽位 / 输入机 / 会话(与官方 dsh-upload-file 同范式)。
20
- // 保存文件不走 remote,而是 fetch POST 宿主 webServer 路由 /dsh-fileAttachment/save。
20
+ // 保存文件不走 remote,而是 fetch POST 宿主 webServer 路由 /dsh-file-attachment/save。
21
21
  const inject = [
22
22
  "slots",
23
23
  "conversation",
@@ -55,7 +55,7 @@ window.__ModuleLoader__.load({
55
55
  if (typeof fetch !== 'function') throw new Error('当前环境无 fetch,无法上传')
56
56
  let response
57
57
  try {
58
- response = await fetch('/dsh-fileAttachment/save', {
58
+ response = await fetch('/dsh-file-attachment/save', {
59
59
  method: 'POST',
60
60
  headers: { 'content-type': 'application/json' },
61
61
  body: JSON.stringify({ name: fileName, data: b64, sessionId: sessionId }),
@@ -428,11 +428,11 @@ window.__ModuleLoader__.load({
428
428
  const slots = ctx.get('slots')
429
429
  if (slots !== undefined) {
430
430
  slots.inject('conversation.composer.dock', () => slots.register(
431
- { name: 'conversation.composer.dock', id: 'dsh-fileAttachment', order: 300 },
431
+ { name: 'conversation.composer.dock', id: 'dsh-file-attachment', order: 300 },
432
432
  FaBridge
433
433
  ))
434
434
  slots.inject('shell.overlay', () => slots.register(
435
- { name: 'shell.overlay', id: 'dsh-fileAttachment-toast', order: 100 },
435
+ { name: 'shell.overlay', id: 'dsh-file-attachment-toast', order: 100 },
436
436
  FaToast
437
437
  ))
438
438
  }
@@ -447,7 +447,7 @@ window.__ModuleLoader__.load({
447
447
  document.removeEventListener('drop', onDrop, true)
448
448
  document.removeEventListener('paste', onPaste, true)
449
449
  }
450
- }, 'dsh-fileAttachment: document dragenter/drop/paste listeners')
450
+ }, 'dsh-file-attachment: document dragenter/drop/paste listeners')
451
451
  }
452
452
 
453
453
  exports.apply = apply
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
- // @local/dsh-fileAttachment Host 半:webServer HTTP 路由方案(参考 dsh-upload-file 同架构)。
2
- // Client 半读取文件全文(base64)后 POST /dsh-fileAttachment/save 到这里,
3
- // 写入 <会话工作区根>/.dsh-fileAttachment/<时间戳>-<文件名> 并返回 { path, dir, name, size }。
1
+ // @wszhoho/dsh-file-attachment Host 半:webServer HTTP 路由方案(参考 dsh-upload-file 同架构)。
2
+ // Client 半读取文件全文(base64)后 POST /dsh-file-attachment/save 到这里,
3
+ // 写入 <会话工作区根>/.dsh-file-attachment/<时间戳>-<文件名> 并返回 { path, dir, name, size }。
4
4
  // 项目根解析:Client 传 sessionId → sessions.get(sessionId).header.cwd →
5
5
  // sandboxPolicy.workspaceRoot → process.cwd()(三平台绝对路径,Node path.join 自适应分隔符)。
6
6
  // 纯 ESM 无构建:不依赖 TypertRemoteService/装饰器,原生 Node 可直接加载。
@@ -8,11 +8,11 @@ import { access, mkdir, writeFile } from 'node:fs/promises'
8
8
  import { join } from 'node:path'
9
9
 
10
10
  /** 本包声明依赖的 Host 服务。 */
11
- export const name = 'dsh-fileAttachment'
11
+ export const name = 'dsh-file-attachment'
12
12
  export const inject = ['webServer']
13
13
 
14
14
  // 临时目录名:落在会话工作区根下;每个项目都能看到自己引用过的文档
15
- const ATTACHMENT_DIR = '.dsh-fileAttachment'
15
+ const ATTACHMENT_DIR = '.dsh-file-attachment'
16
16
  // 单文件上限 50MB(与 Client 半一致)
17
17
  const MAX_BYTES = 50 * 1024 * 1024
18
18
  // 请求体上限:base64(50MB) + 信封余量
@@ -140,16 +140,16 @@ async function handleSave(root, body) {
140
140
  return { path: target, dir, name: t + '-' + safe, size: bytes.length }
141
141
  }
142
142
 
143
- /** 注册 /dsh-fileAttachment 前缀路由(save POST)。 */
143
+ /** 注册 /dsh-file-attachment 前缀路由(save POST)。 */
144
144
  function registerRoutes(ctx) {
145
145
  const webserver = ctx.get('webServer')
146
146
  if (webserver === undefined) return
147
147
  webserver.register({
148
148
  kind: 'prefix',
149
- path: '/dsh-fileAttachment',
149
+ path: '/dsh-file-attachment',
150
150
  handler: async (req, res) => {
151
151
  const pathname = new URL(req.url ?? '/', 'http://x').pathname
152
- if (req.method === 'POST' && pathname === '/dsh-fileAttachment/save') {
152
+ if (req.method === 'POST' && pathname === '/dsh-file-attachment/save') {
153
153
  const body = await readJsonBody(req, MAX_BODY_BYTES)
154
154
  if (body === null || typeof body !== 'object') {
155
155
  json(res, { ok: false, error: '请求体必须是 JSON(68MB 内)' }, 400)
@@ -173,7 +173,7 @@ function registerRoutes(ctx) {
173
173
  return
174
174
  }
175
175
  }
176
- json(res, { ok: false, error: 'only POST /dsh-fileAttachment/save is allowed' }, 405)
176
+ json(res, { ok: false, error: 'only POST /dsh-file-attachment/save is allowed' }, 405)
177
177
  },
178
178
  })
179
179
  }
@@ -183,5 +183,5 @@ function registerRoutes(ctx) {
183
183
  */
184
184
  export function apply(ctx, config = {}) {
185
185
  registerRoutes(ctx)
186
- console.log('[dsh-fileAttachment] host loaded (webServer routes)')
186
+ console.log('[dsh-file-attachment] host loaded (webServer routes)')
187
187
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@wszhoho/dsh-file-attachment",
3
- "version": "0.1.0",
4
- "description": "拖拽/粘贴文件进输入框:图片沿用原有草稿图片流程;文档读取内容存入项目根 .dsh-fileAttachment 临时目录并插入 @绝对路径引用",
3
+ "version": "0.2.1",
4
+ "description": "拖拽/粘贴文件(支持多文件)进输入框:图片沿用原有草稿图片流程;文档读取内容存入项目根 .dsh-file-attachment 临时目录并插入 @绝对路径引用",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "plugin",
8
8
  "file-attachment",
9
9
  "upload",
10
- "drag-drop"
10
+ "drag-drop",
11
+ "multi-file"
11
12
  ],
12
13
  "type": "module",
13
14
  "main": "lib/index.js",