@wszhoho/dsh-file-attachment 0.3.4 → 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.
- package/README.md +15 -8
- package/lib/client.js +98 -24
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,29 +4,32 @@ DeepSeek Harness (dsh) web GUI 插件:在会话输入框中**拖入或 Ctrl+V
|
|
|
4
4
|
|
|
5
5
|
图片文件由插件接管后走 dsh web 既有草稿图片流程(`addImages`):不落盘、不进文件条、不缩放;仅当输入框未就绪且为纯图片时,才原样交回原生。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
另外,输入框工具栏(加号之后)提供了一个**上传按钮(📎 回形针图标)**,点击后弹出系统文件选择框,可多选文档/图片,复用与拖入/粘贴完全一致的落盘 + `@路径` 插入管线。PC 与移动端浏览器均适用(移动端走原生文件选择器)。
|
|
8
8
|
|
|
9
9
|
## 界面
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**演示**:拖入/粘贴文档 → 落盘并在输入框插入 `@` 引用 → 输入框上方 dock 文件条待发送状态(完整流程见 gif)。
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**上传按钮**:输入框工具栏 `+` 之后的 📎 回形针图标,点击弹出系统文件选择框(可多选文档/图片)。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**可上传类型设置**:「设置 → 文件附件」页可按 文档 / 代码 / 配置文件 三类增删扩展名(小写、不带点),图片恒可发送。
|
|
18
|
+
|
|
19
|
+

|
|
18
20
|
|
|
19
21
|
## 行为一览
|
|
20
22
|
|
|
21
23
|
| 操作 | 行为 |
|
|
22
24
|
| --- | --- |
|
|
23
|
-
|
|
|
25
|
+
| 点击**上传按钮(📎)**,在系统文件选择框中**多选**文档/图片 | 与拖入/粘贴同一管线:文档 → 落盘 + 光标插入全部 `@` 路径;图片 → 走既有草稿图片流程;`accept` 限定图片 + 常见文档扩展名,移动端自动弹出文件选择器 |
|
|
24
26
|
| 拖入/粘贴 **文档/代码/配置文件**(非图片,默认 doc·code·config 三类扩展名,如 docx/xlsx/pdf/md/txt/js/json) | 浏览器读全文 → base64 → 宿主落盘 `<会话工作区>/.dsh-file-attachment/<日期>/<文件名>` → 光标插入 `@绝对路径`,成功不提示、失败简短 toast |
|
|
25
27
|
| 一次拖入/粘贴 **多个文件**(支持多文件) | 批量处理:逐个读取全文 → base64 → 落盘 → 在光标处一次性插入全部 `@` 引用;全部成功不提示,部分失败仅简短提示「N 个已跳过」 |
|
|
26
28
|
| 拖入 **目录** | 拒绝,toast 提示「不支持拖入目录」,不插入引用 |
|
|
27
29
|
| 拖入/粘贴 **图片** | 接管后走 dsh 既有草稿图片流程(`addImages`):不落盘、不进文件条、不缩放;输入框未就绪且纯图片时原样交原生 |
|
|
28
30
|
| 粘贴 **纯文本**(含从地址栏复制的目录路径字符串) | 不改写,完全原生行为 |
|
|
29
31
|
| 重复拖入同名文件(同一天内) | 追加 `-1`、`-2` 序号,绝不覆盖 |
|
|
32
|
+
| 点文件条(dock)`×` 移除 | 移除 dock 条目,并清除输入框中指向该文件路径的**全部** `@` 引用 chip(同一文件拖入多次产生的多个 chip 一并清除);其他文件的引用不受影响 |
|
|
30
33
|
| 单文件 > 50MB | 跳过并提示 |
|
|
31
34
|
|
|
32
35
|
### 文件命名
|
|
@@ -85,18 +88,22 @@ packages/dsh-file-attachment/
|
|
|
85
88
|
|
|
86
89
|
- **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}}`。
|
|
87
90
|
- **Client 半**:`conversation.input.dock`(list 槽,同步桥状态,不渲染文本;经会话作用域取输入机,hero 首轮空白态也挂载)+ `shell.overlay`(list 槽,toast 通知展示)+ `conversation.input.left`(list 槽,**上传按钮**,渲染为左动作簇第 3 项),document 级 capture 监听先于应用 bubble 监听执行。
|
|
88
|
-
- **上传按钮**:`FaUploadButton` 组件注入 `conversation.input.left`(`order:10`),内部是隐藏 `<input type="file" multiple accept>`(`accept` 限定 `image/*` + 常见文档扩展名),点击触发原生选择框;`onChange` 按 `file.type` 是否为 `image/` 分派到 `runBatch`(文档落盘插 `@`、图片走草稿图片流程),完成后清空 `input.value` 以便重复选择同一文件。图标为内联 SVG
|
|
91
|
+
- **上传按钮**:`FaUploadButton` 组件注入 `conversation.input.left`(`order:10`),内部是隐藏 `<input type="file" multiple accept>`(`accept` 限定 `image/*` + 常见文档扩展名),点击触发原生选择框;`onChange` 按 `file.type` 是否为 `image/` 分派到 `runBatch`(文档落盘插 `@`、图片走草稿图片流程),完成后清空 `input.value` 以便重复选择同一文件。图标为内联 SVG 回形针(📎,`stroke=currentColor`,随主题变色)。
|
|
89
92
|
- **通知**:`shell.overlay` frame-wide 浮动 toast(不参与文档流,不破坏布局),4 秒自动消失。
|
|
90
93
|
- **浮层**:拖入任何文件时 capture 阶段拦截应用自带「拖入图片…」DropOverlay(文案面向图片,对文档是误导)。
|
|
91
94
|
|
|
92
95
|
## 开发说明
|
|
93
96
|
|
|
94
97
|
- `lib/*.js`、`package.json`、`cordis.patch.yml`、`README.md` 当前均为 **UTF-8 无 BOM**(以仓库实测为准);编辑时保持原编码,不引入 BOM。
|
|
95
|
-
- Host 半用 `webServer.register({ kind: 'prefix', path: '/dsh-file-attachment', handler })`
|
|
98
|
+
- Host 半用 `webServer.register({ kind: 'prefix', path: '/dsh-file-attachment', handler })` 收文件,纯 ESM 无构建,不依赖装饰器/远程反射。
|
|
96
99
|
- Client 半保存用 `fetch('/dsh-file-attachment/save', { method: 'POST', body: JSON.stringify({ name, data, sessionId }) })`,信封为 `{ ok, value | error }`。
|
|
97
100
|
- 项目根 = 会话工作区(`session.header.cwd` → `sandboxPolicy.workspaceRoot` → `process.cwd()` 兜底)。
|
|
98
101
|
- 50MB 上限两侧一致(client 跳过 + host 校验)。
|
|
99
102
|
|
|
103
|
+
## Star
|
|
104
|
+
|
|
105
|
+
如果这个插件帮到了你,欢迎到 [GitHub 仓库](https://github.com/wszhoho/dsh-file-attachment) 点个 ⭐ Star,感谢支持!
|
|
106
|
+
|
|
100
107
|
## License
|
|
101
108
|
|
|
102
109
|
MIT
|
package/lib/client.js
CHANGED
|
@@ -16,7 +16,7 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
17
17
|
const react = require('react')
|
|
18
18
|
|
|
19
|
-
// Client 服务依赖:槽位 / 输入机 /
|
|
19
|
+
// Client 服务依赖:槽位 / 输入机 / 会话。
|
|
20
20
|
// 保存文件不走 remote,而是 fetch POST 宿主 webServer 路由 /dsh-file-attachment/save。
|
|
21
21
|
const inject = [
|
|
22
22
|
"slots",
|
|
@@ -131,7 +131,42 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
if (changed) emitAttached()
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//
|
|
134
|
+
// 文件条移除时清除草稿中指向给定路径的全部 @引用 chip(同一文件拖多次会留多个 chip,需全清)。
|
|
135
|
+
// 兼容两代 dsh 输入机:旧版(Lexical)draft 是 clipboard 投影(chip 展开为 @路径 文本),
|
|
136
|
+
// 新版(纯文本机,chip 外观是文本上的扫描装饰)draft 是纯文本——两代里 occurrence 的
|
|
137
|
+
// [offset, offset+length) 都是 snap.draft 内的文本区间,故统一 setDraft 整文拼接删除
|
|
138
|
+
//(新版 consumeToken 的 span 分支内部本就是 setDraft 拼接)。每轮删一个目标区间
|
|
139
|
+
//(连带 chip 后机器自动补的尾随空格 gap),draftRev 递增后重读 snapshot,循环至无目标。
|
|
140
|
+
function removeDraftRefs(path) {
|
|
141
|
+
const shell = bridge.shell
|
|
142
|
+
if (shell === null || typeof path !== 'string' || path === '') return
|
|
143
|
+
if (typeof shell.setDraft !== 'function') return
|
|
144
|
+
const plain = '@' + path
|
|
145
|
+
const quoted = '@"' + path + '"'
|
|
146
|
+
const isTarget = (oc) => oc.ref === plain || oc.ref === quoted
|
|
147
|
+
|| (oc.clipboardText !== void 0 && (oc.clipboardText === plain || oc.clipboardText === quoted))
|
|
148
|
+
for (let n = 0; n < 64; n++) {
|
|
149
|
+
const snap = readShellSnapshot(shell)
|
|
150
|
+
if (snap === null || snap === void 0 || typeof snap.draft !== 'string') break
|
|
151
|
+
// 发送进行中不动草稿(此时 occurrence 会随发送完成清空)
|
|
152
|
+
if (snap.phase === 'adjudicating' || snap.phase === 'submitting') break
|
|
153
|
+
// 取第一个有效(类型+边界健全)且指向目标路径的 occurrence,边界失配直接跳过防误伤用户文本
|
|
154
|
+
const occs = snap.occurrences || []
|
|
155
|
+
let oc = null
|
|
156
|
+
for (let i = 0; i < occs.length; i++) {
|
|
157
|
+
const o = occs[i]
|
|
158
|
+
if (typeof o.offset !== 'number' || typeof o.length !== 'number' || o.length <= 0) continue
|
|
159
|
+
if (o.offset < 0 || o.offset + o.length > snap.draft.length) continue
|
|
160
|
+
if (isTarget(o)) { oc = o; break }
|
|
161
|
+
}
|
|
162
|
+
if (oc === null) break
|
|
163
|
+
let end = oc.offset + oc.length
|
|
164
|
+
if (snap.draft.charCodeAt(end) === 32) end += 1 // 吃掉插入 chip 时机器自动补的尾随空格 gap
|
|
165
|
+
shell.setDraft(snap.draft.slice(0, oc.offset) + snap.draft.slice(end))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- 宿主保存调用:fetch POST 宿主 webServer 路由----
|
|
135
170
|
async function saveFileToHost(fileName, b64, sessionId) {
|
|
136
171
|
if (typeof fetch !== 'function') throw new Error('当前环境无 fetch,无法上传')
|
|
137
172
|
let response
|
|
@@ -265,10 +300,25 @@ window.__ModuleLoader__.load({
|
|
|
265
300
|
return seg
|
|
266
301
|
}
|
|
267
302
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
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]'))
|
|
272
322
|
const live = list.filter((ta) => {
|
|
273
323
|
if (ta.disabled || ta.readOnly) return false
|
|
274
324
|
const phase = ta.getAttribute('data-phase')
|
|
@@ -277,9 +327,10 @@ window.__ModuleLoader__.load({
|
|
|
277
327
|
})
|
|
278
328
|
if (live.length === 0) return null
|
|
279
329
|
const draft = bridge.input !== void 0 ? bridge.input.draft : undefined
|
|
330
|
+
const textOf = (el) => (el.tagName === 'TEXTAREA' ? el.value : (el.textContent || ''))
|
|
280
331
|
if (typeof draft === 'string') {
|
|
281
332
|
for (let i = 0; i < live.length; i++) {
|
|
282
|
-
if (live[i]
|
|
333
|
+
if (textOf(live[i]) === draft) return live[i]
|
|
283
334
|
}
|
|
284
335
|
}
|
|
285
336
|
return live[0]
|
|
@@ -395,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
395
446
|
announce(`输入框不可用 [mounted:${bridge.mounted} shell:${shell === null ? 'null' : 'ok'} addImg:${typeof bridge.addImages}]`)
|
|
396
447
|
return
|
|
397
448
|
}
|
|
398
|
-
const snap0 = shell
|
|
449
|
+
const snap0 = readShellSnapshot(shell)
|
|
399
450
|
const phase = snap0 ? snap0.phase : 'plain'
|
|
400
451
|
if (phase !== 'plain' && phase !== 'command' && phase !== 'claimed') {
|
|
401
452
|
announce('输入框正忙')
|
|
@@ -449,8 +500,24 @@ window.__ModuleLoader__.load({
|
|
|
449
500
|
const m = formatMention(res.path, false)
|
|
450
501
|
const clipboardText = m !== undefined ? m : '@' + res.path
|
|
451
502
|
// 每次插入前重取 snapshot(draftRev 随插入递增,span 取当前末尾)
|
|
452
|
-
const snap = shell
|
|
453
|
-
|
|
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 }
|
|
454
521
|
const okInsert = shell.insertReference(
|
|
455
522
|
{
|
|
456
523
|
source: 'reference', // 复用内置已注册的 @引用 source(带 codec.serialize),否则报 no serializer
|
|
@@ -488,12 +555,12 @@ window.__ModuleLoader__.load({
|
|
|
488
555
|
handleFilesEvent(e, files, dt.items, 'drop')
|
|
489
556
|
}
|
|
490
557
|
|
|
491
|
-
// document 级 paste(capture):仅当粘贴目标就是 composer
|
|
558
|
+
// document 级 paste(capture):仅当粘贴目标就是 composer 输入表面时接管文件;
|
|
492
559
|
// 纯文本(含目录路径字符串)完全原生,不改写。
|
|
493
560
|
function onPaste(e) {
|
|
494
561
|
const t = e.target
|
|
495
|
-
const ta =
|
|
496
|
-
if (ta === null || t !== ta) return
|
|
562
|
+
const ta = findComposerInput()
|
|
563
|
+
if (ta === null || (t !== ta && !(ta.contains && ta.contains(t)))) return
|
|
497
564
|
const cd = e.clipboardData
|
|
498
565
|
if (cd === null || cd === void 0) return
|
|
499
566
|
const files = Array.prototype.slice.call(cd.files)
|
|
@@ -564,7 +631,7 @@ window.__ModuleLoader__.load({
|
|
|
564
631
|
}
|
|
565
632
|
}
|
|
566
633
|
} catch (err) { shell = null; addImages = null }
|
|
567
|
-
const input = shell
|
|
634
|
+
const input = readShellSnapshot(shell)
|
|
568
635
|
react.useEffect(() => {
|
|
569
636
|
bridge.shell = shell
|
|
570
637
|
bridge.addImages = addImages
|
|
@@ -600,12 +667,12 @@ window.__ModuleLoader__.load({
|
|
|
600
667
|
attachedListeners.add(fn)
|
|
601
668
|
return () => { attachedListeners.delete(fn) }
|
|
602
669
|
}, [])
|
|
603
|
-
//
|
|
670
|
+
// 发送后清空:轮询输入机快照,occurrences 从有→无 即一次发送完成
|
|
604
671
|
const hadOcc = react.useRef(false)
|
|
605
672
|
react.useEffect(() => {
|
|
606
673
|
const timer = setInterval(() => {
|
|
607
674
|
const shell = bridge.shell
|
|
608
|
-
const snap = shell
|
|
675
|
+
const snap = readShellSnapshot(shell)
|
|
609
676
|
const occ = snap ? snap.occurrences : null
|
|
610
677
|
const has = !!(occ && occ.length > 0)
|
|
611
678
|
const s = bridge.sessionId
|
|
@@ -637,7 +704,11 @@ window.__ModuleLoader__.load({
|
|
|
637
704
|
style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 },
|
|
638
705
|
}, e.name),
|
|
639
706
|
react.createElement('span', {
|
|
640
|
-
onClick: () =>
|
|
707
|
+
onClick: () => {
|
|
708
|
+
// 先清草稿中指向该文件的全部 @引用 chip,再移除文件条条目
|
|
709
|
+
try { removeDraftRefs(e.path) } catch (err) { /* 忽略:引用清理失败不阻塞移除 */ }
|
|
710
|
+
detachFile(e.id)
|
|
711
|
+
},
|
|
641
712
|
title: '移除',
|
|
642
713
|
style: { cursor: 'pointer', opacity: 0.6, fontSize: '14px', lineHeight: '1', flex: 'none', padding: '0 2px' },
|
|
643
714
|
}, '×'),
|
|
@@ -704,20 +775,23 @@ window.__ModuleLoader__.load({
|
|
|
704
775
|
style: {
|
|
705
776
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
706
777
|
width: '28px', height: '28px', padding: 0, margin: 0, border: 'none',
|
|
707
|
-
|
|
708
|
-
|
|
778
|
+
// 与 dsh 输入栏加号按钮同款(InputBar.module.css .add):全圆 + selector 填充,无阴影
|
|
779
|
+
background: 'var(--dsw-specific-selector, rgba(128,128,128,0.10))',
|
|
780
|
+
color: 'var(--dsw-alias-label-primary, currentColor)',
|
|
781
|
+
cursor: busy ? 'default' : 'pointer', borderRadius: '999px',
|
|
709
782
|
opacity: busy ? 0.45 : 1, transition: 'background 120ms ease, opacity 120ms ease',
|
|
710
783
|
},
|
|
711
|
-
onMouseEnter: (ev) => { if (!busy) ev.currentTarget.style.background = 'rgba(128,128,128,0.16)' },
|
|
712
|
-
onMouseLeave: (ev) => { ev.currentTarget.style.background = '
|
|
784
|
+
onMouseEnter: (ev) => { if (!busy) ev.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover-solid, rgba(128,128,128,0.16))' },
|
|
785
|
+
onMouseLeave: (ev) => { ev.currentTarget.style.background = 'var(--dsw-specific-selector, rgba(128,128,128,0.10))' },
|
|
713
786
|
},
|
|
714
787
|
react.createElement('svg', {
|
|
715
|
-
|
|
788
|
+
// 回形针按用户反馈取 12px(比加号 14px 小 2px);描边 2/24×12≈1px
|
|
789
|
+
width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none',
|
|
716
790
|
'aria-hidden': true, style: { display: 'block' },
|
|
717
791
|
},
|
|
718
792
|
react.createElement('path', {
|
|
719
|
-
d: '
|
|
720
|
-
stroke: 'currentColor', strokeWidth:
|
|
793
|
+
d: 'm21.44 12.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48',
|
|
794
|
+
stroke: 'currentColor', strokeWidth: 2,
|
|
721
795
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
722
796
|
})
|
|
723
797
|
)
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @wszhoho/dsh-file-attachment Host 半:webServer HTTP
|
|
1
|
+
// @wszhoho/dsh-file-attachment Host 半:webServer HTTP 路由方案。
|
|
2
2
|
// Client 半读取文件全文(base64)后 POST /dsh-file-attachment/save 到这里,
|
|
3
3
|
// 写入 <会话工作区根>/.dsh-file-attachment/<日期>/<文件名>(日期作子目录,文件名保留原始名)并返回 { path, dir, name, size }。
|
|
4
4
|
// 项目根解析:Client 传 sessionId → sessions.get(sessionId).header.cwd →
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wszhoho/dsh-file-attachment",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "文件附件:输入框上传按钮 + 拖拽/粘贴文件(支持多文件);图片走 dsh 原生图片草稿机制(addImages,不缩放/不落盘),文档落盘到 .dsh-file-attachment 并以芯片 @短名 引用(发送时还原 @绝对路径);文档/代码/配置文件可上传类型可在设置页配置;支持 PC 与移动端浏览器",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|