dsh-long-plugins 2.6.2 → 3.0.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 +0 -0
- package/README.md +0 -0
- package/THIRD_PARTY_NOTICES.md +0 -0
- package/client/client.js +3 -879
- package/client/client.js.bak-v264 +3291 -0
- package/client/vendor/chart.umd.min.js +0 -0
- package/client/vendor/docx-preview.min.js +0 -0
- package/client/vendor/jszip.min.js +0 -0
- package/client/vendor/pptxviewjs.min.js +0 -0
- package/client/vendor/xlsx.full.min.js +0 -0
- package/cordis.patch.yml +0 -0
- package/dsh.plugin.json +1 -1
- package/lib/index.js +1 -7
- package/lib/index.js.bak-v264 +2836 -0
- package/package.json +2 -3
- package/skill/dsh-common-plugins-install/SKILL.md +0 -0
- package/skill/dsh-long-plugins-install/SKILL.md +0 -0
- package/skill/dsh-upgrade/SKILL.md +0 -0
- package/skill/dsh-web-start-panel-install/SKILL.md +0 -0
- package/skill/dsh-web-win-service-install/SKILL.md +0 -0
- package/patches/dsh-client-connection-heartbeat.sh +0 -112
package/client/client.js
CHANGED
|
@@ -13,10 +13,6 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
14
14
|
|
|
15
15
|
const React = require('react')
|
|
16
|
-
const API_PATH = '/api/dsh-uploads'
|
|
17
|
-
const DOWNLOAD_PATH = '/api/dsh-uploads/download'
|
|
18
|
-
const PREVIEW_PATH = '/api/dsh-uploads/preview'
|
|
19
|
-
const SOURCE = 'local-upload-files'
|
|
20
16
|
const HIDDEN_LABEL = '__dsh_upload_hidden__:'
|
|
21
17
|
|
|
22
18
|
function errorMessage(error) {
|
|
@@ -29,26 +25,6 @@ window.__ModuleLoader__.load({
|
|
|
29
25
|
return body
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
async function uploadFile(file) {
|
|
33
|
-
const response = await fetch(API_PATH, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
'content-type': 'application/octet-stream',
|
|
37
|
-
'x-file-name': encodeURIComponent(file.name),
|
|
38
|
-
},
|
|
39
|
-
body: file,
|
|
40
|
-
})
|
|
41
|
-
return (await responseJson(response)).file
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function downloadUrl(name) {
|
|
45
|
-
return `${DOWNLOAD_PATH}?name=${encodeURIComponent(name)}`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function previewUrl(name) {
|
|
49
|
-
return `${PREVIEW_PATH}?name=${encodeURIComponent(name)}`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
28
|
/** Extensions the browser can render inline (image/office handled separately). */
|
|
53
29
|
const INLINE_PREVIEW_EXTS = new Set([
|
|
54
30
|
'.pdf', '.txt', '.md', '.markdown', '.json', '.yml', '.yaml', '.xml', '.html', '.htm',
|
|
@@ -121,502 +97,6 @@ window.__ModuleLoader__.load({
|
|
|
121
97
|
document.body.removeChild(a)
|
|
122
98
|
}
|
|
123
99
|
|
|
124
|
-
function modelLine(file) {
|
|
125
|
-
return `上传文件:\`${file.path}\``
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function serializedFile(file) {
|
|
129
|
-
return `\n${modelLine(file)}`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function stripSerializedFiles(draft, files) {
|
|
133
|
-
const lines = new Set(files.map(modelLine))
|
|
134
|
-
return String(draft || '')
|
|
135
|
-
.split('\n')
|
|
136
|
-
.filter((line) => !lines.has(line.trim()))
|
|
137
|
-
.join('\n')
|
|
138
|
-
.replace(/^\n+|\n+$/g, '')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function legacyUploadPaths(draft) {
|
|
142
|
-
const paths = []
|
|
143
|
-
for (const line of String(draft || '').split('\n')) {
|
|
144
|
-
const match = line.trim().match(/^上传文件:\s*`([^`]+)`$/)
|
|
145
|
-
if (match) paths.push(match[1])
|
|
146
|
-
}
|
|
147
|
-
return paths
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function stripLegacyPaths(draft, paths) {
|
|
151
|
-
const remove = new Set(paths)
|
|
152
|
-
return String(draft || '')
|
|
153
|
-
.split('\n')
|
|
154
|
-
.filter((line) => {
|
|
155
|
-
const match = line.trim().match(/^上传文件:\s*`([^`]+)`$/)
|
|
156
|
-
return !match || !remove.has(match[1])
|
|
157
|
-
})
|
|
158
|
-
.join('\n')
|
|
159
|
-
.replace(/^\n+|\n+$/g, '')
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
class FileDraftController {
|
|
163
|
-
constructor(ctx) {
|
|
164
|
-
this.ctx = ctx
|
|
165
|
-
this.pending = new Map()
|
|
166
|
-
this.inFlight = new Map()
|
|
167
|
-
this.refIndex = new Map()
|
|
168
|
-
this.listeners = new Map()
|
|
169
|
-
this.expiry = new Map()
|
|
170
|
-
this.serializing = new Set()
|
|
171
|
-
this.migrated = new Set()
|
|
172
|
-
this.sinkHooked = new Set()
|
|
173
|
-
this.counter = 0
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
pendingFor(sessionId) {
|
|
177
|
-
return this.pending.get(String(sessionId)) || []
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
subscribe(sessionId, listener) {
|
|
181
|
-
const key = String(sessionId)
|
|
182
|
-
let set = this.listeners.get(key)
|
|
183
|
-
if (!set) {
|
|
184
|
-
set = new Set()
|
|
185
|
-
this.listeners.set(key, set)
|
|
186
|
-
}
|
|
187
|
-
set.add(listener)
|
|
188
|
-
return () => {
|
|
189
|
-
set.delete(listener)
|
|
190
|
-
if (set.size === 0) this.listeners.delete(key)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
publish(sessionId) {
|
|
195
|
-
const set = this.listeners.get(String(sessionId))
|
|
196
|
-
if (!set) return
|
|
197
|
-
for (const listener of set) listener()
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
scope(sessionId) {
|
|
201
|
-
const actx = this.ctx.sessions.scope(sessionId)
|
|
202
|
-
if (!actx) throw new Error('当前会话尚未就绪')
|
|
203
|
-
return { actx, shell: this.ctx.conversation.input.for(actx) }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
insertReference(sessionId, entry) {
|
|
207
|
-
const { actx, shell } = this.scope(sessionId)
|
|
208
|
-
const input = shell.snapshot
|
|
209
|
-
if (!input || input.phase !== 'plain') return false
|
|
210
|
-
return actx.bail(actx, 'slash/input-insert-reference', {
|
|
211
|
-
reference: {
|
|
212
|
-
source: SOURCE,
|
|
213
|
-
ref: entry.ref,
|
|
214
|
-
label: `${HIDDEN_LABEL}${entry.ref}`,
|
|
215
|
-
clipboardText: '',
|
|
216
|
-
},
|
|
217
|
-
span: {
|
|
218
|
-
start: input.draft.length,
|
|
219
|
-
end: input.draft.length,
|
|
220
|
-
draftRev: input.draftRev,
|
|
221
|
-
},
|
|
222
|
-
}) === true
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
attach(sessionId, file) {
|
|
226
|
-
const key = String(sessionId)
|
|
227
|
-
this.clearInFlight(key)
|
|
228
|
-
const entry = { ...file, ref: `${key}-${++this.counter}` }
|
|
229
|
-
// 不再往草稿插入引用 token;文件仅以卡片形式显示在输入区上方,
|
|
230
|
-
// 发送时由 ensureSinkHook 安装的 defaultSink 包装统一拼进消息。
|
|
231
|
-
this.ensureSinkHook(key)
|
|
232
|
-
const next = [...this.pendingFor(key), entry]
|
|
233
|
-
this.pending.set(key, next)
|
|
234
|
-
this.refIndex.set(entry.ref, { sessionId: key, entry })
|
|
235
|
-
this.publish(key)
|
|
236
|
-
return entry
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
ensureSinkHook(key) {
|
|
240
|
-
const k = String(key)
|
|
241
|
-
if (this.sinkHooked.has(k)) return
|
|
242
|
-
let shell
|
|
243
|
-
try {
|
|
244
|
-
shell = this.scope(k).shell
|
|
245
|
-
} catch (error) {
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
const deps = shell.deps
|
|
249
|
-
const original = deps && deps.defaultSink
|
|
250
|
-
if (typeof original !== 'function') return
|
|
251
|
-
this.sinkHooked.add(k)
|
|
252
|
-
const controller = this
|
|
253
|
-
deps.defaultSink = function (text, imageIds, mode, signal) {
|
|
254
|
-
const entries = controller.pendingFor(k)
|
|
255
|
-
let extra = ''
|
|
256
|
-
for (const entry of entries) extra += serializedFile(entry)
|
|
257
|
-
if (entries.length > 0) {
|
|
258
|
-
controller.pending.delete(k)
|
|
259
|
-
controller.inFlight.set(k, entries)
|
|
260
|
-
controller.publish(k)
|
|
261
|
-
}
|
|
262
|
-
const result = original(text + extra, imageIds, mode, signal)
|
|
263
|
-
if (entries.length === 0) return result
|
|
264
|
-
return Promise.resolve(result).then((outcome) => {
|
|
265
|
-
if (outcome && outcome.kind === 'success') {
|
|
266
|
-
const expiry = controller.expiry.get(k)
|
|
267
|
-
if (expiry) expiry()
|
|
268
|
-
controller.expiry.delete(k)
|
|
269
|
-
controller.inFlight.delete(k)
|
|
270
|
-
for (const entry of entries) controller.refIndex.delete(entry.ref)
|
|
271
|
-
}
|
|
272
|
-
return outcome
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
remove(sessionId, ref) {
|
|
278
|
-
const key = String(sessionId)
|
|
279
|
-
const entries = this.pendingFor(key)
|
|
280
|
-
const entry = entries.find((item) => item.ref === ref)
|
|
281
|
-
if (!entry) return
|
|
282
|
-
const next = entries.filter((item) => item.ref !== ref)
|
|
283
|
-
if (next.length > 0) this.pending.set(key, next)
|
|
284
|
-
else this.pending.delete(key)
|
|
285
|
-
this.refIndex.delete(ref)
|
|
286
|
-
this.publish(key)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
fileForRef(ref) {
|
|
290
|
-
return this.refIndex.get(ref)?.entry
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
markSerializing(ref) {
|
|
294
|
-
const record = this.refIndex.get(ref)
|
|
295
|
-
if (record) this.serializing.add(record.sessionId)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
reconcile(sessionId, occurrences) {
|
|
299
|
-
// 卡片模式:文件不再以草稿引用/occurrence 形式存在,[pending] 的发送与失败
|
|
300
|
-
// 恢复改由 ensureSinkHook 的 defaultSink 包装 + restoreFailed 负责;这里不再按
|
|
301
|
-
// occurrence 对账,避免草稿一变就把待发送卡片误判为已发送而提前清空。
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
restoreFailed(sessionId) {
|
|
306
|
-
const key = String(sessionId)
|
|
307
|
-
const entries = this.inFlight.get(key)
|
|
308
|
-
if (!entries || entries.length === 0) return
|
|
309
|
-
const expiry = this.expiry.get(key)
|
|
310
|
-
if (expiry) expiry()
|
|
311
|
-
this.expiry.delete(key)
|
|
312
|
-
this.inFlight.delete(key)
|
|
313
|
-
// 卡片模式:失败时只把文件恢复为待发送卡片(不再回写草稿/插入引用)。
|
|
314
|
-
this.pending.set(key, entries)
|
|
315
|
-
for (const entry of entries) this.refIndex.set(entry.ref, { sessionId: key, entry })
|
|
316
|
-
this.publish(key)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
clearInFlight(sessionId) {
|
|
320
|
-
const key = String(sessionId)
|
|
321
|
-
const expiry = this.expiry.get(key)
|
|
322
|
-
if (expiry) expiry()
|
|
323
|
-
this.expiry.delete(key)
|
|
324
|
-
const entries = this.inFlight.get(key) || []
|
|
325
|
-
for (const entry of entries) this.refIndex.delete(entry.ref)
|
|
326
|
-
this.inFlight.delete(key)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async migrateLegacy(sessionId, draft) {
|
|
330
|
-
// 旧格式草稿兼容:只清理残留的「上传文件:`路径`」文本行,不再重新挂载附件。
|
|
331
|
-
// (原先会把它转成待发送附件,导致刷新后幽灵文件反复出现——2026-08-19 修复)
|
|
332
|
-
const key = String(sessionId)
|
|
333
|
-
if (this.migrated.has(key)) return
|
|
334
|
-
this.migrated.add(key)
|
|
335
|
-
const paths = legacyUploadPaths(draft)
|
|
336
|
-
if (paths.length === 0) return
|
|
337
|
-
try {
|
|
338
|
-
const { shell } = this.scope(sessionId)
|
|
339
|
-
shell.setDraft(stripLegacyPaths(shell.snapshot.draft, paths))
|
|
340
|
-
} catch (error) {
|
|
341
|
-
console.error('[dsh-upload-manager] legacy draft cleanup failed', error)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
dispose() {
|
|
346
|
-
for (const cancel of this.expiry.values()) cancel()
|
|
347
|
-
this.expiry.clear()
|
|
348
|
-
this.pending.clear()
|
|
349
|
-
this.inFlight.clear()
|
|
350
|
-
this.refIndex.clear()
|
|
351
|
-
this.listeners.clear()
|
|
352
|
-
this.serializing.clear()
|
|
353
|
-
this.migrated.clear()
|
|
354
|
-
this.sinkHooked.clear()
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function usePending(controller, sessionId) {
|
|
359
|
-
const [, render] = React.useState(0)
|
|
360
|
-
React.useEffect(
|
|
361
|
-
() => controller.subscribe(sessionId, () => render((value) => value + 1)),
|
|
362
|
-
[controller, sessionId],
|
|
363
|
-
)
|
|
364
|
-
return controller.pendingFor(sessionId)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function PaperclipIcon() {
|
|
368
|
-
return React.createElement(
|
|
369
|
-
'svg',
|
|
370
|
-
{
|
|
371
|
-
className: 'dsh-upload-icon',
|
|
372
|
-
viewBox: '0 0 24 24',
|
|
373
|
-
fill: 'none',
|
|
374
|
-
stroke: 'currentColor',
|
|
375
|
-
strokeWidth: 1.8,
|
|
376
|
-
strokeLinecap: 'round',
|
|
377
|
-
strokeLinejoin: 'round',
|
|
378
|
-
'aria-hidden': true,
|
|
379
|
-
},
|
|
380
|
-
React.createElement('path', { d: 'M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48' }),
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function UploadControl(props) {
|
|
385
|
-
const input = props.useInput((state) => state) || props.input
|
|
386
|
-
const pickerRef = React.useRef(null)
|
|
387
|
-
const [busy, setBusy] = React.useState(false)
|
|
388
|
-
const [status, setStatus] = React.useState('')
|
|
389
|
-
|
|
390
|
-
async function onPick(event) {
|
|
391
|
-
const files = Array.from(event.currentTarget.files || [])
|
|
392
|
-
event.currentTarget.value = ''
|
|
393
|
-
if (files.length === 0) return
|
|
394
|
-
|
|
395
|
-
setBusy(true)
|
|
396
|
-
setStatus(`正在上传 ${files.length} 个文件…`)
|
|
397
|
-
let attached = 0
|
|
398
|
-
const failures = []
|
|
399
|
-
try {
|
|
400
|
-
for (const file of files) {
|
|
401
|
-
try {
|
|
402
|
-
const stored = await uploadFile(file)
|
|
403
|
-
props.controller.attach(props.sessionId, stored)
|
|
404
|
-
attached += 1
|
|
405
|
-
} catch (error) {
|
|
406
|
-
failures.push(`${file.name}: ${errorMessage(error)}`)
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
setStatus(failures.length === 0
|
|
410
|
-
? `已添加 ${attached} 个待发送文件`
|
|
411
|
-
: `已添加 ${attached} 个,失败 ${failures.length} 个:${failures.join(';')}`)
|
|
412
|
-
} finally {
|
|
413
|
-
setBusy(false)
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const disabled = busy || !input || input.phase !== 'plain'
|
|
418
|
-
return React.createElement(
|
|
419
|
-
'div',
|
|
420
|
-
{ className: 'dsh-upload-control', title: status || '上传本地文件并加入本次消息' },
|
|
421
|
-
React.createElement('input', {
|
|
422
|
-
ref: pickerRef,
|
|
423
|
-
className: 'dsh-upload-picker',
|
|
424
|
-
type: 'file',
|
|
425
|
-
multiple: true,
|
|
426
|
-
onChange: onPick,
|
|
427
|
-
}),
|
|
428
|
-
React.createElement(
|
|
429
|
-
'button',
|
|
430
|
-
{
|
|
431
|
-
type: 'button',
|
|
432
|
-
className: 'dsh-upload-button',
|
|
433
|
-
disabled,
|
|
434
|
-
'aria-label': busy ? '正在上传文件' : '上传文件',
|
|
435
|
-
title: status || '上传本地文件并加入本次消息',
|
|
436
|
-
onClick: () => pickerRef.current?.click(),
|
|
437
|
-
},
|
|
438
|
-
React.createElement(PaperclipIcon, null),
|
|
439
|
-
),
|
|
440
|
-
React.createElement('span', { className: 'dsh-upload-live', 'aria-live': 'polite' }, status),
|
|
441
|
-
)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// ===== 拖放上传:把本地文件拖到会话框任意位置即可加入本条消息 =====
|
|
445
|
-
// 复用与钉选上传完全相同的 uploadFile() + controller.attach() 管线;
|
|
446
|
-
// 监听 window 级 dragenter/dragover/dragleave/drop(仅在携带 Files 时接管),
|
|
447
|
-
// 视觉层用 fixed 全屏遮罩提示,pointer-events:none 保证不干扰拖拽本身。
|
|
448
|
-
function DragDropOverlay(props) {
|
|
449
|
-
const controller = props.controller
|
|
450
|
-
const sessionId = props.sessionId
|
|
451
|
-
const [active, setActive] = React.useState(false)
|
|
452
|
-
const counterRef = React.useRef(0)
|
|
453
|
-
|
|
454
|
-
React.useEffect(() => {
|
|
455
|
-
// 只在真正拖入「文件」时才接管;拖文本/链接等(无 Files)一律放行给 DSH 原生行为。
|
|
456
|
-
// 四个事件都用 捕获阶段(capture) + stopPropagation,让本插件成为文件拖放的
|
|
457
|
-
// 唯一所有者 —— 这样 DSH 核心的「图像拖放/粘贴」验证器不会触发(否则非图片文件
|
|
458
|
-
// 会报「仅支持 PNG、JPG、WebP、GIF」),核心自己的拖放遮罩也不会出现/卡住。
|
|
459
|
-
const hasFiles = (e) => !!(e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files'))
|
|
460
|
-
const onDragEnter = (e) => {
|
|
461
|
-
if (!hasFiles(e)) return
|
|
462
|
-
e.preventDefault()
|
|
463
|
-
e.stopPropagation()
|
|
464
|
-
counterRef.current += 1
|
|
465
|
-
setActive(true)
|
|
466
|
-
}
|
|
467
|
-
const onDragOver = (e) => {
|
|
468
|
-
if (!hasFiles(e)) return
|
|
469
|
-
e.preventDefault()
|
|
470
|
-
e.stopPropagation()
|
|
471
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
|
|
472
|
-
}
|
|
473
|
-
const onDragLeave = (e) => {
|
|
474
|
-
if (!hasFiles(e)) return
|
|
475
|
-
e.stopPropagation()
|
|
476
|
-
counterRef.current -= 1
|
|
477
|
-
if (counterRef.current <= 0) {
|
|
478
|
-
counterRef.current = 0
|
|
479
|
-
setActive(false)
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
const onDrop = async (e) => {
|
|
483
|
-
if (!hasFiles(e)) return
|
|
484
|
-
e.preventDefault()
|
|
485
|
-
e.stopPropagation()
|
|
486
|
-
counterRef.current = 0
|
|
487
|
-
setActive(false)
|
|
488
|
-
const files = Array.from((e.dataTransfer && e.dataTransfer.files) || [])
|
|
489
|
-
if (files.length === 0) return
|
|
490
|
-
// 复用与回形针完全相同的 uploadFile + attach 管线;结果直接体现在输入区上方的
|
|
491
|
-
// 「待发送文件」卡片上。
|
|
492
|
-
for (const file of files) {
|
|
493
|
-
try {
|
|
494
|
-
const stored = await uploadFile(file)
|
|
495
|
-
controller.attach(sessionId, stored)
|
|
496
|
-
} catch (error) {
|
|
497
|
-
console.warn(`[dsh-long-plugins] 拖放上传失败:${file.name} → ${errorMessage(error)}`)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
// 粘贴非图片文件 → 走与回形针/拖放相同的上传管线,并阻止 DSH 核心的「仅支持图片」门;
|
|
502
|
-
// 纯文本/图片粘贴放行给 DSH 原生;上传失败(不支持的格式等)给可见提示。
|
|
503
|
-
const onPaste = async (e) => {
|
|
504
|
-
const cd = e.clipboardData
|
|
505
|
-
if (!cd) return
|
|
506
|
-
const files = []
|
|
507
|
-
const items = cd.items
|
|
508
|
-
if (items) {
|
|
509
|
-
for (const item of items) {
|
|
510
|
-
if (item.kind === 'file' && typeof item.getAsFile === 'function') {
|
|
511
|
-
const f = item.getAsFile()
|
|
512
|
-
// 只接管非图片文件;图片交给 DSH 原生(内联显示),避免改变已有行为
|
|
513
|
-
if (f && f.size > 0 && !(f.type || '').startsWith('image/')) files.push(f)
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (files.length === 0) return // 无「非图片文件」→ 放行给 DSH
|
|
518
|
-
// 有文件粘贴:接管整段粘贴,阻止 DSH 后续处理器报「仅支持图片」
|
|
519
|
-
e.preventDefault()
|
|
520
|
-
e.stopPropagation()
|
|
521
|
-
e.stopImmediatePropagation()
|
|
522
|
-
for (const file of files) {
|
|
523
|
-
try {
|
|
524
|
-
const stored = await uploadFile(file)
|
|
525
|
-
controller.attach(sessionId, stored)
|
|
526
|
-
} catch (error) {
|
|
527
|
-
console.warn(`[dsh-long-plugins] 粘贴上传失败:${file.name} → ${errorMessage(error)}`)
|
|
528
|
-
showToast(`不支持该文件格式:${file.name}`, 'error')
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
const dragOn = !!(window.__dshLongMod && window.__dshLongMod('uploadDragDrop'))
|
|
533
|
-
const pasteOn = !!(window.__dshLongMod && window.__dshLongMod('uploadPaste'))
|
|
534
|
-
if (dragOn) {
|
|
535
|
-
window.addEventListener('dragenter', onDragEnter, true)
|
|
536
|
-
window.addEventListener('dragover', onDragOver, true)
|
|
537
|
-
window.addEventListener('dragleave', onDragLeave, true)
|
|
538
|
-
window.addEventListener('drop', onDrop, true)
|
|
539
|
-
}
|
|
540
|
-
if (pasteOn) {
|
|
541
|
-
window.addEventListener('paste', onPaste, true)
|
|
542
|
-
}
|
|
543
|
-
return () => {
|
|
544
|
-
if (dragOn) {
|
|
545
|
-
window.removeEventListener('dragenter', onDragEnter, true)
|
|
546
|
-
window.removeEventListener('dragover', onDragOver, true)
|
|
547
|
-
window.removeEventListener('dragleave', onDragLeave, true)
|
|
548
|
-
window.removeEventListener('drop', onDrop, true)
|
|
549
|
-
}
|
|
550
|
-
if (pasteOn) {
|
|
551
|
-
window.removeEventListener('paste', onPaste, true)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}, [controller, sessionId])
|
|
555
|
-
|
|
556
|
-
if (!active) return null
|
|
557
|
-
|
|
558
|
-
return React.createElement(
|
|
559
|
-
'div',
|
|
560
|
-
{ className: 'dsh-upload-dropzone' },
|
|
561
|
-
React.createElement(
|
|
562
|
-
'div',
|
|
563
|
-
{ className: 'dsh-upload-dropzone-card' },
|
|
564
|
-
React.createElement(
|
|
565
|
-
'div',
|
|
566
|
-
{ className: 'dsh-upload-dropzone-icon' },
|
|
567
|
-
React.createElement(PaperclipIcon, null)
|
|
568
|
-
),
|
|
569
|
-
React.createElement(
|
|
570
|
-
'div',
|
|
571
|
-
{ className: 'dsh-upload-dropzone-text' },
|
|
572
|
-
'松开鼠标,将文件加入本条消息'
|
|
573
|
-
)
|
|
574
|
-
)
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function PendingFileRail(props) {
|
|
579
|
-
const input = props.useInput((state) => state)
|
|
580
|
-
const promptError = props.useSession((session) => session.promptError) || null
|
|
581
|
-
const files = usePending(props.controller, props.sessionId)
|
|
582
|
-
|
|
583
|
-
React.useEffect(() => {
|
|
584
|
-
props.controller.migrateLegacy(props.sessionId, input?.draft || '')
|
|
585
|
-
props.controller.reconcile(props.sessionId, input?.occurrences || [])
|
|
586
|
-
}, [props.controller, props.sessionId, input?.draftRev])
|
|
587
|
-
|
|
588
|
-
React.useEffect(() => {
|
|
589
|
-
if (promptError) props.controller.restoreFailed(props.sessionId)
|
|
590
|
-
}, [props.controller, props.sessionId, promptError])
|
|
591
|
-
|
|
592
|
-
if (files.length === 0) return null
|
|
593
|
-
return React.createElement(
|
|
594
|
-
'div',
|
|
595
|
-
{ className: 'dsh-upload-rail', 'aria-label': '待发送文件' },
|
|
596
|
-
files.map((file) => React.createElement(
|
|
597
|
-
'div',
|
|
598
|
-
{ className: 'dsh-upload-chip', key: file.ref },
|
|
599
|
-
React.createElement('span', { className: 'dsh-upload-chip-icon', 'aria-hidden': true }, '▤'),
|
|
600
|
-
React.createElement(
|
|
601
|
-
'span',
|
|
602
|
-
{ className: 'dsh-upload-chip-copy' },
|
|
603
|
-
React.createElement('strong', { title: file.name }, file.name),
|
|
604
|
-
React.createElement('small', null, sizeText(file.size)),
|
|
605
|
-
),
|
|
606
|
-
React.createElement(
|
|
607
|
-
'button',
|
|
608
|
-
{
|
|
609
|
-
type: 'button',
|
|
610
|
-
'aria-label': `移除 ${file.name}`,
|
|
611
|
-
title: '从本次消息移除',
|
|
612
|
-
onClick: () => props.controller.remove(props.sessionId, file.ref),
|
|
613
|
-
},
|
|
614
|
-
'×',
|
|
615
|
-
),
|
|
616
|
-
)),
|
|
617
|
-
)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
100
|
function sizeText(bytes) {
|
|
621
101
|
if (bytes < 1024) return `${bytes} B`
|
|
622
102
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
|
|
@@ -706,305 +186,6 @@ window.__ModuleLoader__.load({
|
|
|
706
186
|
)
|
|
707
187
|
}
|
|
708
188
|
|
|
709
|
-
function UploadSettingsSection() {
|
|
710
|
-
const [state, setState] = React.useState({
|
|
711
|
-
loading: true,
|
|
712
|
-
root: '',
|
|
713
|
-
maxFileBytes: 0,
|
|
714
|
-
totalMaxBytes: 0,
|
|
715
|
-
usedBytes: 0,
|
|
716
|
-
files: [],
|
|
717
|
-
error: '',
|
|
718
|
-
})
|
|
719
|
-
const [deleting, setDeleting] = React.useState('')
|
|
720
|
-
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
721
|
-
const [selected, setSelected] = React.useState(() => new Set())
|
|
722
|
-
const [preview, setPreview] = React.useState(null)
|
|
723
|
-
const [previewMaximized, setPreviewMaximized] = React.useState(false)
|
|
724
|
-
const [dayFilter, setDayFilter] = React.useState('')
|
|
725
|
-
const [search, setSearch] = React.useState('')
|
|
726
|
-
const [deleteEnabled, setDeleteEnabled] = React.useState(false)
|
|
727
|
-
|
|
728
|
-
async function refresh() {
|
|
729
|
-
setState((current) => ({ ...current, loading: true, error: '' }))
|
|
730
|
-
try {
|
|
731
|
-
const response = await fetch(API_PATH, { cache: 'no-store' })
|
|
732
|
-
const body = await responseJson(response)
|
|
733
|
-
setState({
|
|
734
|
-
loading: false,
|
|
735
|
-
root: body.root,
|
|
736
|
-
maxFileBytes: body.maxFileBytes,
|
|
737
|
-
totalMaxBytes: body.totalMaxBytes,
|
|
738
|
-
usedBytes: body.usedBytes,
|
|
739
|
-
files: body.files,
|
|
740
|
-
error: '',
|
|
741
|
-
})
|
|
742
|
-
} catch (error) {
|
|
743
|
-
setState((current) => ({ ...current, loading: false, error: errorMessage(error) }))
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
React.useEffect(() => {
|
|
748
|
-
refresh()
|
|
749
|
-
}, [])
|
|
750
|
-
|
|
751
|
-
async function remove(name) {
|
|
752
|
-
if (!globalThis.confirm(`确定删除“${name}”吗?此操作不可恢复。`)) return
|
|
753
|
-
setDeleting(name)
|
|
754
|
-
try {
|
|
755
|
-
const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
756
|
-
await responseJson(response)
|
|
757
|
-
if (preview !== null && preview.name === name) closePreview()
|
|
758
|
-
await refresh()
|
|
759
|
-
} catch (error) {
|
|
760
|
-
setState((current) => ({ ...current, error: errorMessage(error) }))
|
|
761
|
-
} finally {
|
|
762
|
-
setDeleting('')
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
async function batchDelete() {
|
|
767
|
-
const names = Array.from(selected || [])
|
|
768
|
-
if (names.length === 0) return
|
|
769
|
-
if (!globalThis.confirm(`确定删除所选 ${names.length} 个文件吗?此操作不可恢复。`)) return
|
|
770
|
-
setBatchBusy(true)
|
|
771
|
-
let err = ''
|
|
772
|
-
for (const name of names) {
|
|
773
|
-
try {
|
|
774
|
-
const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
775
|
-
if (!response.ok) { const b = await response.json().catch(() => ({})); err = err || (b.error || `HTTP ${response.status}`) }
|
|
776
|
-
} catch (e) { err = err || errorMessage(e) }
|
|
777
|
-
}
|
|
778
|
-
setState((current) => ({ ...current, error: err }))
|
|
779
|
-
setBatchBusy(false)
|
|
780
|
-
setSelected(new Set())
|
|
781
|
-
await refresh()
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
async function previewFile(name) {
|
|
785
|
-
setPreviewMaximized(false)
|
|
786
|
-
try {
|
|
787
|
-
const isOffice = /\.(docx|xlsx|pptx)$/i.test(name)
|
|
788
|
-
if (isOffice) {
|
|
789
|
-
// 先打开弹窗并提示转换中(NAS 上 docx/xlsx 转换可能耗时 1~2 秒)
|
|
790
|
-
setPreview({ name, officeLoading: true })
|
|
791
|
-
const response = await fetch(previewUrl(name), { cache: 'no-store' })
|
|
792
|
-
if (!response.ok) {
|
|
793
|
-
const body = await response.json().catch(() => ({}))
|
|
794
|
-
throw new Error(body.error || `HTTP ${response.status}`)
|
|
795
|
-
}
|
|
796
|
-
const data = await response.json().catch(() => ({}))
|
|
797
|
-
setPreview({
|
|
798
|
-
name,
|
|
799
|
-
officeHtml: data.officeHtml ?? '<p style="font-family:sans-serif;padding:12px">(无法渲染此文档)</p>',
|
|
800
|
-
})
|
|
801
|
-
return
|
|
802
|
-
}
|
|
803
|
-
// 无法内嵌预览的类型(压缩包、程序、视频、字体等):点预览直接下载。
|
|
804
|
-
if (!isInlinePreviewable(name)) {
|
|
805
|
-
triggerDownload(downloadUrl(name), name)
|
|
806
|
-
return
|
|
807
|
-
}
|
|
808
|
-
const response = await fetch(previewUrl(name), { cache: 'no-store' })
|
|
809
|
-
if (!response.ok) {
|
|
810
|
-
const body = await response.json().catch(() => ({}))
|
|
811
|
-
throw new Error(body.error || `HTTP ${response.status}`)
|
|
812
|
-
}
|
|
813
|
-
const type = response.headers.get('content-type') || ''
|
|
814
|
-
if (type.startsWith('image/')) {
|
|
815
|
-
const blob = await response.blob()
|
|
816
|
-
const url = URL.createObjectURL(blob)
|
|
817
|
-
setPreview({ url, name })
|
|
818
|
-
return
|
|
819
|
-
}
|
|
820
|
-
// PDF / txt / 其它可内嵌文件:弹窗内嵌预览(iframe 指向预览端点),
|
|
821
|
-
// 只有用户点「打开」才在新浏览器标签中打开。
|
|
822
|
-
setPreview({ url: previewUrl(name), name })
|
|
823
|
-
} catch (error) {
|
|
824
|
-
setState((current) => ({ ...current, error: errorMessage(error) }))
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function closePreview() {
|
|
829
|
-
if (preview?.url) URL.revokeObjectURL(preview.url)
|
|
830
|
-
setPreview(null)
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/** Group files by calendar day of their modifiedAt (YYYY-MM-DD, desc). */
|
|
834
|
-
function groupFilesByDate(files) {
|
|
835
|
-
const groups = []
|
|
836
|
-
const byDay = new Map()
|
|
837
|
-
for (const file of files) {
|
|
838
|
-
let day = ''
|
|
839
|
-
try {
|
|
840
|
-
day = localDay(file.modifiedAt)
|
|
841
|
-
} catch {
|
|
842
|
-
day = '未知日期'
|
|
843
|
-
}
|
|
844
|
-
if (!byDay.has(day)) byDay.set(day, [])
|
|
845
|
-
byDay.get(day).push(file)
|
|
846
|
-
}
|
|
847
|
-
const days = [...byDay.keys()].sort((a, b) => b.localeCompare(a))
|
|
848
|
-
for (const day of days) groups.push({ day, files: byDay.get(day) })
|
|
849
|
-
return groups
|
|
850
|
-
}
|
|
851
|
-
// 日期筛选 + 文件名搜索:先按所选天/关键词过滤 state.files,再始终按天分组(空=全部)。
|
|
852
|
-
const shownFiles = (dayFilter || search)
|
|
853
|
-
? state.files.filter((f) => (dayFilter ? (() => { try { return localDay(f.modifiedAt) === dayFilter } catch { return false } })() : true) && matchesSearch(f, search))
|
|
854
|
-
: state.files
|
|
855
|
-
const dateGroups = groupFilesByDate(shownFiles)
|
|
856
|
-
|
|
857
|
-
return React.createElement(
|
|
858
|
-
'section',
|
|
859
|
-
{ className: 'dsh-upload-settings' },
|
|
860
|
-
React.createElement(
|
|
861
|
-
'div',
|
|
862
|
-
{ className: 'dsh-upload-settings-head' },
|
|
863
|
-
React.createElement(
|
|
864
|
-
'div',
|
|
865
|
-
null,
|
|
866
|
-
React.createElement('h2', null, '上传文件'),
|
|
867
|
-
React.createElement('p', null, '管理从输入框上传到 Harness 容器中的文件。'),
|
|
868
|
-
),
|
|
869
|
-
React.createElement(
|
|
870
|
-
'div',
|
|
871
|
-
{ className: 'dsh-upload-head-actions' },
|
|
872
|
-
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
873
|
-
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
874
|
-
React.createElement(
|
|
875
|
-
'button',
|
|
876
|
-
{ type: 'button', className: 'dsh-upload-refresh', disabled: state.loading, onClick: refresh },
|
|
877
|
-
state.loading ? '刷新中…' : '刷新',
|
|
878
|
-
),
|
|
879
|
-
React.createElement(
|
|
880
|
-
'label',
|
|
881
|
-
{ className: 'dsh-upload-del-toggle' },
|
|
882
|
-
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
883
|
-
'开启删除',
|
|
884
|
-
),
|
|
885
|
-
deleteEnabled && React.createElement('label',
|
|
886
|
-
{ className: 'dsh-upload-del-toggle' },
|
|
887
|
-
React.createElement('input', { type: 'checkbox', checked: (state.files || []).length > 0 && (selected || new Set()).size === (state.files || []).length, onChange: (e) => { setSelected(e.target.checked ? new Set((state.files || []).map((f) => f.name)) : new Set()) } }),
|
|
888
|
-
'全选',
|
|
889
|
-
),
|
|
890
|
-
React.createElement(
|
|
891
|
-
'button',
|
|
892
|
-
{ type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
893
|
-
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
894
|
-
),
|
|
895
|
-
),
|
|
896
|
-
),
|
|
897
|
-
React.createElement(
|
|
898
|
-
'div',
|
|
899
|
-
{ className: 'dsh-upload-root' },
|
|
900
|
-
React.createElement('span', null, '固定目录'),
|
|
901
|
-
React.createElement('code', null, state.root || '读取中…'),
|
|
902
|
-
state.maxFileBytes
|
|
903
|
-
? React.createElement('small', null, `单文件上限 ${sizeText(state.maxFileBytes)}`)
|
|
904
|
-
: null,
|
|
905
|
-
state.totalMaxBytes
|
|
906
|
-
? React.createElement('small', null, `已使用 ${sizeText(state.usedBytes)} / ${sizeText(state.totalMaxBytes)}`)
|
|
907
|
-
: null,
|
|
908
|
-
),
|
|
909
|
-
state.error ? React.createElement('div', { className: 'dsh-upload-error' }, state.error) : null,
|
|
910
|
-
!state.loading && state.files.length === 0
|
|
911
|
-
? React.createElement('div', { className: 'dsh-upload-empty' }, '当前没有已上传文件。')
|
|
912
|
-
: null,
|
|
913
|
-
!state.loading && (dayFilter !== '' || search !== '') && shownFiles.length === 0 && state.files.length > 0
|
|
914
|
-
? React.createElement('div', { className: 'dsh-upload-empty' }, '没有匹配的文件。')
|
|
915
|
-
: null,
|
|
916
|
-
preview
|
|
917
|
-
? React.createElement(
|
|
918
|
-
'div',
|
|
919
|
-
{ className: 'dsh-upload-preview-overlay' + (previewMaximized ? ' dsh-upload-preview-overlay-max' : ''), onClick: closePreview },
|
|
920
|
-
React.createElement(
|
|
921
|
-
'div',
|
|
922
|
-
{ className: 'dsh-upload-preview-card' + (previewMaximized ? ' dsh-upload-preview-card-max' : ''), onClick: (event) => event.stopPropagation() },
|
|
923
|
-
React.createElement(
|
|
924
|
-
'div',
|
|
925
|
-
{ className: 'dsh-upload-preview-head' },
|
|
926
|
-
React.createElement('strong', null, preview.name),
|
|
927
|
-
React.createElement(
|
|
928
|
-
'div',
|
|
929
|
-
{ className: 'dsh-upload-preview-actions', style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
930
|
-
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
931
|
-
? React.createElement('a', { href: preview.officeHtml !== void 0 ? downloadUrl(preview.name) : previewUrl(preview.name), target: '_blank', rel: 'noopener noreferrer', className: 'dsh-upload-preview-open' }, '打开')
|
|
932
|
-
: null,
|
|
933
|
-
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
934
|
-
? React.createElement('a', { href: downloadUrl(preview.name), download: preview.name, className: 'dsh-upload-preview-open' }, '下载')
|
|
935
|
-
: null,
|
|
936
|
-
React.createElement('button', { type: 'button', onClick: () => { const m = !previewMaximized; setPreviewMaximized(m); try { const el = document.querySelector('.dsh-upload-preview-card'); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} } }, previewMaximized ? '还原' : '放大'),
|
|
937
|
-
React.createElement('button', { type: 'button', className: 'dsh-upload-preview-del', disabled: deleting === preview.name, onClick: () => remove(preview.name) }, deleting === preview.name ? '删除中…' : '删除'),
|
|
938
|
-
React.createElement('button', { type: 'button', onClick: closePreview }, '关闭'),
|
|
939
|
-
),
|
|
940
|
-
),
|
|
941
|
-
preview.url && preview.url.startsWith('blob:')
|
|
942
|
-
? React.createElement('img', { src: preview.url, alt: preview.name, className: 'dsh-upload-preview-img' })
|
|
943
|
-
: React.createElement(
|
|
944
|
-
'div',
|
|
945
|
-
{ style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, background: '#fff' } },
|
|
946
|
-
preview.officeLoading === true
|
|
947
|
-
? React.createElement('div', { className: 'dsh-upload-preview-loading' }, '转换中…')
|
|
948
|
-
: preview.officeHtml !== void 0
|
|
949
|
-
? React.createElement('iframe', { title: preview.name, srcDoc: preview.officeHtml, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } })
|
|
950
|
-
: preview.url && /\.pdf$/i.test(preview.name)
|
|
951
|
-
? React.createElement('embed', { src: preview.url, type: 'application/pdf', title: preview.name, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } })
|
|
952
|
-
: React.createElement('iframe', { title: preview.name, src: preview.url, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } }),
|
|
953
|
-
),
|
|
954
|
-
),
|
|
955
|
-
)
|
|
956
|
-
: null,
|
|
957
|
-
React.createElement(
|
|
958
|
-
'div',
|
|
959
|
-
{ className: 'dsh-upload-list' },
|
|
960
|
-
(dateGroups === null ? [{ day: null, files: shownFiles }] : dateGroups).map((group) => React.createElement(
|
|
961
|
-
'div',
|
|
962
|
-
{ key: group.day ?? '__all__', className: 'dsh-upload-group' },
|
|
963
|
-
group.day !== null
|
|
964
|
-
? React.createElement(
|
|
965
|
-
'div',
|
|
966
|
-
{ className: 'dsh-upload-group-day' },
|
|
967
|
-
group.day,
|
|
968
|
-
React.createElement('span', null, `${group.files.length} 个文件`),
|
|
969
|
-
)
|
|
970
|
-
: null,
|
|
971
|
-
group.files.map((file) => React.createElement(
|
|
972
|
-
'div',
|
|
973
|
-
{ className: 'dsh-upload-row', key: file.name },
|
|
974
|
-
deleteEnabled && React.createElement('input', { type: 'checkbox', className: 'dsh-upload-select', checked: (selected || new Set()).has(file.name), onChange: (e) => { const s = new Set(selected || []); if (e.target.checked) s.add(file.name); else s.delete(file.name); setSelected(s) } }),
|
|
975
|
-
React.createElement(
|
|
976
|
-
'span',
|
|
977
|
-
{ className: 'dsh-upload-file-name', title: file.path },
|
|
978
|
-
React.createElement('span', { className: 'dsh-upload-file-label' }, file.name.replace(/\.[^.]+$/, '')),
|
|
979
|
-
React.createElement('span', { className: 'dsh-upload-file-type' }, fileTypeLabel(file.name)),
|
|
980
|
-
),
|
|
981
|
-
React.createElement('span', { className: 'dsh-upload-file-meta' }, `${sizeText(file.size)} · ${dateText(file.modifiedAt)}`),
|
|
982
|
-
React.createElement(
|
|
983
|
-
'div',
|
|
984
|
-
{ className: 'dsh-upload-actions' },
|
|
985
|
-
React.createElement(
|
|
986
|
-
'button',
|
|
987
|
-
{ type: 'button', className: 'dsh-upload-copy', onClick: () => {
|
|
988
|
-
const s = String(file.path || file.name)
|
|
989
|
-
const base = state.root ? String(state.root).replace(/\/[^/]*$/, '') : ''
|
|
990
|
-
copyText(base && s.indexOf(base) === 0 ? s.slice(base.length).replace(/^\/+/, '') : s)
|
|
991
|
-
} },
|
|
992
|
-
'复制路径',
|
|
993
|
-
),
|
|
994
|
-
React.createElement(
|
|
995
|
-
'button',
|
|
996
|
-
{ type: 'button', disabled: !deleteEnabled || deleting === file.name, onClick: () => remove(file.name) },
|
|
997
|
-
deleting === file.name ? '删除中…' : '删除',
|
|
998
|
-
),
|
|
999
|
-
React.createElement('a', { href: downloadUrl(file.name), download: file.name }, '下载'),
|
|
1000
|
-
React.createElement('button', { type: 'button', className: 'dsh-upload-preview', onClick: () => previewFile(file.name) }, '预览'),
|
|
1001
|
-
),
|
|
1002
|
-
)),
|
|
1003
|
-
)),
|
|
1004
|
-
),
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
189
|
const CSS = `
|
|
1009
190
|
@font-face{font-family:DshChipCellInput;src:url(data:font/ttf;base64,AAEAAAAKAIAAAwAgT1MvMkT8SmIAAAEoAAAAYGNtYXAADQBPAAABkAAAADRnbHlmAAAAAAAAAcwAAAABaGVhZCwtPGoAAACsAAAANmhoZWEDIg7bAAAA5AAAACRobXR4EZQAAAAAAYgAAAAIbG9jYQAAAAAAAAHEAAAABm1heHAAAwACAAABCAAAACBuYW1lvljk2gAAAdAAAABscG9zdNNweNQAAAI8AAAALQABAAAAAQAAdia1tV8PPPUAAwPoAAAAAOaLfcUAAAAA5ot9xQAAAAAAAAAAAAAAAwACAAAAAAAAAAEAAAMg/zgAAA+gAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAEAAAACAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwjKAZAABQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAPz8/PwAA//z//AMg/zgAAAMgAMgAAAAAAAAAAAAAAAAAAAAgAAAB9AAAAAAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAIAAAAAQABAABAAD//P//AAD//P//AAUAAQAAAAAAAAAAAAAAAAAAAAAAAAAEADYAAQAAAAAAAQALAAAAAQAAAAAAAgAHAAsAAwABBAkAAQAWABIAAwABBAkAAgAOAChEc2hDaGlwQ2VsbFJlZ3VsYXIARABzAGgAQwBoAGkAcABDAGUAbABsAFIAZQBnAHUAbABhAHIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAABAgZvYmpyZXAAAAA=)format("truetype")}
|
|
1010
191
|
.uV2eYG_input,.uV2eYG_mirror{font-family:"DshChipCellInput",var(--dsw-font-family)!important}
|
|
@@ -1438,67 +619,14 @@ window.__ModuleLoader__.load({
|
|
|
1438
619
|
|
|
1439
620
|
function apply(ctx) {
|
|
1440
621
|
const mod = window.__dshLongMod || (() => true)
|
|
1441
|
-
const anyUpload = mod('uploadAttach') || mod('uploadDragDrop') || mod('uploadPaste')
|
|
1442
|
-
if (anyUpload) {
|
|
1443
|
-
const controller = new FileDraftController(ctx)
|
|
1444
|
-
ctx.effect(() => () => controller.dispose(), 'dsh-upload-manager: draft-file state')
|
|
1445
|
-
ctx.effect(() => ctx.inputTriggers.registerSource({
|
|
1446
|
-
trigger: '@',
|
|
1447
|
-
name: SOURCE,
|
|
1448
|
-
order: 10_000,
|
|
1449
|
-
candidates: async () => [],
|
|
1450
|
-
onPick: () => undefined,
|
|
1451
|
-
codec: {
|
|
1452
|
-
clipboardText: () => '',
|
|
1453
|
-
serialize: async (ref) => {
|
|
1454
|
-
const file = controller.fileForRef(ref)
|
|
1455
|
-
if (!file) throw new Error('待发送文件已失效,请重新上传')
|
|
1456
|
-
controller.markSerializing(ref)
|
|
1457
|
-
return serializedFile(file)
|
|
1458
|
-
},
|
|
1459
|
-
},
|
|
1460
|
-
}), 'dsh-upload-manager: hidden file reference codec')
|
|
1461
|
-
// 输入框附件上传(回形针) + 待发送文件栏:由 uploadAttach 控制
|
|
1462
|
-
if (mod('uploadAttach')) {
|
|
1463
|
-
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
1464
|
-
name: 'conversation.input.left',
|
|
1465
|
-
id: 'local-file-upload',
|
|
1466
|
-
order: -20,
|
|
1467
|
-
label: '上传文件',
|
|
1468
|
-
}, (props) => React.createElement(UploadControl, { ...props, controller })))
|
|
1469
|
-
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
1470
|
-
name: 'conversation.input.dock',
|
|
1471
|
-
id: 'local-file-upload-rail',
|
|
1472
|
-
order: 80,
|
|
1473
|
-
label: '待发送文件',
|
|
1474
|
-
}, (props) => React.createElement(PendingFileRail, { ...props, controller })))
|
|
1475
|
-
}
|
|
1476
|
-
// 拖放上传(遮罩+window 监听) / 粘贴上传:由各自开关控制
|
|
1477
|
-
if (mod('uploadDragDrop') || mod('uploadPaste')) {
|
|
1478
|
-
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
1479
|
-
name: 'conversation.input.dock',
|
|
1480
|
-
id: 'local-file-upload-dropzone',
|
|
1481
|
-
order: 90,
|
|
1482
|
-
label: '拖放/粘贴上传',
|
|
1483
|
-
}, (props) => React.createElement(DragDropOverlay, { ...props, controller })))
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
622
|
ctx.effect(() => {
|
|
1487
623
|
const style = document.createElement('style')
|
|
1488
|
-
style.dataset.plugin = 'dsh-
|
|
624
|
+
style.dataset.plugin = 'dsh-long-plugins'
|
|
625
|
+
style.dataset.pluginCss = 'dsh-long-plugins/uploads+workspace'
|
|
1489
626
|
style.textContent = CSS
|
|
1490
627
|
document.head.appendChild(style)
|
|
1491
628
|
return () => style.remove()
|
|
1492
|
-
}, 'dsh-
|
|
1493
|
-
// 上传文件预览/管理设置区:由 uploadPreview 控制
|
|
1494
|
-
if (mod('uploadPreview')) {
|
|
1495
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1496
|
-
name: 'settings.section',
|
|
1497
|
-
id: 'uploaded-files',
|
|
1498
|
-
order: 30,
|
|
1499
|
-
label: '上传文件',
|
|
1500
|
-
}, UploadSettingsSection))
|
|
1501
|
-
}
|
|
629
|
+
}, 'dsh-long-plugins: uploads+workspace styles')
|
|
1502
630
|
// 输出文件(工作区)设置区:由 workspace 控制
|
|
1503
631
|
if (mod('workspace')) {
|
|
1504
632
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
@@ -3169,10 +2297,6 @@ window.__ModuleLoader__.load({
|
|
|
3169
2297
|
.dsh-glass-note{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
3170
2298
|
`
|
|
3171
2299
|
const DSH_LONG_MODULES = [
|
|
3172
|
-
['uploadAttach', '附件上传(回形针)'],
|
|
3173
|
-
['uploadDragDrop', '附件拖放上传'],
|
|
3174
|
-
['uploadPaste', '附件粘贴上传'],
|
|
3175
|
-
['uploadPreview', '上传文件预览/管理'],
|
|
3176
2300
|
['skillDocs', '技能管理'],
|
|
3177
2301
|
['balance', '账户余额'],
|
|
3178
2302
|
['sessionCost', '会话成本'],
|