dsh-long-plugins 2.6.2 → 3.0.2
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 +440 -980
- 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 -1
- 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
|
@@ -16,9 +16,16 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
const API_PATH = '/api/dsh-uploads'
|
|
17
17
|
const DOWNLOAD_PATH = '/api/dsh-uploads/download'
|
|
18
18
|
const PREVIEW_PATH = '/api/dsh-uploads/preview'
|
|
19
|
-
const SOURCE = 'local-upload-files'
|
|
20
19
|
const HIDDEN_LABEL = '__dsh_upload_hidden__:'
|
|
21
20
|
|
|
21
|
+
function downloadUrl(name) {
|
|
22
|
+
return `${DOWNLOAD_PATH}?name=${encodeURIComponent(name)}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function previewUrl(name) {
|
|
26
|
+
return `${PREVIEW_PATH}?name=${encodeURIComponent(name)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
function errorMessage(error) {
|
|
23
30
|
return error instanceof Error ? error.message : String(error)
|
|
24
31
|
}
|
|
@@ -29,26 +36,6 @@ window.__ModuleLoader__.load({
|
|
|
29
36
|
return body
|
|
30
37
|
}
|
|
31
38
|
|
|
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
39
|
/** Extensions the browser can render inline (image/office handled separately). */
|
|
53
40
|
const INLINE_PREVIEW_EXTS = new Set([
|
|
54
41
|
'.pdf', '.txt', '.md', '.markdown', '.json', '.yml', '.yaml', '.xml', '.html', '.htm',
|
|
@@ -121,502 +108,6 @@ window.__ModuleLoader__.load({
|
|
|
121
108
|
document.body.removeChild(a)
|
|
122
109
|
}
|
|
123
110
|
|
|
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
111
|
function sizeText(bytes) {
|
|
621
112
|
if (bytes < 1024) return `${bytes} B`
|
|
622
113
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
|
|
@@ -681,326 +172,27 @@ window.__ModuleLoader__.load({
|
|
|
681
172
|
}
|
|
682
173
|
|
|
683
174
|
/** 🔍 放大镜搜索:点击图标弹出搜索框,输入即过滤;✕ 清除并关闭。不自动隐藏(需人工关闭或刷新)。 */
|
|
684
|
-
function SearchPopup({ value, onChange, placeholder }) {
|
|
685
|
-
const [open, setOpen] = React.useState(false)
|
|
686
|
-
const ref = React.useRef(null)
|
|
687
|
-
React.useEffect(() => { if (open && ref.current) { try { ref.current.focus() } catch (e) {} } }, [open])
|
|
688
|
-
const toggle = () => setOpen((o) => !o)
|
|
689
|
-
return React.createElement('div', { className: 'dsh-searchpop' },
|
|
690
|
-
React.createElement('button', {
|
|
691
|
-
type: 'button', className: 'dsh-searchpop-btn',
|
|
692
|
-
onClick: toggle,
|
|
693
|
-
title: '搜索文件名', 'aria-label': '搜索文件名',
|
|
694
|
-
}, '🔍'),
|
|
695
|
-
open && React.createElement('div', { className: 'dsh-searchpop-box' },
|
|
696
|
-
React.createElement('input', {
|
|
697
|
-
ref, type: 'text', className: 'dsh-searchpop-input', value,
|
|
698
|
-
onChange: (e) => onChange(e.target.value), placeholder: placeholder || '搜索文件名…', autoFocus: true,
|
|
699
|
-
}),
|
|
700
|
-
value !== '' && React.createElement('button', {
|
|
701
|
-
type: 'button', className: 'dsh-searchpop-clear', title: '清除', 'aria-label': '清除搜索',
|
|
702
|
-
onMouseDown: (e) => { e.preventDefault(); e.stopPropagation() },
|
|
703
|
-
onClick: (e) => { e.preventDefault(); e.stopPropagation(); onChange(''); setOpen(false); },
|
|
704
|
-
}, '✕'),
|
|
705
|
-
),
|
|
706
|
-
)
|
|
707
|
-
}
|
|
708
|
-
|
|
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
|
-
)),
|
|
175
|
+
function SearchPopup({ value, onChange, placeholder }) {
|
|
176
|
+
const [open, setOpen] = React.useState(false)
|
|
177
|
+
const ref = React.useRef(null)
|
|
178
|
+
React.useEffect(() => { if (open && ref.current) { try { ref.current.focus() } catch (e) {} } }, [open])
|
|
179
|
+
const toggle = () => setOpen((o) => !o)
|
|
180
|
+
return React.createElement('div', { className: 'dsh-searchpop' },
|
|
181
|
+
React.createElement('button', {
|
|
182
|
+
type: 'button', className: 'dsh-searchpop-btn',
|
|
183
|
+
onClick: toggle,
|
|
184
|
+
title: '搜索文件名', 'aria-label': '搜索文件名',
|
|
185
|
+
}, '🔍'),
|
|
186
|
+
open && React.createElement('div', { className: 'dsh-searchpop-box' },
|
|
187
|
+
React.createElement('input', {
|
|
188
|
+
ref, type: 'text', className: 'dsh-searchpop-input', value,
|
|
189
|
+
onChange: (e) => onChange(e.target.value), placeholder: placeholder || '搜索文件名…', autoFocus: true,
|
|
190
|
+
}),
|
|
191
|
+
value !== '' && React.createElement('button', {
|
|
192
|
+
type: 'button', className: 'dsh-searchpop-clear', title: '清除', 'aria-label': '清除搜索',
|
|
193
|
+
onMouseDown: (e) => { e.preventDefault(); e.stopPropagation() },
|
|
194
|
+
onClick: (e) => { e.preventDefault(); e.stopPropagation(); onChange(''); setOpen(false); },
|
|
195
|
+
}, '✕'),
|
|
1004
196
|
),
|
|
1005
197
|
)
|
|
1006
198
|
}
|
|
@@ -1325,171 +517,426 @@ window.__ModuleLoader__.load({
|
|
|
1325
517
|
let err = null
|
|
1326
518
|
for (const path of paths) {
|
|
1327
519
|
try {
|
|
1328
|
-
const res = await fetch('/api/dsh-uploads/workspace-file/delete', {
|
|
1329
|
-
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }),
|
|
1330
|
-
})
|
|
1331
|
-
const data = await res.json().catch(() => ({}))
|
|
1332
|
-
if (!(data.ok === true)) err = err || (data.error || `HTTP ${res.status}`)
|
|
1333
|
-
} catch (e) { err = err || String((e && e.message) || e) }
|
|
520
|
+
const res = await fetch('/api/dsh-uploads/workspace-file/delete', {
|
|
521
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }),
|
|
522
|
+
})
|
|
523
|
+
const data = await res.json().catch(() => ({}))
|
|
524
|
+
if (!(data.ok === true)) err = err || (data.error || `HTTP ${res.status}`)
|
|
525
|
+
} catch (e) { err = err || String((e && e.message) || e) }
|
|
526
|
+
}
|
|
527
|
+
setError(err)
|
|
528
|
+
setBatchBusy(false)
|
|
529
|
+
setSelected(new Set())
|
|
530
|
+
load()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return React.createElement(
|
|
534
|
+
'div',
|
|
535
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 } },
|
|
536
|
+
React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } }, '工作区输出文件(按文件夹分类,预览 / 下载 / 删除)'),
|
|
537
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 } },
|
|
538
|
+
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
539
|
+
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
540
|
+
React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
541
|
+
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
542
|
+
'开启删除',
|
|
543
|
+
),
|
|
544
|
+
deleteEnabled && React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
545
|
+
React.createElement('input', { type: 'checkbox', checked: (shownGroups || []).reduce((n, g) => n + g.files.length, 0) > 0 && (selected || new Set()).size === (shownGroups || []).reduce((n, g) => n + g.files.length, 0), onChange: (e) => { setSelected(e.target.checked ? new Set((shownGroups || []).flatMap((g) => g.files.map((f) => f.path))) : new Set()) } }),
|
|
546
|
+
'全选',
|
|
547
|
+
),
|
|
548
|
+
React.createElement('button', { type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
549
|
+
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
550
|
+
),
|
|
551
|
+
),
|
|
552
|
+
error !== null && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error),
|
|
553
|
+
groups === null && error === null && React.createElement('div', { style: metaStyle }, '加载中…'),
|
|
554
|
+
groups !== null && groups.length === 0 && React.createElement('div', { style: metaStyle }, '目录为空'),
|
|
555
|
+
(dayFilter || search) && groups !== null && groups.length > 0 && shownGroups !== null && shownGroups.length === 0 && React.createElement('div', { style: metaStyle }, '没有匹配的文件'),
|
|
556
|
+
shownGroups !== null && shownGroups.map((group) => React.createElement(
|
|
557
|
+
'div',
|
|
558
|
+
{ key: group.folder, style: { display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 } },
|
|
559
|
+
React.createElement(
|
|
560
|
+
'button',
|
|
561
|
+
{
|
|
562
|
+
type: 'button',
|
|
563
|
+
className: 'dsh-ws-folder',
|
|
564
|
+
style: folderBtnStyle,
|
|
565
|
+
'aria-expanded': !collapsed[group.folder],
|
|
566
|
+
onClick: () => toggleFolder(group.folder),
|
|
567
|
+
},
|
|
568
|
+
`${collapsed[group.folder] ? '▸' : '▾'} ${group.folder} (${group.files.length})`,
|
|
569
|
+
),
|
|
570
|
+
!collapsed[group.folder] && group.files.map((f) => React.createElement(
|
|
571
|
+
'div',
|
|
572
|
+
{ key: f.path, className: 'dsh-ws-row', style: rowStyle },
|
|
573
|
+
deleteEnabled && React.createElement('input', { type: 'checkbox', className: 'dsh-upload-select', checked: (selected || new Set()).has(f.path), onChange: (e) => { const s = new Set(selected || []); if (e.target.checked) s.add(f.path); else s.delete(f.path); setSelected(s) } }),
|
|
574
|
+
React.createElement(
|
|
575
|
+
'span',
|
|
576
|
+
{ className: 'dsh-ws-name', style: { ...nameStyle, display: 'flex', alignItems: 'center', gap: 6 }, title: f.path },
|
|
577
|
+
React.createElement('span', { className: 'dsh-ws-file-label', style: { flex: 1, minWidth: 0, whiteSpace: 'normal', wordBreak: 'break-all', overflowWrap: 'anywhere' } }, basenameWithoutExt(f.path)),
|
|
578
|
+
React.createElement('span', { className: 'dsh-ws-file-type', style: { flex: 'none' } }, fileTypeLabel(f.path)),
|
|
579
|
+
),
|
|
580
|
+
React.createElement('span', { className: 'dsh-ws-meta', style: metaStyle }, `${sizeText(f.size)} · ${dateText(f.mtime)}`),
|
|
581
|
+
React.createElement(
|
|
582
|
+
'div',
|
|
583
|
+
{ className: 'dsh-ws-actions', style: { display: 'flex', gap: 6, flex: 'none' } },
|
|
584
|
+
React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => copyText(f.path) }, '复制路径'),
|
|
585
|
+
React.createElement('button', { type: 'button', className: 'dsh-ws-del', style: delStyle, disabled: !deleteEnabled || busy, onClick: () => doDelete(f.path) }, '删除'),
|
|
586
|
+
React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(f.path) + '&download=1', download: f.name, style: btnStyle }, '下载'),
|
|
587
|
+
React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => openPreview(f.path) }, '预览'),
|
|
588
|
+
),
|
|
589
|
+
)),
|
|
590
|
+
)),
|
|
591
|
+
preview !== null && React.createElement(
|
|
592
|
+
'div',
|
|
593
|
+
{ className: 'dsh-ws-preview-overlay', style: overlayStyle, onClick: () => setPreview(null) },
|
|
594
|
+
React.createElement(
|
|
595
|
+
'div',
|
|
596
|
+
{ className: 'dsh-ws-preview-card', style: previewCardStyle, onClick: (e) => e.stopPropagation() },
|
|
597
|
+
React.createElement(
|
|
598
|
+
'div',
|
|
599
|
+
{ className: 'dsh-ws-preview-head', style: previewHeadStyle },
|
|
600
|
+
React.createElement('strong', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 13 } }, `${preview.name ?? preview.path}`),
|
|
601
|
+
preview.binary !== true && preview.loading !== true && React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => { if (editing) { setEdited(preview.content !== void 0 ? preview.content : ''); setEditing(false); } else { setEdited(preview.content !== void 0 ? preview.content : ''); setEditing(true); } } }, editing ? '取消编辑' : '编辑'),
|
|
602
|
+
editing && React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: doSave }, savedFlash ? '已保存' : '保存'),
|
|
603
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: copyContent }, copied ? '已复制' : '复制全部'),
|
|
604
|
+
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(preview.path) + '&download=1', download: preview.name, style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '下载'),
|
|
605
|
+
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: preview.url !== void 0 ? preview.url : '/api/dsh-uploads/workspace-preview?path=' + encodeURIComponent(preview.path), target: '_blank', rel: 'noopener noreferrer', style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '打开'),
|
|
606
|
+
React.createElement('button', { type: 'button', style: delStyle, disabled: busy, onClick: () => doDelete(preview.path) }, '删除'),
|
|
607
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => { const m = !maximized; setMaximized(m); try { const el = document.querySelector('.dsh-ws-preview-card'); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} } }, maximized ? '还原' : '放大'),
|
|
608
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => setPreview(null) }, '关闭'),
|
|
609
|
+
),
|
|
610
|
+
React.createElement(
|
|
611
|
+
'div',
|
|
612
|
+
{ style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, overflow: 'auto' } },
|
|
613
|
+
preview.loading === true && React.createElement('div', { style: { ...metaStyle, padding: 10 } }, '加载中…'),
|
|
614
|
+
preview.error !== void 0 && preview.loading !== true && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)', padding: 10 } }, preview.error),
|
|
615
|
+
preview.officeLoading === true && preview.loading !== true && React.createElement('div', { style: { ...metaStyle, padding: 10 } }, '转换中…'),
|
|
616
|
+
preview.url !== void 0 && preview.loading !== true && (preview.name && /\.pdf$/i.test(preview.name)
|
|
617
|
+
? React.createElement('embed', { src: preview.url, type: 'application/pdf', title: preview.name ?? preview.path, style: { width: '100%', height: '70vh', minHeight: 0, border: 'none', background: '#fff', flex: 1 } })
|
|
618
|
+
: React.createElement('iframe', { title: preview.name ?? preview.path, src: preview.url, style: { width: '100%', height: '70vh', minHeight: 0, border: 'none', background: '#fff', flex: 1 } })),
|
|
619
|
+
preview.binary === true && preview.officeHtml === void 0 && preview.url === void 0 && React.createElement('div', { style: { ...metaStyle, padding: 10 } }, '二进制文件,无法预览,请下载后查看'),
|
|
620
|
+
preview.officeHtml !== void 0 && React.createElement('iframe', { title: preview.name ?? preview.path, srcDoc: preview.officeHtml, style: { width: '100%', flex: 1, minHeight: 0, border: 'none', background: '#fff' } }),
|
|
621
|
+
preview.mdHtml !== void 0 && editing !== true && React.createElement('iframe', { title: preview.name ?? preview.path, srcDoc: preview.mdHtml, style: { width: '100%', flex: 1, minHeight: 0, border: 'none', background: '#fff' } }),
|
|
622
|
+
editing && preview.binary !== true && React.createElement('textarea', { style: textareaStyle, value: edited, onChange: (e) => setEdited(e.target.value), spellCheck: false }),
|
|
623
|
+
!editing && preview.binary !== true && preview.content !== void 0 && preview.mdHtml === void 0 && React.createElement('pre', { style: preStyle }, preview.content),
|
|
624
|
+
preview.truncated === true && React.createElement('div', { style: { ...metaStyle, padding: '4px 12px 10px' } }, '(内容过大,仅显示前 256KB)'),
|
|
625
|
+
),
|
|
626
|
+
),
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function UploadSettingsSection() {
|
|
632
|
+
const [state, setState] = React.useState({
|
|
633
|
+
loading: true,
|
|
634
|
+
root: '',
|
|
635
|
+
maxFileBytes: 0,
|
|
636
|
+
totalMaxBytes: 0,
|
|
637
|
+
usedBytes: 0,
|
|
638
|
+
files: [],
|
|
639
|
+
error: '',
|
|
640
|
+
})
|
|
641
|
+
const [deleting, setDeleting] = React.useState('')
|
|
642
|
+
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
643
|
+
const [selected, setSelected] = React.useState(() => new Set())
|
|
644
|
+
const [preview, setPreview] = React.useState(null)
|
|
645
|
+
const [previewMaximized, setPreviewMaximized] = React.useState(false)
|
|
646
|
+
const [dayFilter, setDayFilter] = React.useState('')
|
|
647
|
+
const [search, setSearch] = React.useState('')
|
|
648
|
+
const [deleteEnabled, setDeleteEnabled] = React.useState(false)
|
|
649
|
+
|
|
650
|
+
async function refresh() {
|
|
651
|
+
setState((current) => ({ ...current, loading: true, error: '' }))
|
|
652
|
+
try {
|
|
653
|
+
const response = await fetch(API_PATH, { cache: 'no-store' })
|
|
654
|
+
const body = await responseJson(response)
|
|
655
|
+
setState({
|
|
656
|
+
loading: false,
|
|
657
|
+
root: body.root,
|
|
658
|
+
maxFileBytes: body.maxFileBytes,
|
|
659
|
+
totalMaxBytes: body.totalMaxBytes,
|
|
660
|
+
usedBytes: body.usedBytes,
|
|
661
|
+
files: body.files,
|
|
662
|
+
error: '',
|
|
663
|
+
})
|
|
664
|
+
} catch (error) {
|
|
665
|
+
setState((current) => ({ ...current, loading: false, error: errorMessage(error) }))
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
React.useEffect(() => {
|
|
670
|
+
refresh()
|
|
671
|
+
}, [])
|
|
672
|
+
|
|
673
|
+
async function remove(name) {
|
|
674
|
+
if (!globalThis.confirm(`确定删除“${name}”吗?此操作不可恢复。`)) return
|
|
675
|
+
setDeleting(name)
|
|
676
|
+
try {
|
|
677
|
+
const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
678
|
+
await responseJson(response)
|
|
679
|
+
if (preview !== null && preview.name === name) closePreview()
|
|
680
|
+
await refresh()
|
|
681
|
+
} catch (error) {
|
|
682
|
+
setState((current) => ({ ...current, error: errorMessage(error) }))
|
|
683
|
+
} finally {
|
|
684
|
+
setDeleting('')
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function batchDelete() {
|
|
689
|
+
const names = Array.from(selected || [])
|
|
690
|
+
if (names.length === 0) return
|
|
691
|
+
if (!globalThis.confirm(`确定删除所选 ${names.length} 个文件吗?此操作不可恢复。`)) return
|
|
692
|
+
setBatchBusy(true)
|
|
693
|
+
let err = ''
|
|
694
|
+
for (const name of names) {
|
|
695
|
+
try {
|
|
696
|
+
const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
697
|
+
if (!response.ok) { const b = await response.json().catch(() => ({})); err = err || (b.error || `HTTP ${response.status}`) }
|
|
698
|
+
} catch (e) { err = err || errorMessage(e) }
|
|
699
|
+
}
|
|
700
|
+
setState((current) => ({ ...current, error: err }))
|
|
701
|
+
setBatchBusy(false)
|
|
702
|
+
setSelected(new Set())
|
|
703
|
+
await refresh()
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function previewFile(name) {
|
|
707
|
+
setPreviewMaximized(false)
|
|
708
|
+
try {
|
|
709
|
+
const isOffice = /\.(docx|xlsx|pptx)$/i.test(name)
|
|
710
|
+
if (isOffice) {
|
|
711
|
+
// 先打开弹窗并提示转换中(NAS 上 docx/xlsx 转换可能耗时 1~2 秒)
|
|
712
|
+
setPreview({ name, officeLoading: true })
|
|
713
|
+
const response = await fetch(previewUrl(name), { cache: 'no-store' })
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
const body = await response.json().catch(() => ({}))
|
|
716
|
+
throw new Error(body.error || `HTTP ${response.status}`)
|
|
717
|
+
}
|
|
718
|
+
const data = await response.json().catch(() => ({}))
|
|
719
|
+
setPreview({
|
|
720
|
+
name,
|
|
721
|
+
officeHtml: data.officeHtml ?? '<p style="font-family:sans-serif;padding:12px">(无法渲染此文档)</p>',
|
|
722
|
+
})
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
// 无法内嵌预览的类型(压缩包、程序、视频、字体等):点预览直接下载。
|
|
726
|
+
if (!isInlinePreviewable(name)) {
|
|
727
|
+
triggerDownload(downloadUrl(name), name)
|
|
728
|
+
return
|
|
729
|
+
}
|
|
730
|
+
const response = await fetch(previewUrl(name), { cache: 'no-store' })
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
const body = await response.json().catch(() => ({}))
|
|
733
|
+
throw new Error(body.error || `HTTP ${response.status}`)
|
|
734
|
+
}
|
|
735
|
+
const type = response.headers.get('content-type') || ''
|
|
736
|
+
if (type.startsWith('image/')) {
|
|
737
|
+
const blob = await response.blob()
|
|
738
|
+
const url = URL.createObjectURL(blob)
|
|
739
|
+
setPreview({ url, name })
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
// PDF / txt / 其它可内嵌文件:弹窗内嵌预览(iframe 指向预览端点),
|
|
743
|
+
// 只有用户点「打开」才在新浏览器标签中打开。
|
|
744
|
+
setPreview({ url: previewUrl(name), name })
|
|
745
|
+
} catch (error) {
|
|
746
|
+
setState((current) => ({ ...current, error: errorMessage(error) }))
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function closePreview() {
|
|
751
|
+
if (preview?.url) URL.revokeObjectURL(preview.url)
|
|
752
|
+
setPreview(null)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Group files by calendar day of their modifiedAt (YYYY-MM-DD, desc). */
|
|
756
|
+
function groupFilesByDate(files) {
|
|
757
|
+
const groups = []
|
|
758
|
+
const byDay = new Map()
|
|
759
|
+
for (const file of files) {
|
|
760
|
+
let day = ''
|
|
761
|
+
try {
|
|
762
|
+
day = localDay(file.modifiedAt)
|
|
763
|
+
} catch {
|
|
764
|
+
day = '未知日期'
|
|
765
|
+
}
|
|
766
|
+
if (!byDay.has(day)) byDay.set(day, [])
|
|
767
|
+
byDay.get(day).push(file)
|
|
1334
768
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
load()
|
|
769
|
+
const days = [...byDay.keys()].sort((a, b) => b.localeCompare(a))
|
|
770
|
+
for (const day of days) groups.push({ day, files: byDay.get(day) })
|
|
771
|
+
return groups
|
|
1339
772
|
}
|
|
773
|
+
// 日期筛选 + 文件名搜索:先按所选天/关键词过滤 state.files,再始终按天分组(空=全部)。
|
|
774
|
+
const shownFiles = (dayFilter || search)
|
|
775
|
+
? state.files.filter((f) => (dayFilter ? (() => { try { return localDay(f.modifiedAt) === dayFilter } catch { return false } })() : true) && matchesSearch(f, search))
|
|
776
|
+
: state.files
|
|
777
|
+
const dateGroups = groupFilesByDate(shownFiles)
|
|
1340
778
|
|
|
1341
779
|
return React.createElement(
|
|
1342
|
-
'
|
|
1343
|
-
{
|
|
1344
|
-
React.createElement(
|
|
1345
|
-
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 } },
|
|
1346
|
-
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
1347
|
-
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
1348
|
-
React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
1349
|
-
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
1350
|
-
'开启删除',
|
|
1351
|
-
),
|
|
1352
|
-
deleteEnabled && React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
1353
|
-
React.createElement('input', { type: 'checkbox', checked: (shownGroups || []).reduce((n, g) => n + g.files.length, 0) > 0 && (selected || new Set()).size === (shownGroups || []).reduce((n, g) => n + g.files.length, 0), onChange: (e) => { setSelected(e.target.checked ? new Set((shownGroups || []).flatMap((g) => g.files.map((f) => f.path))) : new Set()) } }),
|
|
1354
|
-
'全选',
|
|
1355
|
-
),
|
|
1356
|
-
React.createElement('button', { type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
1357
|
-
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
1358
|
-
),
|
|
1359
|
-
),
|
|
1360
|
-
error !== null && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error),
|
|
1361
|
-
groups === null && error === null && React.createElement('div', { style: metaStyle }, '加载中…'),
|
|
1362
|
-
groups !== null && groups.length === 0 && React.createElement('div', { style: metaStyle }, '目录为空'),
|
|
1363
|
-
(dayFilter || search) && groups !== null && groups.length > 0 && shownGroups !== null && shownGroups.length === 0 && React.createElement('div', { style: metaStyle }, '没有匹配的文件'),
|
|
1364
|
-
shownGroups !== null && shownGroups.map((group) => React.createElement(
|
|
780
|
+
'section',
|
|
781
|
+
{ className: 'dsh-upload-settings' },
|
|
782
|
+
React.createElement(
|
|
1365
783
|
'div',
|
|
1366
|
-
{
|
|
784
|
+
{ className: 'dsh-upload-settings-head' },
|
|
1367
785
|
React.createElement(
|
|
1368
|
-
'
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
style: folderBtnStyle,
|
|
1373
|
-
'aria-expanded': !collapsed[group.folder],
|
|
1374
|
-
onClick: () => toggleFolder(group.folder),
|
|
1375
|
-
},
|
|
1376
|
-
`${collapsed[group.folder] ? '▸' : '▾'} ${group.folder} (${group.files.length})`,
|
|
786
|
+
'div',
|
|
787
|
+
null,
|
|
788
|
+
React.createElement('h2', null, '上传文件'),
|
|
789
|
+
React.createElement('p', null, '管理从输入框上传到 Harness 容器中的文件。'),
|
|
1377
790
|
),
|
|
1378
|
-
|
|
791
|
+
React.createElement(
|
|
1379
792
|
'div',
|
|
1380
|
-
{
|
|
1381
|
-
|
|
793
|
+
{ className: 'dsh-upload-head-actions' },
|
|
794
|
+
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
795
|
+
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
1382
796
|
React.createElement(
|
|
1383
|
-
'
|
|
1384
|
-
{
|
|
1385
|
-
|
|
1386
|
-
React.createElement('span', { className: 'dsh-ws-file-type', style: { flex: 'none' } }, fileTypeLabel(f.path)),
|
|
797
|
+
'button',
|
|
798
|
+
{ type: 'button', className: 'dsh-upload-refresh', disabled: state.loading, onClick: refresh },
|
|
799
|
+
state.loading ? '刷新中…' : '刷新',
|
|
1387
800
|
),
|
|
1388
|
-
React.createElement('span', { className: 'dsh-ws-meta', style: metaStyle }, `${sizeText(f.size)} · ${dateText(f.mtime)}`),
|
|
1389
801
|
React.createElement(
|
|
1390
|
-
'
|
|
1391
|
-
{ className: 'dsh-
|
|
1392
|
-
React.createElement('
|
|
1393
|
-
|
|
1394
|
-
React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(f.path) + '&download=1', download: f.name, style: btnStyle }, '下载'),
|
|
1395
|
-
React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => openPreview(f.path) }, '预览'),
|
|
802
|
+
'label',
|
|
803
|
+
{ className: 'dsh-upload-del-toggle' },
|
|
804
|
+
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
805
|
+
'开启删除',
|
|
1396
806
|
),
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
{ className: 'dsh-ws-preview-overlay', style: overlayStyle, onClick: () => setPreview(null) },
|
|
1402
|
-
React.createElement(
|
|
1403
|
-
'div',
|
|
1404
|
-
{ className: 'dsh-ws-preview-card', style: previewCardStyle, onClick: (e) => e.stopPropagation() },
|
|
1405
|
-
React.createElement(
|
|
1406
|
-
'div',
|
|
1407
|
-
{ className: 'dsh-ws-preview-head', style: previewHeadStyle },
|
|
1408
|
-
React.createElement('strong', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 13 } }, `${preview.name ?? preview.path}`),
|
|
1409
|
-
preview.binary !== true && preview.loading !== true && React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => { if (editing) { setEdited(preview.content !== void 0 ? preview.content : ''); setEditing(false); } else { setEdited(preview.content !== void 0 ? preview.content : ''); setEditing(true); } } }, editing ? '取消编辑' : '编辑'),
|
|
1410
|
-
editing && React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: doSave }, savedFlash ? '已保存' : '保存'),
|
|
1411
|
-
React.createElement('button', { type: 'button', style: btnStyle, onClick: copyContent }, copied ? '已复制' : '复制全部'),
|
|
1412
|
-
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(preview.path) + '&download=1', download: preview.name, style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '下载'),
|
|
1413
|
-
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: preview.url !== void 0 ? preview.url : '/api/dsh-uploads/workspace-preview?path=' + encodeURIComponent(preview.path), target: '_blank', rel: 'noopener noreferrer', style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '打开'),
|
|
1414
|
-
React.createElement('button', { type: 'button', style: delStyle, disabled: busy, onClick: () => doDelete(preview.path) }, '删除'),
|
|
1415
|
-
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => { const m = !maximized; setMaximized(m); try { const el = document.querySelector('.dsh-ws-preview-card'); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} } }, maximized ? '还原' : '放大'),
|
|
1416
|
-
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => setPreview(null) }, '关闭'),
|
|
807
|
+
deleteEnabled && React.createElement('label',
|
|
808
|
+
{ className: 'dsh-upload-del-toggle' },
|
|
809
|
+
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()) } }),
|
|
810
|
+
'全选',
|
|
1417
811
|
),
|
|
1418
812
|
React.createElement(
|
|
1419
|
-
'
|
|
1420
|
-
{
|
|
1421
|
-
|
|
1422
|
-
preview.error !== void 0 && preview.loading !== true && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)', padding: 10 } }, preview.error),
|
|
1423
|
-
preview.officeLoading === true && preview.loading !== true && React.createElement('div', { style: { ...metaStyle, padding: 10 } }, '转换中…'),
|
|
1424
|
-
preview.url !== void 0 && preview.loading !== true && (preview.name && /\.pdf$/i.test(preview.name)
|
|
1425
|
-
? React.createElement('embed', { src: preview.url, type: 'application/pdf', title: preview.name ?? preview.path, style: { width: '100%', height: '70vh', minHeight: 0, border: 'none', background: '#fff', flex: 1 } })
|
|
1426
|
-
: React.createElement('iframe', { title: preview.name ?? preview.path, src: preview.url, style: { width: '100%', height: '70vh', minHeight: 0, border: 'none', background: '#fff', flex: 1 } })),
|
|
1427
|
-
preview.binary === true && preview.officeHtml === void 0 && preview.url === void 0 && React.createElement('div', { style: { ...metaStyle, padding: 10 } }, '二进制文件,无法预览,请下载后查看'),
|
|
1428
|
-
preview.officeHtml !== void 0 && React.createElement('iframe', { title: preview.name ?? preview.path, srcDoc: preview.officeHtml, style: { width: '100%', flex: 1, minHeight: 0, border: 'none', background: '#fff' } }),
|
|
1429
|
-
preview.mdHtml !== void 0 && editing !== true && React.createElement('iframe', { title: preview.name ?? preview.path, srcDoc: preview.mdHtml, style: { width: '100%', flex: 1, minHeight: 0, border: 'none', background: '#fff' } }),
|
|
1430
|
-
editing && preview.binary !== true && React.createElement('textarea', { style: textareaStyle, value: edited, onChange: (e) => setEdited(e.target.value), spellCheck: false }),
|
|
1431
|
-
!editing && preview.binary !== true && preview.content !== void 0 && preview.mdHtml === void 0 && React.createElement('pre', { style: preStyle }, preview.content),
|
|
1432
|
-
preview.truncated === true && React.createElement('div', { style: { ...metaStyle, padding: '4px 12px 10px' } }, '(内容过大,仅显示前 256KB)'),
|
|
813
|
+
'button',
|
|
814
|
+
{ type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
815
|
+
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
1433
816
|
),
|
|
1434
817
|
),
|
|
1435
818
|
),
|
|
819
|
+
React.createElement(
|
|
820
|
+
'div',
|
|
821
|
+
{ className: 'dsh-upload-root' },
|
|
822
|
+
React.createElement('span', null, '固定目录'),
|
|
823
|
+
React.createElement('code', null, state.root || '读取中…'),
|
|
824
|
+
state.maxFileBytes
|
|
825
|
+
? React.createElement('small', null, `单文件上限 ${sizeText(state.maxFileBytes)}`)
|
|
826
|
+
: null,
|
|
827
|
+
state.totalMaxBytes
|
|
828
|
+
? React.createElement('small', null, `已使用 ${sizeText(state.usedBytes)} / ${sizeText(state.totalMaxBytes)}`)
|
|
829
|
+
: null,
|
|
830
|
+
),
|
|
831
|
+
state.error ? React.createElement('div', { className: 'dsh-upload-error' }, state.error) : null,
|
|
832
|
+
!state.loading && state.files.length === 0
|
|
833
|
+
? React.createElement('div', { className: 'dsh-upload-empty' }, '当前没有已上传文件。')
|
|
834
|
+
: null,
|
|
835
|
+
!state.loading && (dayFilter !== '' || search !== '') && shownFiles.length === 0 && state.files.length > 0
|
|
836
|
+
? React.createElement('div', { className: 'dsh-upload-empty' }, '没有匹配的文件。')
|
|
837
|
+
: null,
|
|
838
|
+
preview
|
|
839
|
+
? React.createElement(
|
|
840
|
+
'div',
|
|
841
|
+
{ className: 'dsh-upload-preview-overlay' + (previewMaximized ? ' dsh-upload-preview-overlay-max' : ''), onClick: closePreview },
|
|
842
|
+
React.createElement(
|
|
843
|
+
'div',
|
|
844
|
+
{ className: 'dsh-upload-preview-card' + (previewMaximized ? ' dsh-upload-preview-card-max' : ''), onClick: (event) => event.stopPropagation() },
|
|
845
|
+
React.createElement(
|
|
846
|
+
'div',
|
|
847
|
+
{ className: 'dsh-upload-preview-head' },
|
|
848
|
+
React.createElement('strong', null, preview.name),
|
|
849
|
+
React.createElement(
|
|
850
|
+
'div',
|
|
851
|
+
{ className: 'dsh-upload-preview-actions', style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
852
|
+
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
853
|
+
? React.createElement('a', { href: preview.officeHtml !== void 0 ? downloadUrl(preview.name) : previewUrl(preview.name), target: '_blank', rel: 'noopener noreferrer', className: 'dsh-upload-preview-open' }, '打开')
|
|
854
|
+
: null,
|
|
855
|
+
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
856
|
+
? React.createElement('a', { href: downloadUrl(preview.name), download: preview.name, className: 'dsh-upload-preview-open' }, '下载')
|
|
857
|
+
: null,
|
|
858
|
+
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 ? '还原' : '放大'),
|
|
859
|
+
React.createElement('button', { type: 'button', className: 'dsh-upload-preview-del', disabled: deleting === preview.name, onClick: () => remove(preview.name) }, deleting === preview.name ? '删除中…' : '删除'),
|
|
860
|
+
React.createElement('button', { type: 'button', onClick: closePreview }, '关闭'),
|
|
861
|
+
),
|
|
862
|
+
),
|
|
863
|
+
preview.url && preview.url.startsWith('blob:')
|
|
864
|
+
? React.createElement('img', { src: preview.url, alt: preview.name, className: 'dsh-upload-preview-img' })
|
|
865
|
+
: React.createElement(
|
|
866
|
+
'div',
|
|
867
|
+
{ style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, background: '#fff' } },
|
|
868
|
+
preview.officeLoading === true
|
|
869
|
+
? React.createElement('div', { className: 'dsh-upload-preview-loading' }, '转换中…')
|
|
870
|
+
: preview.officeHtml !== void 0
|
|
871
|
+
? 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' } })
|
|
872
|
+
: preview.url && /\.pdf$/i.test(preview.name)
|
|
873
|
+
? 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' } })
|
|
874
|
+
: 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' } }),
|
|
875
|
+
),
|
|
876
|
+
),
|
|
877
|
+
)
|
|
878
|
+
: null,
|
|
879
|
+
React.createElement(
|
|
880
|
+
'div',
|
|
881
|
+
{ className: 'dsh-upload-list' },
|
|
882
|
+
(dateGroups === null ? [{ day: null, files: shownFiles }] : dateGroups).map((group) => React.createElement(
|
|
883
|
+
'div',
|
|
884
|
+
{ key: group.day ?? '__all__', className: 'dsh-upload-group' },
|
|
885
|
+
group.day !== null
|
|
886
|
+
? React.createElement(
|
|
887
|
+
'div',
|
|
888
|
+
{ className: 'dsh-upload-group-day' },
|
|
889
|
+
group.day,
|
|
890
|
+
React.createElement('span', null, `${group.files.length} 个文件`),
|
|
891
|
+
)
|
|
892
|
+
: null,
|
|
893
|
+
group.files.map((file) => React.createElement(
|
|
894
|
+
'div',
|
|
895
|
+
{ className: 'dsh-upload-row', key: file.name },
|
|
896
|
+
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) } }),
|
|
897
|
+
React.createElement(
|
|
898
|
+
'span',
|
|
899
|
+
{ className: 'dsh-upload-file-name', title: file.path },
|
|
900
|
+
React.createElement('span', { className: 'dsh-upload-file-label' }, file.name.replace(/\.[^.]+$/, '')),
|
|
901
|
+
React.createElement('span', { className: 'dsh-upload-file-type' }, fileTypeLabel(file.name)),
|
|
902
|
+
),
|
|
903
|
+
React.createElement('span', { className: 'dsh-upload-file-meta' }, `${sizeText(file.size)} · ${dateText(file.modifiedAt)}`),
|
|
904
|
+
React.createElement(
|
|
905
|
+
'div',
|
|
906
|
+
{ className: 'dsh-upload-actions' },
|
|
907
|
+
React.createElement(
|
|
908
|
+
'button',
|
|
909
|
+
{ type: 'button', className: 'dsh-upload-copy', onClick: () => {
|
|
910
|
+
const s = String(file.path || file.name)
|
|
911
|
+
const base = state.root ? String(state.root).replace(/\/[^/]*$/, '') : ''
|
|
912
|
+
copyText(base && s.indexOf(base) === 0 ? s.slice(base.length).replace(/^\/+/, '') : s)
|
|
913
|
+
} },
|
|
914
|
+
'复制路径',
|
|
915
|
+
),
|
|
916
|
+
React.createElement(
|
|
917
|
+
'button',
|
|
918
|
+
{ type: 'button', disabled: !deleteEnabled || deleting === file.name, onClick: () => remove(file.name) },
|
|
919
|
+
deleting === file.name ? '删除中…' : '删除',
|
|
920
|
+
),
|
|
921
|
+
React.createElement('a', { href: downloadUrl(file.name), download: file.name }, '下载'),
|
|
922
|
+
React.createElement('button', { type: 'button', className: 'dsh-upload-preview', onClick: () => previewFile(file.name) }, '预览'),
|
|
923
|
+
),
|
|
924
|
+
)),
|
|
925
|
+
)),
|
|
926
|
+
),
|
|
1436
927
|
)
|
|
1437
928
|
}
|
|
1438
929
|
|
|
1439
930
|
function apply(ctx) {
|
|
1440
931
|
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
932
|
ctx.effect(() => {
|
|
1487
933
|
const style = document.createElement('style')
|
|
1488
|
-
style.dataset.plugin = 'dsh-
|
|
934
|
+
style.dataset.plugin = 'dsh-long-plugins'
|
|
935
|
+
style.dataset.pluginCss = 'dsh-long-plugins/uploads+workspace'
|
|
1489
936
|
style.textContent = CSS
|
|
1490
937
|
document.head.appendChild(style)
|
|
1491
938
|
return () => style.remove()
|
|
1492
|
-
}, 'dsh-
|
|
939
|
+
}, 'dsh-long-plugins: uploads+workspace styles')
|
|
1493
940
|
// 上传文件预览/管理设置区:由 uploadPreview 控制
|
|
1494
941
|
if (mod('uploadPreview')) {
|
|
1495
942
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
@@ -3169,9 +2616,6 @@ window.__ModuleLoader__.load({
|
|
|
3169
2616
|
.dsh-glass-note{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
3170
2617
|
`
|
|
3171
2618
|
const DSH_LONG_MODULES = [
|
|
3172
|
-
['uploadAttach', '附件上传(回形针)'],
|
|
3173
|
-
['uploadDragDrop', '附件拖放上传'],
|
|
3174
|
-
['uploadPaste', '附件粘贴上传'],
|
|
3175
2619
|
['uploadPreview', '上传文件预览/管理'],
|
|
3176
2620
|
['skillDocs', '技能管理'],
|
|
3177
2621
|
['balance', '账户余额'],
|
|
@@ -3250,9 +2694,25 @@ window.__ModuleLoader__.load({
|
|
|
3250
2694
|
|
|
3251
2695
|
// 模块开关缓存:页面加载时同步读取(让 main apply 的 modEnabled 门控生效);
|
|
3252
2696
|
// 配置加载(applyTheme)/保存(dsh-long 保存)后更新。开关在下次刷新生效。
|
|
2697
|
+
// 模块开关:以服务端为准(启动时同步拉取,避免 localStorage 缓存过期导致模块被误关)
|
|
2698
|
+
try {
|
|
2699
|
+
const _x = new XMLHttpRequest()
|
|
2700
|
+
_x.open('GET', '/api/dsh-uploads/modules-config', false)
|
|
2701
|
+
_x.send()
|
|
2702
|
+
if (_x.status === 200) {
|
|
2703
|
+
const _b = JSON.parse(_x.responseText)
|
|
2704
|
+
if (_b && _b.cfg && _b.cfg.modules && typeof _b.cfg.modules === 'object') {
|
|
2705
|
+
window.__dshLongModules = _b.cfg.modules
|
|
2706
|
+
try { localStorage.setItem('dsh-long:modules', JSON.stringify(_b.cfg.modules)) } catch (e) {}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
} catch (_) {}
|
|
2710
|
+
// 回退:服务端不可用时用 localStorage 缓存
|
|
3253
2711
|
try {
|
|
3254
|
-
|
|
3255
|
-
|
|
2712
|
+
if (!window.__dshLongModules) {
|
|
2713
|
+
const _m = localStorage.getItem('dsh-long:modules')
|
|
2714
|
+
if (_m) window.__dshLongModules = JSON.parse(_m)
|
|
2715
|
+
}
|
|
3256
2716
|
} catch (_) {}
|
|
3257
2717
|
// 全局模块开关查询(供各组件/apply 内部判断)
|
|
3258
2718
|
window.__dshLongMod = (n) => (window.__dshLongModules ? window.__dshLongModules[n] !== false : true)
|