dsh-workbuddy-files 0.1.5 → 0.2.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/LICENSE +21 -21
- package/README.md +218 -218
- package/lib/client.js +38 -29
- package/package.json +84 -88
- package/src/client/app.ts +23 -14
- package/src/client/components/file-cards.tsx +21 -8
- package/src/client/components/pick-button.tsx +14 -2
- package/src/client/lib/insert.ts +10 -8
- package/src/types/dsh-ambient.d.ts +54 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 喵芯科技(Miaoxin Technology)
|
|
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
CHANGED
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
# dsh-workbuddy-files
|
|
2
|
-
|
|
3
|
-
WorkBuddy 风格的 DeepSeek Harness (DSH) Web 插件:**拖拽 / 粘贴文件与文件夹 → 全屏接收遮罩 → 输入框原生引用气泡(File Pill)→ 发送后对话区文件卡片**,Agent 通过 `read_document` 工具或任意读取工具直接消费引用。
|
|
4
|
-
|
|
5
|
-
> 本插件同时存在两个形态:
|
|
6
|
-
> - **本目录**:可发布/可安装的正式插件包(TypeScript,Cordis,Host + Client 双半侧);
|
|
7
|
-
> - **动态演示版**:会话内动态 Cordis 插件(pluginId `wbdrop-1`),当前版本 pkg-5,与正式包行为一致,拖拽/粘贴/`@`/卡片立即可体验。
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 功能对照(与 WorkBuddy 对齐)
|
|
12
|
-
|
|
13
|
-
| 需求 | 实现 |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| 拖拽任意文件到输入框(PDF/Word/Excel/图片/代码…) | ✅ 统一接管:**所有**文件拖入都显示本插件全屏「松开以接收文件」遮罩并走气泡管线 |
|
|
16
|
-
| 截图/文件粘贴(Ctrl+V) | ✅ 统一接管:图片与文件粘贴都走气泡管线(拖入即缓存 + 引用绝对路径) |
|
|
17
|
-
| `@` 弹出菜单搜索并引用文件/文件夹 | ✅ 工作区文件由 DSH 官方 `@` 源提供;本插件追加「文件缓存」分组 |
|
|
18
|
-
| 光标位置原生引用气泡 | ✅ 输入机 `insertReference` 铸造原生 occurrence 气泡(文件名 + 类型图标) |
|
|
19
|
-
| 气泡整体 Backspace/Delete 一次删除 | ✅ 原生 occurrence 语义(整段删除、撤销/重做一体) |
|
|
20
|
-
| 发送后对话区文件卡片 | ✅ `conversation.chat.turnTail` 链式槽按轮次渲染「📎 消息引用的文件」卡片,点击经 `openFile` 打开 |
|
|
21
|
-
| 引用附带绝对路径 | ✅ **拖入瞬间**按预分配路径(`~/.dsh-drops/<批次>/<相对路径>`)在光标处插入气泡,文件**后台异步缓存**,引用即 `@"C:\...\file.pdf"` |
|
|
22
|
-
| 文件夹拖拽保留目录树 | ✅ `webkitGetAsEntry` 递归遍历;顶层文件夹一个 `📁` 气泡,内部树结构原样落盘 |
|
|
23
|
-
| Agent 可读性 | ✅ Host 注册 `read_document` 工具:文本返回内容、目录返回树、图片/二进制/大文件返回元数据与建议 |
|
|
24
|
-
|
|
25
|
-
> 设计取舍:v1 曾实现 Chromium「FS Access API 零上传直引」(拖入时零字节、发送时才落地)。实测中发送时物化偶发「直引落地失败」会阻断发送,且浏览器安全模型拿不到拖入文件的绝对路径。为保证**拖入必可用、发送必成功**,现统一为「拖入即缓存 + 真实绝对路径」——localhost 场景下缓存即本地拷贝,无网络开销;零上传直引作为后续可选增强(见「后续方向」)。
|
|
26
|
-
|
|
27
|
-
## 架构
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
浏览器(Client 半侧) DSH Host(Node 进程)
|
|
31
|
-
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
|
32
|
-
│ window 拖拽/粘贴监听(capture)│ │ 私有 RPC / webServer 路由 │
|
|
33
|
-
│ 任何文件拖拽 → stopPropagation│ │ save → 二进制落盘 │
|
|
34
|
-
│ (原生图片遮罩不再激活) │ ─────► │ list/stat/home │
|
|
35
|
-
│ 全屏遮罩「松开以接收文件」 │ │ │
|
|
36
|
-
│ │ │ ~/.dsh-drops/<批次>/<相对路径>│
|
|
37
|
-
│ 目录树解析(webkitGetAsEntry) │ │ (目录树结构保留,绝对路径) │
|
|
38
|
-
│ → 立即上传(拖入即缓存) │ │ │
|
|
39
|
-
│ │ │ read_document 工具 │
|
|
40
|
-
│ 气泡注入管线 │ │ 解析引用 → 内容/树/元数据 │
|
|
41
|
-
│ conversation.input.shell(id)│ └──────────────────────────────┘
|
|
42
|
-
│ shell.state.getSnapshot() │
|
|
43
|
-
│ shell.insertReference( │
|
|
44
|
-
│ {source:'workbuddy', │
|
|
45
|
-
│ ref, label, appearance}, │
|
|
46
|
-
│ {start,end,draftRev}) ← │ 草稿修订号 CAS + 重试(8 次)
|
|
47
|
-
│ │
|
|
48
|
-
│ @ 触发源「workbuddy」 │
|
|
49
|
-
│ candidates: drops.list → │
|
|
50
|
-
│ onPick → insert │
|
|
51
|
-
│ codec.serialize: 恒等 │
|
|
52
|
-
│ (拖入时已落地,发送必成功) │
|
|
53
|
-
│ │
|
|
54
|
-
│ conversationEvents 定义 │
|
|
55
|
-
│ workbuddy-file-refs │
|
|
56
|
-
│ (按轮次聚合用户消息引用) │
|
|
57
|
-
│ │
|
|
58
|
-
│ Slot 注册 │
|
|
59
|
-
│ shell.overlay(遮罩+toast) │
|
|
60
|
-
│ conversation.input.left │
|
|
61
|
-
│ (📎 选择文件/文件夹) │
|
|
62
|
-
│ conversation.chat.turnTail │
|
|
63
|
-
│ (文件卡片,select 精确匹配)│
|
|
64
|
-
└──────────────────────────────┘
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 为什么气泡是「原生」的
|
|
68
|
-
|
|
69
|
-
DSH 的输入状态机(`dsh-client-ui-input-trigger` + `InputMachine`)内置**引用气泡(occurrence)**机制:官方 `@` 文件引用、`/` 命令都以气泡呈现,气泡在草稿中占用原子区间 —— Backspace 一次删除、撤销/重做、提交时序列化都走同一套机器。插件通过 `conversation.input.shell(sessionId).insertReference(reference, span)` 铸造气泡(id 寻址服务面,不经过 scoped context),`span` 携带 `draftRev` 做 CAS:插入期间用户继续输入导致修订号变化时自动重读状态重试;非 plain 阶段(提交中)被拒则 toast 提示。
|
|
70
|
-
|
|
71
|
-
### 与原生图片轨道的互斥共存
|
|
72
|
-
|
|
73
|
-
DSH 原生的图片拖放轨道在 document 上监听 dragenter 并无条件显示全屏遮罩。本插件在 **window capture 阶段(传播第一站)** 从 dragenter 起 `stopPropagation()`,原生轨道感知不到被接管的拖拽,其遮罩不会激活,也不会因收不到收尾事件而卡住页面;本插件的遮罩自身在 dragleave/dragend/drop 时必然归零,页面随时可恢复。
|
|
74
|
-
|
|
75
|
-
### 可靠性设计
|
|
76
|
-
|
|
77
|
-
- **拖入即插气泡**:最终缓存路径在拖入时即可确定(`<dropsRoot>/<批次>/<文件名或目录树>`),气泡**立即**插入光标处,不等上传;文件落盘在后台异步进行(本地写盘,用户无感),完成/失败以 toast 告知;
|
|
78
|
-
- 发送时 `codec.serialize` 恒等,**发送消息不可能因文件未落地而失败**;极端情况下(超大文件拖入后立即发送)Agent 读取时文件可能仍在写入,`read_document` 对不存在路径返回明确错误,稍候重试即可;
|
|
79
|
-
- 气泡插入失败(输入区忙/无会话)自动降级:直接写草稿文本(`setDraft`)/ 聚焦 textarea 时 `execCommand`,保证输入框有内容;
|
|
80
|
-
- Host 写盘加固:fs 服务写 base64 临时文本 + PowerShell 从文件解码写盘(不依赖 stdin 管道),目录自动创建、临时文件自动清理;
|
|
81
|
-
- 单文件上限(动态版 ~31MB / 正式包 256MB 可配)超出时跳过并提示,其余文件正常接收。
|
|
82
|
-
|
|
83
|
-
## 项目结构
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
dsh-workbuddy-files/
|
|
87
|
-
├── package.json # dsh.client 声明 + dsh.bundle.patch 声明
|
|
88
|
-
├── cordis.patch.yml # bundle patch:把 host 半侧挂进配置树
|
|
89
|
-
├── tsconfig.json
|
|
90
|
-
├── tsdown.config.ts # 构建 lib/index.js(host)+ lib/client.js(client)
|
|
91
|
-
├── README.md
|
|
92
|
-
└── src/
|
|
93
|
-
├── host/
|
|
94
|
-
│ └── index.ts # /workbuddy-drops 路由 + read_document 工具
|
|
95
|
-
└── client/
|
|
96
|
-
├── index.ts # 副作用外壳:__ModuleLoader__.load
|
|
97
|
-
├── app.ts # factory:require('react') + 组装插件
|
|
98
|
-
├── at-source.ts # @ 触发源(文件缓存分组)
|
|
99
|
-
├── definitions.ts # 会话事件定义 + turnTail selector
|
|
100
|
-
├── css.ts
|
|
101
|
-
├── types.ts
|
|
102
|
-
├── components/
|
|
103
|
-
│ ├── overlay.tsx # 全屏拖拽遮罩 + toast
|
|
104
|
-
│ ├── pick-button.tsx # 📎 选择文件/文件夹
|
|
105
|
-
│ └── file-cards.tsx # 对话区文件卡片
|
|
106
|
-
└── lib/
|
|
107
|
-
├── bus.ts # 拖拽/提示总线
|
|
108
|
-
├── insert.ts # 气泡注入管线(核心)
|
|
109
|
-
├── drop.ts # 拖拽/粘贴处理
|
|
110
|
-
├── transfer.ts # 目录树遍历 + 上传 + 引用格式
|
|
111
|
-
└── icons.ts # 类型图标
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## 安装
|
|
115
|
-
|
|
116
|
-
前置:Node 18+、pnpm(`dsh plugin` 内部转发给 pnpm)。
|
|
117
|
-
|
|
118
|
-
### 方式一:本地项目安装(开发/验证,推荐先走这条)
|
|
119
|
-
|
|
120
|
-
```powershell
|
|
121
|
-
cd F:\dsh工作空间\dsh-workbuddy-files
|
|
122
|
-
npm install # 安装 tsdown(0.22+)/ typescript 等构建依赖
|
|
123
|
-
npm run bundle # 产出 lib/index.js 与 lib/client.js
|
|
124
|
-
npm run typecheck # 可选:类型检查(已通过)
|
|
125
|
-
|
|
126
|
-
# link 安装:符号链接(改完代码重新 bundle + 重启 dsh 即生效)
|
|
127
|
-
dsh plugin --profile web add link:..\dsh-workbuddy-files
|
|
128
|
-
# 或拷贝安装(发布前验证打包产物用):
|
|
129
|
-
dsh plugin --profile web add file:..\dsh-workbuddy-files
|
|
130
|
-
|
|
131
|
-
dsh web # 重启 web 界面
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
`dsh plugin add` 会:在 `$DSH_HOME/profiles/web` 里 `pnpm add` 该包,并因为本包声明了 `dsh.bundle.patch`,自动把 `dsh-workbuddy-files` 追加进 profile 的 `dsh.profile.bundles` 层栈(可在 `~/.dsh/profiles/web/package.json` 中核对)。卸载:`dsh plugin --profile web remove dsh-workbuddy-files`。
|
|
135
|
-
|
|
136
|
-
### 方式二:npm registry 安装(在其他设备上安装,推荐)
|
|
137
|
-
|
|
138
|
-
前置:目标设备已安装 DSH 与 pnpm。
|
|
139
|
-
|
|
140
|
-
```powershell
|
|
141
|
-
npm i -g pnpm # 若尚未安装
|
|
142
|
-
# DSH 未安装时:npm i -g @deepseek-ai/dsh
|
|
143
|
-
|
|
144
|
-
dsh plugin --profile web add dsh-workbuddy-files
|
|
145
|
-
dsh web # 重启 web 界面,刷新页面即生效
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 方式三:GitHub 直装(npm registry 不可达时)
|
|
149
|
-
|
|
150
|
-
仓库自带 `lib/` 构建产物(clone 即可用,无需构建):
|
|
151
|
-
|
|
152
|
-
```powershell
|
|
153
|
-
dsh plugin --profile web add "github:miaoxintechnology/dsh-workbuddy-files"
|
|
154
|
-
dsh web
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### 方式四:离线拷贝
|
|
158
|
-
|
|
159
|
-
把整个项目目录拷到目标设备,然后:
|
|
160
|
-
|
|
161
|
-
```powershell
|
|
162
|
-
dsh plugin --profile web add file:<项目目录绝对路径>
|
|
163
|
-
dsh web
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### 更新与卸载
|
|
167
|
-
|
|
168
|
-
```powershell
|
|
169
|
-
# 更新到指定版本(显式版本号可绕过 pnpm 的「新版本最小发布年龄」供应链门禁)
|
|
170
|
-
dsh plugin --profile web add dsh-workbuddy-files@0.1.5
|
|
171
|
-
|
|
172
|
-
# 卸载
|
|
173
|
-
dsh plugin --profile web remove dsh-workbuddy-files
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### 配置(可选)
|
|
177
|
-
|
|
178
|
-
`cordis.patch.yml` 中 host 半侧的 `config`(浏览器半侧目前收不到配置,走代码默认值):
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
config:
|
|
182
|
-
dropsDir: '~/.dsh-drops' # 缓存根目录
|
|
183
|
-
maxFileBytes: 268435456 # 单文件上限(默认 256MB)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### 依赖说明
|
|
187
|
-
|
|
188
|
-
- Host 半侧硬依赖 `webServer` 服务(`inject: ['webServer']`),写文件用 `node:fs`(真实插件非沙箱环境,与 dsh-pet 同款写法);
|
|
189
|
-
- Client 半侧在运行时探测 `sessions` / `conversation` / `inputTriggers` / `conversationEvents` / `slots` 服务,缺失时优雅降级(例如无 `conversation` 时气泡退化为纯文本插入)。
|
|
190
|
-
|
|
191
|
-
### 沙箱模式说明
|
|
192
|
-
|
|
193
|
-
- **正式插件包**:Host 半侧用 `node:fs` 直写,不经过 DSH 的沙箱化 shell/fs 服务(沙箱只约束模型工具的执行,不约束插件自身的 Node 代码)——因此 `read-only` / `workspace-write` / `danger-full-access` 任何会话模式下,拖入即缓存都能正常工作;
|
|
194
|
-
- **动态演示版**:动态插件环境没有 Node 文件系统,二进制写盘需借用沙箱化 shell 服务;当部署的沙箱 runner 不可用(如 Windows ACL runner 配置异常)或缓存目录位于会话 workspace 之外时,写盘命令需要 `danger-full-access` 策略(演示版已显式携带,与当前会话文件策略一致);
|
|
195
|
-
- **折中方案**:将 `dropsDir` 配置到 workspace 内(如 `<workspace>/.dsh-drops`),workspace-write 沙箱即可放行;或让插件先继承会话模式、被沙箱拒绝时再升级重试并提示。
|
|
196
|
-
|
|
197
|
-
## 测试步骤(逐条验收)
|
|
198
|
-
|
|
199
|
-
1. **拖拽文件**:从资源管理器把 `report.pdf` 拖入页面 → 全屏虚线遮罩「松开以接收文件」(不是原生的图片遮罩)→ 松手 → 输入框光标处出现 `📕 report.pdf` 气泡;`~/.dsh-drops/drop-*` 下文件已落地。
|
|
200
|
-
2. **拖拽图片**:拖入一张 PNG → 同一个遮罩 → `🖼️` 气泡(引用 `~/.dsh-drops` 下的真实路径);Agent 可用 `read_image` 查看。
|
|
201
|
-
3. **光标位置**:输入「请分析 」后把光标停在句号前拖入文件 → 气泡插在光标处,而非末尾。
|
|
202
|
-
4. **气泡整体删除**:气泡前/后按 Backspace / Delete → 整个气泡一次删除;Ctrl+Z 完整还原。
|
|
203
|
-
5. **粘贴**:资源管理器复制一个 `.py` 文件 → 输入框 Ctrl+V → `💻` 代码气泡;微信/QQ 截图 Ctrl+V → `🖼️` 图片气泡(统一体验)。
|
|
204
|
-
6. **文件夹**:拖入一个含子目录的文件夹 → 一个 `📁` 文件夹气泡;`~/.dsh-drops/<批次>/<文件夹>/` 树结构完整。
|
|
205
|
-
7. **取消拖拽**:拖入后把文件拖出窗口 / 按 Esc → 遮罩消失,页面完全恢复,无残留覆盖。
|
|
206
|
-
8. **@ 菜单**:输入 `@` → 官方「文件与文件夹」组可搜工作区文件;「文件缓存 · ~/.dsh-drops」组列出拖入过的文件,可搜索、可选中插入。
|
|
207
|
-
9. **文件卡片**:发送带引用的消息,等本轮结束 → 轮次尾部出现「📎 消息引用的文件」卡片(图标+名称+大小),点击卡片打开文件。
|
|
208
|
-
10. **read_document**:对 Agent 说「用 read_document 读取我刚引用的文件并总结」→ Agent 调用 `read_document` 拿到文本内容(或目录树/元数据/建议)。
|
|
209
|
-
11. **超大文件**:拖入一个 >31MB(动态版)的文件 → toast 明确提示「超过 31MB 上限」,其余文件正常接收。
|
|
210
|
-
12. **卸载清理**:`dsh plugin --profile web remove dsh-workbuddy-files` 后重启,遮罩/按钮/卡片全部消失;`~/.dsh-drops` 按需手动清理。
|
|
211
|
-
|
|
212
|
-
## 已知限制与后续方向
|
|
213
|
-
|
|
214
|
-
- 动态演示版上传走 `host.call` + base64(单文件 ≤ ~31MB),正式包走 fetch 二进制流(≤ 256MB 可配);
|
|
215
|
-
- turnTail 为链式槽(先匹配者胜出):当同一轮次既有用户引用又有 Agent 产出文件时,与官方「产出文件」行按注册顺序互斥 —— 扩展:合并渲染或注册到更早优先级;
|
|
216
|
-
- 二进制/超大文件的内容读取依赖宿主侧转换(read_document 返回元数据与建议);
|
|
217
|
-
- 未做多语言字典注册(当前界面文案为中文),扩展点:`locale.register('workbuddy', { zh, en })`;
|
|
218
|
-
- 零上传直引(FS Access API 句柄 + 发送时物化 + 持久化句柄到 IndexedDB)可作为可选增强回补:默认仍走「拖入即缓存」的可靠路径,直引仅用于超大目录/远程部署场景。
|
|
1
|
+
# dsh-workbuddy-files
|
|
2
|
+
|
|
3
|
+
WorkBuddy 风格的 DeepSeek Harness (DSH) Web 插件:**拖拽 / 粘贴文件与文件夹 → 全屏接收遮罩 → 输入框原生引用气泡(File Pill)→ 发送后对话区文件卡片**,Agent 通过 `read_document` 工具或任意读取工具直接消费引用。
|
|
4
|
+
|
|
5
|
+
> 本插件同时存在两个形态:
|
|
6
|
+
> - **本目录**:可发布/可安装的正式插件包(TypeScript,Cordis,Host + Client 双半侧);
|
|
7
|
+
> - **动态演示版**:会话内动态 Cordis 插件(pluginId `wbdrop-1`),当前版本 pkg-5,与正式包行为一致,拖拽/粘贴/`@`/卡片立即可体验。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 功能对照(与 WorkBuddy 对齐)
|
|
12
|
+
|
|
13
|
+
| 需求 | 实现 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| 拖拽任意文件到输入框(PDF/Word/Excel/图片/代码…) | ✅ 统一接管:**所有**文件拖入都显示本插件全屏「松开以接收文件」遮罩并走气泡管线 |
|
|
16
|
+
| 截图/文件粘贴(Ctrl+V) | ✅ 统一接管:图片与文件粘贴都走气泡管线(拖入即缓存 + 引用绝对路径) |
|
|
17
|
+
| `@` 弹出菜单搜索并引用文件/文件夹 | ✅ 工作区文件由 DSH 官方 `@` 源提供;本插件追加「文件缓存」分组 |
|
|
18
|
+
| 光标位置原生引用气泡 | ✅ 输入机 `insertReference` 铸造原生 occurrence 气泡(文件名 + 类型图标) |
|
|
19
|
+
| 气泡整体 Backspace/Delete 一次删除 | ✅ 原生 occurrence 语义(整段删除、撤销/重做一体) |
|
|
20
|
+
| 发送后对话区文件卡片 | ✅ `conversation.chat.turnTail` 链式槽按轮次渲染「📎 消息引用的文件」卡片,点击经 `openFile` 打开 |
|
|
21
|
+
| 引用附带绝对路径 | ✅ **拖入瞬间**按预分配路径(`~/.dsh-drops/<批次>/<相对路径>`)在光标处插入气泡,文件**后台异步缓存**,引用即 `@"C:\...\file.pdf"` |
|
|
22
|
+
| 文件夹拖拽保留目录树 | ✅ `webkitGetAsEntry` 递归遍历;顶层文件夹一个 `📁` 气泡,内部树结构原样落盘 |
|
|
23
|
+
| Agent 可读性 | ✅ Host 注册 `read_document` 工具:文本返回内容、目录返回树、图片/二进制/大文件返回元数据与建议 |
|
|
24
|
+
|
|
25
|
+
> 设计取舍:v1 曾实现 Chromium「FS Access API 零上传直引」(拖入时零字节、发送时才落地)。实测中发送时物化偶发「直引落地失败」会阻断发送,且浏览器安全模型拿不到拖入文件的绝对路径。为保证**拖入必可用、发送必成功**,现统一为「拖入即缓存 + 真实绝对路径」——localhost 场景下缓存即本地拷贝,无网络开销;零上传直引作为后续可选增强(见「后续方向」)。
|
|
26
|
+
|
|
27
|
+
## 架构
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
浏览器(Client 半侧) DSH Host(Node 进程)
|
|
31
|
+
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
|
32
|
+
│ window 拖拽/粘贴监听(capture)│ │ 私有 RPC / webServer 路由 │
|
|
33
|
+
│ 任何文件拖拽 → stopPropagation│ │ save → 二进制落盘 │
|
|
34
|
+
│ (原生图片遮罩不再激活) │ ─────► │ list/stat/home │
|
|
35
|
+
│ 全屏遮罩「松开以接收文件」 │ │ │
|
|
36
|
+
│ │ │ ~/.dsh-drops/<批次>/<相对路径>│
|
|
37
|
+
│ 目录树解析(webkitGetAsEntry) │ │ (目录树结构保留,绝对路径) │
|
|
38
|
+
│ → 立即上传(拖入即缓存) │ │ │
|
|
39
|
+
│ │ │ read_document 工具 │
|
|
40
|
+
│ 气泡注入管线 │ │ 解析引用 → 内容/树/元数据 │
|
|
41
|
+
│ conversation.input.shell(id)│ └──────────────────────────────┘
|
|
42
|
+
│ shell.state.getSnapshot() │
|
|
43
|
+
│ shell.insertReference( │
|
|
44
|
+
│ {source:'workbuddy', │
|
|
45
|
+
│ ref, label, appearance}, │
|
|
46
|
+
│ {start,end,draftRev}) ← │ 草稿修订号 CAS + 重试(8 次)
|
|
47
|
+
│ │
|
|
48
|
+
│ @ 触发源「workbuddy」 │
|
|
49
|
+
│ candidates: drops.list → │
|
|
50
|
+
│ onPick → insert │
|
|
51
|
+
│ codec.serialize: 恒等 │
|
|
52
|
+
│ (拖入时已落地,发送必成功) │
|
|
53
|
+
│ │
|
|
54
|
+
│ conversationEvents 定义 │
|
|
55
|
+
│ workbuddy-file-refs │
|
|
56
|
+
│ (按轮次聚合用户消息引用) │
|
|
57
|
+
│ │
|
|
58
|
+
│ Slot 注册 │
|
|
59
|
+
│ shell.overlay(遮罩+toast) │
|
|
60
|
+
│ conversation.input.left │
|
|
61
|
+
│ (📎 选择文件/文件夹) │
|
|
62
|
+
│ conversation.chat.turnTail │
|
|
63
|
+
│ (文件卡片,select 精确匹配)│
|
|
64
|
+
└──────────────────────────────┘
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 为什么气泡是「原生」的
|
|
68
|
+
|
|
69
|
+
DSH 的输入状态机(`dsh-client-ui-input-trigger` + `InputMachine`)内置**引用气泡(occurrence)**机制:官方 `@` 文件引用、`/` 命令都以气泡呈现,气泡在草稿中占用原子区间 —— Backspace 一次删除、撤销/重做、提交时序列化都走同一套机器。插件通过 `conversation.input.shell(sessionId).insertReference(reference, span)` 铸造气泡(id 寻址服务面,不经过 scoped context),`span` 携带 `draftRev` 做 CAS:插入期间用户继续输入导致修订号变化时自动重读状态重试;非 plain 阶段(提交中)被拒则 toast 提示。
|
|
70
|
+
|
|
71
|
+
### 与原生图片轨道的互斥共存
|
|
72
|
+
|
|
73
|
+
DSH 原生的图片拖放轨道在 document 上监听 dragenter 并无条件显示全屏遮罩。本插件在 **window capture 阶段(传播第一站)** 从 dragenter 起 `stopPropagation()`,原生轨道感知不到被接管的拖拽,其遮罩不会激活,也不会因收不到收尾事件而卡住页面;本插件的遮罩自身在 dragleave/dragend/drop 时必然归零,页面随时可恢复。
|
|
74
|
+
|
|
75
|
+
### 可靠性设计
|
|
76
|
+
|
|
77
|
+
- **拖入即插气泡**:最终缓存路径在拖入时即可确定(`<dropsRoot>/<批次>/<文件名或目录树>`),气泡**立即**插入光标处,不等上传;文件落盘在后台异步进行(本地写盘,用户无感),完成/失败以 toast 告知;
|
|
78
|
+
- 发送时 `codec.serialize` 恒等,**发送消息不可能因文件未落地而失败**;极端情况下(超大文件拖入后立即发送)Agent 读取时文件可能仍在写入,`read_document` 对不存在路径返回明确错误,稍候重试即可;
|
|
79
|
+
- 气泡插入失败(输入区忙/无会话)自动降级:直接写草稿文本(`setDraft`)/ 聚焦 textarea 时 `execCommand`,保证输入框有内容;
|
|
80
|
+
- Host 写盘加固:fs 服务写 base64 临时文本 + PowerShell 从文件解码写盘(不依赖 stdin 管道),目录自动创建、临时文件自动清理;
|
|
81
|
+
- 单文件上限(动态版 ~31MB / 正式包 256MB 可配)超出时跳过并提示,其余文件正常接收。
|
|
82
|
+
|
|
83
|
+
## 项目结构
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
dsh-workbuddy-files/
|
|
87
|
+
├── package.json # dsh.client 声明 + dsh.bundle.patch 声明
|
|
88
|
+
├── cordis.patch.yml # bundle patch:把 host 半侧挂进配置树
|
|
89
|
+
├── tsconfig.json
|
|
90
|
+
├── tsdown.config.ts # 构建 lib/index.js(host)+ lib/client.js(client)
|
|
91
|
+
├── README.md
|
|
92
|
+
└── src/
|
|
93
|
+
├── host/
|
|
94
|
+
│ └── index.ts # /workbuddy-drops 路由 + read_document 工具
|
|
95
|
+
└── client/
|
|
96
|
+
├── index.ts # 副作用外壳:__ModuleLoader__.load
|
|
97
|
+
├── app.ts # factory:require('react') + 组装插件
|
|
98
|
+
├── at-source.ts # @ 触发源(文件缓存分组)
|
|
99
|
+
├── definitions.ts # 会话事件定义 + turnTail selector
|
|
100
|
+
├── css.ts
|
|
101
|
+
├── types.ts
|
|
102
|
+
├── components/
|
|
103
|
+
│ ├── overlay.tsx # 全屏拖拽遮罩 + toast
|
|
104
|
+
│ ├── pick-button.tsx # 📎 选择文件/文件夹
|
|
105
|
+
│ └── file-cards.tsx # 对话区文件卡片
|
|
106
|
+
└── lib/
|
|
107
|
+
├── bus.ts # 拖拽/提示总线
|
|
108
|
+
├── insert.ts # 气泡注入管线(核心)
|
|
109
|
+
├── drop.ts # 拖拽/粘贴处理
|
|
110
|
+
├── transfer.ts # 目录树遍历 + 上传 + 引用格式
|
|
111
|
+
└── icons.ts # 类型图标
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 安装
|
|
115
|
+
|
|
116
|
+
前置:Node 18+、pnpm(`dsh plugin` 内部转发给 pnpm)。
|
|
117
|
+
|
|
118
|
+
### 方式一:本地项目安装(开发/验证,推荐先走这条)
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
cd F:\dsh工作空间\dsh-workbuddy-files
|
|
122
|
+
npm install # 安装 tsdown(0.22+)/ typescript 等构建依赖
|
|
123
|
+
npm run bundle # 产出 lib/index.js 与 lib/client.js
|
|
124
|
+
npm run typecheck # 可选:类型检查(已通过)
|
|
125
|
+
|
|
126
|
+
# link 安装:符号链接(改完代码重新 bundle + 重启 dsh 即生效)
|
|
127
|
+
dsh plugin --profile web add link:..\dsh-workbuddy-files
|
|
128
|
+
# 或拷贝安装(发布前验证打包产物用):
|
|
129
|
+
dsh plugin --profile web add file:..\dsh-workbuddy-files
|
|
130
|
+
|
|
131
|
+
dsh web # 重启 web 界面
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`dsh plugin add` 会:在 `$DSH_HOME/profiles/web` 里 `pnpm add` 该包,并因为本包声明了 `dsh.bundle.patch`,自动把 `dsh-workbuddy-files` 追加进 profile 的 `dsh.profile.bundles` 层栈(可在 `~/.dsh/profiles/web/package.json` 中核对)。卸载:`dsh plugin --profile web remove dsh-workbuddy-files`。
|
|
135
|
+
|
|
136
|
+
### 方式二:npm registry 安装(在其他设备上安装,推荐)
|
|
137
|
+
|
|
138
|
+
前置:目标设备已安装 DSH 与 pnpm。
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
npm i -g pnpm # 若尚未安装
|
|
142
|
+
# DSH 未安装时:npm i -g @deepseek-ai/dsh
|
|
143
|
+
|
|
144
|
+
dsh plugin --profile web add dsh-workbuddy-files
|
|
145
|
+
dsh web # 重启 web 界面,刷新页面即生效
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 方式三:GitHub 直装(npm registry 不可达时)
|
|
149
|
+
|
|
150
|
+
仓库自带 `lib/` 构建产物(clone 即可用,无需构建):
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
dsh plugin --profile web add "github:miaoxintechnology/dsh-workbuddy-files"
|
|
154
|
+
dsh web
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 方式四:离线拷贝
|
|
158
|
+
|
|
159
|
+
把整个项目目录拷到目标设备,然后:
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
dsh plugin --profile web add file:<项目目录绝对路径>
|
|
163
|
+
dsh web
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 更新与卸载
|
|
167
|
+
|
|
168
|
+
```powershell
|
|
169
|
+
# 更新到指定版本(显式版本号可绕过 pnpm 的「新版本最小发布年龄」供应链门禁)
|
|
170
|
+
dsh plugin --profile web add dsh-workbuddy-files@0.1.5
|
|
171
|
+
|
|
172
|
+
# 卸载
|
|
173
|
+
dsh plugin --profile web remove dsh-workbuddy-files
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 配置(可选)
|
|
177
|
+
|
|
178
|
+
`cordis.patch.yml` 中 host 半侧的 `config`(浏览器半侧目前收不到配置,走代码默认值):
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
config:
|
|
182
|
+
dropsDir: '~/.dsh-drops' # 缓存根目录
|
|
183
|
+
maxFileBytes: 268435456 # 单文件上限(默认 256MB)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 依赖说明
|
|
187
|
+
|
|
188
|
+
- Host 半侧硬依赖 `webServer` 服务(`inject: ['webServer']`),写文件用 `node:fs`(真实插件非沙箱环境,与 dsh-pet 同款写法);
|
|
189
|
+
- Client 半侧在运行时探测 `sessions` / `conversation` / `inputTriggers` / `conversationEvents` / `slots` 服务,缺失时优雅降级(例如无 `conversation` 时气泡退化为纯文本插入)。
|
|
190
|
+
|
|
191
|
+
### 沙箱模式说明
|
|
192
|
+
|
|
193
|
+
- **正式插件包**:Host 半侧用 `node:fs` 直写,不经过 DSH 的沙箱化 shell/fs 服务(沙箱只约束模型工具的执行,不约束插件自身的 Node 代码)——因此 `read-only` / `workspace-write` / `danger-full-access` 任何会话模式下,拖入即缓存都能正常工作;
|
|
194
|
+
- **动态演示版**:动态插件环境没有 Node 文件系统,二进制写盘需借用沙箱化 shell 服务;当部署的沙箱 runner 不可用(如 Windows ACL runner 配置异常)或缓存目录位于会话 workspace 之外时,写盘命令需要 `danger-full-access` 策略(演示版已显式携带,与当前会话文件策略一致);
|
|
195
|
+
- **折中方案**:将 `dropsDir` 配置到 workspace 内(如 `<workspace>/.dsh-drops`),workspace-write 沙箱即可放行;或让插件先继承会话模式、被沙箱拒绝时再升级重试并提示。
|
|
196
|
+
|
|
197
|
+
## 测试步骤(逐条验收)
|
|
198
|
+
|
|
199
|
+
1. **拖拽文件**:从资源管理器把 `report.pdf` 拖入页面 → 全屏虚线遮罩「松开以接收文件」(不是原生的图片遮罩)→ 松手 → 输入框光标处出现 `📕 report.pdf` 气泡;`~/.dsh-drops/drop-*` 下文件已落地。
|
|
200
|
+
2. **拖拽图片**:拖入一张 PNG → 同一个遮罩 → `🖼️` 气泡(引用 `~/.dsh-drops` 下的真实路径);Agent 可用 `read_image` 查看。
|
|
201
|
+
3. **光标位置**:输入「请分析 」后把光标停在句号前拖入文件 → 气泡插在光标处,而非末尾。
|
|
202
|
+
4. **气泡整体删除**:气泡前/后按 Backspace / Delete → 整个气泡一次删除;Ctrl+Z 完整还原。
|
|
203
|
+
5. **粘贴**:资源管理器复制一个 `.py` 文件 → 输入框 Ctrl+V → `💻` 代码气泡;微信/QQ 截图 Ctrl+V → `🖼️` 图片气泡(统一体验)。
|
|
204
|
+
6. **文件夹**:拖入一个含子目录的文件夹 → 一个 `📁` 文件夹气泡;`~/.dsh-drops/<批次>/<文件夹>/` 树结构完整。
|
|
205
|
+
7. **取消拖拽**:拖入后把文件拖出窗口 / 按 Esc → 遮罩消失,页面完全恢复,无残留覆盖。
|
|
206
|
+
8. **@ 菜单**:输入 `@` → 官方「文件与文件夹」组可搜工作区文件;「文件缓存 · ~/.dsh-drops」组列出拖入过的文件,可搜索、可选中插入。
|
|
207
|
+
9. **文件卡片**:发送带引用的消息,等本轮结束 → 轮次尾部出现「📎 消息引用的文件」卡片(图标+名称+大小),点击卡片打开文件。
|
|
208
|
+
10. **read_document**:对 Agent 说「用 read_document 读取我刚引用的文件并总结」→ Agent 调用 `read_document` 拿到文本内容(或目录树/元数据/建议)。
|
|
209
|
+
11. **超大文件**:拖入一个 >31MB(动态版)的文件 → toast 明确提示「超过 31MB 上限」,其余文件正常接收。
|
|
210
|
+
12. **卸载清理**:`dsh plugin --profile web remove dsh-workbuddy-files` 后重启,遮罩/按钮/卡片全部消失;`~/.dsh-drops` 按需手动清理。
|
|
211
|
+
|
|
212
|
+
## 已知限制与后续方向
|
|
213
|
+
|
|
214
|
+
- 动态演示版上传走 `host.call` + base64(单文件 ≤ ~31MB),正式包走 fetch 二进制流(≤ 256MB 可配);
|
|
215
|
+
- turnTail 为链式槽(先匹配者胜出):当同一轮次既有用户引用又有 Agent 产出文件时,与官方「产出文件」行按注册顺序互斥 —— 扩展:合并渲染或注册到更早优先级;
|
|
216
|
+
- 二进制/超大文件的内容读取依赖宿主侧转换(read_document 返回元数据与建议);
|
|
217
|
+
- 未做多语言字典注册(当前界面文案为中文),扩展点:`locale.register('workbuddy', { zh, en })`;
|
|
218
|
+
- 零上传直引(FS Access API 句柄 + 发送时物化 + 持久化句柄到 IndexedDB)可作为可选增强回补:默认仍走「拖入即缓存」的可靠路径,直引仅用于超大目录/远程部署场景。
|
package/lib/client.js
CHANGED
|
@@ -247,10 +247,11 @@
|
|
|
247
247
|
//#endregion
|
|
248
248
|
//#region src/client/components/file-cards.tsx
|
|
249
249
|
/**
|
|
250
|
-
* 对话区文件卡片(挂在 conversation.chat.turnTail
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
250
|
+
* 对话区文件卡片(挂在 conversation.chat.turnTail —— 0.2.0 起是 **list 槽**):
|
|
251
|
+
* 每个完成的轮次都会渲染本条目,从 ownerProps.turn 的轮次位置数据里读
|
|
252
|
+
* `workbuddy-file-refs`(由 definitions.ts 注册的会话定义聚合自用户消息),
|
|
253
|
+
* 有引用时渲染「类型图标 + 文件名 + 大小/文件夹」卡片,无引用返回 null。
|
|
254
|
+
* 点击卡片经 owner 的 openFile 打开文件。
|
|
254
255
|
*/
|
|
255
256
|
function createFileCardsComponent(React) {
|
|
256
257
|
function FileCard(props) {
|
|
@@ -287,9 +288,11 @@
|
|
|
287
288
|
}, React.createElement("span", { className: "wbd-card-icon" }, icon), React.createElement("span", { className: "wbd-card-name" }, name), React.createElement("span", { className: "wbd-card-sub" }, sub));
|
|
288
289
|
}
|
|
289
290
|
return function FileCards(props) {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
const turn = props.turn;
|
|
292
|
+
const data = turn !== void 0 && turn !== null && turn.data !== void 0 && typeof turn.data.get === "function" ? turn.data.get("workbuddy-file-refs") : void 0;
|
|
293
|
+
const refs = data !== void 0 && data !== null && Array.isArray(data.refs) ? data.refs : [];
|
|
294
|
+
if (refs.length === 0) return null;
|
|
295
|
+
return React.createElement("div", { className: "wbd-cards" }, React.createElement("span", { className: "wbd-cards-label" }, "📎 消息引用的文件"), refs.map((path, i) => React.createElement(FileCard, {
|
|
293
296
|
key: String(path) + ":" + i,
|
|
294
297
|
path,
|
|
295
298
|
openFile: props.openFile
|
|
@@ -316,11 +319,17 @@
|
|
|
316
319
|
* 📎 引用按钮(挂在 conversation.input.left 列表槽):
|
|
317
320
|
* 统一走 <input type=file>(多选 / webkitdirectory)→ 立即插入气泡 →
|
|
318
321
|
* 后台缓存(所有浏览器行为一致)。
|
|
322
|
+
*
|
|
323
|
+
* 该槽位是 session 作用域,标准属性里的 `sessionId` 是 0.2.0 获取当前会话的
|
|
324
|
+
* 官方途径(sessions 服务已不再暴露列表快照),在此上报给插件共享状态。
|
|
319
325
|
*/
|
|
320
|
-
function createPickButtonComponent(React, bus, handlers) {
|
|
321
|
-
return function PickButton() {
|
|
326
|
+
function createPickButtonComponent(React, bus, handlers, onSession) {
|
|
327
|
+
return function PickButton(props) {
|
|
322
328
|
const [open, setOpen] = React.useState(false);
|
|
323
329
|
const newBatch = () => "drop-" + Date.now().toString(36);
|
|
330
|
+
React.useEffect(() => {
|
|
331
|
+
onSession(props.sessionId);
|
|
332
|
+
}, [props.sessionId]);
|
|
324
333
|
const pickFolder = async () => {
|
|
325
334
|
setOpen(false);
|
|
326
335
|
const input = document.createElement("input");
|
|
@@ -474,14 +483,6 @@
|
|
|
474
483
|
};
|
|
475
484
|
}
|
|
476
485
|
};
|
|
477
|
-
/** turnTail chain selector:仅当该轮次存在文件引用时返回 matched,避免抢占其他链条目 */
|
|
478
|
-
function selectTurnFileRefs(owner) {
|
|
479
|
-
const t = owner !== null && owner !== void 0 ? owner.turn : void 0;
|
|
480
|
-
if (t === void 0 || t.data === void 0 || typeof t.data.get !== "function") return null;
|
|
481
|
-
const data = t.data.get("workbuddy-file-refs");
|
|
482
|
-
if (data === void 0 || data === null || !Array.isArray(data.refs) || data.refs.length === 0) return null;
|
|
483
|
-
return { refs: data.refs };
|
|
484
|
-
}
|
|
485
486
|
//#endregion
|
|
486
487
|
//#region src/client/lib/bus.ts
|
|
487
488
|
function createDropBus() {
|
|
@@ -820,7 +821,7 @@
|
|
|
820
821
|
}
|
|
821
822
|
function createInsertPipeline(deps) {
|
|
822
823
|
return async function insertItems(items) {
|
|
823
|
-
const sessionId = deps.
|
|
824
|
+
const sessionId = deps.currentSessionId();
|
|
824
825
|
if (sessionId === void 0) {
|
|
825
826
|
deps.toast("请先打开或新建一个会话,再拖入文件", "error");
|
|
826
827
|
return 0;
|
|
@@ -903,8 +904,9 @@
|
|
|
903
904
|
name: "workbuddy-files",
|
|
904
905
|
inject: [
|
|
905
906
|
"slots",
|
|
906
|
-
"
|
|
907
|
-
"
|
|
907
|
+
"conversation",
|
|
908
|
+
"uiConversation",
|
|
909
|
+
"inputTriggers"
|
|
908
910
|
],
|
|
909
911
|
apply(ctx) {
|
|
910
912
|
try {
|
|
@@ -919,13 +921,17 @@
|
|
|
919
921
|
function applyClient(ctx, React) {
|
|
920
922
|
console.log("[workbuddy-files] client apply 开始");
|
|
921
923
|
const get = (name) => ctx.get(name);
|
|
922
|
-
const sessions = get("sessions");
|
|
923
924
|
const slots = get("slots");
|
|
924
925
|
const conversation = get("conversation");
|
|
925
926
|
const inputTriggers = get("inputTriggers");
|
|
926
|
-
const
|
|
927
|
+
const uiConversation = get("uiConversation");
|
|
927
928
|
const styles = get("styles");
|
|
928
929
|
if (slots === void 0) return;
|
|
930
|
+
let currentSessionId;
|
|
931
|
+
const setSessionId = (id) => {
|
|
932
|
+
currentSessionId = id;
|
|
933
|
+
};
|
|
934
|
+
const getSessionId = () => currentSessionId;
|
|
929
935
|
const injectCssViaHead = (css) => {
|
|
930
936
|
if (typeof document === "undefined") return;
|
|
931
937
|
const tagId = "dsh-workbuddy-files/styles";
|
|
@@ -943,7 +949,7 @@
|
|
|
943
949
|
};
|
|
944
950
|
const effect = ctx.effect.bind(ctx);
|
|
945
951
|
effect(insertStyles, "workbuddy: styles");
|
|
946
|
-
console.log("[workbuddy-files] client apply 开始:services slots=" + (slots !== void 0) + " conversation=" + (conversation !== void 0) + " inputTriggers=" + (inputTriggers !== void 0) + "
|
|
952
|
+
console.log("[workbuddy-files] client apply 开始:services slots=" + (slots !== void 0) + " conversation=" + (conversation !== void 0) + " inputTriggers=" + (inputTriggers !== void 0) + " uiConversation=" + (uiConversation !== void 0) + " styles=" + (styles !== void 0));
|
|
947
953
|
const bus = createDropBus();
|
|
948
954
|
let rootCache = null;
|
|
949
955
|
dropsHome().then((r) => {
|
|
@@ -966,8 +972,8 @@
|
|
|
966
972
|
const handlers = createDropHandlers({
|
|
967
973
|
bus,
|
|
968
974
|
insert: createInsertPipeline({
|
|
969
|
-
sessions,
|
|
970
975
|
conversation,
|
|
976
|
+
currentSessionId: getSessionId,
|
|
971
977
|
toast: bus.toast
|
|
972
978
|
}),
|
|
973
979
|
ensureRoot,
|
|
@@ -982,9 +988,10 @@
|
|
|
982
988
|
const source = createAtSource();
|
|
983
989
|
effect(() => inputTriggers.registerSource(source), "workbuddy: @ source");
|
|
984
990
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const
|
|
991
|
+
const events = uiConversation !== void 0 ? uiConversation.events : void 0;
|
|
992
|
+
if (events !== void 0) effect(() => {
|
|
993
|
+
const d1 = events.register(boundaryDef);
|
|
994
|
+
const d2 = events.register(fileRefsDef);
|
|
988
995
|
return () => {
|
|
989
996
|
if (typeof d1 === "function") d1();
|
|
990
997
|
if (typeof d2 === "function") d2();
|
|
@@ -1001,10 +1008,12 @@
|
|
|
1001
1008
|
id: "workbuddy-pick",
|
|
1002
1009
|
order: 0,
|
|
1003
1010
|
label: "引用文件/文件夹"
|
|
1004
|
-
}, createPickButtonComponent(React, bus, handlers)));
|
|
1011
|
+
}, createPickButtonComponent(React, bus, handlers, setSessionId)));
|
|
1005
1012
|
slots.inject("conversation.chat.turnTail", () => slots.register({
|
|
1006
1013
|
name: "conversation.chat.turnTail",
|
|
1007
|
-
|
|
1014
|
+
id: "dsh-workbuddy-files",
|
|
1015
|
+
order: 20,
|
|
1016
|
+
label: "文件引用卡片"
|
|
1008
1017
|
}, createFileCardsComponent(React)));
|
|
1009
1018
|
console.log("[workbuddy-files] client 就绪:拖入即插气泡 + 后台缓存 / 统一遮罩 / 文件卡片");
|
|
1010
1019
|
}
|
package/package.json
CHANGED
|
@@ -1,88 +1,84 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-workbuddy-files",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "WorkBuddy 风格的 DeepSeek Harness Web 插件:拖拽/粘贴文件与文件夹生成输入框引用气泡(附绝对路径)、@ 菜单检索缓存文件、对话区文件卡片、read_document 工具。",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"dsh",
|
|
7
|
-
"dsh-plugin",
|
|
8
|
-
"deepseek-harness",
|
|
9
|
-
"workbuddy",
|
|
10
|
-
"drag-drop",
|
|
11
|
-
"file-reference",
|
|
12
|
-
"file-pill",
|
|
13
|
-
"web-ui",
|
|
14
|
-
"cordis"
|
|
15
|
-
],
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"homepage": "https://github.com/miaoxintechnology/dsh-workbuddy-files#readme",
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/miaoxintechnology/dsh-workbuddy-files.git"
|
|
21
|
-
},
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/miaoxintechnology/dsh-workbuddy-files/issues"
|
|
24
|
-
},
|
|
25
|
-
"type": "module",
|
|
26
|
-
"main": "lib/index.js",
|
|
27
|
-
"exports": {
|
|
28
|
-
".": {
|
|
29
|
-
"default": "./lib/index.js"
|
|
30
|
-
},
|
|
31
|
-
"./client": {
|
|
32
|
-
"default": "./lib/client.js"
|
|
33
|
-
},
|
|
34
|
-
"./src/*": "./src/*",
|
|
35
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
36
|
-
"./package.json": "./package.json"
|
|
37
|
-
},
|
|
38
|
-
"files": [
|
|
39
|
-
"lib",
|
|
40
|
-
"src",
|
|
41
|
-
"cordis.patch.yml",
|
|
42
|
-
"README.md"
|
|
43
|
-
],
|
|
44
|
-
"scripts": {
|
|
45
|
-
"bundle": "tsdown --config tsdown.host.config.ts && tsdown --config tsdown.client.config.ts && node scripts/rename.mjs",
|
|
46
|
-
"watch": "tsdown --config tsdown.client.config.ts --watch",
|
|
47
|
-
"typecheck": "tsc --noEmit",
|
|
48
|
-
"prepublishOnly": "npm run typecheck && npm run bundle"
|
|
49
|
-
},
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"access": "public"
|
|
52
|
-
},
|
|
53
|
-
"_comment": [
|
|
54
|
-
"main (lib/index.js) 是 Host 半侧入口(Cordis 插件 apply/inject)",
|
|
55
|
-
"exports['./client'] 是浏览器半侧入口,DSH 模块系统据此提供 /plugins/dsh-workbuddy-files/client.js",
|
|
56
|
-
"dsh.bundle.patch 声明本包是 bundle:dsh plugin add 后自动加入 profile 的 bundles 层栈并挂载 host 半侧",
|
|
57
|
-
"dsh.client 声明浏览器半侧的注入依赖与平台(web)",
|
|
58
|
-
"peerDependencies 是宿主(DSH)提供的模块,本包不打包它们"
|
|
59
|
-
],
|
|
60
|
-
"dsh": {
|
|
61
|
-
"bundle": {
|
|
62
|
-
"patch": "./cordis.patch.yml"
|
|
63
|
-
},
|
|
64
|
-
"client": {
|
|
65
|
-
"inject": [
|
|
66
|
-
"@deepseek-ai/dsh-client-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"@deepseek-ai/
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"@deepseek-ai/
|
|
79
|
-
"@
|
|
80
|
-
"react": "^18.2.0"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"tsdown": "^0.22.14",
|
|
86
|
-
"typescript": "^5.5.0"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-workbuddy-files",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "WorkBuddy 风格的 DeepSeek Harness Web 插件:拖拽/粘贴文件与文件夹生成输入框引用气泡(附绝对路径)、@ 菜单检索缓存文件、对话区文件卡片、read_document 工具。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"workbuddy",
|
|
10
|
+
"drag-drop",
|
|
11
|
+
"file-reference",
|
|
12
|
+
"file-pill",
|
|
13
|
+
"web-ui",
|
|
14
|
+
"cordis"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/miaoxintechnology/dsh-workbuddy-files#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/miaoxintechnology/dsh-workbuddy-files.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/miaoxintechnology/dsh-workbuddy-files/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "lib/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"default": "./lib/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"default": "./lib/client.js"
|
|
33
|
+
},
|
|
34
|
+
"./src/*": "./src/*",
|
|
35
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"lib",
|
|
40
|
+
"src",
|
|
41
|
+
"cordis.patch.yml",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"bundle": "tsdown --config tsdown.host.config.ts && tsdown --config tsdown.client.config.ts && node scripts/rename.mjs",
|
|
46
|
+
"watch": "tsdown --config tsdown.client.config.ts --watch",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"prepublishOnly": "npm run typecheck && npm run bundle"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"_comment": [
|
|
54
|
+
"main (lib/index.js) 是 Host 半侧入口(Cordis 插件 apply/inject)",
|
|
55
|
+
"exports['./client'] 是浏览器半侧入口,DSH 模块系统据此提供 /plugins/dsh-workbuddy-files/client.js",
|
|
56
|
+
"dsh.bundle.patch 声明本包是 bundle:dsh plugin add 后自动加入 profile 的 bundles 层栈并挂载 host 半侧",
|
|
57
|
+
"dsh.client 声明浏览器半侧的注入依赖与平台(web)",
|
|
58
|
+
"peerDependencies 是宿主(DSH)提供的模块,本包不打包它们"
|
|
59
|
+
],
|
|
60
|
+
"dsh": {
|
|
61
|
+
"bundle": {
|
|
62
|
+
"patch": "./cordis.patch.yml"
|
|
63
|
+
},
|
|
64
|
+
"client": {
|
|
65
|
+
"inject": [
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-input-trigger",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-renderer"
|
|
69
|
+
],
|
|
70
|
+
"platform": "web"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
75
|
+
"react": "^18.2.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
79
|
+
"@types/node": "^26.2.0",
|
|
80
|
+
"@types/react": "^18.2.0",
|
|
81
|
+
"tsdown": "^0.22.14",
|
|
82
|
+
"typescript": "^5.5.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/client/app.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { createFileCardsComponent } from './components/file-cards'
|
|
|
14
14
|
import { createOverlayComponent, ReactLike } from './components/overlay'
|
|
15
15
|
import { createPickButtonComponent } from './components/pick-button'
|
|
16
16
|
import { CSS } from './css'
|
|
17
|
-
import { boundaryDef, fileRefsDef
|
|
17
|
+
import { boundaryDef, fileRefsDef } from './definitions'
|
|
18
18
|
import { createDropBus } from './lib/bus'
|
|
19
19
|
import { createDropHandlers } from './lib/drop'
|
|
20
20
|
import { createInsertPipeline } from './lib/insert'
|
|
@@ -35,8 +35,8 @@ export function makeFactory() {
|
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
37
|
name: 'workbuddy-files',
|
|
38
|
-
// slots
|
|
39
|
-
inject: ['slots', '
|
|
38
|
+
// 0.2.0:slots / conversation / uiConversation(会话定义注册)/ inputTriggers
|
|
39
|
+
inject: ['slots', 'conversation', 'uiConversation', 'inputTriggers'],
|
|
40
40
|
apply(ctx) {
|
|
41
41
|
try {
|
|
42
42
|
applyClient(ctx, React)
|
|
@@ -53,7 +53,6 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
53
53
|
const get = (name: string) => (ctx.get as (n: string) => unknown)(name)
|
|
54
54
|
|
|
55
55
|
// ---- 能力探测(缺失则优雅退出)----
|
|
56
|
-
const sessions = get('sessions') as never
|
|
57
56
|
const slots = get('slots') as {
|
|
58
57
|
inject(key: string, callback: () => unknown): () => void
|
|
59
58
|
register(options: Record<string, unknown>, component: unknown): unknown
|
|
@@ -62,12 +61,19 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
62
61
|
const inputTriggers = get('inputTriggers') as {
|
|
63
62
|
registerSource(source: unknown): () => void
|
|
64
63
|
} | undefined
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// 0.2.0:conversationEvents 服务改名为 uiConversation(定义注册在 .events 上)
|
|
65
|
+
const uiConversation = get('uiConversation') as {
|
|
66
|
+
events?: { register(definition: unknown): (() => void) | undefined }
|
|
67
67
|
} | undefined
|
|
68
68
|
const styles = get('styles') as { insert(css: string): () => void } | undefined
|
|
69
69
|
if (slots === undefined) return
|
|
70
70
|
|
|
71
|
+
// ---- 当前会话 id ----
|
|
72
|
+
// 0.2.0 的 sessions 服务不再暴露 list;会话 id 由 session 作用域的插槽标准属性提供
|
|
73
|
+
let currentSessionId: string | undefined
|
|
74
|
+
const setSessionId = (id: string | undefined): void => { currentSessionId = id }
|
|
75
|
+
const getSessionId = (): string | undefined => currentSessionId
|
|
76
|
+
|
|
71
77
|
// ---- 包内样式 ----
|
|
72
78
|
// 正式插件包没有动态版的 styles 内置服务:回退到 document.head style 注入(dsh-pet 同款)
|
|
73
79
|
const injectCssViaHead = (css: string): void => {
|
|
@@ -87,7 +93,7 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
87
93
|
}
|
|
88
94
|
const effect = (ctx.effect as (fn: () => (() => void) | undefined, label?: string) => void).bind(ctx)
|
|
89
95
|
effect(insertStyles, 'workbuddy: styles')
|
|
90
|
-
console.log('[workbuddy-files] client apply 开始:services slots=' + (slots !== undefined) + ' conversation=' + (conversation !== undefined) + ' inputTriggers=' + (inputTriggers !== undefined) + '
|
|
96
|
+
console.log('[workbuddy-files] client apply 开始:services slots=' + (slots !== undefined) + ' conversation=' + (conversation !== undefined) + ' inputTriggers=' + (inputTriggers !== undefined) + ' uiConversation=' + (uiConversation !== undefined) + ' styles=' + (styles !== undefined))
|
|
91
97
|
|
|
92
98
|
// ---- 共享实例 ----
|
|
93
99
|
const bus = createDropBus()
|
|
@@ -116,7 +122,7 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
// ---- 气泡注入管线 ----
|
|
119
|
-
const insert = createInsertPipeline({
|
|
125
|
+
const insert = createInsertPipeline({ conversation, currentSessionId: getSessionId, toast: bus.toast })
|
|
120
126
|
|
|
121
127
|
// ---- 拖拽 / 粘贴处理 + 窗口级监听 ----
|
|
122
128
|
const handlers = createDropHandlers({ bus, insert, ensureRoot, enqueueUpload })
|
|
@@ -132,11 +138,12 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
132
138
|
effect(() => inputTriggers.registerSource(source), 'workbuddy: @ source')
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
// ----
|
|
136
|
-
|
|
141
|
+
// ---- 会话事件定义(0.2.0:注册在 uiConversation.events 上)----
|
|
142
|
+
const events = uiConversation !== undefined ? uiConversation.events : undefined
|
|
143
|
+
if (events !== undefined) {
|
|
137
144
|
effect(() => {
|
|
138
|
-
const d1 =
|
|
139
|
-
const d2 =
|
|
145
|
+
const d1 = events.register(boundaryDef)
|
|
146
|
+
const d2 = events.register(fileRefsDef)
|
|
140
147
|
return () => {
|
|
141
148
|
if (typeof d1 === 'function') d1()
|
|
142
149
|
if (typeof d2 === 'function') d2()
|
|
@@ -149,12 +156,14 @@ function applyClient(ctx: Record<string, unknown>, React: ReactLike): void {
|
|
|
149
156
|
{ name: 'shell.overlay', id: 'workbuddy-drop', order: 300, label: 'WorkBuddy 拖拽遮罩' },
|
|
150
157
|
createOverlayComponent(React, bus),
|
|
151
158
|
))
|
|
159
|
+
// conversation.input.left:session 作用域,标准属性含 sessionId —— 在此捕获当前会话
|
|
152
160
|
slots.inject('conversation.input.left', () => slots.register(
|
|
153
161
|
{ name: 'conversation.input.left', id: 'workbuddy-pick', order: 0, label: '引用文件/文件夹' },
|
|
154
|
-
createPickButtonComponent(React, bus, handlers),
|
|
162
|
+
createPickButtonComponent(React, bus, handlers, setSessionId),
|
|
155
163
|
))
|
|
164
|
+
// 0.2.0:turnTail 从 chain 变为 list —— 用 id 注册,组件直接拿到 { turn, seq, openFile }
|
|
156
165
|
slots.inject('conversation.chat.turnTail', () => slots.register(
|
|
157
|
-
{ name: 'conversation.chat.turnTail',
|
|
166
|
+
{ name: 'conversation.chat.turnTail', id: 'dsh-workbuddy-files', order: 20, label: '文件引用卡片' },
|
|
158
167
|
createFileCardsComponent(React),
|
|
159
168
|
))
|
|
160
169
|
|
|
@@ -3,10 +3,11 @@ import { dropsStat } from '../lib/transfer'
|
|
|
3
3
|
import type { ReactLike } from './overlay'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* 对话区文件卡片(挂在 conversation.chat.turnTail
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* 对话区文件卡片(挂在 conversation.chat.turnTail —— 0.2.0 起是 **list 槽**):
|
|
7
|
+
* 每个完成的轮次都会渲染本条目,从 ownerProps.turn 的轮次位置数据里读
|
|
8
|
+
* `workbuddy-file-refs`(由 definitions.ts 注册的会话定义聚合自用户消息),
|
|
9
|
+
* 有引用时渲染「类型图标 + 文件名 + 大小/文件夹」卡片,无引用返回 null。
|
|
10
|
+
* 点击卡片经 owner 的 openFile 打开文件。
|
|
10
11
|
*/
|
|
11
12
|
export function createFileCardsComponent(React: ReactLike) {
|
|
12
13
|
function FileCard(props: { path: string; openFile?: (path: string) => void }) {
|
|
@@ -36,12 +37,24 @@ export function createFileCardsComponent(React: ReactLike) {
|
|
|
36
37
|
)
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
interface TurnTailProps {
|
|
41
|
+
/** 0.2.0 的 turnTail list ownerProps:完成的轮次位置 */
|
|
42
|
+
turn?: { data: { get(key: string): { refs?: string[] } | undefined } }
|
|
43
|
+
seq?: number
|
|
44
|
+
openFile?: (path: string) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return function FileCards(props: TurnTailProps) {
|
|
48
|
+
// 0.2.0:turnTail 是 list 条目,ownerProps 直接给出 { turn, seq, openFile }
|
|
49
|
+
const turn = props.turn
|
|
50
|
+
const data = turn !== undefined && turn !== null && turn.data !== undefined && typeof turn.data.get === 'function'
|
|
51
|
+
? turn.data.get('workbuddy-file-refs')
|
|
52
|
+
: undefined
|
|
53
|
+
const refs = data !== undefined && data !== null && Array.isArray(data.refs) ? data.refs : []
|
|
54
|
+
if (refs.length === 0) return null
|
|
42
55
|
return React.createElement('div', { className: 'wbd-cards' },
|
|
43
56
|
React.createElement('span', { className: 'wbd-cards-label' }, '📎 消息引用的文件'),
|
|
44
|
-
|
|
57
|
+
refs.map((path: string, i: number) => React.createElement(FileCard, { key: String(path) + ':' + i, path, openFile: props.openFile })),
|
|
45
58
|
)
|
|
46
59
|
}
|
|
47
60
|
}
|
|
@@ -7,12 +7,24 @@ import type { ReactLike } from './overlay'
|
|
|
7
7
|
* 📎 引用按钮(挂在 conversation.input.left 列表槽):
|
|
8
8
|
* 统一走 <input type=file>(多选 / webkitdirectory)→ 立即插入气泡 →
|
|
9
9
|
* 后台缓存(所有浏览器行为一致)。
|
|
10
|
+
*
|
|
11
|
+
* 该槽位是 session 作用域,标准属性里的 `sessionId` 是 0.2.0 获取当前会话的
|
|
12
|
+
* 官方途径(sessions 服务已不再暴露列表快照),在此上报给插件共享状态。
|
|
10
13
|
*/
|
|
11
|
-
export function createPickButtonComponent(
|
|
12
|
-
|
|
14
|
+
export function createPickButtonComponent(
|
|
15
|
+
React: ReactLike,
|
|
16
|
+
bus: DropBus,
|
|
17
|
+
handlers: Pick<DropHandlers, 'acceptTree'>,
|
|
18
|
+
onSession: (sessionId: string | undefined) => void,
|
|
19
|
+
) {
|
|
20
|
+
return function PickButton(props: { sessionId?: string }) {
|
|
13
21
|
const [open, setOpen] = React.useState(false)
|
|
14
22
|
const newBatch = () => 'drop-' + Date.now().toString(36)
|
|
15
23
|
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
onSession(props.sessionId)
|
|
26
|
+
}, [props.sessionId])
|
|
27
|
+
|
|
16
28
|
const pickFolder = async () => {
|
|
17
29
|
setOpen(false)
|
|
18
30
|
const input = document.createElement('input')
|
package/src/client/lib/insert.ts
CHANGED
|
@@ -13,15 +13,12 @@ import type { InsertItem } from '../types'
|
|
|
13
13
|
* 兜底 2:无 facade → 聚焦 textarea 时 document.execCommand 纯文本插入。
|
|
14
14
|
*/
|
|
15
15
|
export interface InsertDeps {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 会话输入 shell 注册表(0.2.0:ctx.conversation.input,服务键与 0.1.x 相同)。
|
|
18
|
+
*/
|
|
19
19
|
conversation: {
|
|
20
20
|
input: {
|
|
21
|
-
/**
|
|
22
|
-
* id 寻址服务面(InputHub.shell)—— 直接返回会话输入 shell,
|
|
23
|
-
* 无需 sessions.scope()(后者返回 cordis Context,动态门面拒绝暴露)。
|
|
24
|
-
*/
|
|
21
|
+
/** id 寻址服务面 InputHub.shell(sessionId) —— 直接返回会话输入 shell */
|
|
25
22
|
shell(id: string): {
|
|
26
23
|
state: { getSnapshot(): { draft: string; draftRev: number } }
|
|
27
24
|
insertReference(reference: InsertItem['reference'], span: { start: number; end: number; draftRev: number }): boolean
|
|
@@ -30,6 +27,11 @@ export interface InsertDeps {
|
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 当前会话 id。0.2.0 的 sessions 服务不再暴露列表快照,
|
|
32
|
+
* 由 conversation.input.left 插槽的 sessionId 标准属性捕获。
|
|
33
|
+
*/
|
|
34
|
+
currentSessionId(): string | undefined
|
|
33
35
|
toast(text: string, level?: 'info' | 'error'): void
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -47,7 +49,7 @@ function readCaret(fallbackLen: number): number {
|
|
|
47
49
|
|
|
48
50
|
export function createInsertPipeline(deps: InsertDeps): InsertPipeline {
|
|
49
51
|
return async function insertItems(items: InsertItem[]): Promise<number> {
|
|
50
|
-
const sessionId = deps.
|
|
52
|
+
const sessionId = deps.currentSessionId()
|
|
51
53
|
if (sessionId === undefined) {
|
|
52
54
|
deps.toast('请先打开或新建一个会话,再拖入文件', 'error')
|
|
53
55
|
return 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主(DSH)运行时模块的最小环境声明。
|
|
3
|
+
*
|
|
4
|
+
* 这两个包由 Harness 在运行时提供(宿主模块解析),第三方插件不把它们装成
|
|
5
|
+
* 依赖 —— 官方 0.2.0 插件同样只 peer 依赖 @deepseek-ai/cordis。这里声明本
|
|
6
|
+
* 插件实际消费的接口形状,使本地 `npm run typecheck` 不依赖宿主包。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare module '@deepseek-ai/dsh-tools' {
|
|
10
|
+
/** 模型工具定义(本插件只消费 defineTool 的宽松签名) */
|
|
11
|
+
export interface ToolDefinition {
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
parameters: Record<string, unknown>
|
|
15
|
+
output: {
|
|
16
|
+
schema: unknown
|
|
17
|
+
render: (args: unknown, value: unknown) => Array<{ type: string; text: string }>
|
|
18
|
+
presentationMeta?: (args: unknown, value: unknown) => unknown
|
|
19
|
+
}
|
|
20
|
+
execute: (args: never, exec: unknown) => Promise<unknown>
|
|
21
|
+
presentCall?: (args: never) => unknown
|
|
22
|
+
presentResult?: (args: never, result: unknown) => unknown
|
|
23
|
+
}
|
|
24
|
+
/** 定义并校验一个模型工具 */
|
|
25
|
+
export function defineTool<T extends Record<string, unknown>>(definition: T): T
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module '@deepseek-ai/dsh-client-ui-input-trigger/client' {
|
|
29
|
+
/** 一条 @ / 菜单候选 */
|
|
30
|
+
export interface InputTriggerCandidate {
|
|
31
|
+
name: string
|
|
32
|
+
description?: string
|
|
33
|
+
icon?: string
|
|
34
|
+
hint?: string
|
|
35
|
+
section?: string
|
|
36
|
+
value?: string
|
|
37
|
+
}
|
|
38
|
+
/** 一次 @ / 菜单触发源的注册面 */
|
|
39
|
+
export interface InputTriggerSource {
|
|
40
|
+
trigger: '@' | '/'
|
|
41
|
+
name: string
|
|
42
|
+
order?: number
|
|
43
|
+
showGroupTitle?: boolean
|
|
44
|
+
candidates(
|
|
45
|
+
session: { readonly sessionId: string },
|
|
46
|
+
request: { query: string; quoted?: boolean; signal: AbortSignal },
|
|
47
|
+
): Promise<InputTriggerCandidate[]> | InputTriggerCandidate[]
|
|
48
|
+
onPick(pick: { candidate: InputTriggerCandidate }): { insert?: unknown } | undefined
|
|
49
|
+
codec?: {
|
|
50
|
+
clipboardText(ref: string): string
|
|
51
|
+
serialize(ref: string, signal: AbortSignal): Promise<string>
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|