dsh-plugin-tool-management 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -136
- package/README_EN.md +231 -136
- package/cordis.patch.yml +18 -0
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
- package/lib/agents-md/service.js +240 -0
- package/lib/client.js +2173 -850
- package/lib/history/projcache.js +335 -0
- package/lib/history/tombstone.js +32 -0
- package/lib/history/workspace.js +892 -0
- package/lib/imports/parsers.js +181 -0
- package/lib/index.js +768 -2
- package/lib/rules/provider.js +121 -0
- package/lib/rules/service.js +1657 -0
- package/lib/skills/core.js +10 -1
- package/lib/skills/service.js +5 -5
- package/package.json +18 -8
- package/screenshots.json +7 -0
package/lib/client.js
CHANGED
|
@@ -1,850 +1,2173 @@
|
|
|
1
|
-
// dsh-plugin-tool-management — durable client half (web bundle, ModuleLoader CJS format).
|
|
2
|
-
// Registers the "MCP 管理" settings page. Talks to the host half through the
|
|
3
|
-
// exact-path HTTP route /dsh-plugin-tool-management/api (same origin) instead of the
|
|
4
|
-
// dynamic-only host.call channel.
|
|
5
|
-
window.__ModuleLoader__.load({
|
|
6
|
-
id: 'dsh-plugin-tool-management',
|
|
7
|
-
factory: (require) => {
|
|
8
|
-
var module = { exports: {} }
|
|
9
|
-
var exports = module.exports
|
|
10
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
-
const React = require('react')
|
|
12
|
-
|
|
13
|
-
const CSS =
|
|
14
|
-
'.dsm-summary.dsm-summary-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'.dsm-
|
|
18
|
-
'.dsm-
|
|
19
|
-
'.dsm-
|
|
20
|
-
'.dsm-
|
|
21
|
-
'.dsm-tool{
|
|
22
|
-
'.dsm-tool-
|
|
23
|
-
'.dsm-tool-
|
|
24
|
-
'.dsm-tool-
|
|
25
|
-
'.dsm-tool-
|
|
26
|
-
'.dsm-tool-
|
|
27
|
-
'.dsm-tool-param{
|
|
28
|
-
'.dsm-tool-param-
|
|
29
|
-
'.dsm-
|
|
30
|
-
'.dsm-
|
|
31
|
-
'.dsm-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.dsm-
|
|
36
|
-
.dsm-
|
|
37
|
-
.dsm-
|
|
38
|
-
.dsm-
|
|
39
|
-
.dsm-
|
|
40
|
-
.dsm-
|
|
41
|
-
.dsm-
|
|
42
|
-
|
|
43
|
-
.dsm-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
tag
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{ value: '
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
{ value: '
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management
|
|
131
|
-
React.createElement(
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// MCP 服务页:与 Skills 页共用 dsm-* 设计语言(统计卡 / 筛选 / 分组卡片 / 模态框)。
|
|
135
|
-
function MCPPage() {
|
|
136
|
-
const [state, setState] = React.useState({ loading: true, error: null, rows: [], paths: null, errors: [], warnings: [] })
|
|
137
|
-
const [msg, setMsg] = React.useState(null)
|
|
138
|
-
const [busy, setBusy] = React.useState(null)
|
|
139
|
-
const [query, setQuery] = React.useState('')
|
|
140
|
-
const [levelFilter, setLevelFilter] = React.useState('')
|
|
141
|
-
const [collapsed, setCollapsed] = React.useState({})
|
|
142
|
-
const [formModal, setFormModal] = React.useState(null)
|
|
143
|
-
const [detail, setDetail] = React.useState(null)
|
|
144
|
-
const [confirmRow, setConfirmRow] = React.useState(null)
|
|
145
|
-
const [restartInfo, setRestartInfo] = React.useState(null)
|
|
146
|
-
const [reveal, setReveal] = React.useState(false)
|
|
147
|
-
const [settings, setSettings] = React.useState(null)
|
|
148
|
-
const [noteDraft, setNoteDraft] = React.useState('')
|
|
149
|
-
const [, setTick] = React.useState(0)
|
|
150
|
-
|
|
151
|
-
const refresh = (withReveal, onRows) => {
|
|
152
|
-
const useReveal = withReveal === undefined ? reveal : withReveal === true
|
|
153
|
-
// 明文视图走独立的 mcpm-reveal op(host 端受 token 保护),
|
|
154
|
-
// mcpm-list 始终脱敏,无 token 也能渲染页面。
|
|
155
|
-
apiCall(useReveal ? 'mcpm-reveal' : 'mcpm-list', {}).then((res) => {
|
|
156
|
-
const rows = (res && res.rows) || []
|
|
157
|
-
setState({
|
|
158
|
-
loading: false,
|
|
159
|
-
error: res && res.ok ? null : ((res && res.error) || '加载失败'),
|
|
160
|
-
rows,
|
|
161
|
-
paths: (res && res.paths) || null,
|
|
162
|
-
errors: (res && res.errors) || [],
|
|
163
|
-
warnings: (res && res.warnings) || [],
|
|
164
|
-
})
|
|
165
|
-
if (onRows) onRows(rows)
|
|
166
|
-
}).catch((e) => setState({ loading: false, error: String((e && e.message) || e), rows: [], paths: null, errors: [], warnings: [] }))
|
|
167
|
-
}
|
|
168
|
-
// The polling tick must always call the newest refresh closure, while
|
|
169
|
-
// the interval itself only changes when the saved settings change.
|
|
170
|
-
const refreshRef = React.useRef(refresh)
|
|
171
|
-
refreshRef.current = refresh
|
|
172
|
-
|
|
173
|
-
React.useEffect(() => { refresh(false) }, [])
|
|
174
|
-
React.useEffect(() => {
|
|
175
|
-
apiCall('mcpm-settings', {}).then((res) => {
|
|
176
|
-
if (res && res.ok && res.settings) setSettings(res.settings)
|
|
177
|
-
}).catch(() => {})
|
|
178
|
-
}, [])
|
|
179
|
-
React.useEffect(() => ctx.interval(() => setTick((t) => t + 1), 1000), [])
|
|
180
|
-
React.useEffect(() => {
|
|
181
|
-
const ms = (settings && settings.pollIntervalMs) || 5000
|
|
182
|
-
return ctx.interval(() => refreshRef.current(), ms)
|
|
183
|
-
}, [settings])
|
|
184
|
-
React.useEffect(() => {
|
|
185
|
-
if (!msg) return undefined
|
|
186
|
-
return ctx.timeout(() => setMsg(null), 4000)
|
|
187
|
-
}, [msg])
|
|
188
|
-
|
|
189
|
-
const run = (method, args, label, onOk) => {
|
|
190
|
-
setMsg(null)
|
|
191
|
-
setBusy(label)
|
|
192
|
-
apiCall(method, args).then((res) => {
|
|
193
|
-
if (res && res.ok) {
|
|
194
|
-
setMsg(res.warning ? { kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning } : { kind: 'ok', text: '操作成功' })
|
|
195
|
-
refresh()
|
|
196
|
-
if (onOk) onOk()
|
|
197
|
-
} else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
198
|
-
}).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => { setBusy(null); setRestartInfo(null) })
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const openAdd = () => setFormModal(Object.assign({ mode: 'add', id: null }, emptyForm()))
|
|
202
|
-
const openEdit = (row) => setFormModal({
|
|
203
|
-
mode: 'edit',
|
|
204
|
-
id: row.id,
|
|
205
|
-
serverName: row.serverName || '',
|
|
206
|
-
transport: row.transport === 'stdio' ? 'stdio' : 'streamable-http',
|
|
207
|
-
url: row.url || '',
|
|
208
|
-
command: row.command || '',
|
|
209
|
-
args: (row.args || []).join('\n'),
|
|
210
|
-
headers: kvToLines(row.headers),
|
|
211
|
-
env: kvToLines(row.env),
|
|
212
|
-
level: row.level === 'global' ? 'global' : 'project',
|
|
213
|
-
})
|
|
214
|
-
const closeForm = () => setFormModal(null)
|
|
215
|
-
const setFormField = (key) => (ev) => setFormModal(Object.assign({}, formModal, { [key]: ev.target.value }))
|
|
216
|
-
const setFormValue = (key, value) => setFormModal(Object.assign({}, formModal, { [key]: value }))
|
|
217
|
-
const submitForm = () => {
|
|
218
|
-
const payload = Object.assign({}, formModal)
|
|
219
|
-
if (payload.transport === 'stdio') { payload.url = ''; payload.headers = '' }
|
|
220
|
-
else { payload.command = ''; payload.args = ''; payload.env = '' }
|
|
221
|
-
const args = {
|
|
222
|
-
serverName: payload.serverName,
|
|
223
|
-
transport: payload.transport,
|
|
224
|
-
level: payload.level,
|
|
225
|
-
url: payload.url,
|
|
226
|
-
command: payload.command,
|
|
227
|
-
args: payload.args,
|
|
228
|
-
headers: payload.headers,
|
|
229
|
-
env: payload.env,
|
|
230
|
-
}
|
|
231
|
-
if (payload.mode === 'edit') args.id = payload.id
|
|
232
|
-
run(payload.mode === 'edit' ? 'mcpm-edit' : 'mcpm-add', args, 'form', closeForm)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Toggling updates only the clicked row (scroll position and expanded
|
|
236
|
-
// groups survive); a failed write re-reads the server state instead.
|
|
237
|
-
const toggleRow = (row) => {
|
|
238
|
-
const nextDisabled = !row.disabled
|
|
239
|
-
setMsg(null)
|
|
240
|
-
setState((prev) => Object.assign({}, prev, {
|
|
241
|
-
rows: (prev.rows || []).map((item) => (
|
|
242
|
-
item.id === row.id && item.level === row.level
|
|
243
|
-
? Object.assign({}, item, { disabled: nextDisabled })
|
|
244
|
-
: item
|
|
245
|
-
)),
|
|
246
|
-
}))
|
|
247
|
-
setBusy(row.id + ':toggle')
|
|
248
|
-
apiCall('mcpm-set-enabled', { id: row.id, level: row.level, enabled: row.disabled }).then((res) => {
|
|
249
|
-
if (res && !res.ok) {
|
|
250
|
-
setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
251
|
-
refresh()
|
|
252
|
-
} else if (res && res.warning) {
|
|
253
|
-
setMsg({ kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning })
|
|
254
|
-
}
|
|
255
|
-
}).catch((e) => {
|
|
256
|
-
setMsg({ kind: 'err', text: String((e && e.message) || e) })
|
|
257
|
-
refresh()
|
|
258
|
-
}).then(() => setBusy(null))
|
|
259
|
-
}
|
|
260
|
-
const toggleReveal = () => {
|
|
261
|
-
const next = !reveal
|
|
262
|
-
setReveal(next)
|
|
263
|
-
// Re-read the list and feed the fresh row back into the open dialog,
|
|
264
|
-
// so the masking actually flips where the user is looking.
|
|
265
|
-
refresh(next, (rows) => {
|
|
266
|
-
if (!detail) return
|
|
267
|
-
const fresh = rows.find((item) => item.id === detail.row.id && item.level === detail.row.level)
|
|
268
|
-
if (fresh) setDetail(Object.assign({}, detail, { row: fresh }))
|
|
269
|
-
})
|
|
270
|
-
}
|
|
271
|
-
const saveNote = () => {
|
|
272
|
-
const row = detail && detail.row
|
|
273
|
-
if (!row) return
|
|
274
|
-
run('mcpm-note', { id: row.id, note: noteDraft }, 'note', () => {
|
|
275
|
-
setDetail(Object.assign({}, detail, { row: Object.assign({}, row, { notes: noteDraft.trim() }) }))
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
const restartRow = (row) => {
|
|
279
|
-
setTick(0)
|
|
280
|
-
setRestartInfo({ id: row.id, name: row.serverName, startedAt: Date.now() })
|
|
281
|
-
run('mcpm-restart', { id: row.id, level: row.level }, row.id + ':restart')
|
|
282
|
-
}
|
|
283
|
-
const confirmRemove = () => {
|
|
284
|
-
const row = confirmRow
|
|
285
|
-
setConfirmRow(null)
|
|
286
|
-
if (row) run('mcpm-remove', { id: row.id, level: row.level }, row.id + ':remove')
|
|
287
|
-
}
|
|
288
|
-
const openDetail = (row) => {
|
|
289
|
-
setNoteDraft(row.notes || '')
|
|
290
|
-
setDetail({ row, loading: true, error: null, tools: [] })
|
|
291
|
-
apiCall('mcpm-tools', { serverName: row.serverName }).then((res) => {
|
|
292
|
-
if (res && res.ok) setDetail({ row, loading: false, error: null, tools: res.tools || [] })
|
|
293
|
-
else setDetail({ row, loading: false, error: (res && res.error) || '加载工具失败', tools: [] })
|
|
294
|
-
}).catch((e) => setDetail({ row, loading: false, error: String((e && e.message) || e), tools: [] }))
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const toggleTool = (row, tool) => {
|
|
298
|
-
const nextEnabled = tool.enabled === false
|
|
299
|
-
setMsg(null)
|
|
300
|
-
setBusy('tool:' + tool.name)
|
|
301
|
-
apiCall('mcpm-tool-enabled', { serverName: row.serverName, tool: tool.name, enabled: nextEnabled }).then((res) => {
|
|
302
|
-
if (res && res.ok) {
|
|
303
|
-
setDetail(Object.assign({}, detail, { tools: (detail.tools || []).map((item) => (item.name === tool.name ? Object.assign({}, item, { enabled: nextEnabled }) : item)) }))
|
|
304
|
-
setMsg({ kind: 'ok', text: '已' + (nextEnabled ? '启用' : '停用') + '工具:' + tool.name })
|
|
305
|
-
} else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
306
|
-
}).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => setBusy(null))
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const rows = state.rows || []
|
|
310
|
-
const normalizedQuery = query.trim().toLowerCase()
|
|
311
|
-
// 「已加载」= 当前真正在运行的条目(loader 已启用且 active)。
|
|
312
|
-
const isRunning = (row) => !!(row.live && row.live.enabled && row.live.phase === 'active')
|
|
313
|
-
const matchesRow = (row) => {
|
|
314
|
-
if (levelFilter === 'loader') { if (!isRunning(row)) return false }
|
|
315
|
-
else if (levelFilter && row.level !== levelFilter) return false
|
|
316
|
-
if (!normalizedQuery) return true
|
|
317
|
-
return [row.serverName, row.id, row.url, row.command, row.transport].some((value) => String(value == null ? '' : value).toLowerCase().indexOf(normalizedQuery) >= 0)
|
|
318
|
-
}
|
|
319
|
-
const visibleRows = rows.filter(matchesRow)
|
|
320
|
-
const summary = {
|
|
321
|
-
total: rows.length,
|
|
322
|
-
enabled: rows.filter((row) => !row.disabled).length,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{ key: '
|
|
331
|
-
{ key: '
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
if (row.live.
|
|
338
|
-
if (
|
|
339
|
-
if (row.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
if (row.live.phase === '
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
React.createElement('div', { className: 'dsm-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
React.createElement('div', { className: 'dsm-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () =>
|
|
366
|
-
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () =>
|
|
367
|
-
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
React.createElement('span', { className: 'dsm-
|
|
380
|
-
React.createElement('span', { className: 'dsm-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
React.createElement('span', null, '
|
|
386
|
-
React.createElement('span', null, '
|
|
387
|
-
React.createElement('span', null, '
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
React.createElement('div', { className: 'dsm-fm-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
React.createElement('div', { className: 'dsm-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
React.createElement(
|
|
407
|
-
|
|
408
|
-
tool.
|
|
409
|
-
|
|
410
|
-
React.createElement('span', { className: 'dsm-tool-param-
|
|
411
|
-
React.createElement('span', { className: 'dsm-tool-param-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
React.createElement('
|
|
430
|
-
React.createElement('
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
React.createElement(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
React.createElement(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
{ value: '
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
React.createElement('
|
|
448
|
-
React.createElement('
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
React.createElement('
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
React.createElement('
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
React.createElement('span', { className: 'dsm-
|
|
459
|
-
React.createElement('
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
React.createElement('
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
'
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
configRow('
|
|
485
|
-
configRow('
|
|
486
|
-
configRow('
|
|
487
|
-
detail.row.
|
|
488
|
-
detail.row.
|
|
489
|
-
(detail.row.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
React.createElement('div', { className: 'dsm-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
React.createElement('
|
|
497
|
-
React.createElement('
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
React.createElement('
|
|
511
|
-
|
|
512
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
React.createElement(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
React.createElement('
|
|
523
|
-
|
|
524
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-
|
|
525
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn', onClick:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
state.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
()
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1
|
+
// dsh-plugin-tool-management — durable client half (web bundle, ModuleLoader CJS format).
|
|
2
|
+
// Registers the "MCP 管理" settings page. Talks to the host half through the
|
|
3
|
+
// exact-path HTTP route /dsh-plugin-tool-management/api (same origin) instead of the
|
|
4
|
+
// dynamic-only host.call channel.
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-plugin-tool-management',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
+
const React = require('react')
|
|
12
|
+
|
|
13
|
+
const CSS =
|
|
14
|
+
'.dsm-summary.dsm-summary-3{grid-template-columns:repeat(3,minmax(0,1fr))}' +
|
|
15
|
+
'.dsm-version{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;letter-spacing:.3px}' +
|
|
16
|
+
'.dsm-failed{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:nowrap}' +
|
|
17
|
+
'.dsm-row-hint{grid-column:1/-1;margin:-4px 0 9px;color:#d49245;font-size:11px;line-height:16px}' +
|
|
18
|
+
'.dsm-tools{display:flex;flex-direction:column;gap:8px}' +
|
|
19
|
+
'.dsm-tool{padding:9px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
|
|
20
|
+
'.dsm-tool-name{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;font-weight:600;word-break:break-all}' +
|
|
21
|
+
'.dsm-tool-name-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
22
|
+
'.dsm-tool-off .dsm-tool-name,.dsm-tool-off .dsm-tool-desc{opacity:.45}' +
|
|
23
|
+
'.dsm-tool-desc{margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}' +
|
|
24
|
+
'.dsm-tool-params{display:flex;flex-direction:column;gap:3px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l2)}' +
|
|
25
|
+
'.dsm-tool-param{display:flex;gap:8px;font-size:11px;align-items:baseline}' +
|
|
26
|
+
'.dsm-tool-param-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-weight:600;min-width:80px;word-break:break-all}' +
|
|
27
|
+
'.dsm-tool-param-type{color:var(--dsw-alias-label-tertiary);min-width:56px}' +
|
|
28
|
+
'.dsm-tool-param-desc{flex:1;color:var(--dsw-alias-label-secondary);word-break:break-all}' +
|
|
29
|
+
'.dsm-modal textarea.dsm-control.dsm-textarea-sm{min-height:88px}' +
|
|
30
|
+
'.dsm-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
31
|
+
'.dsm-note-user{color:var(--dsw-alias-label-tertiary)}' +
|
|
32
|
+
`
|
|
33
|
+
.dsm-tabs{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:14px}.dsm-tab{padding:8px 14px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.dsm-tab-active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-success-primary)}.dsm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dsm-head{display:flex;flex-direction:column;align-items:stretch;gap:16px}.dsm-title-block{min-width:0}.dsm-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}.dsm-feedback-links{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.dsm-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap}.dsm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-feedback-link svg{flex:none}.dsm-desc{margin:12px 0 0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}.dsm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-left:0;flex:none}
|
|
34
|
+
.dsm-btn{box-sizing:border-box;display:inline-flex;min-height:34px;align-items:center;justify-content:center;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:580;white-space:nowrap;cursor:pointer}.dsm-btn:hover:not(:disabled){filter:brightness(1.08)}.dsm-btn:disabled{opacity:.48;cursor:default}.dsm-btn-secondary,.dsm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dsm-btn-quiet{min-height:28px;padding:0 9px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-btn-danger{border-color:var(--dsw-alias-state-error-primary);background:transparent;color:var(--dsw-alias-state-error-primary)}.dsm-btn:focus-visible,.dsm-control:focus-visible,.dsm-select-trigger:focus-visible,.dsm-source-head:focus-visible,.dsm-switch:focus-visible,.dsm-upload-link:focus-visible,.dsm-file-remove:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
|
|
35
|
+
.dsm-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dsm-stat:last-child{border-right:0}.dsm-stat strong{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dsm-filters{display:flex;gap:9px}.dsm-search{flex:1}.dsm-source-filter{width:210px;flex:none}
|
|
36
|
+
.dsm-control,.dsm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dsm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dsm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dsm-select{position:relative}.dsm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dsm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dsm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dsm-option:hover,.dsm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
37
|
+
.dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:9px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dsm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
|
|
38
|
+
.dsm-table-head,.dsm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dsm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-row:last-child{border-bottom:0}.dsm-main{min-width:0}.dsm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dsm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dsm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dsm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dsm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dsm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dsm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dsm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dsm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
|
|
39
|
+
.dsm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dsm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dsm-switch-on{background:var(--dsw-alias-state-success-primary)}.dsm-switch-on:after{transform:translateX(14px)}.dsm-switch:disabled{opacity:.45;cursor:default}.dsm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dsm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dsm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-warning{border-color:#d49245;color:#d49245}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
40
|
+
.dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-import{width:min(480px,100%)!important;padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-item{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-trash-item:last-child{border-bottom:0}.dsm-trash-main{min-width:0;flex:1}
|
|
41
|
+
.dsm-fm{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-fm-row{display:grid;grid-template-columns:minmax(96px,150px) minmax(0,1fr);gap:10px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:18px}.dsm-fm-row:last-child{border-bottom:0}.dsm-fm-key{color:var(--dsw-alias-label-secondary);word-break:break-word}.dsm-fm-raw{margin-left:5px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.dsm-fm-val{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap}
|
|
42
|
+
@container(max-width:780px){.dsm-table-head{display:none}.dsm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dsm-row>.dsm-tags,.dsm-row>.dsm-status{grid-column:1}.dsm-row-actions{grid-column:2;grid-row:1 / span 3}.dsm-path{display:none}}@media(max-width:760px){.dsm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.dsm-title-row{flex-wrap:wrap}}@container(max-width:520px){.dsm-head{flex-direction:column}.dsm-actions{width:100%;margin-left:0}.dsm-actions .dsm-btn{flex:1}.dsm-filters{flex-direction:column}.dsm-source-filter{width:100%}.dsm-summary{grid-template-columns:1fr}}
|
|
43
|
+
.dsm-hist-row{display:flex;align-items:center;gap:11px;padding:10px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-hist-row:last-child{border-bottom:0}.dsm-hist-main{min-width:0;flex:1}.dsm-hist-title{overflow:hidden;font-size:13px;font-weight:580;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd-missing{color:var(--dsw-alias-state-error-primary)}.dsm-hist-actions{display:flex;gap:6px;flex:none}.dsm-hist-check,.dsm-hist-group-check{flex:none;width:15px;height:15px;margin:0 4px 0 0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}.dsm-hist-group-head{gap:10px}.dsm-hist-batch{display:flex;align-items:center;gap:8px;margin-left:2px;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l2)}.dsm-hist-batch-count{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dsm-hist-batch-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;flex:none}.dsm-dir-row{display:flex;gap:8px}.dsm-dir-row .dsm-control{flex:1}.dsm-dir-list{display:flex;max-height:220px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-dir-item{display:flex;width:100%;align-items:center;padding:8px 12px;border:0;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}.dsm-dir-item:last-child{border-bottom:0}.dsm-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-dir-item::before{content:"📁";margin-right:8px;font-size:12px}
|
|
44
|
+
.dsm-rule-shadowed .dsm-name,.dsm-rule-shadowed .dsm-note{color:var(--dsw-alias-state-error-primary)}.dsm-rule-shadow-hint{margin-top:3px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-invalid{border-color:var(--dsw-alias-state-error-primary)!important}.dsm-rule-hint{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-budget{display:flex;flex-direction:column;gap:6px}.dsm-budget-meta{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsm-budget-meta strong{margin-right:0;font-size:13px;font-weight:680}.dsm-budget-over-text{color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-budget-bar{height:6px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover)}.dsm-budget-fill{height:100%;border-radius:99px;background:var(--dsw-alias-state-success-primary);transition:width 160ms ease}.dsm-budget-fill.dsm-budget-over{background:var(--dsw-alias-state-error-primary)}.dsm-char-count{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-char-over{color:var(--dsw-alias-state-error-primary)}
|
|
45
|
+
.dsm-combo-row{display:flex;gap:8px}.dsm-combo-row .dsm-control{flex:1}
|
|
46
|
+
.dsm-attach-list{display:flex;flex-direction:column;gap:6px}.dsm-attach-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
47
|
+
.dsm-toast{position:fixed;right:22px;bottom:22px;z-index:1250;box-sizing:border-box;max-width:min(420px,calc(100vw - 44px));padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;animation:dsm-toast-in 160ms ease-out}@keyframes dsm-toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.dsm-toast{animation:none}}
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
function ensureCss() {
|
|
51
|
+
if (typeof document === 'undefined') return
|
|
52
|
+
const id = 'dsh-plugin-tool-management'
|
|
53
|
+
if (document.querySelector('style[data-plugin-css="' + id + '"]')) return
|
|
54
|
+
const tag = document.createElement('style')
|
|
55
|
+
tag.dataset.plugin = id
|
|
56
|
+
tag.dataset.pluginCss = id
|
|
57
|
+
tag.textContent = CSS
|
|
58
|
+
document.head.appendChild(tag)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Optional access token for write ops (host config.token or
|
|
62
|
+
// DSH_SKILL_MCP_MANAGER_TOKEN). Read once from localStorage — there is no
|
|
63
|
+
// UI row for it — and sent as `x-dsh-token` on every request (the host
|
|
64
|
+
// ignores it when unset).
|
|
65
|
+
let TOKEN = ''
|
|
66
|
+
try {
|
|
67
|
+
// 读取新键;旧版(dsh-skill-mcp-manager-token)数据一次性迁移到新键。
|
|
68
|
+
TOKEN = window.localStorage.getItem('dsh-plugin-tool-management-token')
|
|
69
|
+
|| window.localStorage.getItem('dsh-skill-mcp-manager-token') || ''
|
|
70
|
+
if (TOKEN && !window.localStorage.getItem('dsh-plugin-tool-management-token')) {
|
|
71
|
+
window.localStorage.setItem('dsh-plugin-tool-management-token', TOKEN)
|
|
72
|
+
}
|
|
73
|
+
} catch (e) { /* storage unavailable */ }
|
|
74
|
+
|
|
75
|
+
function apiCall(op, args) {
|
|
76
|
+
// `x-dsh-plugin` is the cross-site (CSRF) gate header the host half
|
|
77
|
+
// requires on every request; a cross-origin page cannot attach it
|
|
78
|
+
// without a CORS preflight that this route never answers.
|
|
79
|
+
const headers = { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-plugin-tool-management' }
|
|
80
|
+
if (TOKEN) headers['x-dsh-token'] = TOKEN
|
|
81
|
+
return fetch('/dsh-plugin-tool-management/api', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers,
|
|
84
|
+
body: JSON.stringify({ op, args: args || {} }),
|
|
85
|
+
}).then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Small version badge shown next to each settings-page title; reads the
|
|
89
|
+
// package version from the host (plugin-version op) so it always matches
|
|
90
|
+
// the installed release.
|
|
91
|
+
function VersionBadge() {
|
|
92
|
+
const [v, setV] = React.useState(null)
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
let alive = true
|
|
95
|
+
apiCall('plugin-version', {}).then((r) => { if (alive && r && r.ok) setV(r.version) }).catch(() => {})
|
|
96
|
+
return () => { alive = false }
|
|
97
|
+
}, [])
|
|
98
|
+
return v ? React.createElement('span', { className: 'dsm-version' }, 'v' + v) : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
name: 'dsh-plugin-tool-management-client',
|
|
103
|
+
inject: ['timer'],
|
|
104
|
+
apply(ctx) {
|
|
105
|
+
ensureCss()
|
|
106
|
+
const slots = ctx.get('slots')
|
|
107
|
+
if (slots === undefined) return
|
|
108
|
+
|
|
109
|
+
const LEVEL_LABEL = { project: 'Profile 级', global: '全局', loader: '已加载' }
|
|
110
|
+
const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'project' })
|
|
111
|
+
const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
|
|
112
|
+
const MCP_LEVEL_OPTIONS = [
|
|
113
|
+
{ value: '', label: '全部级别' },
|
|
114
|
+
{ value: 'project', label: 'Profile 级' },
|
|
115
|
+
{ value: 'global', label: '全局' },
|
|
116
|
+
{ value: 'loader', label: '已加载' },
|
|
117
|
+
]
|
|
118
|
+
const MCP_TRANSPORT_OPTIONS = [
|
|
119
|
+
{ value: 'streamable-http', label: 'streamable-http' },
|
|
120
|
+
{ value: 'stdio', label: 'stdio' },
|
|
121
|
+
]
|
|
122
|
+
const maskSecret = (value) => {
|
|
123
|
+
const s = String(value == null ? '' : value)
|
|
124
|
+
return s.length <= 4 ? '****' : s.slice(0, 4) + '****'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 顶部栏反馈入口:合并成一个「GitHub 和反馈」链接,指向仓库(issues 在那)。
|
|
128
|
+
function FeedbackLinks() {
|
|
129
|
+
return React.createElement('div', { className: 'dsm-feedback-links' },
|
|
130
|
+
React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management', target: '_blank', rel: 'noreferrer', 'aria-label': 'GitHub' },
|
|
131
|
+
React.createElement(GithubMark16, null), 'GitHub'))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MCP 服务页:与 Skills 页共用 dsm-* 设计语言(统计卡 / 筛选 / 分组卡片 / 模态框)。
|
|
135
|
+
function MCPPage() {
|
|
136
|
+
const [state, setState] = React.useState({ loading: true, error: null, rows: [], paths: null, errors: [], warnings: [] })
|
|
137
|
+
const [msg, setMsg] = React.useState(null)
|
|
138
|
+
const [busy, setBusy] = React.useState(null)
|
|
139
|
+
const [query, setQuery] = React.useState('')
|
|
140
|
+
const [levelFilter, setLevelFilter] = React.useState('')
|
|
141
|
+
const [collapsed, setCollapsed] = React.useState({})
|
|
142
|
+
const [formModal, setFormModal] = React.useState(null)
|
|
143
|
+
const [detail, setDetail] = React.useState(null)
|
|
144
|
+
const [confirmRow, setConfirmRow] = React.useState(null)
|
|
145
|
+
const [restartInfo, setRestartInfo] = React.useState(null)
|
|
146
|
+
const [reveal, setReveal] = React.useState(false)
|
|
147
|
+
const [settings, setSettings] = React.useState(null)
|
|
148
|
+
const [noteDraft, setNoteDraft] = React.useState('')
|
|
149
|
+
const [, setTick] = React.useState(0)
|
|
150
|
+
|
|
151
|
+
const refresh = (withReveal, onRows) => {
|
|
152
|
+
const useReveal = withReveal === undefined ? reveal : withReveal === true
|
|
153
|
+
// 明文视图走独立的 mcpm-reveal op(host 端受 token 保护),
|
|
154
|
+
// mcpm-list 始终脱敏,无 token 也能渲染页面。
|
|
155
|
+
apiCall(useReveal ? 'mcpm-reveal' : 'mcpm-list', {}).then((res) => {
|
|
156
|
+
const rows = (res && res.rows) || []
|
|
157
|
+
setState({
|
|
158
|
+
loading: false,
|
|
159
|
+
error: res && res.ok ? null : ((res && res.error) || '加载失败'),
|
|
160
|
+
rows,
|
|
161
|
+
paths: (res && res.paths) || null,
|
|
162
|
+
errors: (res && res.errors) || [],
|
|
163
|
+
warnings: (res && res.warnings) || [],
|
|
164
|
+
})
|
|
165
|
+
if (onRows) onRows(rows)
|
|
166
|
+
}).catch((e) => setState({ loading: false, error: String((e && e.message) || e), rows: [], paths: null, errors: [], warnings: [] }))
|
|
167
|
+
}
|
|
168
|
+
// The polling tick must always call the newest refresh closure, while
|
|
169
|
+
// the interval itself only changes when the saved settings change.
|
|
170
|
+
const refreshRef = React.useRef(refresh)
|
|
171
|
+
refreshRef.current = refresh
|
|
172
|
+
|
|
173
|
+
React.useEffect(() => { refresh(false) }, [])
|
|
174
|
+
React.useEffect(() => {
|
|
175
|
+
apiCall('mcpm-settings', {}).then((res) => {
|
|
176
|
+
if (res && res.ok && res.settings) setSettings(res.settings)
|
|
177
|
+
}).catch(() => {})
|
|
178
|
+
}, [])
|
|
179
|
+
React.useEffect(() => ctx.interval(() => setTick((t) => t + 1), 1000), [])
|
|
180
|
+
React.useEffect(() => {
|
|
181
|
+
const ms = (settings && settings.pollIntervalMs) || 5000
|
|
182
|
+
return ctx.interval(() => refreshRef.current(), ms)
|
|
183
|
+
}, [settings])
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
if (!msg) return undefined
|
|
186
|
+
return ctx.timeout(() => setMsg(null), 4000)
|
|
187
|
+
}, [msg])
|
|
188
|
+
|
|
189
|
+
const run = (method, args, label, onOk) => {
|
|
190
|
+
setMsg(null)
|
|
191
|
+
setBusy(label)
|
|
192
|
+
apiCall(method, args).then((res) => {
|
|
193
|
+
if (res && res.ok) {
|
|
194
|
+
setMsg(res.warning ? { kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning } : { kind: 'ok', text: '操作成功' })
|
|
195
|
+
refresh()
|
|
196
|
+
if (onOk) onOk()
|
|
197
|
+
} else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
198
|
+
}).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => { setBusy(null); setRestartInfo(null) })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const openAdd = () => setFormModal(Object.assign({ mode: 'add', id: null }, emptyForm()))
|
|
202
|
+
const openEdit = (row) => setFormModal({
|
|
203
|
+
mode: 'edit',
|
|
204
|
+
id: row.id,
|
|
205
|
+
serverName: row.serverName || '',
|
|
206
|
+
transport: row.transport === 'stdio' ? 'stdio' : 'streamable-http',
|
|
207
|
+
url: row.url || '',
|
|
208
|
+
command: row.command || '',
|
|
209
|
+
args: (row.args || []).join('\n'),
|
|
210
|
+
headers: kvToLines(row.headers),
|
|
211
|
+
env: kvToLines(row.env),
|
|
212
|
+
level: row.level === 'global' ? 'global' : 'project',
|
|
213
|
+
})
|
|
214
|
+
const closeForm = () => setFormModal(null)
|
|
215
|
+
const setFormField = (key) => (ev) => setFormModal(Object.assign({}, formModal, { [key]: ev.target.value }))
|
|
216
|
+
const setFormValue = (key, value) => setFormModal(Object.assign({}, formModal, { [key]: value }))
|
|
217
|
+
const submitForm = () => {
|
|
218
|
+
const payload = Object.assign({}, formModal)
|
|
219
|
+
if (payload.transport === 'stdio') { payload.url = ''; payload.headers = '' }
|
|
220
|
+
else { payload.command = ''; payload.args = ''; payload.env = '' }
|
|
221
|
+
const args = {
|
|
222
|
+
serverName: payload.serverName,
|
|
223
|
+
transport: payload.transport,
|
|
224
|
+
level: payload.level,
|
|
225
|
+
url: payload.url,
|
|
226
|
+
command: payload.command,
|
|
227
|
+
args: payload.args,
|
|
228
|
+
headers: payload.headers,
|
|
229
|
+
env: payload.env,
|
|
230
|
+
}
|
|
231
|
+
if (payload.mode === 'edit') args.id = payload.id
|
|
232
|
+
run(payload.mode === 'edit' ? 'mcpm-edit' : 'mcpm-add', args, 'form', closeForm)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Toggling updates only the clicked row (scroll position and expanded
|
|
236
|
+
// groups survive); a failed write re-reads the server state instead.
|
|
237
|
+
const toggleRow = (row) => {
|
|
238
|
+
const nextDisabled = !row.disabled
|
|
239
|
+
setMsg(null)
|
|
240
|
+
setState((prev) => Object.assign({}, prev, {
|
|
241
|
+
rows: (prev.rows || []).map((item) => (
|
|
242
|
+
item.id === row.id && item.level === row.level
|
|
243
|
+
? Object.assign({}, item, { disabled: nextDisabled })
|
|
244
|
+
: item
|
|
245
|
+
)),
|
|
246
|
+
}))
|
|
247
|
+
setBusy(row.id + ':toggle')
|
|
248
|
+
apiCall('mcpm-set-enabled', { id: row.id, level: row.level, enabled: row.disabled }).then((res) => {
|
|
249
|
+
if (res && !res.ok) {
|
|
250
|
+
setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
251
|
+
refresh()
|
|
252
|
+
} else if (res && res.warning) {
|
|
253
|
+
setMsg({ kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning })
|
|
254
|
+
}
|
|
255
|
+
}).catch((e) => {
|
|
256
|
+
setMsg({ kind: 'err', text: String((e && e.message) || e) })
|
|
257
|
+
refresh()
|
|
258
|
+
}).then(() => setBusy(null))
|
|
259
|
+
}
|
|
260
|
+
const toggleReveal = () => {
|
|
261
|
+
const next = !reveal
|
|
262
|
+
setReveal(next)
|
|
263
|
+
// Re-read the list and feed the fresh row back into the open dialog,
|
|
264
|
+
// so the masking actually flips where the user is looking.
|
|
265
|
+
refresh(next, (rows) => {
|
|
266
|
+
if (!detail) return
|
|
267
|
+
const fresh = rows.find((item) => item.id === detail.row.id && item.level === detail.row.level)
|
|
268
|
+
if (fresh) setDetail(Object.assign({}, detail, { row: fresh }))
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
const saveNote = () => {
|
|
272
|
+
const row = detail && detail.row
|
|
273
|
+
if (!row) return
|
|
274
|
+
run('mcpm-note', { id: row.id, note: noteDraft }, 'note', () => {
|
|
275
|
+
setDetail(Object.assign({}, detail, { row: Object.assign({}, row, { notes: noteDraft.trim() }) }))
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
const restartRow = (row) => {
|
|
279
|
+
setTick(0)
|
|
280
|
+
setRestartInfo({ id: row.id, name: row.serverName, startedAt: Date.now() })
|
|
281
|
+
run('mcpm-restart', { id: row.id, level: row.level }, row.id + ':restart')
|
|
282
|
+
}
|
|
283
|
+
const confirmRemove = () => {
|
|
284
|
+
const row = confirmRow
|
|
285
|
+
setConfirmRow(null)
|
|
286
|
+
if (row) run('mcpm-remove', { id: row.id, level: row.level }, row.id + ':remove')
|
|
287
|
+
}
|
|
288
|
+
const openDetail = (row) => {
|
|
289
|
+
setNoteDraft(row.notes || '')
|
|
290
|
+
setDetail({ row, loading: true, error: null, tools: [] })
|
|
291
|
+
apiCall('mcpm-tools', { serverName: row.serverName }).then((res) => {
|
|
292
|
+
if (res && res.ok) setDetail({ row, loading: false, error: null, tools: res.tools || [] })
|
|
293
|
+
else setDetail({ row, loading: false, error: (res && res.error) || '加载工具失败', tools: [] })
|
|
294
|
+
}).catch((e) => setDetail({ row, loading: false, error: String((e && e.message) || e), tools: [] }))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const toggleTool = (row, tool) => {
|
|
298
|
+
const nextEnabled = tool.enabled === false
|
|
299
|
+
setMsg(null)
|
|
300
|
+
setBusy('tool:' + tool.name)
|
|
301
|
+
apiCall('mcpm-tool-enabled', { serverName: row.serverName, tool: tool.name, enabled: nextEnabled }).then((res) => {
|
|
302
|
+
if (res && res.ok) {
|
|
303
|
+
setDetail(Object.assign({}, detail, { tools: (detail.tools || []).map((item) => (item.name === tool.name ? Object.assign({}, item, { enabled: nextEnabled }) : item)) }))
|
|
304
|
+
setMsg({ kind: 'ok', text: '已' + (nextEnabled ? '启用' : '停用') + '工具:' + tool.name })
|
|
305
|
+
} else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
|
|
306
|
+
}).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => setBusy(null))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const rows = state.rows || []
|
|
310
|
+
const normalizedQuery = query.trim().toLowerCase()
|
|
311
|
+
// 「已加载」= 当前真正在运行的条目(loader 已启用且 active)。
|
|
312
|
+
const isRunning = (row) => !!(row.live && row.live.enabled && row.live.phase === 'active')
|
|
313
|
+
const matchesRow = (row) => {
|
|
314
|
+
if (levelFilter === 'loader') { if (!isRunning(row)) return false }
|
|
315
|
+
else if (levelFilter && row.level !== levelFilter) return false
|
|
316
|
+
if (!normalizedQuery) return true
|
|
317
|
+
return [row.serverName, row.id, row.url, row.command, row.transport].some((value) => String(value == null ? '' : value).toLowerCase().indexOf(normalizedQuery) >= 0)
|
|
318
|
+
}
|
|
319
|
+
const visibleRows = rows.filter(matchesRow)
|
|
320
|
+
const summary = {
|
|
321
|
+
total: rows.length,
|
|
322
|
+
enabled: rows.filter((row) => !row.disabled).length,
|
|
323
|
+
tools: rows.reduce((sum, row) => sum + (typeof row.toolCount === 'number' ? row.toolCount : 0), 0),
|
|
324
|
+
}
|
|
325
|
+
const profilePath = state.paths && state.paths.profile ? 'profile: ' + state.paths.profile : null
|
|
326
|
+
const groups = levelFilter === 'loader'
|
|
327
|
+
? [{ key: 'live', title: '已加载', path: profilePath, match: isRunning }]
|
|
328
|
+
: [
|
|
329
|
+
{ key: 'project', title: 'Profile 级', path: state.paths ? state.paths.project : null, match: (row) => row.level === 'project' },
|
|
330
|
+
{ key: 'global', title: '全局', path: state.paths ? state.paths.global : null, match: (row) => row.level === 'global' },
|
|
331
|
+
{ key: 'loader', title: '已加载', path: profilePath, match: (row) => row.level === 'loader' && isRunning(row) },
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
const liveStatus = (row) => {
|
|
335
|
+
if (!row.live) return { text: '未加载', cls: 'dsm-shadowed' }
|
|
336
|
+
if (row.live.phase === 'failed') return { text: '启动失败', cls: 'dsm-failed' }
|
|
337
|
+
if (!row.live.enabled) return { text: '未运行', cls: 'dsm-shadowed' }
|
|
338
|
+
if (row.live.phase && row.live.phase !== 'active') return { text: '加载中', cls: 'dsm-shadowed' }
|
|
339
|
+
if (typeof row.toolCount === 'number' && row.toolCount === 0) return { text: '无工具', cls: 'dsm-disabled' }
|
|
340
|
+
return { text: '运行中', cls: 'dsm-enabled' }
|
|
341
|
+
}
|
|
342
|
+
const liveHint = (row) => {
|
|
343
|
+
if (!row.live) return null
|
|
344
|
+
if (row.live.phase === 'failed') return '启动失败,检查配置后点「重启」重试'
|
|
345
|
+
if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) return '已连接但没有工具:服务端可能未就绪'
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const renderRow = (row) => {
|
|
350
|
+
const status = liveStatus(row)
|
|
351
|
+
const hint = liveHint(row)
|
|
352
|
+
const editable = row.level !== 'loader'
|
|
353
|
+
return React.createElement('div', { key: row.id, className: 'dsm-row' },
|
|
354
|
+
React.createElement('div', { className: 'dsm-main' },
|
|
355
|
+
React.createElement('div', { className: 'dsm-name' }, row.serverName),
|
|
356
|
+
row.notes ? React.createElement('div', { className: 'dsm-note dsm-note-user', title: row.notes }, '备注:' + row.notes) : null),
|
|
357
|
+
React.createElement('div', { className: 'dsm-tags' },
|
|
358
|
+
(levelFilter === 'loader' && row.level && row.level !== 'loader') ? React.createElement('span', { className: 'dsm-tag' }, LEVEL_LABEL[row.level] || row.level) : null,
|
|
359
|
+
(typeof row.toolCount === 'number' && row.toolCount > 0) ? React.createElement('span', { className: 'dsm-tag' }, row.toolCount + ' 个工具') : null,
|
|
360
|
+
row.duplicate ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, '重复 id') : null),
|
|
361
|
+
React.createElement('div', { className: 'dsm-status ' + status.cls }, status.text),
|
|
362
|
+
React.createElement('div', { className: 'dsm-row-actions' },
|
|
363
|
+
editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null, label: '启停服务 ' + row.serverName, onClick: () => toggleRow(row) }) : null,
|
|
364
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () => openDetail(row) }, '详情'),
|
|
365
|
+
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => openEdit(row) }, '编辑') : null,
|
|
366
|
+
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => restartRow(row) }, '重启') : null,
|
|
367
|
+
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null, onClick: () => setConfirmRow(row) }, '删除') : null),
|
|
368
|
+
hint ? React.createElement('div', { className: 'dsm-row-hint' }, '⚠ ' + hint) : null)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const renderGroup = (group) => {
|
|
372
|
+
const groupRows = visibleRows.filter(group.match)
|
|
373
|
+
if (groupRows.length === 0) return null
|
|
374
|
+
const open = collapsed[group.key] !== true || !!normalizedQuery
|
|
375
|
+
return React.createElement('section', { key: group.key, className: 'dsm-source' },
|
|
376
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
377
|
+
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: () => setCollapsed(Object.assign({}, collapsed, { [group.key]: !open })) },
|
|
378
|
+
React.createElement('span', { className: 'dsm-source-title' }, group.title),
|
|
379
|
+
React.createElement('span', { className: 'dsm-count' }, groupRows.length + ' 个服务'),
|
|
380
|
+
group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
|
|
381
|
+
open ? React.createElement('div', { className: 'dsm-source-body' },
|
|
382
|
+
React.createElement(React.Fragment, null,
|
|
383
|
+
React.createElement('div', { className: 'dsm-table-head' },
|
|
384
|
+
React.createElement('span', null, '服务名称与地址'),
|
|
385
|
+
React.createElement('span', null, '传输与工具'),
|
|
386
|
+
React.createElement('span', null, '运行状态'),
|
|
387
|
+
React.createElement('span', null, '')),
|
|
388
|
+
groupRows.map(renderRow))) : null)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const configRow = (label, value) => React.createElement('div', { className: 'dsm-fm-row', key: label },
|
|
392
|
+
React.createElement('div', { className: 'dsm-fm-key' }, label),
|
|
393
|
+
React.createElement('div', { className: 'dsm-fm-val' }, value))
|
|
394
|
+
|
|
395
|
+
const toolListNode = detail && (detail.loading
|
|
396
|
+
? React.createElement('div', { className: 'dsm-help' }, '正在获取工具列表…')
|
|
397
|
+
: detail.error
|
|
398
|
+
? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, '加载工具失败:' + detail.error)
|
|
399
|
+
: (detail.tools || []).length === 0
|
|
400
|
+
? React.createElement('div', { className: 'dsm-help' }, '该服务暂无已注册工具。')
|
|
401
|
+
: React.createElement(React.Fragment, null,
|
|
402
|
+
React.createElement('div', { className: 'dsm-help' }, '停用的工具对模型不可见,改动即时生效'),
|
|
403
|
+
React.createElement('div', { className: 'dsm-tools' }, (detail.tools || []).map((tool) => React.createElement('div', { className: 'dsm-tool' + (tool.enabled === false ? ' dsm-tool-off' : ''), key: tool.name },
|
|
404
|
+
React.createElement('div', { className: 'dsm-tool-name-row' },
|
|
405
|
+
React.createElement('div', { className: 'dsm-tool-name' }, tool.name),
|
|
406
|
+
React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null, label: '启停工具 ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
|
|
407
|
+
tool.description ? React.createElement('div', { className: 'dsm-tool-desc' }, tool.description) : null,
|
|
408
|
+
(tool.parameters && tool.parameters.length > 0) ? React.createElement('div', { className: 'dsm-tool-params' }, tool.parameters.map((param) => React.createElement('div', { className: 'dsm-tool-param', key: param.key },
|
|
409
|
+
React.createElement('span', { className: 'dsm-tool-param-key' }, param.key + (param.required ? ' *' : '')),
|
|
410
|
+
React.createElement('span', { className: 'dsm-tool-param-type' }, param.type || 'any'),
|
|
411
|
+
React.createElement('span', { className: 'dsm-tool-param-desc' }, param.description || '')))) : null)))))
|
|
412
|
+
|
|
413
|
+
const anyGroupVisible = groups.some((group) => visibleRows.some(group.match))
|
|
414
|
+
const groupsNode = (state.loading && rows.length === 0)
|
|
415
|
+
? React.createElement('div', { className: 'dsm-empty' }, '正在加载 MCP 服务…')
|
|
416
|
+
: anyGroupVisible
|
|
417
|
+
? React.createElement('div', { className: 'dsm-sources' }, groups.map(renderGroup))
|
|
418
|
+
: React.createElement('div', { className: 'dsm-empty' }, (normalizedQuery || levelFilter) ? '没有匹配的服务。' : '暂无 MCP 服务,点「新增服务」添加')
|
|
419
|
+
|
|
420
|
+
const formModalNode = formModal && React.createElement(Modal, {
|
|
421
|
+
key: 'mcp-form',
|
|
422
|
+
title: formModal.mode === 'edit' ? '编辑 MCP 服务:' + formModal.id : '新增 MCP 服务',
|
|
423
|
+
closeLabel: '关闭',
|
|
424
|
+
onClose: closeForm,
|
|
425
|
+
},
|
|
426
|
+
React.createElement('div', { className: 'dsm-form' },
|
|
427
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
428
|
+
React.createElement('span', { className: 'dsm-label' }, '服务名称 serverName'),
|
|
429
|
+
React.createElement('input', { className: 'dsm-control', value: formModal.serverName, placeholder: 'e.g. github', onChange: setFormField('serverName') }),
|
|
430
|
+
React.createElement('span', { className: 'dsm-help' }, '1-32 位 [A-Za-z0-9_-],补丁里按此名注册')),
|
|
431
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
432
|
+
React.createElement('span', { className: 'dsm-label' }, '传输方式'),
|
|
433
|
+
React.createElement(SourceSelect, { value: formModal.transport, options: MCP_TRANSPORT_OPTIONS, onChange: (value) => setFormValue('transport', value) })),
|
|
434
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
435
|
+
React.createElement('span', { className: 'dsm-label' }, '级别'),
|
|
436
|
+
React.createElement(SourceSelect, {
|
|
437
|
+
value: formModal.level,
|
|
438
|
+
options: [
|
|
439
|
+
{ value: 'project', label: 'Profile 级(本应用:' + (state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml') + ')' },
|
|
440
|
+
{ value: 'global', label: '全局(跨 Profile:' + (state.paths ? state.paths.global : '~/.dsh/cordis.patch.yml') + ')' },
|
|
441
|
+
],
|
|
442
|
+
onChange: (value) => setFormValue('level', value),
|
|
443
|
+
})),
|
|
444
|
+
formModal.transport === 'streamable-http'
|
|
445
|
+
? React.createElement('label', { className: 'dsm-field' },
|
|
446
|
+
React.createElement('span', { className: 'dsm-label' }, '服务 URL'),
|
|
447
|
+
React.createElement('input', { className: 'dsm-control', value: formModal.url, placeholder: 'https://host/mcp', onChange: setFormField('url') }),
|
|
448
|
+
React.createElement('span', { className: 'dsm-help' }, '需以 http(s):// 开头'))
|
|
449
|
+
: React.createElement(React.Fragment, null,
|
|
450
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
451
|
+
React.createElement('span', { className: 'dsm-label' }, '启动命令'),
|
|
452
|
+
React.createElement('input', { className: 'dsm-control', value: formModal.command, placeholder: 'npx -y @modelcontextprotocol/server-github', onChange: setFormField('command') })),
|
|
453
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
454
|
+
React.createElement('span', { className: 'dsm-label' }, '参数(空格或换行分隔)'),
|
|
455
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.args, placeholder: '-y\n@modelcontextprotocol/server-github', onChange: setFormField('args') })),
|
|
456
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
457
|
+
React.createElement('span', { className: 'dsm-label' }, '环境变量(每行 key=value)'),
|
|
458
|
+
React.createElement('span', { className: 'dsm-help' }, '路径按系统路径写法填(Windows 用 \\,macOS/Linux 用 /)。'),
|
|
459
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.env, placeholder: 'GITHUB_TOKEN=xxx', onChange: setFormField('env') }))),
|
|
460
|
+
formModal.transport === 'streamable-http'
|
|
461
|
+
? React.createElement('label', { className: 'dsm-field' },
|
|
462
|
+
React.createElement('span', { className: 'dsm-label' }, '请求头(每行 key=value)'),
|
|
463
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.headers, placeholder: 'Authorization=Bearer xxx', onChange: setFormField('headers') }))
|
|
464
|
+
: null),
|
|
465
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
466
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: closeForm }, '取消'),
|
|
467
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy === 'form' || !formModal.serverName.trim(), onClick: submitForm }, formModal.mode === 'edit' ? '保存' : '添加')))
|
|
468
|
+
|
|
469
|
+
const detailStatus = detail ? liveStatus(detail.row) : null
|
|
470
|
+
const detailHint = detail ? liveHint(detail.row) : null
|
|
471
|
+
const detailNode = detail && React.createElement(Modal, {
|
|
472
|
+
key: 'mcp-detail',
|
|
473
|
+
wide: true,
|
|
474
|
+
title: '服务详情:' + detail.row.serverName,
|
|
475
|
+
closeLabel: '关闭',
|
|
476
|
+
onClose: () => setDetail(null),
|
|
477
|
+
},
|
|
478
|
+
React.createElement('div', { className: 'dsm-detail-section' },
|
|
479
|
+
React.createElement('div', { className: 'dsm-detail-title dsm-detail-title-row' },
|
|
480
|
+
'配置',
|
|
481
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: toggleReveal }, reveal ? '隐藏密钥' : '显示密钥')),
|
|
482
|
+
React.createElement('div', { className: 'dsm-fm' },
|
|
483
|
+
configRow('条目 ID', detail.row.id),
|
|
484
|
+
configRow('级别', LEVEL_LABEL[detail.row.level] || detail.row.level),
|
|
485
|
+
configRow('传输方式', detail.row.transport || '—'),
|
|
486
|
+
detail.row.url ? configRow('服务 URL', detail.row.url) : null,
|
|
487
|
+
detail.row.command ? configRow('启动命令', detail.row.command + ((detail.row.args && detail.row.args.length) ? ' ' + detail.row.args.join(' ') : '')) : null,
|
|
488
|
+
(detail.row.headers && Object.keys(detail.row.headers).length > 0) ? configRow('请求头', Object.keys(detail.row.headers).map((k) => k + ': ' + maskSecret(detail.row.headers[k])).join('\n')) : null,
|
|
489
|
+
(detail.row.env && Object.keys(detail.row.env).length > 0) ? configRow('环境变量', Object.keys(detail.row.env).map((k) => k + ' = ' + maskSecret(detail.row.env[k])).join('\n')) : null)),
|
|
490
|
+
React.createElement('div', { className: 'dsm-detail-section' },
|
|
491
|
+
React.createElement('div', { className: 'dsm-detail-title' }, '运行状态'),
|
|
492
|
+
React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' : detailStatus.cls === 'dsm-disabled' ? ' dsm-warning' : '') },
|
|
493
|
+
detailStatus.text + ':' + (detailHint || '该服务已登记在 Loader 中。'))),
|
|
494
|
+
React.createElement('div', { className: 'dsm-detail-section' },
|
|
495
|
+
React.createElement('div', { className: 'dsm-detail-title' }, '备注(仅本机可见)'),
|
|
496
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, placeholder: '例如:A 不可用时改用 B 兜底', onChange: (ev) => setNoteDraft(ev.target.value) }),
|
|
497
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
498
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, '保存备注'))),
|
|
499
|
+
React.createElement('div', { className: 'dsm-detail-section' },
|
|
500
|
+
React.createElement('div', { className: 'dsm-detail-title' }, '工具(' + (detail.loading ? '…' : (detail.tools || []).length) + ')'),
|
|
501
|
+
toolListNode))
|
|
502
|
+
|
|
503
|
+
const confirmNode = confirmRow && React.createElement(Modal, {
|
|
504
|
+
key: 'mcp-remove',
|
|
505
|
+
title: '删除 MCP 服务',
|
|
506
|
+
closeLabel: '关闭',
|
|
507
|
+
onClose: () => setConfirmRow(null),
|
|
508
|
+
},
|
|
509
|
+
React.createElement('p', { className: 'dsm-desc' }, '确定要删除「' + confirmRow.serverName + '」?配置将从补丁文件移除、工具立即下线,不可撤销'),
|
|
510
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
511
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: () => setConfirmRow(null) }, '取消'),
|
|
512
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', onClick: confirmRemove }, '删除')))
|
|
513
|
+
|
|
514
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
515
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
516
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
517
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
518
|
+
React.createElement('h2', { className: 'dsm-title' }, 'MCP'),
|
|
519
|
+
React.createElement(VersionBadge, null)),
|
|
520
|
+
React.createElement('p', { className: 'dsm-desc' }, '统一管理本机的 MCP 服务与运行状态。')),
|
|
521
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
522
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, '刷新'),
|
|
523
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openAdd }, '新增服务'),
|
|
524
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: true }, 'setall') }, '全部启用'),
|
|
525
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: false }, 'setall') }, '全部停用'))),
|
|
526
|
+
React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
|
|
527
|
+
[[summary.total, '个服务'], [summary.enabled, '个已启用'], [summary.tools, '个工具']].map((item) =>
|
|
528
|
+
React.createElement('div', { key: item[1], className: 'dsm-stat' },
|
|
529
|
+
React.createElement('strong', null, item[0]), item[1]))),
|
|
530
|
+
React.createElement('div', { className: 'dsm-filters' },
|
|
531
|
+
React.createElement('input', { className: 'dsm-control dsm-search', value: query, 'aria-label': '搜索服务', placeholder: '搜索服务名称、地址或命令', onChange: (ev) => setQuery(ev.target.value) }),
|
|
532
|
+
React.createElement('div', { className: 'dsm-source-filter' },
|
|
533
|
+
React.createElement(SourceSelect, { value: levelFilter, options: MCP_LEVEL_OPTIONS, onChange: setLevelFilter }))),
|
|
534
|
+
restartInfo ? React.createElement('div', { className: 'dsm-feedback' }, '重启中… ' + restartInfo.name + '(已等待 ' + Math.max(0, Math.floor((Date.now() - restartInfo.startedAt) / 1000)) + ' 秒,完成后自动刷新)') : null,
|
|
535
|
+
msg ? React.createElement('div', { className: 'dsm-feedback' + (msg.kind === 'err' ? ' dsm-error' : msg.kind === 'warn' ? ' dsm-warning' : ''), role: msg.kind === 'err' ? 'alert' : undefined }, msg.text) : null,
|
|
536
|
+
state.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, state.error) : null,
|
|
537
|
+
(state.errors && state.errors.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, '读取补丁告警:' + state.errors.join(';')) : null,
|
|
538
|
+
(state.warnings && state.warnings.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, state.warnings.join(';')) : null,
|
|
539
|
+
groupsNode,
|
|
540
|
+
formModalNode,
|
|
541
|
+
detailNode,
|
|
542
|
+
confirmNode)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ---------- AGENTS.md 预设页:多套全局指令基线,应用=写入 ~/.dsh/AGENTS.md ----------
|
|
546
|
+
function AgentsMdPage() {
|
|
547
|
+
var st = React.useState({ loading: true, error: null, presets: [], current: null })
|
|
548
|
+
var state = st[0], setState = st[1]
|
|
549
|
+
var busyState = React.useState(null), busy = busyState[0], setBusy = busyState[1]
|
|
550
|
+
var em = React.useState(null), editModal = em[0], setEditModal = em[1]
|
|
551
|
+
var cm = React.useState(null), createModal = cm[0], setCreateModal = cm[1]
|
|
552
|
+
var ac = React.useState(null), applyConfirm = ac[0], setApplyConfirm = ac[1]
|
|
553
|
+
var importInput = React.useRef(null)
|
|
554
|
+
function doImport(file) {
|
|
555
|
+
if (!file) return
|
|
556
|
+
var reader = new FileReader()
|
|
557
|
+
reader.onload = function () {
|
|
558
|
+
var id = String(file.name).replace(/\.(md|markdown|txt)$/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'imported'
|
|
559
|
+
setBusy('import')
|
|
560
|
+
apiCall('agentsmd-import', { id: id, content: String(reader.result) }).then(function (res) {
|
|
561
|
+
setBusy(null)
|
|
562
|
+
if (res && res.ok) refresh()
|
|
563
|
+
else if (res) setState(function (s) { return Object.assign({}, s, { error: res.error }) })
|
|
564
|
+
}).catch(function () { setBusy(null) })
|
|
565
|
+
}
|
|
566
|
+
reader.readAsText(file)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function refresh() {
|
|
570
|
+
apiCall('agentsmd-list', {}).then(function (res) {
|
|
571
|
+
setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || '加载失败'), presets: (res && res.presets) || [], current: s.current } })
|
|
572
|
+
}).catch(function (e) { setState({ loading: false, error: String((e && e.message) || e), presets: [], current: null }) })
|
|
573
|
+
apiCall('agentsmd-get-current', {}).then(function (res) {
|
|
574
|
+
if (res && res.ok) setState(function (s) { return Object.assign({}, s, { current: res }) })
|
|
575
|
+
}).catch(function () {})
|
|
576
|
+
}
|
|
577
|
+
React.useEffect(function () { refresh() }, [])
|
|
578
|
+
|
|
579
|
+
var cur = state.current
|
|
580
|
+
var curLabel = cur && cur.exists ? (cur.presetId || '(已外部修改)') : '(未设置)'
|
|
581
|
+
|
|
582
|
+
function openEdit(p) {
|
|
583
|
+
apiCall('agentsmd-read', { id: p.id }).then(function (res) {
|
|
584
|
+
if (res && res.ok) setEditModal({ id: p.id, content: res.content })
|
|
585
|
+
}).catch(function () {})
|
|
586
|
+
}
|
|
587
|
+
function doApply(id) {
|
|
588
|
+
setBusy('apply-' + id)
|
|
589
|
+
apiCall('agentsmd-apply', { id: id }).then(function () { setBusy(null); setApplyConfirm(null); refresh() }).catch(function () { setBusy(null) })
|
|
590
|
+
}
|
|
591
|
+
function doRemove(p) {
|
|
592
|
+
setBusy('rm-' + p.id)
|
|
593
|
+
apiCall('agentsmd-remove', { id: p.id }).then(function () { setBusy(null); setApplyConfirm(null); refresh() }).catch(function () { setBusy(null) })
|
|
594
|
+
}
|
|
595
|
+
function doCreate(id, from) {
|
|
596
|
+
setBusy('create')
|
|
597
|
+
apiCall('agentsmd-create', { id: id, from: from || undefined }).then(function (res) {
|
|
598
|
+
setBusy(null)
|
|
599
|
+
if (res && res.ok) { setCreateModal(null); refresh() }
|
|
600
|
+
else if (res) setCreateModal(Object.assign({}, createModal, { error: res.error }))
|
|
601
|
+
}).catch(function () { setBusy(null) })
|
|
602
|
+
}
|
|
603
|
+
function doUpdate() {
|
|
604
|
+
setBusy('update')
|
|
605
|
+
apiCall('agentsmd-update', { id: editModal.id, content: editModal.content }).then(function (res) {
|
|
606
|
+
setBusy(null)
|
|
607
|
+
if (res && res.ok) { setEditModal(null); refresh() }
|
|
608
|
+
}).catch(function () { setBusy(null) })
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
var body = state.loading
|
|
612
|
+
? React.createElement('div', { className: 'dsm-empty' }, '加载中…')
|
|
613
|
+
: state.error
|
|
614
|
+
? React.createElement('div', { className: 'dsm-feedback dsm-error' }, state.error)
|
|
615
|
+
: !state.presets.length
|
|
616
|
+
? React.createElement('div', { className: 'dsm-empty' }, '暂无预设,点「新建预设」创建')
|
|
617
|
+
: React.createElement('div', { className: 'dsm-sources' }, state.presets.map(function (p) {
|
|
618
|
+
return React.createElement('div', { key: p.id, className: 'dsm-source' },
|
|
619
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
620
|
+
React.createElement('div', { className: 'dsm-source-head-main' },
|
|
621
|
+
React.createElement('span', { className: 'dsm-source-title' }, p.id),
|
|
622
|
+
p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, '生效中') : null),
|
|
623
|
+
React.createElement('div', { className: 'dsm-source-actions' },
|
|
624
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: function () { openEdit(p) } }, '编辑'),
|
|
625
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: function () { setApplyConfirm({ id: p.id }) }, title: '应用后新会话生效,当前会话不变' }, p.active ? '重新应用' : '应用'),
|
|
626
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null, onClick: function () { setApplyConfirm({ id: p.id, remove: true }) } }, '删除'))))
|
|
627
|
+
}))
|
|
628
|
+
|
|
629
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
630
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
631
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
632
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
633
|
+
React.createElement('h2', { className: 'dsm-title' }, 'AGENTS.md'),
|
|
634
|
+
React.createElement(VersionBadge, null)),
|
|
635
|
+
React.createElement('p', { className: 'dsm-desc' }, '管理全局指令基线;「应用」写入 ~/.dsh/AGENTS.md')),
|
|
636
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
637
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { setCreateModal({ id: '', from: '', error: null }) } }, '新建预设'),
|
|
638
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null, onClick: function () { if (importInput.current) importInput.current.click() } }, '导入'))),
|
|
639
|
+
body,
|
|
640
|
+
editModal ? React.createElement(Modal, { title: '编辑预设 · ' + editModal.id, closeLabel: '取消', onClose: function () { setEditModal(null) } },
|
|
641
|
+
React.createElement('textarea', { className: 'dsm-control', style: { minHeight: '320px' }, value: editModal.content, onChange: function (e) { setEditModal(Object.assign({}, editModal, { content: e.target.value })) } }),
|
|
642
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
643
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null, onClick: doUpdate }, '保存'))) : null,
|
|
644
|
+
createModal ? React.createElement(Modal, { title: '新建预设', closeLabel: '取消', onClose: function () { setCreateModal(null) } },
|
|
645
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
646
|
+
React.createElement('label', { className: 'dsm-label' }, 'id(小写字母/数字/连字符)'),
|
|
647
|
+
React.createElement('input', { className: 'dsm-control', value: createModal.id, onChange: function (e) { setCreateModal(Object.assign({}, createModal, { id: e.target.value })) } })),
|
|
648
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
649
|
+
React.createElement('label', { className: 'dsm-label' }, '从现有预设复制(可选,留空=空白模板)'),
|
|
650
|
+
React.createElement('input', { className: 'dsm-control', value: createModal.from, onChange: function (e) { setCreateModal(Object.assign({}, createModal, { from: e.target.value })) } })),
|
|
651
|
+
createModal && createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
|
|
652
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
653
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null, onClick: function () { doCreate(createModal.id, createModal.from) } }, '创建'))) : null,
|
|
654
|
+
applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? ('删除预设 · ' + applyConfirm.id) : ('应用预设 · ' + applyConfirm.id), closeLabel: '取消', onClose: function () { setApplyConfirm(null) } },
|
|
655
|
+
applyConfirm.remove
|
|
656
|
+
? React.createElement('p', { className: 'dsm-help' }, '删除预设 ' + applyConfirm.id + ' ?此操作不可恢复。')
|
|
657
|
+
: React.createElement('div', null,
|
|
658
|
+
React.createElement('p', { className: 'dsm-help' }, '将把 ' + applyConfirm.id + ' 的内容写入 ~/.dsh/AGENTS.md。'),
|
|
659
|
+
React.createElement('div', { className: 'dsm-feedback dsm-warning' }, '新会话生效;当前内容已备份到 __last-applied__')),
|
|
660
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
661
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else doApply(applyConfirm.id) } }, applyConfirm.remove ? '确认删除' : '确认应用'))) : null,
|
|
662
|
+
React.createElement('input', { ref: importInput, type: 'file', accept: '.md,.markdown,.txt', className: 'dsm-hidden-input', onChange: function (e) { if (e.target.files && e.target.files[0]) doImport(e.target.files[0]); e.target.value = ''; } }))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// TOOLS 设置页:一个侧栏项,内部 tab 切换 MCP / Skills / AGENTS.md / History / Rules。
|
|
666
|
+
function ToolsSection() {
|
|
667
|
+
var tabState = React.useState('mcp')
|
|
668
|
+
var active = tabState[0], setActive = tabState[1]
|
|
669
|
+
var page = active === 'mcp' ? React.createElement(MCPPage)
|
|
670
|
+
: active === 'skills' ? React.createElement(SkillManagerSection, { t: t })
|
|
671
|
+
: active === 'agents-md' ? React.createElement(AgentsMdPage)
|
|
672
|
+
: active === 'history' ? React.createElement(HistoryPage)
|
|
673
|
+
: React.createElement(SceneMemoryPage)
|
|
674
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
675
|
+
React.createElement('div', { className: 'dsm-tabs' },
|
|
676
|
+
React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'mcp' ? ' dsm-tab-active' : ''), onClick: function () { setActive('mcp') } }, 'MCP'),
|
|
677
|
+
React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'skills' ? ' dsm-tab-active' : ''), onClick: function () { setActive('skills') } }, 'Skills'),
|
|
678
|
+
React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'agents-md' ? ' dsm-tab-active' : ''), onClick: function () { setActive('agents-md') } }, 'AGENTS.md'),
|
|
679
|
+
React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'history' ? ' dsm-tab-active' : ''), onClick: function () { setActive('history') } }, 'History'),
|
|
680
|
+
React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'memory' ? ' dsm-tab-active' : ''), onClick: function () { setActive('memory') } }, t('memory.title')),
|
|
681
|
+
React.createElement('div', { style: { marginLeft: 'auto' } }, React.createElement(FeedbackLinks, null))),
|
|
682
|
+
page)
|
|
683
|
+
}
|
|
684
|
+
slots.inject('settings.section', () => slots.register(
|
|
685
|
+
{ name: 'settings.section', id: 'dsm-tools', order: 16, label: 'TOOLS' },
|
|
686
|
+
() => React.createElement(ToolsSection)
|
|
687
|
+
))
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
// ---------- Skills 页(移植自 dsh-skills-manager,Apache-2.0;类名前缀 dsm-) ----------
|
|
691
|
+
var react = React // 上游组件使用小写 react,别名到本文件的 React
|
|
692
|
+
var h = React.createElement
|
|
693
|
+
|
|
694
|
+
// SKILL.md frontmatter 常见键 → 中文标签(未知键原样显示,已知键附原始键名提示)。
|
|
695
|
+
var FRONTMATTER_LABELS = {
|
|
696
|
+
name: '名称', description: '描述', 'when-to-use': '适用场景', 'user-invocable': '用户可调用',
|
|
697
|
+
'disable-model-invocation': '禁止模型调用', 'allowed-tools': '允许的工具', license: '许可协议',
|
|
698
|
+
version: '版本', author: '作者', category: '分类', tags: '标签', metadata: '附加信息', homepage: '主页',
|
|
699
|
+
}
|
|
700
|
+
function frontmatterValue(value) {
|
|
701
|
+
if (Array.isArray(value)) return value.join('、')
|
|
702
|
+
if (value === true) return '是'
|
|
703
|
+
if (value === false) return '否'
|
|
704
|
+
if (value === null || value === undefined) return ''
|
|
705
|
+
if (typeof value === 'object') return JSON.stringify(value, null, 2)
|
|
706
|
+
return String(value)
|
|
707
|
+
}
|
|
708
|
+
// 详情页把 frontmatter 渲染成中文键值表(原始 JSON 对使用者几乎不可读)。
|
|
709
|
+
function renderFrontmatter(t, frontmatter) {
|
|
710
|
+
var keys = frontmatter && typeof frontmatter === 'object' ? Object.keys(frontmatter) : []
|
|
711
|
+
if (!keys.length) return h('div', { className: 'dsm-note' }, t('detail.noFrontmatter'))
|
|
712
|
+
return h('div', { className: 'dsm-fm' }, keys.map(function (key) {
|
|
713
|
+
var label = FRONTMATTER_LABELS[key] || key
|
|
714
|
+
return h('div', { className: 'dsm-fm-row', key: key },
|
|
715
|
+
h('div', { className: 'dsm-fm-key' }, label, label === key ? null : h('span', { className: 'dsm-fm-raw' }, key)),
|
|
716
|
+
h('div', { className: 'dsm-fm-val' }, frontmatterValue(frontmatter[key])))
|
|
717
|
+
}))
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
var DICT = {
|
|
721
|
+
zh: {
|
|
722
|
+
"title": "Skills", "desc": "统一加载和管理本机 Agent Skills。", "link.project": "GitHub", "link.feedback": "问题反馈",
|
|
723
|
+
"btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开",
|
|
724
|
+
"btn.custom.add": "添加目录", "btn.custom.remove": "移除", "btn.openDir": "选择",
|
|
725
|
+
"custom.add.title": "添加自定义技能目录", "custom.add.submit": "添加",
|
|
726
|
+
"custom.add.path": "目录绝对路径", "custom.add.path.placeholder": "例如 D:\\skills\\my-skills",
|
|
727
|
+
"custom.add.openDir.title": "浏览并选择文件夹", "custom.add.picker.title": "选择文件夹",
|
|
728
|
+
"custom.add.label": "显示名称(可选)", "custom.add.label.placeholder": "自定义目录",
|
|
729
|
+
"custom.add.help": "只读接入该目录;目录与源文件都不会被修改",
|
|
730
|
+
"confirm.custom.remove.title": "移除自定义目录",
|
|
731
|
+
"confirm.custom.remove.desc": "移除后该目录的技能不再列出(文件不受影响):{name}",
|
|
732
|
+
"result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录",
|
|
733
|
+
"status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
|
|
734
|
+
"summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
|
|
735
|
+
"filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
|
|
736
|
+
"empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
|
|
737
|
+
"source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;启停只改管理器状态",
|
|
738
|
+
"detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
|
|
739
|
+
"create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 create_skill 建的是用户级技能",
|
|
740
|
+
"import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
|
|
741
|
+
"trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
|
|
742
|
+
"confirm.trash.title": "移到回收站?", "confirm.trash.desc": "「{name}」将移入回收站,可恢复", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "「{name}」将永久删除,无法恢复",
|
|
743
|
+
"result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "result.opened": "已用系统默认程序打开:{path}", "error.action": "操作失败:{error}",
|
|
744
|
+
"warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "状态文件不可读,已安全停用所有技能:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
|
|
745
|
+
"error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "缺少 frontmatter,无法{action}:{name}", "error.skill.notLoadable": "技能结构不完整,无法{action}: {name}",
|
|
746
|
+
"error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持符号链接来源:{path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
|
|
747
|
+
"error.import.overlap": "该目录与 DSH 技能目录重叠", "error.import.emptySource": "目录下没有 skill 条目:{path}", "error.import.invalidName": "名称不合法:{name}", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
|
|
748
|
+
"error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
|
|
749
|
+
"error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
|
|
750
|
+
"error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
|
|
751
|
+
"error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
|
|
752
|
+
"error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
|
|
753
|
+
"diagnostic.frontmatter.missing": "缺少完整 YAML frontmatter", "diagnostic.name.missing": "frontmatter 缺少 name", "diagnostic.name.invalid": "技能名需 kebab-case:{name}", "diagnostic.description.missing": "frontmatter 缺少 description", "diagnostic.invocation.invalid": "调用策略字段值无效", "diagnostic.shadowed": "被更高优先级来源 {root} 覆盖",
|
|
754
|
+
"action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
|
|
755
|
+
"root.dsh": "DSH 技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
756
|
+
"memory.title": "场景记忆",
|
|
757
|
+
"memory.desc": "勾选启用的场景,里面的记忆会自动进入系统提示词,不用每次重复解释。",
|
|
758
|
+
"memory.btn.refresh": "刷新", "memory.btn.new": "新建记忆", "memory.btn.create": "创建", "memory.btn.save": "保存",
|
|
759
|
+
"memory.btn.diagnose": "体检", "memory.btn.delete.confirm": "移入回收站",
|
|
760
|
+
"memory.diagnose.title": "记忆体检", "memory.diagnose.summary": "{rules} 条记忆 · {scenes} 个场景 · {issues} 个问题", "memory.diagnose.clean": "未发现问题,一切正常。",
|
|
761
|
+
"memory.diag.error": "错误", "memory.diag.warning": "警告", "memory.diag.info": "提示",
|
|
762
|
+
"memory.stat.total": "条记忆", "memory.stat.scenes": "个启用场景", "memory.stat.sceneCount": "个场景", "memory.status.sceneOff": "场景未启用", "memory.status.off": "本条已停用",
|
|
763
|
+
"memory.budget.label": "注入预算", "memory.budget.bytes": "{used} / {max} 字节",
|
|
764
|
+
"memory.budget.over": "已超限",
|
|
765
|
+
"memory.budget.truncated": "超出预算,段尾已列出未注入的记忆",
|
|
766
|
+
"memory.budget.dropped": "本次未注入:{names}",
|
|
767
|
+
"memory.scene.enable": "启用", "memory.scene.enableAll": "全部启用", "memory.scene.off": "未启用", "memory.scene.shared": "常开",
|
|
768
|
+
"memory.scene.global": "全局", "memory.scene.count": "{count} 条记忆", "memory.scene.new": "新建记忆", "memory.scene.pick": "选已有场景,或直接输入新场景名", "memory.scene.browse": "选择已有",
|
|
769
|
+
"memory.scene.empty": "该场景暂无记忆",
|
|
770
|
+
"memory.search.placeholder": "搜索名称 / 描述 / 场景", "memory.filter.all": "全部场景",
|
|
771
|
+
"memory.empty": "还没有记忆,点「新建记忆」", "memory.empty.search": "没有匹配的记忆",
|
|
772
|
+
"memory.loading": "正在加载…",
|
|
773
|
+
"memory.enable": "启用", "memory.edit": "编辑", "memory.delete": "删除",
|
|
774
|
+
"memory.form.flat": "flat", "memory.form.bundle": "bundle", "memory.derived": "派生", "memory.fill.frontmatter": "补齐 frontmatter",
|
|
775
|
+
"memory.shadowed.hint": "同名 bundle 存在,本条不加载",
|
|
776
|
+
"memory.create.title": "新建记忆", "memory.edit.title": "编辑记忆", "memory.edit.group.lock": "编辑时场景不可修改",
|
|
777
|
+
"memory.field.group": "场景", "memory.field.group.placeholder": "例如 办公 / 日常 / code-review", "memory.field.group.hint": "即 ~/.dsh/scene-memory/ 下的目录名,中英文皆可",
|
|
778
|
+
"memory.field.name": "名称", "memory.field.name.placeholder": "例如 站会流程 / standup-notes", "memory.name.hint": "就是 .md 文件名;中文可以(如 站会流程)", "memory.name.invalid": "名称不合法(非空、≤64 字符、不含 / \\ < > : \" | ? *、不以 . 开头)",
|
|
779
|
+
"memory.field.description": "描述", "memory.field.description.placeholder": "一句话说明用途(留空取正文首行)",
|
|
780
|
+
"memory.field.body": "正文(Markdown)", "memory.field.body.placeholder": "写下要记住的约定、流程和边界…",
|
|
781
|
+
"memory.field.form": "形态", "memory.field.attach": "附件",
|
|
782
|
+
"memory.attach.add": "添加附件", "memory.attach.empty": "暂无附件", "memory.attach.pending": "待上传", "memory.attach.remove": "移除附件",
|
|
783
|
+
"memory.attach.hint": "附件随记忆存放,不会进入提示词", "memory.attach.flat": "flat 是单文件,不能带附件", "memory.attach.tooLarge": "附件过大:{name}(上限 {limit} MB)",
|
|
784
|
+
"memory.delete.title": "移入回收站?", "memory.delete.desc": "「{name}」将移入回收站,可恢复",
|
|
785
|
+
"memory.result.created": "已创建记忆:{name}", "memory.result.updated": "已保存记忆:{name}", "memory.result.removed": "已移入回收站:{name}", "memory.result.toggled": "已更新记忆状态:{name}", "memory.result.active": "已更新启用场景",
|
|
786
|
+
"memory.result.restored": "已恢复记忆:{name}", "memory.result.trashRemoved": "已永久删除:{name}",
|
|
787
|
+
"memory.result.createdAttached": "已创建记忆:{name}(含 {count} 个附件)", "memory.result.updatedAttached": "已保存记忆:{name}(含 {count} 个附件)", "memory.result.attachFailed": "记忆已保存,但附件失败:{error}",
|
|
788
|
+
"memory.trash.open": "回收站", "memory.trash.title": "记忆回收站", "memory.trash.empty": "回收站为空",
|
|
789
|
+
"memory.trash.count": "{count} 条已删除的记忆", "memory.trash.deletedAt": "删除于 {time}",
|
|
790
|
+
"memory.trash.restore": "恢复", "memory.trash.purge": "永久删除",
|
|
791
|
+
"memory.trash.confirmTitle": "永久删除?", "memory.trash.confirmDesc": "「{name}」将永久删除,无法恢复",
|
|
792
|
+
"memory.btn.newScene": "新建场景", "memory.btn.deleteScene": "删除场景",
|
|
793
|
+
"memory.scene.createTitle": "新建场景", "memory.scene.createHelp": "建好后往里丢 .md 即可,每个文件就是一条记忆",
|
|
794
|
+
"memory.deleteScene.title": "删除场景?", "memory.deleteScene.desc": "将删除空文件夹「{name}」(非空不能删)",
|
|
795
|
+
"memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}",
|
|
796
|
+
"memory.table.name": "记忆名称与描述", "memory.table.tags": "标记", "memory.table.status": "状态",
|
|
797
|
+
"error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件"
|
|
798
|
+
},
|
|
799
|
+
en: {
|
|
800
|
+
"title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.", "link.project": "GitHub", "link.feedback": "Issues",
|
|
801
|
+
"btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.open.editor": "Open in editor",
|
|
802
|
+
"btn.custom.add": "Add folder",
|
|
803
|
+
"btn.custom.remove": "Remove",
|
|
804
|
+
"btn.openDir": "Browse",
|
|
805
|
+
"custom.add.title": "Add custom skill folder",
|
|
806
|
+
"custom.add.submit": "Add",
|
|
807
|
+
"custom.add.path": "Absolute folder path",
|
|
808
|
+
"custom.add.path.placeholder": "e.g. D:\\skills\\my-skills",
|
|
809
|
+
"custom.add.openDir.title": "Browse and pick a folder",
|
|
810
|
+
"custom.add.picker.title": "Pick a folder",
|
|
811
|
+
"custom.add.label": "Display name (optional)",
|
|
812
|
+
"custom.add.label.placeholder": "Custom folder",
|
|
813
|
+
"custom.add.help": "Read-only: neither the folder nor its files are modified",
|
|
814
|
+
"confirm.custom.remove.title": "Remove custom folder?",
|
|
815
|
+
"confirm.custom.remove.desc": "Its skills stop being listed (files are untouched): {name}",
|
|
816
|
+
"result.custom.added": "Custom folder added: {path}",
|
|
817
|
+
"result.custom.removed": "Custom folder removed: {name}",
|
|
818
|
+
"result.opened": "Opened with the system default app: {path}",
|
|
819
|
+
"status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
|
|
820
|
+
"summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
|
|
821
|
+
"filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
|
|
822
|
+
"empty.search": "No matching skills", "empty.source": "This source is missing or has no skills", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
|
|
823
|
+
"source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; toggles only change manager state",
|
|
824
|
+
"detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
|
|
825
|
+
"create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "create_skill in chat creates user-level skills",
|
|
826
|
+
"import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
|
|
827
|
+
"trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
|
|
828
|
+
"confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” moves to trash and can be restored", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” is deleted forever and cannot be recovered",
|
|
829
|
+
"result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "error.action": "Action failed: {error}",
|
|
830
|
+
"warning.scan.truncated": "Some skills were not shown because the directory is too large or deeply nested: {path}", "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
|
|
831
|
+
"error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}", "error.skill.notLoadable": "Skill structure is incomplete, cannot {action}: {name}",
|
|
832
|
+
"error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Symlinked sources are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
|
|
833
|
+
"error.import.overlap": "This folder overlaps the DSH skills folder", "error.import.emptySource": "No skill entries in this folder: {path}", "error.import.invalidName": "Invalid name: {name}", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
|
|
834
|
+
"error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
|
|
835
|
+
"error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
|
|
836
|
+
"error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
|
|
837
|
+
"error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
|
|
838
|
+
"error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
|
|
839
|
+
"diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name must be kebab-case: {name}", "diagnostic.description.missing": "Frontmatter is missing description", "diagnostic.invocation.invalid": "Invocation policy value is invalid", "diagnostic.shadowed": "Shadowed by higher-priority source {root}",
|
|
840
|
+
"action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
|
|
841
|
+
"root.dsh": "DSH skills", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
842
|
+
"memory.title": "Scene Memory",
|
|
843
|
+
"memory.desc": "Everything inside an enabled scene is injected into the system prompt automatically — no need to repeat yourself.",
|
|
844
|
+
"memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
|
|
845
|
+
"memory.btn.diagnose": "Check", "memory.btn.delete.confirm": "Move to trash",
|
|
846
|
+
"memory.diagnose.title": "Memory checkup", "memory.diagnose.summary": "{rules} memories · {scenes} scenes · {issues} issues", "memory.diagnose.clean": "No issues found.",
|
|
847
|
+
"memory.diag.error": "Error", "memory.diag.warning": "Warning", "memory.diag.info": "Info",
|
|
848
|
+
"memory.stat.total": "memories", "memory.stat.scenes": "enabled scenes", "memory.stat.sceneCount": "scenes", "memory.status.sceneOff": "scene off", "memory.status.off": "this memory is off",
|
|
849
|
+
"memory.budget.label": "Injection budget", "memory.budget.bytes": "{used} / {max} bytes",
|
|
850
|
+
"memory.budget.over": "Over budget",
|
|
851
|
+
"memory.budget.truncated": "Over budget — the memories left out are listed at the end",
|
|
852
|
+
"memory.budget.dropped": "Not injected this time: {names}",
|
|
853
|
+
"memory.scene.enable": "Enabled", "memory.scene.enableAll": "Enable all", "memory.scene.off": "Off", "memory.scene.shared": "always on",
|
|
854
|
+
"memory.scene.global": "Global", "memory.scene.count": "{count} memories", "memory.scene.new": "New memory", "memory.scene.pick": "Pick a scene, or type a new one", "memory.scene.browse": "Pick existing",
|
|
855
|
+
"memory.scene.empty": "No memories in this scene yet",
|
|
856
|
+
"memory.search.placeholder": "Search name / description / scene", "memory.filter.all": "All scenes",
|
|
857
|
+
"memory.empty": "No memories yet — click “New memory”", "memory.empty.search": "No matching memories",
|
|
858
|
+
"memory.loading": "Loading…",
|
|
859
|
+
"memory.enable": "Enabled", "memory.edit": "Edit", "memory.delete": "Delete",
|
|
860
|
+
"memory.form.flat": "flat", "memory.form.bundle": "bundle", "memory.derived": "derived", "memory.fill.frontmatter": "Fill frontmatter",
|
|
861
|
+
"memory.shadowed.hint": "A bundle with the same name exists, so this one is not loaded",
|
|
862
|
+
"memory.create.title": "New memory", "memory.edit.title": "Edit memory", "memory.edit.group.lock": "The scene cannot be changed while editing",
|
|
863
|
+
"memory.field.group": "Scene", "memory.field.group.placeholder": "e.g. office / daily / code-review", "memory.field.group.hint": "Folder name under ~/.dsh/scene-memory/; any Unicode",
|
|
864
|
+
"memory.field.name": "Name", "memory.field.name.placeholder": "e.g. 站会流程 / standup-notes", "memory.name.hint": "The .md file name — Chinese is fine (e.g. 站会流程)", "memory.name.invalid": "Invalid name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)",
|
|
865
|
+
"memory.field.description": "Description", "memory.field.description.placeholder": "One line on what it is for (blank = first body line)",
|
|
866
|
+
"memory.field.body": "Body (Markdown)", "memory.field.body.placeholder": "Write down the conventions, steps, and boundaries to remember…",
|
|
867
|
+
"memory.field.form": "Form", "memory.field.attach": "Attachments",
|
|
868
|
+
"memory.attach.add": "Add files", "memory.attach.empty": "No attachments yet", "memory.attach.pending": "pending", "memory.attach.remove": "Remove attachment",
|
|
869
|
+
"memory.attach.hint": "Stored with the memory; never injected into the prompt", "memory.attach.flat": "The flat form is a single file and cannot carry attachments", "memory.attach.tooLarge": "File too large: {name} (max {limit} MB)",
|
|
870
|
+
"memory.delete.title": "Move to trash?", "memory.delete.desc": "“{name}” moves to trash and can be restored",
|
|
871
|
+
"memory.result.created": "Created memory: {name}", "memory.result.updated": "Saved memory: {name}", "memory.result.removed": "Moved to trash: {name}", "memory.result.toggled": "Memory state updated: {name}", "memory.result.active": "Enabled scenes updated",
|
|
872
|
+
"memory.result.restored": "Restored memory: {name}", "memory.result.trashRemoved": "Permanently deleted: {name}",
|
|
873
|
+
"memory.result.createdAttached": "Created memory: {name} ({count} attachments)", "memory.result.updatedAttached": "Saved memory: {name} ({count} attachments)", "memory.result.attachFailed": "Memory saved, but attachments failed: {error}",
|
|
874
|
+
"memory.trash.open": "Trash", "memory.trash.title": "Memory trash", "memory.trash.empty": "Trash is empty",
|
|
875
|
+
"memory.trash.count": "{count} deleted memories", "memory.trash.deletedAt": "Deleted {time}",
|
|
876
|
+
"memory.trash.restore": "Restore", "memory.trash.purge": "Delete forever",
|
|
877
|
+
"memory.trash.confirmTitle": "Delete forever?", "memory.trash.confirmDesc": "“{name}” is deleted forever and cannot be recovered",
|
|
878
|
+
"memory.btn.newScene": "New scene", "memory.btn.deleteScene": "Delete scene",
|
|
879
|
+
"memory.scene.createTitle": "New scene", "memory.scene.createHelp": "Drop .md files in afterwards — each file is one memory",
|
|
880
|
+
"memory.deleteScene.title": "Delete scene?", "memory.deleteScene.desc": "Deletes the empty folder “{name}”; non-empty scenes cannot be deleted",
|
|
881
|
+
"memory.result.sceneCreated": "Scene created: {name}", "memory.result.sceneRemoved": "Scene deleted: {name}",
|
|
882
|
+
"memory.table.name": "Memory name and description", "memory.table.tags": "Tags", "memory.table.status": "Status",
|
|
883
|
+
"error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload"
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
|
|
889
|
+
function translateError(t, payload) {
|
|
890
|
+
if (payload instanceof Error && !payload.code) return payload.message;
|
|
891
|
+
if (payload && typeof payload === "object") {
|
|
892
|
+
if (payload.code) { var params = Object.assign({}, payload.params || {}); if (params.action) params.action = translateOrFallback(t, "action." + params.action, params.action); var translated = t(payload.code, params); if (typeof translated === "string" && translated !== payload.code) return translated; }
|
|
893
|
+
if (payload.error !== undefined) return translateError(t, payload.error); if (payload.message !== undefined) return String(payload.message);
|
|
894
|
+
}
|
|
895
|
+
return String(payload == null ? "" : payload);
|
|
896
|
+
}
|
|
897
|
+
function parseApiResponse(response) { return response.json().catch(function () { var error = new Error("non-json response"); error.code = "error.proto.nonJson"; error.params = { status: response.status }; throw error; }).then(function (payload) { if (!response.ok || payload.ok === false) throw payload; return payload.data; }); }
|
|
898
|
+
var OP_BY_PATH = { "/state": "skill-state", "/enable": "skill-enable", "/disable": "skill-disable",
|
|
899
|
+
"/source-enable": "skill-source-enable", "/source-disable": "skill-source-disable",
|
|
900
|
+
"/delete": "skill-delete", "/trash-restore": "skill-trash-restore", "/trash-delete": "skill-trash-delete",
|
|
901
|
+
"/detail": "skill-detail", "/create": "skill-create", "/import": "skill-import",
|
|
902
|
+
"/upload": "skill-upload", "/browse": "skill-browse" };
|
|
903
|
+
function callApi(path, options) {
|
|
904
|
+
var op = OP_BY_PATH[path];
|
|
905
|
+
if (!op) return Promise.reject({ error: "unknown action: " + path });
|
|
906
|
+
var args = {};
|
|
907
|
+
if (options && typeof options.body === "string") { try { args = JSON.parse(options.body) || {}; } catch (e) { args = {}; } }
|
|
908
|
+
return apiCall(op, args).then(function (payload) {
|
|
909
|
+
if (!payload || payload.ok !== true) throw payload || { error: "request failed" };
|
|
910
|
+
return payload.data;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function isSkillEnabled(skill) { if (skill.enabled !== undefined) return skill.enabled === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
|
|
914
|
+
function countKey(key, count) { return key + (Number(count) === 1 ? ".one" : ".other"); }
|
|
915
|
+
function rootDisplayName(t, root) { var base = translateOrFallback(t, "root." + (root.localeKey || root.kind || root.key), root.label); return root.projectName ? base + " · " + root.projectName : base; }
|
|
916
|
+
function summarizeImportResult(t, data) {
|
|
917
|
+
var importedItems = data && data.imported || [];
|
|
918
|
+
var imported = importedItems.map(function (item) { return item.name; });
|
|
919
|
+
var skipped = (data && data.skipped || []).map(function (item) { return item.name; });
|
|
920
|
+
var warnings = [];
|
|
921
|
+
importedItems.forEach(function (item) {
|
|
922
|
+
(item.warnings || []).forEach(function (warning) { warnings.push(translateError(t, warning)); });
|
|
923
|
+
});
|
|
924
|
+
var summary;
|
|
925
|
+
if (imported.length && skipped.length) summary = { ok: true, warning: true, imported: true, text: t("result.importPartial", { imported: imported.join(", "), skipped: skipped.join(", ") }) };
|
|
926
|
+
else if (imported.length) summary = { ok: true, warning: false, imported: true, text: t("result.imported", { names: imported.join(", ") }) };
|
|
927
|
+
else if (skipped.length) summary = { ok: false, warning: true, imported: false, text: t("result.importSkipped", { names: skipped.join(", ") }) };
|
|
928
|
+
else summary = { ok: false, warning: false, imported: false, text: t("result.importEmpty") };
|
|
929
|
+
if (warnings.length) {
|
|
930
|
+
summary.warning = true;
|
|
931
|
+
summary.text = t("result.importWarnings", { result: summary.text, warnings: warnings.join(";") });
|
|
932
|
+
}
|
|
933
|
+
return summary;
|
|
934
|
+
}
|
|
935
|
+
function normalizeSkillQuery(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
|
|
936
|
+
function matchSkillQuery(skill, query) { var q = normalizeSkillQuery(query); if (!q) return true; return [skill.name, skill.declaredName, skill.description, skill.kind, skill.kindLabel, skill.statusLabel, skill.rootKey, skill.rootLabel].some(function (value) { return String(value == null ? "" : value).toLowerCase().includes(q); }); }
|
|
937
|
+
function filterSkills(list, options) { var rootKey = options && options.rootKey != null ? options.rootKey : ""; return list.filter(function (skill) { return (!rootKey || skill.rootKey === rootKey) && matchSkillQuery(skill, options && options.query); }); }
|
|
938
|
+
function visibleSkillRoots(roots) { return (roots || []).filter(function (root) { return root.scope !== "project" || (root.skills || []).length > 0; }); }
|
|
939
|
+
var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
|
|
940
|
+
function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
|
|
941
|
+
function inspectUploadSelection(files) {
|
|
942
|
+
var list = Array.prototype.slice.call(files || []); if (!list.length) return null;
|
|
943
|
+
if (list.length > MAX_UPLOAD_ENTRIES) return { error: { code: "error.upload.tooMany", params: { limit: MAX_UPLOAD_ENTRIES } } };
|
|
944
|
+
var relative = list.some(function (file) { return uploadFilePath(file).includes("/"); });
|
|
945
|
+
if (relative) {
|
|
946
|
+
if (!list.some(function (file) { return /(^|\/)skill\.md$/i.test(uploadFilePath(file)); })) return { error: { code: "select.folder.invalid" } };
|
|
947
|
+
var oversized = list.find(function (file) { return Number(file.size || 0) > MAX_UPLOAD_ENTRY_BYTES; });
|
|
948
|
+
if (oversized) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_ENTRY_BYTES } } };
|
|
949
|
+
var total = list.reduce(function (sum, file) { return sum + Number(file.size || 0); }, 0);
|
|
950
|
+
if (total > MAX_UPLOAD_TOTAL_BYTES) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_TOTAL_BYTES } } };
|
|
951
|
+
return { kind: "folder", name: uploadFilePath(list[0]).split("/")[0], files: list, count: list.length, size: total };
|
|
952
|
+
}
|
|
953
|
+
if (list.length !== 1) return { error: { code: "select.file.invalid" } };
|
|
954
|
+
var name = String(list[0].name || ""), lower = name.toLowerCase();
|
|
955
|
+
var size = Number(list[0].size || 0);
|
|
956
|
+
if (lower === "skill.md") return size > MAX_UPLOAD_ENTRY_BYTES ? { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_ENTRY_BYTES } } } : { kind: "skill", name: name, files: list, count: 1, size: size };
|
|
957
|
+
if (lower.endsWith(".zip")) return size > MAX_UPLOAD_ARCHIVE_BYTES ? { error: { code: "error.upload.archiveTooLarge", params: { limit: MAX_UPLOAD_ARCHIVE_BYTES } } } : { kind: "zip", name: name, files: list, count: 1, size: size };
|
|
958
|
+
return { error: { code: "select.file.invalid" } };
|
|
959
|
+
}
|
|
960
|
+
function bytesToBase64(buffer) { var bytes = new Uint8Array(buffer), binary = "", chunk = 0x8000; for (var i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(i + chunk, bytes.length))); return btoa(binary); }
|
|
961
|
+
function buildUploadPayload(selection) { return Promise.all(selection.files.map(function (file) { return file.arrayBuffer().then(function (buffer) { return { path: uploadFilePath(file), data: bytesToBase64(buffer) }; }); })).then(function (entries) { return selection.kind === "zip" ? { name: selection.name, zip: entries[0].data } : { name: selection.name, entries: entries }; }); }
|
|
962
|
+
function readDroppedEntry(entry, prefix, output) { if (entry.isFile) return new Promise(function (resolve, reject) { entry.file(function (file) { file._dssmPath = prefix + file.name; output.push(file); resolve(); }, reject); }); if (!entry.isDirectory) return Promise.resolve(); return new Promise(function (resolve, reject) { var reader = entry.createReader(), children = []; function next() { reader.readEntries(function (batch) { if (!batch.length) { Promise.all(children.map(function (child) { return readDroppedEntry(child, prefix + entry.name + "/", output); })).then(resolve, reject); return; } children = children.concat(batch); next(); }, reject); } next(); }); }
|
|
963
|
+
function droppedFiles(dataTransfer) { var items = Array.prototype.slice.call(dataTransfer && dataTransfer.items || []), entries = items.map(function (item) { return item.webkitGetAsEntry && item.webkitGetAsEntry(); }).filter(Boolean); if (!entries.length) return Promise.resolve(Array.prototype.slice.call(dataTransfer && dataTransfer.files || [])); var files = []; return Promise.all(entries.map(function (entry) { return readDroppedEntry(entry, "", files); })).then(function () { return files; }); }
|
|
964
|
+
function modalFocusable(modal) { return Array.prototype.slice.call(modal.querySelectorAll("button:not(:disabled), [href], input:not([type=hidden]):not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])") || []); }
|
|
965
|
+
function trapModalFocus(modal, event) { if (event.key !== "Tab") return; var focusable = modalFocusable(modal); if (!focusable.length) return; var active = document.activeElement; if (!modal.contains(active) || (event.shiftKey ? active === focusable[0] : active === focusable[focusable.length - 1])) { event.preventDefault(); (event.shiftKey ? focusable[focusable.length - 1] : focusable[0]).focus(); } }
|
|
966
|
+
function handleModalEscape(event, onClose) { if (event.key !== "Escape") return false; event.preventDefault(); event.stopPropagation(); if (event.nativeEvent && typeof event.nativeEvent.stopImmediatePropagation === "function") event.nativeEvent.stopImmediatePropagation(); onClose(); return true; }
|
|
967
|
+
|
|
968
|
+
function SourceSelect(props) {
|
|
969
|
+
var state = react.useState(false), open = state[0], setOpen = state[1], selected = props.options.find(function (o) { return o.value === props.value; }) || props.options[0], ref = react.useRef(null);
|
|
970
|
+
react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
|
|
971
|
+
return h("div", { className: "dsm-select", ref: ref }, h("button", { type: "button", className: "dsm-select-trigger", "aria-haspopup": "listbox", "aria-expanded": open, onClick: function () { setOpen(!open); } }, h("span", null, selected.label)), open ? h("div", { className: "dsm-select-menu", role: "listbox" }, props.options.map(function (o) { return h("button", { key: o.value || "all", type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
|
|
972
|
+
}
|
|
973
|
+
// 场景输入(combobox):既能从已有场景里挑,也能直接手输一个新场景名——
|
|
974
|
+
// 宿主 rules-create 用 mkdir(recursive) 落盘,所以「自定义场景」无需先建目录。
|
|
975
|
+
function SceneCombo(props) {
|
|
976
|
+
var state = react.useState(false), open = state[0], setOpen = state[1], ref = react.useRef(null);
|
|
977
|
+
react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
|
|
978
|
+
var options = props.options || [];
|
|
979
|
+
return h("div", { className: "dsm-select", ref: ref },
|
|
980
|
+
h("div", { className: "dsm-combo-row" },
|
|
981
|
+
h("input", { className: "dsm-control" + (props.invalid ? " dsm-rule-invalid" : ""), type: "text", value: props.value || "", placeholder: props.placeholder, "aria-label": props.label, onChange: function (e) { props.onChange(e.target.value); } }),
|
|
982
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel)),
|
|
983
|
+
open && options.length ? h("div", { className: "dsm-select-menu", role: "listbox" }, options.map(function (o) { return h("button", { key: o.value, type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
|
|
984
|
+
}
|
|
985
|
+
function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, disabled: props.disabled, onClick: props.onClick }); }
|
|
986
|
+
function GithubMark16() { return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true, focusable: "false" }, h("path", { fill: "currentColor", d: "M8 0a8 8 0 0 0-2.53 15.59c.4.074.547-.173.547-.385 0-.19-.007-.693-.01-1.36-2.226.484-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.726-.496.055-.486.055-.486.803.056 1.225.824 1.225.824.714 1.223 1.872.87 2.328.665.072-.517.28-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.956 0-.874.31-1.588.823-2.148-.083-.202-.357-1.017.078-2.12 0 0 .672-.215 2.2.82A7.65 7.65 0 0 1 8 4.8c.68.003 1.365.092 2.004.27 1.527-1.035 2.197-.82 2.197-.82.437 1.103.162 1.918.08 2.12.513.56.822 1.274.822 2.148 0 3.076-1.872 3.752-3.654 3.95.288.248.544.735.544 1.482 0 1.07-.01 1.932-.01 2.195 0 .214.144.463.55.384A8.001 8.001 0 0 0 8 0Z" })); }
|
|
987
|
+
function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); return h("div", { className: "dsm-mask", onMouseDown: function (e) { if (e.target === e.currentTarget) props.onClose(); } }, h("div", { ref: ref, tabIndex: -1, className: "dsm-modal" + (props.wide ? " dsm-modal-wide" : "") + (props.className ? " " + props.className : ""), role: "dialog", "aria-modal": "true", onKeyDown: function (e) { if (!handleModalEscape(e, props.onClose)) trapModalFocus(e.currentTarget, e); } }, h("div", { className: "dsm-modal-head" }, h("h3", { className: "dsm-modal-title" }, props.title), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, props.closeLabel)), props.children)); }
|
|
988
|
+
|
|
989
|
+
// 目录选择器:经 host 的 dir-list 逐级浏览目录树,选中后把绝对路径回填给调用方。
|
|
990
|
+
// 标准浏览器不暴露所选文件夹的绝对路径(File.path 仅 Electron 可用),故不走
|
|
991
|
+
// webkitdirectory,改由宿主端列目录 + 前端目录树完成「选择文件夹」。
|
|
992
|
+
function DirPickerModal(props) {
|
|
993
|
+
var stState = react.useState({ loading: true, error: null, current: '', parent: null, entries: [] })
|
|
994
|
+
var st = stState[0], setSt = stState[1]
|
|
995
|
+
var txState = react.useState(props.initial || '')
|
|
996
|
+
var text = txState[0], setText = txState[1]
|
|
997
|
+
function load(dir) {
|
|
998
|
+
setSt({ loading: true, error: null, current: dir || '', parent: null, entries: [] })
|
|
999
|
+
apiCall('dir-list', { dir: dir || '' }).then(function (res) {
|
|
1000
|
+
if (res && res.ok) setSt({ loading: false, error: null, current: res.current || '', parent: res.parent || null, entries: res.entries || [] })
|
|
1001
|
+
else setSt({ loading: false, error: (res && res.error) || '无法读取目录', current: dir || '', parent: null, entries: [] })
|
|
1002
|
+
}).catch(function () {
|
|
1003
|
+
setSt({ loading: false, error: '无法读取目录', current: dir || '', parent: null, entries: [] })
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
react.useEffect(function () { load(props.initial || '') }, [])
|
|
1007
|
+
function jump() { var v = String(text || '').trim(); if (v) load(v) }
|
|
1008
|
+
function goUp() { if (!st.parent) return; setText(st.parent); load(st.parent) }
|
|
1009
|
+
function enter(path) { setText(path); load(path) }
|
|
1010
|
+
function pick() { var target = String(st.current || '').trim() || String(text || '').trim(); if (target && props.onPick) props.onPick(target) }
|
|
1011
|
+
return h(Modal, { title: props.title || '选择文件夹', closeLabel: props.closeLabel || '关闭', onClose: props.onClose },
|
|
1012
|
+
h("div", { className: "dsm-form" },
|
|
1013
|
+
h("label", { className: "dsm-field" },
|
|
1014
|
+
h("span", { className: "dsm-label" }, '当前路径'),
|
|
1015
|
+
h("div", { className: "dsm-dir-row" },
|
|
1016
|
+
h("input", { className: "dsm-control", value: text, placeholder: '输入目录路径,回车跳转', onChange: function (e) { setText(e.target.value); }, onKeyDown: function (e) { if (e.key === 'Enter') { e.preventDefault(); jump(); } } }),
|
|
1017
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !String(text || '').trim(), onClick: jump }, '跳转'))),
|
|
1018
|
+
st.error ? h("div", { className: "dsm-feedback dsm-error", role: "alert" }, st.error) : null,
|
|
1019
|
+
h("div", { className: "dsm-dir-list" },
|
|
1020
|
+
st.loading ? h("div", { className: "dsm-empty" }, '加载中…')
|
|
1021
|
+
: st.entries.length ? st.entries.map(function (e) { return h("button", { key: e.path, type: "button", className: "dsm-dir-item", title: e.path, onClick: function () { enter(e.path); } }, e.name); })
|
|
1022
|
+
: h("div", { className: "dsm-empty" }, '(当前目录没有子文件夹)')),
|
|
1023
|
+
h("div", { className: "dsm-modal-actions" },
|
|
1024
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !st.parent, onClick: goUp }, '上级'),
|
|
1025
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, '取消'),
|
|
1026
|
+
h("button", { type: "button", className: "dsm-btn", disabled: st.loading || !(String(st.current || '').trim() || String(text || '').trim()), onClick: pick }, '选择此文件夹'))))
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function SkillManagerSection(props) {
|
|
1030
|
+
var t = props.t;
|
|
1031
|
+
var ss = react.useState({ loading: true, error: null, data: null }), snapshot = ss[0], setSnapshot = ss[1];
|
|
1032
|
+
var bs = react.useState(false), busy = bs[0], setBusy = bs[1];
|
|
1033
|
+
var qs = react.useState(""), query = qs[0], setQuery = qs[1];
|
|
1034
|
+
var fs = react.useState(""), source = fs[0], setSource = fs[1];
|
|
1035
|
+
var es = react.useState({}), expanded = es[0], setExpanded = es[1];
|
|
1036
|
+
var ms = react.useState(null), modal = ms[0], setModal = ms[1];
|
|
1037
|
+
var rs = react.useState(null), result = rs[0], setResult = rs[1];
|
|
1038
|
+
var fms = react.useState({ root: "dsh", name: "", description: "", body: "" }), form = fms[0], setForm = fms[1];
|
|
1039
|
+
var us = react.useState(null), upload = us[0], setUpload = us[1];
|
|
1040
|
+
var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
|
|
1041
|
+
var cs = react.useState({ path: "", label: "" }), customForm = cs[0], setCustomForm = cs[1];
|
|
1042
|
+
var inflightRef = react.useRef(false);
|
|
1043
|
+
var importInputRef = react.useRef(null);
|
|
1044
|
+
var folderInputRef = react.useRef(null);
|
|
1045
|
+
var pickerOpenRef = react.useRef(false);
|
|
1046
|
+
function refresh(silent) { if (!silent) setSnapshot({ loading: true, error: null, data: snapshot.data }); return callApi("/state").then(function (data) { setSnapshot({ loading: false, error: null, data: data }); return data; }).catch(function (error) { setSnapshot({ loading: false, error: translateError(t, error), data: snapshot.data }); }); }
|
|
1047
|
+
react.useEffect(function () { refresh(false); }, []);
|
|
1048
|
+
function post(path, body, successKey, successParams) { if (inflightRef.current) return Promise.reject({ error: "operation already in progress" }); inflightRef.current = true; setBusy(true); setResult(null); return callApi(path, { body: JSON.stringify(body || {}) }).then(function (data) { setResult({ ok: true, text: successKey ? t(successKey, successParams || data || {}) : t("result.updated") }); return refresh(true).then(function () { return data; }); }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); throw error; }).finally(function () { inflightRef.current = false; setBusy(false); }); }
|
|
1049
|
+
function openDetail(root, skill) { setBusy(true); setDetail(null); setModal("detail"); callApi("/detail", { body: JSON.stringify({ root: root.key, name: skill.name }) }).then(setDetail).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); setModal(null); }).finally(function () { setBusy(false); }); }
|
|
1050
|
+
function openSource() { if (!detail || !detail.path) return; setBusy(true); setResult(null); apiCall("skill-open", { path: detail.path }).then(function (res) { if (res && res.ok) setResult({ ok: true, text: t("result.opened", { path: detail.path }) }); else setResult({ ok: false, text: t("error.action", { error: translateError(t, res) }) }); }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); }); }
|
|
1051
|
+
function updateForm(key, value) { setForm(Object.assign({}, form, { [key]: value })); }
|
|
1052
|
+
// 自定义技能目录:添加 / 移除(走 skill-custom-* ops,成功后静默刷新)。
|
|
1053
|
+
function openCustomDir() {
|
|
1054
|
+
if (busy) return;
|
|
1055
|
+
setCustomForm(Object.assign({}, customForm, { picking: true }));
|
|
1056
|
+
}
|
|
1057
|
+
function submitCustomAdd() {
|
|
1058
|
+
var payload = { path: String(customForm.path || "").trim(), label: String(customForm.label || "").trim() };
|
|
1059
|
+
if (!payload.path) return;
|
|
1060
|
+
setBusy(true); setResult(null);
|
|
1061
|
+
apiCall("skill-custom-add", payload).then(function (res) {
|
|
1062
|
+
if (res && res.ok) { setResult({ ok: true, text: t("result.custom.added", { path: res.data && res.data.path || payload.path }) }); setCustomForm({ path: "", label: "" }); setModal(null); return refresh(true); }
|
|
1063
|
+
setResult({ ok: false, text: (res && res.error) || t("error.action", { error: "unknown" }) });
|
|
1064
|
+
}).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
|
|
1065
|
+
}
|
|
1066
|
+
function submitCustomRemove(key, name) {
|
|
1067
|
+
setBusy(true); setResult(null);
|
|
1068
|
+
apiCall("skill-custom-remove", { key: key }).then(function (res) {
|
|
1069
|
+
if (res && res.ok) { setResult({ ok: true, text: t("result.custom.removed", { name: name }) }); setModal(null); return refresh(true); }
|
|
1070
|
+
setResult({ ok: false, text: (res && res.error) || t("error.action", { error: "unknown" }) });
|
|
1071
|
+
}).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
|
|
1072
|
+
}
|
|
1073
|
+
function submitCreate() { post("/create", form, null).then(function (data) { setModal(null); setForm({ root: "dsh", name: "", description: "", body: "" }); setResult({ ok: true, text: t("result.created", { name: data.name }) }); }).catch(function () {}); }
|
|
1074
|
+
function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
|
|
1075
|
+
function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
|
|
1076
|
+
function submitImport() { if (!upload) return; buildUploadPayload(upload).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
|
|
1077
|
+
var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
|
|
1078
|
+
var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "dsh", label: t("root.dsh") });
|
|
1079
|
+
function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
|
|
1080
|
+
function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
|
|
1081
|
+
var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: rootDisplayName(t, root), count: root.count == null ? root.skills.length : root.count }) }; }));
|
|
1082
|
+
|
|
1083
|
+
function renderSkill(root, skill) {
|
|
1084
|
+
var enabled = isSkillEnabled(skill), key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dsm-shadowed" : enabled ? "dsm-enabled" : "dsm-disabled";
|
|
1085
|
+
return h("div", { key: skill.name, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h("div", { className: "dsm-note" }, skill.description || t("note.missing"))), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null), h("div", { className: "dsm-status " + cls }, t(key)), h("div", { className: "dsm-row-actions" }, root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
|
|
1086
|
+
}
|
|
1087
|
+
function renderRoot(root) {
|
|
1088
|
+
if (activeSource && activeSource !== root.key) return null;
|
|
1089
|
+
var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
|
|
1090
|
+
var rootCount = root.count == null ? root.skills.length : root.count;
|
|
1091
|
+
return h("section", { key: root.key, className: "dsm-source" }, h("div", { className: "dsm-source-head" }, h("button", { type: "button", className: "dsm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dsm-source-title" }, displayName), h("span", { className: "dsm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dsm-tag " + (root.scope === "project" ? "dsm-tag-on" : root.key === "dsh" ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dsm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dsm-path", title: root.path }, root.path)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dsm-source-actions" }, root.key.indexOf("custom-") === 0 ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "custom-remove-confirm", key: root.key, name: displayName }); } }, t("btn.custom.remove")) : null, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dsm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dsm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dsm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
|
|
1095
|
+
var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dsm-head" }, h("div", { className: "dsm-title-block" }, h("div", { className: "dsm-title-row" }, h("h2", { className: "dsm-title" }, t("title")), h(VersionBadge, null)), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")))), h("div", { key: "summary", className: "dsm-summary dsm-summary-3" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dsm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dsm-filters" }, h("input", { className: "dsm-control dsm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dsm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dsm-feedback dsm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dsm-feedback dsm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dsm-empty" }, t("loading")) : h("div", { key: "sources", className: "dsm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dsm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dsm-trash-count" }, (data.trash || []).length))]);
|
|
1096
|
+
|
|
1097
|
+
if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dsm-field" }, h("span", { className: "dsm-label" }, t(field[1])), h("input", { className: "dsm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.body")), h("textarea", { className: "dsm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dsm-help" }, t("create.chat.note"))), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
|
|
1098
|
+
if (modal === "import") content.push(h(Modal, { key: "import", className: "dsm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
|
|
1099
|
+
h("input", { ref: importInputRef, className: "dsm-hidden-input", type: "file", accept: ".zip,.md", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
|
|
1100
|
+
h("input", { ref: folderInputRef, className: "dsm-hidden-input", type: "file", multiple: true, webkitdirectory: "", directory: "", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
|
|
1101
|
+
h("div", { className: "dsm-dropzone", onClick: function () { openNativePicker(importInputRef); }, onDragOver: function (event) { event.preventDefault(); event.currentTarget.classList.add("dsm-dropzone-active"); }, onDragLeave: function (event) { event.currentTarget.classList.remove("dsm-dropzone-active"); }, onDrop: function (event) { event.preventDefault(); event.currentTarget.classList.remove("dsm-dropzone-active"); droppedFiles(event.dataTransfer).then(selectUploadFiles).catch(function (error) { setResult({ ok: false, text: translateError(t, error) }); }); } },
|
|
1102
|
+
h("span", { className: "dsm-dropzone-title" }, t("upload.drop.title")),
|
|
1103
|
+
h("span", { className: "dsm-dropzone-copy" }, t("upload.drop.copy"))),
|
|
1104
|
+
upload ? h("div", { className: "dsm-file", title: upload.name }, h("span", { className: "dsm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dsm-file-name" }, upload.name), h("span", { className: "dsm-file-meta" }, t(countKey("upload.selected", upload.count), { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dsm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
|
|
1105
|
+
result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null,
|
|
1106
|
+
h("div", { className: "dsm-upload-requirements" }, h("div", { className: "dsm-label" }, t("upload.requirements")), h("ul", null, h("li", null, t("upload.requirement.skill")), h("li", null, t("upload.requirement.frontmatter")), h("li", null, t("upload.requirement.copy")))),
|
|
1107
|
+
h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dsm-btn", disabled: busy || !upload, onClick: submitImport }, busy ? t("upload.importing") : t("btn.import.now")))
|
|
1108
|
+
)); if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dsm-detail-path" }, detail.path), h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openSource }, t("btn.open.editor"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dsm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dsm-note" }, t("detail.noIssues"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.frontmatter")), renderFrontmatter(t, detail.frontmatter)), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.body")), h("pre", { className: "dsm-code" }, detail.body || ""))) : h("div", { className: "dsm-empty" }, t("loading"))));
|
|
1109
|
+
if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-count" }, t(countKey("trash.count", data.trash.length), { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dsm-trash-item" }, h("div", { className: "dsm-trash-main" }, h("div", { className: "dsm-name" }, item.name), h("div", { className: "dsm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dsm-empty" }, t("trash.empty"))));
|
|
1110
|
+
if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.trash")))));
|
|
1111
|
+
if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dsm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal("trash"); } }, t("btn.cancel")), h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
|
|
1112
|
+
if (modal === "custom-add") content.push(h(Modal, { key: "custom-add", title: t("custom.add.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("custom.add.path")), h("div", { className: "dsm-dir-row" }, h("input", { className: "dsm-control", value: customForm.path, placeholder: t("custom.add.path.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { path: e.target.value })); } }), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openCustomDir, title: t("custom.add.openDir.title") }, t("btn.openDir")))), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("custom.add.label")), h("input", { className: "dsm-control", value: customForm.label, placeholder: t("custom.add.label.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { label: e.target.value })); } })), h("p", { className: "dsm-help" }, t("custom.add.help")), result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null), customForm.picking ? h(DirPickerModal, { key: "custom-dir-picker", title: t("custom.add.picker.title"), initial: String(customForm.path || ""), closeLabel: t("btn.close"), onClose: function () { setCustomForm(Object.assign({}, customForm, { picking: false })); }, onPick: function (path) { setCustomForm(Object.assign({}, customForm, { path: path, picking: false })); } }) : null, h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dsm-btn", disabled: busy || !String(customForm.path || "").trim(), onClick: submitCustomAdd }, t("custom.add.submit")))));
|
|
1113
|
+
if (modal && modal.type === "custom-remove-confirm") content.push(h(Modal, { key: "custom-remove-confirm", title: t("confirm.custom.remove.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.custom.remove.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { submitCustomRemove(modal.key, modal.name); } }, t("btn.custom.remove")))));
|
|
1114
|
+
return h("section", { className: "dsm-section" }, content);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
var NS = 'dsh-plugin-tool-management'
|
|
1118
|
+
var t = function (key, params) {
|
|
1119
|
+
var s = (DICT.zh && DICT.zh[key]) || key
|
|
1120
|
+
if (params) Object.keys(params).forEach(function (k) { s = String(s).replace("{" + k + "}", params[k]) })
|
|
1121
|
+
return s
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
1125
|
+
ctx.locale.register(NS, DICT)
|
|
1126
|
+
t = ctx.locale.bind(NS)
|
|
1127
|
+
}
|
|
1128
|
+
} catch (e) { /* locale 服务缺失时退回内置词典 */ }
|
|
1129
|
+
|
|
1130
|
+
// ---------- History 页(归档会话管理:恢复 / 永久删除 / 保留期)----------
|
|
1131
|
+
// 折叠自 dsh-archive-manager 的归档会话设置页,但客户端经本插件 HTTP API
|
|
1132
|
+
// 驱动(history-list / -archive / -unarchive / -delete / -retention-*),不依赖
|
|
1133
|
+
// ui-workspace 客户端 store 或 typert remote——会话标题由 host 端 best-effort 读取。
|
|
1134
|
+
function HistoryPage() {
|
|
1135
|
+
var state = React.useState({ loading: true, items: [], workspaces: {}, retentionDays: 0, error: null })
|
|
1136
|
+
var data = state[0], setData = state[1]
|
|
1137
|
+
var modalState = React.useState(null)
|
|
1138
|
+
var modal = modalState[0], setModal = modalState[1]
|
|
1139
|
+
var searchState = React.useState('')
|
|
1140
|
+
var query = searchState[0], setQuery = searchState[1]
|
|
1141
|
+
var busyState = React.useState(false)
|
|
1142
|
+
var busy = busyState[0], setBusy = busyState[1]
|
|
1143
|
+
// 多选集合:勾选的 sessionId。最小 React 支持函数式初值(Set 惰性创建)。
|
|
1144
|
+
var selState = React.useState(function () { return new Set() })
|
|
1145
|
+
var selected = selState[0], setSelected = selState[1]
|
|
1146
|
+
// 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
|
|
1147
|
+
var impState = React.useState(null)
|
|
1148
|
+
var imp = impState[0], setImp = impState[1]
|
|
1149
|
+
var importInputRef = React.useRef(null)
|
|
1150
|
+
// 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
|
|
1151
|
+
var expState = React.useState(null)
|
|
1152
|
+
var exp = expState[0], setExp = expState[1]
|
|
1153
|
+
// 分组卡片折叠状态:key → true(收起)。默认全部展开。
|
|
1154
|
+
var collState = React.useState({})
|
|
1155
|
+
var collapsed = collState[0], setCollapsed = collState[1]
|
|
1156
|
+
|
|
1157
|
+
function refresh() {
|
|
1158
|
+
apiCall('history-list', {}).then(function (r) {
|
|
1159
|
+
if (r && r.ok) {
|
|
1160
|
+
var items = r.items || []
|
|
1161
|
+
setData({ loading: false, items: items, workspaces: r.workspaces || {}, retentionDays: r.retentionDays || 0, error: null })
|
|
1162
|
+
// 裁剪选中集合:已被外部恢复/删除的会话不再计入。
|
|
1163
|
+
setSelected(function (prev) {
|
|
1164
|
+
var has = new Set(items.map(function (it) { return it.sessionId }))
|
|
1165
|
+
var next = new Set()
|
|
1166
|
+
prev.forEach(function (id) { if (has.has(id)) next.add(id) })
|
|
1167
|
+
return next
|
|
1168
|
+
})
|
|
1169
|
+
} else setData({ loading: false, items: [], workspaces: {}, retentionDays: 0, error: (r && r.error) || '加载失败' })
|
|
1170
|
+
})
|
|
1171
|
+
}
|
|
1172
|
+
React.useEffect(function () { refresh() }, [])
|
|
1173
|
+
|
|
1174
|
+
function shortId(id) { var s = String(id || ''); return s.length > 12 ? s.slice(0, 8) + '…' + s.slice(-4) : s }
|
|
1175
|
+
|
|
1176
|
+
var filtered = data.items
|
|
1177
|
+
if (query) {
|
|
1178
|
+
var q = query.toLowerCase()
|
|
1179
|
+
filtered = data.items.filter(function (it) {
|
|
1180
|
+
return (it.title && it.title.toLowerCase().indexOf(q) >= 0)
|
|
1181
|
+
|| (it.sessionId && it.sessionId.toLowerCase().indexOf(q) >= 0)
|
|
1182
|
+
|| (it.cwd && it.cwd.toLowerCase().indexOf(q) >= 0)
|
|
1183
|
+
})
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// 按项目分组(host 提供 workspaces 时启用;否则扁平列表)。
|
|
1187
|
+
var grouped = !!(data.workspaces && Object.keys(data.workspaces).length)
|
|
1188
|
+
var visibleIds = filtered.map(function (it) { return it.sessionId })
|
|
1189
|
+
var groups = []
|
|
1190
|
+
if (grouped) {
|
|
1191
|
+
var byWs = new Map()
|
|
1192
|
+
filtered.forEach(function (it) {
|
|
1193
|
+
var wid = data.workspaces[it.workspaceId] ? it.workspaceId : null
|
|
1194
|
+
var key = wid ? ('ws:' + wid) : 'ungrouped'
|
|
1195
|
+
if (!byWs.has(key)) byWs.set(key, [])
|
|
1196
|
+
byWs.get(key).push(it)
|
|
1197
|
+
})
|
|
1198
|
+
byWs.forEach(function (items2, key) {
|
|
1199
|
+
var wid = key === 'ungrouped' ? null : key.slice(3)
|
|
1200
|
+
var wsRec = wid ? data.workspaces[wid] : null
|
|
1201
|
+
var title = wid ? ((wsRec && wsRec.title) || wid) : '未分组'
|
|
1202
|
+
items2.sort(function (a, b) { return ((b.archivedAt || b.createdAt) || 0) - ((a.archivedAt || a.createdAt) || 0) })
|
|
1203
|
+
var maxAt = 0
|
|
1204
|
+
items2.forEach(function (it) { var at = (it.archivedAt || it.createdAt) || 0; if (at > maxAt) maxAt = at })
|
|
1205
|
+
groups.push({ key: key, title: title, workspaceId: wid, path: wsRec && wsRec.path, items: items2, maxAt: maxAt })
|
|
1206
|
+
})
|
|
1207
|
+
// 组按组内最新时间戳降序;未分组固定最后。
|
|
1208
|
+
groups.sort(function (g1, g2) {
|
|
1209
|
+
if (g1.key === 'ungrouped') return 1
|
|
1210
|
+
if (g2.key === 'ungrouped') return -1
|
|
1211
|
+
return g2.maxAt - g1.maxAt
|
|
1212
|
+
})
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// 「全选」状态:当前过滤结果是否全部选中(按钮文案切换为「取消全选」)。
|
|
1216
|
+
var allVisible = filtered.length > 0 && visibleIds.every(function (id) { return selected.has(id) })
|
|
1217
|
+
|
|
1218
|
+
function doUnarchive(sessionId) {
|
|
1219
|
+
setBusy(true)
|
|
1220
|
+
apiCall('history-unarchive', { sessionId: sessionId }).then(function (r) {
|
|
1221
|
+
setBusy(false)
|
|
1222
|
+
if (r && r.ok) refresh(); else setModal({ type: 'error', message: (r && r.error) || '恢复失败' })
|
|
1223
|
+
})
|
|
1224
|
+
}
|
|
1225
|
+
function doDelete(sessionId) {
|
|
1226
|
+
setBusy(true)
|
|
1227
|
+
apiCall('history-delete', { sessionId: sessionId }).then(function (r) {
|
|
1228
|
+
setBusy(false)
|
|
1229
|
+
if (r && r.ok) { setModal(null); refresh() } else setModal({ type: 'error', message: (r && r.error) || '删除失败' })
|
|
1230
|
+
})
|
|
1231
|
+
}
|
|
1232
|
+
function setRetention(days) {
|
|
1233
|
+
apiCall('history-retention-set', { retentionDays: days }).then(function (r) {
|
|
1234
|
+
if (r && r.ok) setData(Object.assign({}, data, { retentionDays: days }))
|
|
1235
|
+
else setModal({ type: 'error', message: (r && r.error) || '设置失败' })
|
|
1236
|
+
})
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// ---------- 多选 / 批量交互 ----------
|
|
1240
|
+
function toggleSelect(id) {
|
|
1241
|
+
setSelected(function (prev) {
|
|
1242
|
+
var next = new Set(prev)
|
|
1243
|
+
if (next.has(id)) next.delete(id); else next.add(id)
|
|
1244
|
+
return next
|
|
1245
|
+
})
|
|
1246
|
+
}
|
|
1247
|
+
function toggleGroup(ids) {
|
|
1248
|
+
setSelected(function (prev) {
|
|
1249
|
+
var next = new Set(prev)
|
|
1250
|
+
var all = ids.every(function (id) { return next.has(id) })
|
|
1251
|
+
ids.forEach(function (id) { if (all) next.delete(id); else next.add(id) })
|
|
1252
|
+
return next
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
function toggleAllVisible(ids) {
|
|
1256
|
+
if (!ids.length) return
|
|
1257
|
+
setSelected(function (prev) {
|
|
1258
|
+
var next = new Set(prev)
|
|
1259
|
+
var all = ids.every(function (id) { return next.has(id) })
|
|
1260
|
+
ids.forEach(function (id) { if (all) next.delete(id); else next.add(id) })
|
|
1261
|
+
return next
|
|
1262
|
+
})
|
|
1263
|
+
}
|
|
1264
|
+
function doUnarchiveBatch() {
|
|
1265
|
+
var ids = Array.from(selected)
|
|
1266
|
+
if (!ids.length) return
|
|
1267
|
+
setBusy(true)
|
|
1268
|
+
apiCall('history-unarchive-batch', { target: { scope: 'sessions', sessionIds: ids } }).then(function (r) {
|
|
1269
|
+
setBusy(false)
|
|
1270
|
+
if (r && r.ok) {
|
|
1271
|
+
var gone = new Set(r.unarchivedSessionIds || [])
|
|
1272
|
+
setSelected(function (prev) { var next = new Set(prev); gone.forEach(function (id) { next.delete(id) }); return next })
|
|
1273
|
+
refresh()
|
|
1274
|
+
} else setModal({ type: 'error', message: (r && r.error) || '恢复失败' })
|
|
1275
|
+
})
|
|
1276
|
+
}
|
|
1277
|
+
function doDeleteBatch() {
|
|
1278
|
+
var ids = Array.from(selected)
|
|
1279
|
+
if (!ids.length) return
|
|
1280
|
+
setBusy(true)
|
|
1281
|
+
apiCall('history-delete-batch', { target: { scope: 'sessions', sessionIds: ids } }).then(function (r) {
|
|
1282
|
+
setBusy(false)
|
|
1283
|
+
if (r && r.ok) { setModal(null); setSelected(new Set()); refresh() }
|
|
1284
|
+
else setModal({ type: 'error', message: (r && r.error) || '删除失败' })
|
|
1285
|
+
})
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// ---------- 导入对话(从其他 Agent)----------
|
|
1289
|
+
function doImportFile(file) {
|
|
1290
|
+
if (!file || !imp) return
|
|
1291
|
+
var reader = new FileReader()
|
|
1292
|
+
reader.onload = function () {
|
|
1293
|
+
var snap = imp
|
|
1294
|
+
setImp(Object.assign({}, imp, { busy: true, error: null, result: null }))
|
|
1295
|
+
apiCall('history-import', {
|
|
1296
|
+
fileName: String(file.name || 'transcript.txt'),
|
|
1297
|
+
content: String(reader.result || ''),
|
|
1298
|
+
cwd: String(snap.cwd || '').trim(),
|
|
1299
|
+
}).then(function (res) {
|
|
1300
|
+
if (res && res.ok) {
|
|
1301
|
+
setImp(Object.assign({}, snap, { busy: false, result: res }))
|
|
1302
|
+
refresh()
|
|
1303
|
+
} else setImp(Object.assign({}, snap, { busy: false, error: (res && res.error) || '导入失败' }))
|
|
1304
|
+
}).catch(function () {
|
|
1305
|
+
setImp(Object.assign({}, snap, { busy: false, error: '导入失败' }))
|
|
1306
|
+
})
|
|
1307
|
+
}
|
|
1308
|
+
reader.readAsText(file)
|
|
1309
|
+
}
|
|
1310
|
+
function onImportDrop(e) {
|
|
1311
|
+
if (e && e.preventDefault) e.preventDefault()
|
|
1312
|
+
if (imp && imp.busy) return
|
|
1313
|
+
droppedFiles(e && e.dataTransfer).then(function (files) {
|
|
1314
|
+
if (files && files.length) doImportFile(files[0])
|
|
1315
|
+
}).catch(function () {})
|
|
1316
|
+
}
|
|
1317
|
+
function openImportPicker() {
|
|
1318
|
+
setImp({ busy: false, error: null, result: null, cwd: '' })
|
|
1319
|
+
}
|
|
1320
|
+
// ---------- 导出对话:弹窗内选范围/工作区 → 勾选会话 → 写入指定目录 ----------
|
|
1321
|
+
function openExportPicker() {
|
|
1322
|
+
setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
|
|
1323
|
+
// 拉取全部持久化会话(含未归档/冷会话),供选择导出。
|
|
1324
|
+
apiCall('history-sessions', {}).then(function (res) {
|
|
1325
|
+
setExp(function (prev) {
|
|
1326
|
+
if (!prev) return prev
|
|
1327
|
+
return Object.assign({}, prev, {
|
|
1328
|
+
loading: false,
|
|
1329
|
+
error: (res && res.ok) ? prev.error : ((res && res.error) || '加载会话列表失败'),
|
|
1330
|
+
items: (res && res.ok && Array.isArray(res.items)) ? res.items : prev.items,
|
|
1331
|
+
})
|
|
1332
|
+
})
|
|
1333
|
+
}).catch(function () {
|
|
1334
|
+
setExp(function (prev) { return prev ? Object.assign({}, prev, { loading: false, error: '加载会话列表失败' }) : prev })
|
|
1335
|
+
})
|
|
1336
|
+
// 拉取默认导出目录(桌面),仅当用户尚未输入时填充。
|
|
1337
|
+
apiCall('history-export-defaults', {}).then(function (res) {
|
|
1338
|
+
setExp(function (prev) {
|
|
1339
|
+
if (!prev || prev.outDir) return prev
|
|
1340
|
+
if (res && res.ok && typeof res.defaultDir === 'string' && res.defaultDir) {
|
|
1341
|
+
return Object.assign({}, prev, { outDir: res.defaultDir })
|
|
1342
|
+
}
|
|
1343
|
+
return prev
|
|
1344
|
+
})
|
|
1345
|
+
}).catch(function () {})
|
|
1346
|
+
}
|
|
1347
|
+
// 弹出目录选择器:选中后把导出目录回填到输入框。
|
|
1348
|
+
function openExportDir() {
|
|
1349
|
+
if (exp.busy) return
|
|
1350
|
+
setExp(Object.assign({}, exp, { pickingDir: true }))
|
|
1351
|
+
}
|
|
1352
|
+
function toggleExportSelect(id) {
|
|
1353
|
+
setExp(function (prev) {
|
|
1354
|
+
var next = new Set(prev.selectedIds)
|
|
1355
|
+
if (next.has(id)) next.delete(id); else next.add(id)
|
|
1356
|
+
return Object.assign({}, prev, { selectedIds: next })
|
|
1357
|
+
})
|
|
1358
|
+
}
|
|
1359
|
+
function doExport() {
|
|
1360
|
+
var ids = Array.from(exp.selectedIds)
|
|
1361
|
+
if (!ids.length || !String(exp.outDir || '').trim()) return
|
|
1362
|
+
setExp(Object.assign({}, exp, { busy: true, error: null, result: null }))
|
|
1363
|
+
apiCall('history-export', { sessionIds: ids, format: exp.format, outDir: String(exp.outDir).trim() }).then(function (res) {
|
|
1364
|
+
if (res && res.ok) setExp(Object.assign({}, exp, { busy: false, result: res }))
|
|
1365
|
+
else setExp(Object.assign({}, exp, { busy: false, error: (res && res.error) || '导出失败' }))
|
|
1366
|
+
}).catch(function () {
|
|
1367
|
+
setExp(Object.assign({}, exp, { busy: false, error: '导出失败' }))
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
// 把选中的会话批量归档到 History(纳入保留期管理),随后刷新列表与页面。
|
|
1371
|
+
function doArchiveSelected() {
|
|
1372
|
+
var ids = Array.from(exp.selectedIds)
|
|
1373
|
+
if (!ids.length || exp.busy) return
|
|
1374
|
+
setExp(Object.assign({}, exp, { busy: true, error: null, result: null }))
|
|
1375
|
+
apiCall('history-archive-batch', { sessionIds: ids }).then(function (res) {
|
|
1376
|
+
if (res && res.ok) {
|
|
1377
|
+
refresh()
|
|
1378
|
+
return apiCall('history-sessions', {}).then(function (r2) {
|
|
1379
|
+
setExp(function (prev) {
|
|
1380
|
+
if (!prev) return prev
|
|
1381
|
+
return Object.assign({}, prev, {
|
|
1382
|
+
busy: false,
|
|
1383
|
+
selectedIds: new Set(),
|
|
1384
|
+
result: { archived: (res.archived || []).length },
|
|
1385
|
+
items: (r2 && r2.ok && Array.isArray(r2.items)) ? r2.items : prev.items,
|
|
1386
|
+
})
|
|
1387
|
+
})
|
|
1388
|
+
})
|
|
1389
|
+
}
|
|
1390
|
+
setExp(function (prev) { return prev ? Object.assign({}, prev, { busy: false, error: (res && res.error) || '归档失败' }) : prev })
|
|
1391
|
+
}).catch(function () {
|
|
1392
|
+
setExp(function (prev) { return prev ? Object.assign({}, prev, { busy: false, error: '归档失败' }) : prev })
|
|
1393
|
+
})
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
var RETENTION_OPTS = [
|
|
1397
|
+
{ value: 0, label: '永久保留' },
|
|
1398
|
+
{ value: 7, label: '7 天' },
|
|
1399
|
+
{ value: 30, label: '30 天' },
|
|
1400
|
+
]
|
|
1401
|
+
var EXPORT_FORMAT_OPTS = [
|
|
1402
|
+
{ value: 'markdown', label: 'Markdown' },
|
|
1403
|
+
{ value: 'jsonl', label: 'JSONL' },
|
|
1404
|
+
]
|
|
1405
|
+
var EXPORT_ARCH_OPTS = [
|
|
1406
|
+
{ value: 'all', label: '全部会话' },
|
|
1407
|
+
{ value: 'archived', label: '仅已归档' },
|
|
1408
|
+
{ value: 'live', label: '仅未归档' },
|
|
1409
|
+
{ value: 'missing', label: '仅目录丢失' },
|
|
1410
|
+
]
|
|
1411
|
+
// 导出弹窗内的工作区筛选选项与候选会话(随弹窗状态/数据刷新重算)。
|
|
1412
|
+
var exportWsOptions = [{ value: '', label: '全部工作区' }]
|
|
1413
|
+
if (exp && data.workspaces) {
|
|
1414
|
+
Object.keys(data.workspaces).forEach(function (wid) {
|
|
1415
|
+
exportWsOptions.push({ value: 'ws:' + wid, label: (data.workspaces[wid].title || wid) })
|
|
1416
|
+
})
|
|
1417
|
+
exportWsOptions.push({ value: 'ungrouped', label: '未分组' })
|
|
1418
|
+
}
|
|
1419
|
+
var exportItems = []
|
|
1420
|
+
if (exp) {
|
|
1421
|
+
exportItems = (exp.items || []).filter(function (it) {
|
|
1422
|
+
if (exp.archFilter === 'archived' && !it.archived) return false
|
|
1423
|
+
if (exp.archFilter === 'live' && it.archived) return false
|
|
1424
|
+
if (exp.archFilter === 'missing' && !it.cwdMissing) return false
|
|
1425
|
+
if (!exp.wsFilter) return true
|
|
1426
|
+
if (exp.wsFilter === 'ungrouped') return !it.workspaceId || !data.workspaces[it.workspaceId]
|
|
1427
|
+
var wid = exp.wsFilter.slice(3)
|
|
1428
|
+
return it.workspaceId === wid && !!data.workspaces[wid]
|
|
1429
|
+
})
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function renderRow(it) {
|
|
1433
|
+
return React.createElement('div', { className: 'dsm-hist-row', key: it.sessionId },
|
|
1434
|
+
React.createElement('input', { type: 'checkbox', className: 'dsm-hist-check', checked: selected.has(it.sessionId), onChange: function () { toggleSelect(it.sessionId) } }),
|
|
1435
|
+
React.createElement('div', { className: 'dsm-hist-main' },
|
|
1436
|
+
React.createElement('div', { className: 'dsm-hist-title' }, it.title || ('Session ' + shortId(it.sessionId))),
|
|
1437
|
+
it.cwd ? React.createElement('div', { className: 'dsm-hist-cwd', title: it.cwd }, it.cwd) : null),
|
|
1438
|
+
React.createElement('div', { className: 'dsm-hist-actions' },
|
|
1439
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { doUnarchive(it.sessionId) } }, '恢复'),
|
|
1440
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'delete', sessionId: it.sessionId, title: it.title || ('Session ' + shortId(it.sessionId)) }) } }, '永久删除')))
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// 分组卡片:dsm-source 卡片 + 可折叠表头(与 MCP / Skills 页一致的源卡片语言)。
|
|
1444
|
+
function toggleGroupCollapse(key) {
|
|
1445
|
+
setCollapsed(function (prev) {
|
|
1446
|
+
var next = Object.assign({}, prev)
|
|
1447
|
+
next[key] = !next[key]
|
|
1448
|
+
return next
|
|
1449
|
+
})
|
|
1450
|
+
}
|
|
1451
|
+
function renderGroup(g) {
|
|
1452
|
+
var groupIds = g.items.map(function (it) { return it.sessionId })
|
|
1453
|
+
var allChecked = groupIds.length > 0 && groupIds.every(function (id) { return selected.has(id) })
|
|
1454
|
+
var open = !collapsed[g.key]
|
|
1455
|
+
return React.createElement('div', { className: 'dsm-source', key: 'g:' + g.key },
|
|
1456
|
+
React.createElement('div', { className: 'dsm-source-head dsm-hist-group-head' },
|
|
1457
|
+
React.createElement('input', { type: 'checkbox', className: 'dsm-hist-group-check', checked: allChecked, onChange: function () { toggleGroup(groupIds) }, 'aria-label': '全选组 ' + g.title }),
|
|
1458
|
+
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleGroupCollapse(g.key) } },
|
|
1459
|
+
React.createElement('span', { className: 'dsm-source-title dsm-hist-group-title', title: g.title }, g.title),
|
|
1460
|
+
React.createElement('span', { className: 'dsm-count' }, g.items.length + ' 个')),
|
|
1461
|
+
g.path ? React.createElement('span', { className: 'dsm-path', title: g.path }, g.path) : null),
|
|
1462
|
+
open ? React.createElement('div', { className: 'dsm-source-body' }, g.items.map(renderRow)) : null)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
1466
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
1467
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
1468
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
1469
|
+
React.createElement('h2', { className: 'dsm-title' }, 'History'),
|
|
1470
|
+
React.createElement(VersionBadge, null)),
|
|
1471
|
+
React.createElement('p', { className: 'dsm-desc' }, '管理已归档会话;超保留期自动清理')),
|
|
1472
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
1473
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, allVisible ? '取消全选' : '全选'),
|
|
1474
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openImportPicker }, '导入对话'),
|
|
1475
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openExportPicker }, '导出对话'),
|
|
1476
|
+
selected.size > 0 ? React.createElement('div', { className: 'dsm-hist-batch' },
|
|
1477
|
+
React.createElement('span', { className: 'dsm-hist-batch-count' }, '已选 ' + selected.size + ' 项'),
|
|
1478
|
+
React.createElement('div', { className: 'dsm-hist-batch-actions' },
|
|
1479
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { doUnarchiveBatch() } }, '恢复所选 (' + selected.size + ')'),
|
|
1480
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'deleteBatch', count: selected.size }) } }, '删除所选 (' + selected.size + ')'))) : null)),
|
|
1481
|
+
React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
|
|
1482
|
+
[[data.items.length, '个归档'], [grouped ? groups.length : '—', '个项目'], [selected.size, '个已选']].map(function (item) {
|
|
1483
|
+
return React.createElement('div', { key: item[1], className: 'dsm-stat' },
|
|
1484
|
+
React.createElement('strong', null, item[0]), item[1])
|
|
1485
|
+
})),
|
|
1486
|
+
React.createElement('div', { className: 'dsm-filters' },
|
|
1487
|
+
React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: '搜索标题 / 会话 ID / 项目路径', value: query, onChange: function (e) { setQuery(e.target.value) } }),
|
|
1488
|
+
React.createElement('div', { className: 'dsm-source-filter' },
|
|
1489
|
+
React.createElement(SourceSelect, {
|
|
1490
|
+
options: RETENTION_OPTS, value: data.retentionDays, onChange: function (v) { setRetention(Number(v)) },
|
|
1491
|
+
}))),
|
|
1492
|
+
data.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
|
|
1493
|
+
data.loading ? React.createElement('div', { className: 'dsm-empty' }, '加载中…')
|
|
1494
|
+
: filtered.length === 0 ? React.createElement('div', { className: 'dsm-empty' }, query ? '无匹配的归档会话' : '暂无归档会话')
|
|
1495
|
+
: React.createElement('div', { className: 'dsm-sources' },
|
|
1496
|
+
grouped ? groups.map(renderGroup) : React.createElement('div', { className: 'dsm-source' }, filtered.map(renderRow))),
|
|
1497
|
+
modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: '永久删除?', closeLabel: '取消', onClose: function () { setModal(null) } },
|
|
1498
|
+
React.createElement('p', { className: 'dsm-help' }, '永久删除会话「' + modal.title + '」及其全部记录,不可恢复'),
|
|
1499
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
1500
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, '取消'),
|
|
1501
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDelete(modal.sessionId) } }, '确认删除'))) : null,
|
|
1502
|
+
modal && modal.type === 'deleteBatch' ? React.createElement(Modal, { key: 'delb', title: '永久删除?', closeLabel: '取消', onClose: function () { setModal(null) } },
|
|
1503
|
+
React.createElement('p', { className: 'dsm-help' }, '永久删除所选 ' + modal.count + ' 个会话及其全部记录,不可恢复'),
|
|
1504
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
1505
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, '取消'),
|
|
1506
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDeleteBatch() } }, '确认删除'))) : null,
|
|
1507
|
+
imp ? React.createElement(Modal, { key: 'imp', className: 'dsm-modal-import', title: '导入对话', closeLabel: '关闭', onClose: function () { setImp(null) } },
|
|
1508
|
+
React.createElement('p', { className: 'dsm-help' }, '导入其他 Agent 的对话记录,生成可继续对话的新会话'),
|
|
1509
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1510
|
+
React.createElement('label', { className: 'dsm-label' }, '项目目录(可留空)'),
|
|
1511
|
+
React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: '绝对路径,可留空(会话归入未分组)', value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } })),
|
|
1512
|
+
React.createElement('div', { className: 'dsm-dropzone', onDragOver: function (e) { if (e.preventDefault) e.preventDefault() }, onDrop: onImportDrop, onClick: function () { if (importInputRef.current) importInputRef.current.click() } },
|
|
1513
|
+
React.createElement('div', { className: 'dsm-dropzone-title' }, '选择或拖入对话文件'),
|
|
1514
|
+
React.createElement('div', { className: 'dsm-dropzone-copy' }, '.jsonl / .json / .md / .txt')),
|
|
1515
|
+
React.createElement('input', { type: 'file', ref: importInputRef, className: 'dsm-hidden-input', accept: '.jsonl,.json,.md,.markdown,.txt', onChange: function (e) { var f = e.target && e.target.files && e.target.files[0]; if (f) doImportFile(f) } }),
|
|
1516
|
+
imp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(imp.error)) : null,
|
|
1517
|
+
imp.result ? React.createElement('div', { className: 'dsm-feedback' }, '已创建会话 ' + imp.result.sessionId + '(' + imp.result.count + ' 条消息),可在 DSH 会话列表中继续对话。') : null) : null,
|
|
1518
|
+
exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-import', title: '导出对话', closeLabel: '关闭', onClose: function () { setExp(null) } },
|
|
1519
|
+
React.createElement('p', { className: 'dsm-help' }, '导出选中会话为转录文件,可再次导入'),
|
|
1520
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1521
|
+
React.createElement('label', { className: 'dsm-label' }, '导出目录(绝对路径,自动创建)'),
|
|
1522
|
+
React.createElement('div', { className: 'dsm-dir-row' },
|
|
1523
|
+
React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: 'D:\\backups\\dsh\\exports', value: exp.outDir || '', onChange: function (e) { setExp(Object.assign({}, exp, { outDir: e.target.value })) } }),
|
|
1524
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: exp.busy, onClick: openExportDir, title: '浏览并选择文件夹' }, '选择')),
|
|
1525
|
+
exp.pickingDir ? React.createElement(DirPickerModal, { key: 'exp-dir-picker', title: '选择导出目录', initial: String(exp.outDir || ''), closeLabel: '关闭', onClose: function () { setExp(Object.assign({}, exp, { pickingDir: false })) }, onPick: function (path) { setExp(Object.assign({}, exp, { outDir: path, pickingDir: false })) } }) : null),
|
|
1526
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1527
|
+
React.createElement('label', { className: 'dsm-label' }, '格式'),
|
|
1528
|
+
React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
|
|
1529
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1530
|
+
React.createElement('label', { className: 'dsm-label' }, '会话范围'),
|
|
1531
|
+
React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
|
|
1532
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1533
|
+
React.createElement('label', { className: 'dsm-label' }, '工作区'),
|
|
1534
|
+
React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } })),
|
|
1535
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
1536
|
+
React.createElement('div', { className: 'dsm-label' }, '会话(' + exportItems.length + ' 个)'),
|
|
1537
|
+
React.createElement('div', { className: 'dsm-source' },
|
|
1538
|
+
exp.loading ? React.createElement('div', { className: 'dsm-empty' }, '加载会话列表…')
|
|
1539
|
+
: exportItems.length ? exportItems.map(function (it) {
|
|
1540
|
+
return React.createElement('label', { key: it.sessionId, className: 'dsm-hist-row' },
|
|
1541
|
+
React.createElement('input', { type: 'checkbox', className: 'dsm-hist-check', checked: exp.selectedIds.has(it.sessionId), onChange: function () { toggleExportSelect(it.sessionId) } }),
|
|
1542
|
+
React.createElement('div', { className: 'dsm-hist-main' },
|
|
1543
|
+
React.createElement('div', { className: 'dsm-hist-title' }, it.title || ('Session ' + shortId(it.sessionId))),
|
|
1544
|
+
it.cwd ? React.createElement('div', { className: 'dsm-hist-cwd' + (it.cwdMissing ? ' dsm-hist-cwd-missing' : ''), title: (it.cwdMissing ? '⚠ 工作区目录已不存在:' : '') + it.cwd }, (it.cwdMissing ? '⚠ ' : '') + it.cwd) : null),
|
|
1545
|
+
React.createElement('div', { className: 'dsm-hist-actions' },
|
|
1546
|
+
React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? '已归档' : '未归档')))
|
|
1547
|
+
}) : React.createElement('div', { className: 'dsm-empty' }, '没有符合条件的会话'))),
|
|
1548
|
+
exp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(exp.error)) : null,
|
|
1549
|
+
exp.result ? React.createElement('div', { className: 'dsm-feedback' },
|
|
1550
|
+
exp.result.exported
|
|
1551
|
+
? ('已导出 ' + (exp.result.exported || []).length + ' 个会话到 ' + String(exp.outDir || '').trim()
|
|
1552
|
+
+ ((exp.result.skipped && exp.result.skipped.length) ? ';跳过 ' + exp.result.skipped.length + ' 个(' + exp.result.skipped.map(function (s) { return s.sessionId }).join('、') + ')' : ''))
|
|
1553
|
+
: ('已归档 ' + exp.result.archived + ' 个会话到 History,可到 History 页继续管理')) : null,
|
|
1554
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
1555
|
+
React.createElement('span', { className: 'dsm-hist-batch-count' }, '已选 ' + exp.selectedIds.size + ' 个'),
|
|
1556
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: exp.busy || !exp.selectedIds.size, onClick: doArchiveSelected, title: '把选中的会话收进 History,纳入保留期管理' }, '归档所选'),
|
|
1557
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: exp.busy || !exp.selectedIds.size || !String(exp.outDir || '').trim(), onClick: doExport }, exp.busy ? '处理中…' : '导出 ' + exp.selectedIds.size + ' 个会话'))) : null,
|
|
1558
|
+
modal && modal.type === 'error' ? React.createElement(Modal, { key: 'err', title: '操作失败', closeLabel: '关闭', onClose: function () { setModal(null) } },
|
|
1559
|
+
React.createElement('p', { className: 'dsm-help' }, String(modal.message || '未知错误')),
|
|
1560
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
1561
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { setModal(null) } }, '关闭'))) : null)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// ---------- Rules 页(v0.3 规则层:场景 = 一级目录,勾选即自动生效)----------
|
|
1565
|
+
// 场景/分组名校验:与宿主 src/rules/service.ts 的 isValidGroupSegment 保持一致。
|
|
1566
|
+
// 变更单 01 放宽为**任意 Unicode**(`办公` / `日常` 必须通过),只保留文件系统安全约束。
|
|
1567
|
+
var SCENE_RESERVED_RE = /[<>:"|?*\\/]/
|
|
1568
|
+
function isValidSceneSegment(segment) {
|
|
1569
|
+
var s = String(segment == null ? '' : segment)
|
|
1570
|
+
if (s === '' || s === '.' || s === '..') return false
|
|
1571
|
+
if (s.length > 64) return false
|
|
1572
|
+
if (s.charAt(0) === '.') return false
|
|
1573
|
+
if (SCENE_RESERVED_RE.test(s)) return false
|
|
1574
|
+
if (s !== s.trim()) return false
|
|
1575
|
+
if (/[.\s]$/.test(s)) return false
|
|
1576
|
+
return true
|
|
1577
|
+
}
|
|
1578
|
+
function isValidScenePath(group) {
|
|
1579
|
+
var s = String(group == null ? '' : group)
|
|
1580
|
+
if (s === '' || s.charAt(0) === '/' || s.charAt(s.length - 1) === '/') return false
|
|
1581
|
+
return s.split('/').every(isValidSceneSegment)
|
|
1582
|
+
}
|
|
1583
|
+
var RULE_FORM_OPTIONS = [
|
|
1584
|
+
{ value: 'flat', label: 'flat' },
|
|
1585
|
+
{ value: 'bundle', label: 'bundle' },
|
|
1586
|
+
] // ---------- 场景记忆页(场景 = scene-memory/ 下的一级目录;内容自动生效)----------
|
|
1587
|
+
//
|
|
1588
|
+
// 变更单 01 之后 Rules 与 Scenes 合并为本页:**场景(一级目录)是分组维度,
|
|
1589
|
+
// 记忆(.md)是内容**;勾选启用后该目录树内所有 .md 正文自动进入系统提示词。
|
|
1590
|
+
// 原 Scenes 页的「agent preset ↔ 分组绑定」降级为页底的「高级」折叠区(可选)。
|
|
1591
|
+
function SceneMemoryPage() {
|
|
1592
|
+
var EMPTY_STATS = { total: 0, enabled: 0, scenes: 0 }
|
|
1593
|
+
var EMPTY_MEMORY = { usedBytes: 0, maxBytes: 0, truncated: false }
|
|
1594
|
+
var state = React.useState({
|
|
1595
|
+
loading: true, error: null,
|
|
1596
|
+
rules: [], scenes: [], activeMode: 'all',
|
|
1597
|
+
sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS,
|
|
1598
|
+
})
|
|
1599
|
+
var data = state[0], setData = state[1]
|
|
1600
|
+
var qs = React.useState('')
|
|
1601
|
+
var query = qs[0], setQuery = qs[1]
|
|
1602
|
+
var sfs = React.useState('')
|
|
1603
|
+
var sceneFilter = sfs[0], setSceneFilter = sfs[1]
|
|
1604
|
+
var cs = React.useState({})
|
|
1605
|
+
var collapsed = cs[0], setCollapsed = cs[1]
|
|
1606
|
+
var ms = React.useState(null)
|
|
1607
|
+
var modal = ms[0], setModal = ms[1]
|
|
1608
|
+
var rs = React.useState(null)
|
|
1609
|
+
var result = rs[0], setResult = rs[1]
|
|
1610
|
+
var bs = React.useState(false)
|
|
1611
|
+
var busy = bs[0], setBusy = bs[1]
|
|
1612
|
+
var es = React.useState(null)
|
|
1613
|
+
var editor = es[0], setEditor = es[1]
|
|
1614
|
+
// 新建场景弹窗(场景 = scene-memory/ 下的一级目录)。
|
|
1615
|
+
var scfs = React.useState({ name: '', error: null })
|
|
1616
|
+
var sceneForm = scfs[0], setSceneForm = scfs[1]
|
|
1617
|
+
// 记忆回收站:列表在打开弹窗时按需拉取(rules-trash-list)。
|
|
1618
|
+
var trs = React.useState({ loading: false, error: null, entries: [] })
|
|
1619
|
+
var trash = trs[0], setTrash = trs[1]
|
|
1620
|
+
// bundle 附件的隐藏文件选择器;上限与服务端 MAX_ATTACH_ENTRY_BYTES 对齐。
|
|
1621
|
+
var attachRef = React.useRef(null)
|
|
1622
|
+
var ATTACH_MAX_MB = 8
|
|
1623
|
+
var ATTACH_MAX_BYTES = ATTACH_MAX_MB << 20
|
|
1624
|
+
|
|
1625
|
+
/** 场景名 = 分组路径的第一段(`web/frontend` 属于场景 `web`)。 */
|
|
1626
|
+
function sceneOf(group) {
|
|
1627
|
+
var s = String(group == null ? '' : group)
|
|
1628
|
+
var i = s.indexOf('/')
|
|
1629
|
+
return i >= 0 ? s.slice(0, i) : s
|
|
1630
|
+
}
|
|
1631
|
+
function sceneLabel(name) { return name === '' ? t('memory.scene.global') : name }
|
|
1632
|
+
function toggleCollapse(key) {
|
|
1633
|
+
setCollapsed(function (prev) {
|
|
1634
|
+
var next = Object.assign({}, prev)
|
|
1635
|
+
next[key] = !next[key]
|
|
1636
|
+
return next
|
|
1637
|
+
})
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function refresh(silent) {
|
|
1641
|
+
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
1642
|
+
apiCall('rules-list', {}).then(function (r) {
|
|
1643
|
+
if (r && r.ok) {
|
|
1644
|
+
setData({
|
|
1645
|
+
loading: false, error: null,
|
|
1646
|
+
rules: r.rules || [], scenes: r.scenes || [],
|
|
1647
|
+
activeMode: r.activeMode === 'custom' ? 'custom' : 'all',
|
|
1648
|
+
sceneMemory: r.sceneMemory || EMPTY_MEMORY,
|
|
1649
|
+
stats: r.stats || EMPTY_STATS,
|
|
1650
|
+
})
|
|
1651
|
+
} else {
|
|
1652
|
+
setData({ loading: false, error: translateError(t, r), rules: [], scenes: [], activeMode: 'all', sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS })
|
|
1653
|
+
}
|
|
1654
|
+
}).catch(function (e) {
|
|
1655
|
+
setData({ loading: false, error: String((e && e.message) || e), rules: [], scenes: [], activeMode: 'all', sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS })
|
|
1656
|
+
})
|
|
1657
|
+
}
|
|
1658
|
+
React.useEffect(function () { refresh() }, [])
|
|
1659
|
+
|
|
1660
|
+
// 成功提示自动消失(2.6s):浮层只是「刚做完什么」的短暂回执,不该长期占屏。
|
|
1661
|
+
// 错误(ok === false)不自动清理,一直留到下一次操作把它替换掉。
|
|
1662
|
+
React.useEffect(function () {
|
|
1663
|
+
if (!result || result.ok !== true) return undefined
|
|
1664
|
+
var timer = setTimeout(function () { setResult(null) }, 2600)
|
|
1665
|
+
return function () { clearTimeout(timer) }
|
|
1666
|
+
}, [result])
|
|
1667
|
+
|
|
1668
|
+
// ── 启用场景(全局持久化,切换后下一个请求即生效)──
|
|
1669
|
+
// 勾满全部场景 → 回到「默认全部启用」(清空显式集合,新建场景自动生效)。
|
|
1670
|
+
function setActiveScenes(names) {
|
|
1671
|
+
if (busy) return
|
|
1672
|
+
setBusy(true); setResult(null)
|
|
1673
|
+
var real = (data.scenes || []).filter(function (s) { return !s.shared }).map(function (s) { return s.name })
|
|
1674
|
+
var payload = real.length > 0 && real.every(function (n) { return names.indexOf(n) >= 0 }) ? { all: true } : { scenes: names }
|
|
1675
|
+
apiCall('rules-set-active', payload).then(function (res) {
|
|
1676
|
+
setBusy(false)
|
|
1677
|
+
if (res && res.ok) { setResult({ ok: true, text: t('memory.result.active') }); refresh(true) }
|
|
1678
|
+
else setResult({ ok: false, text: translateError(t, res) })
|
|
1679
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
1680
|
+
}
|
|
1681
|
+
function toggleScene(scene) {
|
|
1682
|
+
if (busy || scene.shared) return
|
|
1683
|
+
var names = (data.scenes || []).filter(function (s) { return s.active && !s.shared }).map(function (s) { return s.name })
|
|
1684
|
+
var i = names.indexOf(scene.name)
|
|
1685
|
+
if (i >= 0) names.splice(i, 1)
|
|
1686
|
+
else names.push(scene.name)
|
|
1687
|
+
setActiveScenes(names)
|
|
1688
|
+
}
|
|
1689
|
+
function enableAllScenes() {
|
|
1690
|
+
if (busy) return
|
|
1691
|
+
setActiveScenes((data.scenes || []).filter(function (s) { return !s.shared }).map(function (s) { return s.name }))
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// ── 记忆 CRUD ──
|
|
1695
|
+
function openCreate(sceneName) {
|
|
1696
|
+
setEditor({ mode: 'create', id: '', group: sceneName || '', name: '', description: '', body: '', form: 'flat', path: '', error: null, files: [], attachments: [] })
|
|
1697
|
+
setModal({ type: 'editor' })
|
|
1698
|
+
}
|
|
1699
|
+
function openEditor(rule) {
|
|
1700
|
+
if (busy) return
|
|
1701
|
+
setBusy(true); setResult(null)
|
|
1702
|
+
apiCall('rules-read', { id: rule.id }).then(function (res) {
|
|
1703
|
+
setBusy(false)
|
|
1704
|
+
if (res && res.ok && res.rule) {
|
|
1705
|
+
var rd = res.rule
|
|
1706
|
+
setEditor({ mode: 'edit', id: rd.id, group: rd.group, name: rd.name, description: rd.description || '', body: rd.body || '', form: rd.form === 'bundle' ? 'bundle' : 'flat', path: rd.path || '', error: null, files: [], attachments: rd.attachments || [] })
|
|
1707
|
+
setModal({ type: 'editor' })
|
|
1708
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
1709
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
1710
|
+
}
|
|
1711
|
+
function closeEditor() { setModal(null) }
|
|
1712
|
+
|
|
1713
|
+
// ── bundle 附件(只有 bundle 有目录可放;flat 是单文件)──
|
|
1714
|
+
function fmtBytes(n) {
|
|
1715
|
+
var v = Number(n) || 0
|
|
1716
|
+
if (v < 1024) return v + ' B'
|
|
1717
|
+
if (v < 1024 * 1024) return Math.ceil(v / 1024) + ' KB'
|
|
1718
|
+
return (Math.round((v / (1024 * 1024)) * 10) / 10) + ' MB'
|
|
1719
|
+
}
|
|
1720
|
+
function pickAttach() { if (attachRef.current) attachRef.current.click() }
|
|
1721
|
+
function onAttachPicked(ev) {
|
|
1722
|
+
var picked = Array.prototype.slice.call((ev.target && ev.target.files) || [])
|
|
1723
|
+
if (ev.target) ev.target.value = '' // 允许连续选中同一个文件
|
|
1724
|
+
if (!picked.length) return
|
|
1725
|
+
Promise.all(picked.map(function (f) {
|
|
1726
|
+
return f.arrayBuffer().then(function (buf) {
|
|
1727
|
+
return { name: String(f.name || ''), size: buf.byteLength, data: bytesToBase64(buf) }
|
|
1728
|
+
})
|
|
1729
|
+
})).then(function (entries) {
|
|
1730
|
+
var tooBig = entries.filter(function (e) { return e.size > ATTACH_MAX_BYTES })
|
|
1731
|
+
var keep = entries.filter(function (e) { return e.size <= ATTACH_MAX_BYTES })
|
|
1732
|
+
setEditor(function (prev) {
|
|
1733
|
+
var next = Object.assign({}, prev, { files: (prev.files || []).concat(keep) })
|
|
1734
|
+
// 超限的当场拦下(服务端还会再拦一次),其余照常加入
|
|
1735
|
+
if (tooBig.length) next.error = t('memory.attach.tooLarge', { name: tooBig[0].name, limit: ATTACH_MAX_MB })
|
|
1736
|
+
return next
|
|
1737
|
+
})
|
|
1738
|
+
}).catch(function (e) { setEditor(function (prev) { return Object.assign({}, prev, { error: String((e && e.message) || e) }) }) })
|
|
1739
|
+
}
|
|
1740
|
+
function dropPending(index) {
|
|
1741
|
+
setEditor(function (prev) {
|
|
1742
|
+
var next = (prev.files || []).slice()
|
|
1743
|
+
next.splice(index, 1)
|
|
1744
|
+
return Object.assign({}, prev, { files: next })
|
|
1745
|
+
})
|
|
1746
|
+
}
|
|
1747
|
+
function removeAttachment(name) {
|
|
1748
|
+
if (busy || !editor) return
|
|
1749
|
+
setBusy(true)
|
|
1750
|
+
apiCall('rules-detach', { id: editor.id, name: name }).then(function (res) {
|
|
1751
|
+
setBusy(false)
|
|
1752
|
+
if (res && res.ok) setEditor(function (prev) { return Object.assign({}, prev, { attachments: res.attachments || [] }) })
|
|
1753
|
+
else setEditor(function (prev) { return Object.assign({}, prev, { error: translateError(t, res) }) })
|
|
1754
|
+
}).catch(function (e) { setBusy(false); setEditor(function (prev) { return Object.assign({}, prev, { error: String((e && e.message) || e) }) }) })
|
|
1755
|
+
}
|
|
1756
|
+
/** 附件与记忆本体分两步:先存记忆,再传附件;附件失败只如实报错,不回滚已保存的记忆。 */
|
|
1757
|
+
function flushAttachments(id, files) {
|
|
1758
|
+
if (!files || !files.length) return Promise.resolve({ ok: true, empty: true })
|
|
1759
|
+
return apiCall('rules-attach', { id: id, files: files.map(function (f) { return { path: f.name, data: f.data } }) })
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// ── 场景(= scene-memory/ 下的一级目录)建 / 删 ──
|
|
1763
|
+
function openCreateScene() {
|
|
1764
|
+
setSceneForm({ name: '', error: null })
|
|
1765
|
+
setModal({ type: 'scene-create' })
|
|
1766
|
+
}
|
|
1767
|
+
function submitCreateScene() {
|
|
1768
|
+
var name = String(sceneForm.name || '').trim()
|
|
1769
|
+
if (!isValidScenePath(name)) { setSceneForm(Object.assign({}, sceneForm, { error: t('error.rules.invalidGroup') })); return }
|
|
1770
|
+
setBusy(true)
|
|
1771
|
+
apiCall('rules-create-scene', { name: name }).then(function (res) {
|
|
1772
|
+
setBusy(false)
|
|
1773
|
+
if (res && res.ok) {
|
|
1774
|
+
setModal(null)
|
|
1775
|
+
setResult({ ok: true, text: t('memory.result.sceneCreated', { name: name }) })
|
|
1776
|
+
refresh(true)
|
|
1777
|
+
} else setSceneForm(Object.assign({}, sceneForm, { error: translateError(t, res) }))
|
|
1778
|
+
}).catch(function (e) { setBusy(false); setSceneForm(Object.assign({}, sceneForm, { error: String((e && e.message) || e) })) })
|
|
1779
|
+
}
|
|
1780
|
+
function submitDeleteScene(name) {
|
|
1781
|
+
setBusy(true)
|
|
1782
|
+
apiCall('rules-remove-scene', { name: name }).then(function (res) {
|
|
1783
|
+
setBusy(false); setModal(null)
|
|
1784
|
+
if (res && res.ok) { setResult({ ok: true, text: t('memory.result.sceneRemoved', { name: name }) }); refresh(true) }
|
|
1785
|
+
else setResult({ ok: false, text: translateError(t, res) })
|
|
1786
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// ── 记忆回收站(rules-remove 移入 / rules-restore 恢复 / rules-trash-remove 永久删除)──
|
|
1790
|
+
// 此前删除后只能看到一句"回收站 ID",界面上无法恢复;这里把入口补齐。
|
|
1791
|
+
function loadTrash() {
|
|
1792
|
+
setTrash({ loading: true, error: null, entries: [] })
|
|
1793
|
+
apiCall('rules-trash-list', {}).then(function (res) {
|
|
1794
|
+
if (res && res.ok) setTrash({ loading: false, error: null, entries: res.entries || [] })
|
|
1795
|
+
else setTrash({ loading: false, error: translateError(t, res), entries: [] })
|
|
1796
|
+
}).catch(function (e) { setTrash({ loading: false, error: String((e && e.message) || e), entries: [] }) })
|
|
1797
|
+
}
|
|
1798
|
+
function openTrash() { setModal({ type: 'trash' }); loadTrash() }
|
|
1799
|
+
function submitRestore(entry) {
|
|
1800
|
+
if (busy) return
|
|
1801
|
+
setBusy(true)
|
|
1802
|
+
apiCall('rules-restore', { trashId: entry.trashId }).then(function (res) {
|
|
1803
|
+
setBusy(false)
|
|
1804
|
+
if (res && res.ok) {
|
|
1805
|
+
setModal(null)
|
|
1806
|
+
setResult({ ok: true, text: t('memory.result.restored', { name: entry.name }) })
|
|
1807
|
+
refresh(true)
|
|
1808
|
+
} else setTrash(Object.assign({}, trash, { error: translateError(t, res) }))
|
|
1809
|
+
}).catch(function (e) { setBusy(false); setTrash(Object.assign({}, trash, { error: String((e && e.message) || e) })) })
|
|
1810
|
+
}
|
|
1811
|
+
function submitTrashDelete(entry) {
|
|
1812
|
+
if (busy) return
|
|
1813
|
+
setBusy(true)
|
|
1814
|
+
apiCall('rules-trash-remove', { trashId: entry.trashId }).then(function (res) {
|
|
1815
|
+
setBusy(false)
|
|
1816
|
+
if (res && res.ok) {
|
|
1817
|
+
setResult({ ok: true, text: t('memory.result.trashRemoved', { name: entry.name }) })
|
|
1818
|
+
setModal({ type: 'trash' })
|
|
1819
|
+
loadTrash()
|
|
1820
|
+
} else setTrash(Object.assign({}, trash, { error: translateError(t, res) }))
|
|
1821
|
+
}).catch(function (e) { setBusy(false); setTrash(Object.assign({}, trash, { error: String((e && e.message) || e) })) })
|
|
1822
|
+
}
|
|
1823
|
+
// ── 单条记忆启停(索引层开关,不改文件;停用的记忆仍留在磁盘上)──
|
|
1824
|
+
function toggleRule(rule) {
|
|
1825
|
+
if (busy) return
|
|
1826
|
+
setBusy(true); setResult(null)
|
|
1827
|
+
apiCall('rules-toggle', { id: rule.id, enabled: rule.enabled === false }).then(function (res) {
|
|
1828
|
+
setBusy(false)
|
|
1829
|
+
if (res && res.ok) { setResult({ ok: true, text: t('memory.result.toggled', { name: rule.name }) }); refresh(true) }
|
|
1830
|
+
else setResult({ ok: false, text: translateError(t, res) })
|
|
1831
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/** 记忆存好后统一收尾:上传附件 → 关弹窗 → 报结果(附件失败不回滚记忆)。 */
|
|
1835
|
+
function finishEditor(id, name, pending, successKey, attachedKey) {
|
|
1836
|
+
return flushAttachments(id, pending).then(function (att) {
|
|
1837
|
+
setBusy(false)
|
|
1838
|
+
setModal(null); setEditor(null)
|
|
1839
|
+
if (att && att.ok === false) setResult({ ok: false, text: t('memory.result.attachFailed', { error: translateError(t, att) }) })
|
|
1840
|
+
else if (pending.length) setResult({ ok: true, text: t(attachedKey, { name: name, count: pending.length }) })
|
|
1841
|
+
else setResult({ ok: true, text: t(successKey, { name: name }) })
|
|
1842
|
+
refresh(true)
|
|
1843
|
+
})
|
|
1844
|
+
}
|
|
1845
|
+
function submitCreate() {
|
|
1846
|
+
var payload = {
|
|
1847
|
+
group: String(editor.group || '').trim(),
|
|
1848
|
+
name: String(editor.name || '').trim(),
|
|
1849
|
+
description: String(editor.description || '').trim(),
|
|
1850
|
+
body: editor.body || '',
|
|
1851
|
+
form: editor.form || 'flat',
|
|
1852
|
+
}
|
|
1853
|
+
var pending = editor.files || []
|
|
1854
|
+
setBusy(true)
|
|
1855
|
+
apiCall('rules-create', payload).then(function (res) {
|
|
1856
|
+
if (!res || !res.ok) {
|
|
1857
|
+
setBusy(false)
|
|
1858
|
+
setEditor(Object.assign({}, editor, { error: translateError(t, res) }))
|
|
1859
|
+
return null
|
|
1860
|
+
}
|
|
1861
|
+
var id = (res.rule && res.rule.id) || (payload.group ? payload.group + '/' + payload.name : payload.name)
|
|
1862
|
+
var name = (res.rule && (res.rule.id || res.rule.name)) || payload.name
|
|
1863
|
+
return finishEditor(id, name, pending, 'memory.result.created', 'memory.result.createdAttached')
|
|
1864
|
+
}).catch(function (e) { setBusy(false); setEditor(Object.assign({}, editor, { error: String((e && e.message) || e) })) })
|
|
1865
|
+
}
|
|
1866
|
+
function submitUpdate() {
|
|
1867
|
+
var payload = {
|
|
1868
|
+
id: editor.id,
|
|
1869
|
+
name: String(editor.name || '').trim(),
|
|
1870
|
+
description: String(editor.description || '').trim(),
|
|
1871
|
+
body: editor.body || '',
|
|
1872
|
+
form: editor.form || 'flat',
|
|
1873
|
+
}
|
|
1874
|
+
var pending = editor.files || []
|
|
1875
|
+
setBusy(true)
|
|
1876
|
+
apiCall('rules-update', payload).then(function (res) {
|
|
1877
|
+
if (!res || !res.ok) {
|
|
1878
|
+
setBusy(false)
|
|
1879
|
+
setEditor(Object.assign({}, editor, { error: translateError(t, res) }))
|
|
1880
|
+
return null
|
|
1881
|
+
}
|
|
1882
|
+
var name = (res.rule && (res.rule.id || res.rule.name)) || payload.name
|
|
1883
|
+
return finishEditor(editor.id, name, pending, 'memory.result.updated', 'memory.result.updatedAttached')
|
|
1884
|
+
}).catch(function (e) { setBusy(false); setEditor(Object.assign({}, editor, { error: String((e && e.message) || e) })) })
|
|
1885
|
+
}
|
|
1886
|
+
function submitDelete(rule) {
|
|
1887
|
+
setBusy(true)
|
|
1888
|
+
apiCall('rules-remove', { id: rule.id }).then(function (res) {
|
|
1889
|
+
setBusy(false)
|
|
1890
|
+
if (res && res.ok) {
|
|
1891
|
+
setModal(null)
|
|
1892
|
+
setResult({ ok: true, text: t('memory.result.removed', { name: rule.name, trashId: res.trashId != null ? res.trashId : '' }) })
|
|
1893
|
+
refresh(true)
|
|
1894
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
1895
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// ── 渲染 ──
|
|
1899
|
+
var stats = data.stats || EMPTY_STATS
|
|
1900
|
+
// 注入预算:宿主 rules-list 一直在返回 sceneMemory,但此前从未渲染 ——
|
|
1901
|
+
// 于是"超出预算、部分记忆没进提示词"对用户完全不可见。
|
|
1902
|
+
var memInfo = data.sceneMemory || EMPTY_MEMORY
|
|
1903
|
+
var memMax = Number(memInfo.maxBytes) || 0
|
|
1904
|
+
var memUsed = Number(memInfo.usedBytes) || 0
|
|
1905
|
+
var budgetOver = memMax > 0 && memUsed > memMax
|
|
1906
|
+
var budgetPct = memMax > 0 ? Math.min(100, Math.round((memUsed / memMax) * 100)) : 0
|
|
1907
|
+
var droppedNames = (memInfo.dropped || []).map(function (d) { return (d.scene ? d.scene + '/' : '') + d.name })
|
|
1908
|
+
var q = String(query || '').toLowerCase()
|
|
1909
|
+
function matchRule(r) {
|
|
1910
|
+
if (sceneFilter !== '' && sceneOf(r.group) !== sceneFilter) return false
|
|
1911
|
+
if (!q) return true
|
|
1912
|
+
return (r.name || '').toLowerCase().indexOf(q) >= 0
|
|
1913
|
+
|| (r.description || '').toLowerCase().indexOf(q) >= 0
|
|
1914
|
+
|| (r.group || '').toLowerCase().indexOf(q) >= 0
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// 场景 → 记忆:场景元数据来自 scenes(含空目录),记忆按一级目录归位;
|
|
1918
|
+
// 根层记忆(group 为空)归入「全局」桶,始终生效。
|
|
1919
|
+
var buckets = {}
|
|
1920
|
+
var order = []
|
|
1921
|
+
function ensureBucket(name, meta) {
|
|
1922
|
+
if (!buckets[name]) { buckets[name] = { name: name, meta: meta, rules: [] }; order.push(name) }
|
|
1923
|
+
else if (meta && !buckets[name].meta) buckets[name].meta = meta
|
|
1924
|
+
return buckets[name]
|
|
1925
|
+
}
|
|
1926
|
+
;(data.scenes || []).forEach(function (s) { ensureBucket(s.name, s) })
|
|
1927
|
+
data.rules.forEach(function (r) {
|
|
1928
|
+
var b = ensureBucket(sceneOf(r.group), null)
|
|
1929
|
+
if (matchRule(r)) b.rules.push(r)
|
|
1930
|
+
})
|
|
1931
|
+
order.sort(function (a, b) {
|
|
1932
|
+
if (a === b) return 0
|
|
1933
|
+
if (a === '') return -1
|
|
1934
|
+
if (b === '') return 1
|
|
1935
|
+
var am = buckets[a].meta, bm = buckets[b].meta
|
|
1936
|
+
if (!!(am && am.shared) !== !!(bm && bm.shared)) return (am && am.shared) ? -1 : 1
|
|
1937
|
+
return String(a).localeCompare(String(b))
|
|
1938
|
+
})
|
|
1939
|
+
var sceneOptions = [{ value: '', label: t('memory.filter.all') }].concat(
|
|
1940
|
+
order.map(function (n) { return { value: n, label: sceneLabel(n) } }))
|
|
1941
|
+
var sceneChoices = order.filter(function (n) { return n !== '' }).map(function (n) { return { value: n, label: sceneLabel(n) } })
|
|
1942
|
+
// 名称 = .md 文件名(单个路径段,不是路径):用 segment 校验器,与宿主逐字对齐。
|
|
1943
|
+
// 中文等任意 Unicode 都合法(`站会流程` → `站会流程.md`)。
|
|
1944
|
+
// 未输入时不标红/不报「不合法」——一打开弹窗就飘红会让人以为中文名被拒;
|
|
1945
|
+
// 空值改由提交按钮的 disabled 兜住(见下方 disabled 表达式)。
|
|
1946
|
+
var editorNameInvalid = editor ? String(editor.name || '').trim() !== '' && !isValidSceneSegment(String(editor.name || '').trim()) : false
|
|
1947
|
+
var editorGroupInvalid = editor && editor.mode === 'create' ? String(editor.group || '').trim() !== '' && !isValidScenePath(String(editor.group || '').trim()) : false
|
|
1948
|
+
var editorDescLen = editor ? String(editor.description || '').length : 0
|
|
1949
|
+
// 空场景(含刚「新建场景」建好的空目录)必须列出来:否则它在页面上直接消失,
|
|
1950
|
+
// 卡片上的「新建记忆」入口也就点不到(只能靠顶部按钮再手填场景名)。
|
|
1951
|
+
// 只在搜索 / 场景筛选生效时按命中收敛,避免空卡片噪音。
|
|
1952
|
+
var filtering = q !== '' || sceneFilter !== ''
|
|
1953
|
+
var visibleBuckets = order.filter(function (n) {
|
|
1954
|
+
if (sceneFilter !== '' && n !== sceneFilter) return false
|
|
1955
|
+
if (!filtering) return true
|
|
1956
|
+
return buckets[n].rules.length > 0 || sceneFilter === n
|
|
1957
|
+
})
|
|
1958
|
+
var hasAnyRule = data.rules.length > 0
|
|
1959
|
+
|
|
1960
|
+
function renderRuleRow(r, sceneActive) {
|
|
1961
|
+
var shadowed = r.shadowed === true
|
|
1962
|
+
// shadowed = 存在同名 bundle,本条 flat 不会被加载;与文件名是否 kebab-case 无关——
|
|
1963
|
+
// 记忆名就是 .md 文件名,中文名同样可以在本页编辑 / 删除。
|
|
1964
|
+
var enabled = r.enabled !== false
|
|
1965
|
+
// 单条停用(索引层开关)优先于场景状态:原来只有 已启用/被覆盖/场景未启用 三个分支,
|
|
1966
|
+
// 单条停用的记忆会被误显示为「已启用」。
|
|
1967
|
+
var statusKey = shadowed ? 'status.shadowed' : !enabled ? 'memory.status.off' : sceneActive === false ? 'memory.status.sceneOff' : 'status.enabled'
|
|
1968
|
+
var statusCls = shadowed ? 'dsm-shadowed' : (!enabled || sceneActive === false) ? 'dsm-disabled' : 'dsm-enabled'
|
|
1969
|
+
return React.createElement('div', { key: r.id, className: 'dsm-row' },
|
|
1970
|
+
React.createElement('div', { className: 'dsm-main' },
|
|
1971
|
+
React.createElement('div', { className: 'dsm-name' }, r.name),
|
|
1972
|
+
React.createElement('div', { className: 'dsm-note' }, r.description || ''),
|
|
1973
|
+
shadowed ? React.createElement('div', { className: 'dsm-rule-shadow-hint' }, t('memory.shadowed.hint')) : null),
|
|
1974
|
+
React.createElement('div', { className: 'dsm-tags' },
|
|
1975
|
+
r.group && r.group.indexOf('/') >= 0 ? React.createElement('span', { className: 'dsm-tag' }, r.group) : null,
|
|
1976
|
+
r.form === 'bundle' ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.form.bundle')) : null,
|
|
1977
|
+
r.descriptionDerived ? React.createElement('span', { className: 'dsm-tag' }, t('memory.derived')) : null),
|
|
1978
|
+
React.createElement('div', { className: 'dsm-status ' + statusCls }, t(statusKey)),
|
|
1979
|
+
React.createElement('div', { className: 'dsm-row-actions' },
|
|
1980
|
+
React.createElement(Switch, { on: enabled, disabled: busy || shadowed, label: t('memory.enable') + ' ' + r.name, onClick: function () { toggleRule(r) } }),
|
|
1981
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || shadowed, onClick: function () { openEditor(r) } }, t('memory.edit')),
|
|
1982
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || shadowed, onClick: function () { setModal({ type: 'delete', rule: r }) } }, t('memory.delete'))))
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function renderSceneCard(name) {
|
|
1986
|
+
var bucket = buckets[name]
|
|
1987
|
+
var meta = bucket.meta || { name: name, count: 0, active: name === '', shared: false }
|
|
1988
|
+
var isShared = meta.shared === true
|
|
1989
|
+
var alwaysOn = isShared || name === '' // 根层「全局」桶没有开关(不是目录)
|
|
1990
|
+
// 空场景(且不是 _shared / 全局桶)才允许删除:避免一次操作带走整组记忆。
|
|
1991
|
+
var emptyRemovable = name !== '' && !isShared && bucket.rules.length === 0
|
|
1992
|
+
var open = !collapsed['s:' + name]
|
|
1993
|
+
return React.createElement('div', { key: 's:' + name, className: 'dsm-source' + (meta.active === false ? ' dsm-rule-shadowed' : '') },
|
|
1994
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
1995
|
+
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleCollapse('s:' + name) } },
|
|
1996
|
+
React.createElement('span', { className: 'dsm-source-title' }, sceneLabel(name)),
|
|
1997
|
+
React.createElement('span', { className: 'dsm-count' }, t('memory.scene.count', { count: bucket.rules.length })),
|
|
1998
|
+
isShared ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
|
|
1999
|
+
meta.active === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null),
|
|
2000
|
+
React.createElement('div', { className: 'dsm-source-actions' },
|
|
2001
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openCreate(name) } }, t('memory.scene.new')),
|
|
2002
|
+
emptyRemovable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'scene-delete', name: name }) } }, t('memory.btn.deleteScene')) : null,
|
|
2003
|
+
alwaysOn ? null : React.createElement(Switch, { on: meta.active === true, disabled: busy, label: t('memory.scene.enable') + ' ' + sceneLabel(name), onClick: function () { toggleScene(meta) } }))),
|
|
2004
|
+
open ? React.createElement('div', { className: 'dsm-source-body' },
|
|
2005
|
+
bucket.rules.length
|
|
2006
|
+
? React.createElement(React.Fragment, null,
|
|
2007
|
+
React.createElement('div', { className: 'dsm-table-head' },
|
|
2008
|
+
React.createElement('span', null, t('memory.table.name')),
|
|
2009
|
+
React.createElement('span', null, t('memory.table.tags')),
|
|
2010
|
+
React.createElement('span', null, t('memory.table.status')),
|
|
2011
|
+
React.createElement('span', null, '')),
|
|
2012
|
+
bucket.rules.map(function (r) { return renderRuleRow(r, meta.active !== false) }))
|
|
2013
|
+
: React.createElement('div', { className: 'dsm-empty' }, t('memory.scene.empty'))) : null)
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
2017
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
2018
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
2019
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
2020
|
+
React.createElement('h2', { className: 'dsm-title' }, t('memory.title')),
|
|
2021
|
+
React.createElement(VersionBadge, null)),
|
|
2022
|
+
React.createElement('p', { className: 'dsm-desc' }, t('memory.desc'))),
|
|
2023
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
2024
|
+
data.activeMode === 'custom' ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: enableAllScenes }, t('memory.scene.enableAll')) : null,
|
|
2025
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
|
|
2026
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open')),
|
|
2027
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')),
|
|
2028
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { openCreate(sceneFilter || '') } }, t('memory.btn.new')))),
|
|
2029
|
+
React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
|
|
2030
|
+
React.createElement('div', { key: 'total', className: 'dsm-stat' }, React.createElement('strong', null, stats.total || 0), t('memory.stat.total')),
|
|
2031
|
+
React.createElement('div', { key: 'scenes', className: 'dsm-stat' }, React.createElement('strong', null, (data.scenes || []).length), t('memory.stat.sceneCount')),
|
|
2032
|
+
React.createElement('div', { key: 'active', className: 'dsm-stat' }, React.createElement('strong', null, stats.scenes || 0), t('memory.stat.scenes'))),
|
|
2033
|
+
memMax > 0 ? React.createElement('div', { className: 'dsm-rule-budget' },
|
|
2034
|
+
React.createElement('div', { className: 'dsm-budget-meta' },
|
|
2035
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.budget.label')),
|
|
2036
|
+
React.createElement('span', { className: budgetOver ? 'dsm-budget-over-text' : 'dsm-char-count' },
|
|
2037
|
+
t('memory.budget.bytes', { used: memUsed, max: memMax }) + (budgetOver ? ' · ' + t('memory.budget.over') : ''))),
|
|
2038
|
+
React.createElement('div', { className: 'dsm-budget-bar', role: 'progressbar', 'aria-valuenow': budgetPct, 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-label': t('memory.budget.label') },
|
|
2039
|
+
React.createElement('div', { className: 'dsm-budget-fill' + (budgetOver ? ' dsm-budget-over' : ''), style: { width: budgetPct + '%' } })),
|
|
2040
|
+
memInfo.truncated ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.budget.truncated')) : null,
|
|
2041
|
+
droppedNames.length ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.budget.dropped', { names: droppedNames.join('、') })) : null) : null,
|
|
2042
|
+
React.createElement('div', { className: 'dsm-filters' },
|
|
2043
|
+
React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('memory.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
|
|
2044
|
+
React.createElement('div', { className: 'dsm-source-filter' },
|
|
2045
|
+
React.createElement(SourceSelect, { options: sceneOptions, value: sceneFilter, onChange: setSceneFilter }))),
|
|
2046
|
+
// 成功反馈走右下角浮层(.dsm-toast,固定定位、不占布局),2.6 秒后自动消失,
|
|
2047
|
+
// 见上方 result 自动清理 effect。之前的做法是插进文档流的横幅:每点一次场景
|
|
2048
|
+
// 开关就把下面整列卡片顶下去、还一直挂着不走,观感很突兀。
|
|
2049
|
+
// 错误反馈仍然留在文档流里且不会自动消失——错误需要一直看得见。
|
|
2050
|
+
result && !result.ok ? React.createElement('div', { key: 'result', className: 'dsm-feedback dsm-error', role: 'alert' }, result.text) : null,
|
|
2051
|
+
result && result.ok ? React.createElement('div', { key: 'toast', className: 'dsm-toast', role: 'status', 'aria-live': 'polite' }, result.text) : null,
|
|
2052
|
+
data.error ? React.createElement('div', { key: 'error', className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
|
|
2053
|
+
data.loading && !hasAnyRule ? React.createElement('div', { key: 'loading', className: 'dsm-empty' }, t('memory.loading'))
|
|
2054
|
+
: visibleBuckets.length ? React.createElement('div', { key: 'scenes', className: 'dsm-sources' }, visibleBuckets.map(renderSceneCard))
|
|
2055
|
+
: React.createElement('div', { key: 'empty', className: 'dsm-empty' }, q || sceneFilter ? t('memory.empty.search') : t('memory.empty')),
|
|
2056
|
+
modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('memory.delete.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
|
|
2057
|
+
React.createElement('p', { className: 'dsm-help' }, t('memory.delete.desc', { name: modal.rule.name })),
|
|
2058
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
2059
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
|
|
2060
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.rule) } }, t('memory.btn.delete.confirm')))) : null,
|
|
2061
|
+
editor && modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'editor', className: 'dsm-modal-wide', title: editor.mode === 'create' ? t('memory.create.title') : t('memory.edit.title'), closeLabel: t('btn.cancel'), onClose: closeEditor },
|
|
2062
|
+
React.createElement('div', { className: 'dsm-form' },
|
|
2063
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
2064
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.group')),
|
|
2065
|
+
editor.mode === 'create'
|
|
2066
|
+
? React.createElement(SceneCombo, {
|
|
2067
|
+
options: sceneChoices,
|
|
2068
|
+
value: editor.group || '',
|
|
2069
|
+
placeholder: t('memory.field.group.placeholder'),
|
|
2070
|
+
browseLabel: t('memory.scene.browse'),
|
|
2071
|
+
label: t('memory.field.group'),
|
|
2072
|
+
disabled: busy,
|
|
2073
|
+
invalid: editorGroupInvalid,
|
|
2074
|
+
onChange: function (v) { setEditor(Object.assign({}, editor, { group: v })) },
|
|
2075
|
+
})
|
|
2076
|
+
: React.createElement('input', { className: 'dsm-control', value: editor.group || '', disabled: true, onChange: function () {} }),
|
|
2077
|
+
editor.mode === 'create'
|
|
2078
|
+
? React.createElement('p', { className: 'dsm-help' }, t('memory.scene.pick'))
|
|
2079
|
+
: null,
|
|
2080
|
+
editorGroupInvalid ? React.createElement('p', { className: 'dsm-rule-hint' }, t('error.rules.invalidGroup')) : null),
|
|
2081
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
2082
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.name')),
|
|
2083
|
+
React.createElement('input', { className: 'dsm-control' + (editorNameInvalid ? ' dsm-rule-invalid' : ''), value: editor.name || '', placeholder: t('memory.field.name.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { name: e.target.value })) } }),
|
|
2084
|
+
React.createElement('p', { className: editorNameInvalid ? 'dsm-rule-hint' : 'dsm-help' }, editorNameInvalid ? t('memory.name.invalid') : t('memory.name.hint'))),
|
|
2085
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
2086
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.description')),
|
|
2087
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: editor.description || '', placeholder: t('memory.field.description.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { description: e.target.value })) } }),
|
|
2088
|
+
React.createElement('div', { className: 'dsm-char-count' + (editorDescLen > 500 ? ' dsm-char-over' : '') }, editorDescLen + ' / 500')),
|
|
2089
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
2090
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.body')),
|
|
2091
|
+
React.createElement('textarea', { className: 'dsm-control', style: { minHeight: '220px' }, value: editor.body || '', placeholder: t('memory.field.body.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { body: e.target.value })) } })),
|
|
2092
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
2093
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.form')),
|
|
2094
|
+
React.createElement(SourceSelect, { options: RULE_FORM_OPTIONS, value: editor.form || 'flat', onChange: function (v) { setEditor(Object.assign({}, editor, { form: v })) } }),
|
|
2095
|
+
editor.form === 'bundle'
|
|
2096
|
+
? React.createElement('div', { className: 'dsm-field' },
|
|
2097
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.attach')),
|
|
2098
|
+
React.createElement('input', { ref: attachRef, type: 'file', multiple: true, className: 'dsm-hidden-input', onChange: onAttachPicked }),
|
|
2099
|
+
(editor.attachments || []).length || (editor.files || []).length
|
|
2100
|
+
? React.createElement('div', { className: 'dsm-attach-list' },
|
|
2101
|
+
(editor.attachments || []).map(function (a) {
|
|
2102
|
+
return React.createElement('div', { key: 'a:' + a.name, className: 'dsm-file', title: a.name },
|
|
2103
|
+
React.createElement('span', { className: 'dsm-file-kind', 'aria-hidden': 'true' }, 'FILE'),
|
|
2104
|
+
React.createElement('span', { className: 'dsm-file-name' }, a.name),
|
|
2105
|
+
React.createElement('span', { className: 'dsm-file-meta' }, fmtBytes(a.size)),
|
|
2106
|
+
React.createElement('button', { type: 'button', className: 'dsm-file-remove', disabled: busy, 'aria-label': t('memory.attach.remove') + ' ' + a.name, onClick: function () { removeAttachment(a.name) } }, '×'))
|
|
2107
|
+
}),
|
|
2108
|
+
(editor.files || []).map(function (f, i) {
|
|
2109
|
+
return React.createElement('div', { key: 'p:' + i + ':' + f.name, className: 'dsm-file', title: f.name },
|
|
2110
|
+
React.createElement('span', { className: 'dsm-file-kind', 'aria-hidden': 'true' }, 'NEW'),
|
|
2111
|
+
React.createElement('span', { className: 'dsm-file-name' }, f.name),
|
|
2112
|
+
React.createElement('span', { className: 'dsm-file-meta' }, fmtBytes(f.size) + ' · ' + t('memory.attach.pending')),
|
|
2113
|
+
React.createElement('button', { type: 'button', className: 'dsm-file-remove', disabled: busy, 'aria-label': t('memory.attach.remove') + ' ' + f.name, onClick: function () { dropPending(i) } }, '×'))
|
|
2114
|
+
}))
|
|
2115
|
+
: React.createElement('p', { className: 'dsm-help' }, t('memory.attach.empty')),
|
|
2116
|
+
React.createElement('div', { className: 'dsm-attach-actions' },
|
|
2117
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: pickAttach }, t('memory.attach.add')),
|
|
2118
|
+
React.createElement('span', { className: 'dsm-help' }, t('memory.attach.hint'))))
|
|
2119
|
+
: React.createElement('p', { className: 'dsm-help' }, t('memory.attach.flat'))),
|
|
2120
|
+
editor.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(editor.error)) : null),
|
|
2121
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
2122
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: closeEditor }, t('btn.cancel')),
|
|
2123
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || editorNameInvalid || editorGroupInvalid || editorDescLen > 500 || !String(editor.name || '').trim() || !String(editor.body || '').trim() || (editor.mode === 'create' && !String(editor.group || '').trim()), onClick: editor.mode === 'create' ? submitCreate : submitUpdate }, editor.mode === 'create' ? t('memory.btn.create') : t('memory.btn.save')))) : null,
|
|
2124
|
+
modal && modal.type === 'scene-create' ? React.createElement(Modal, { key: 'screate', title: t('memory.scene.createTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
|
|
2125
|
+
React.createElement('div', { className: 'dsm-form' },
|
|
2126
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
2127
|
+
React.createElement('span', { className: 'dsm-label' }, t('memory.field.group')),
|
|
2128
|
+
React.createElement('input', { className: 'dsm-control' + (sceneForm.error ? ' dsm-rule-invalid' : ''), value: sceneForm.name || '', placeholder: t('memory.field.group.placeholder'), onChange: function (e) { setSceneForm({ name: e.target.value, error: null }) } }),
|
|
2129
|
+
React.createElement('p', { className: sceneForm.error ? 'dsm-rule-hint' : 'dsm-help' }, sceneForm.error || t('memory.field.group.hint'))),
|
|
2130
|
+
React.createElement('p', { className: 'dsm-help' }, t('memory.scene.createHelp'))),
|
|
2131
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
2132
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
|
|
2133
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || !String(sceneForm.name || '').trim(), onClick: submitCreateScene }, t('memory.btn.create')))) : null,
|
|
2134
|
+
modal && modal.type === 'scene-delete' ? React.createElement(Modal, { key: 'sdel', title: t('memory.deleteScene.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
|
|
2135
|
+
React.createElement('p', { className: 'dsm-help' }, t('memory.deleteScene.desc', { name: modal.name })),
|
|
2136
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
2137
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
|
|
2138
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDeleteScene(modal.name) } }, t('memory.btn.deleteScene')))) : null,
|
|
2139
|
+
modal && modal.type === 'trash' ? React.createElement(Modal, { key: 'trash', wide: true, title: t('memory.trash.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
|
|
2140
|
+
trash.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
2141
|
+
: React.createElement(React.Fragment, null,
|
|
2142
|
+
trash.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(trash.error)) : null,
|
|
2143
|
+
trash.entries.length
|
|
2144
|
+
? React.createElement(React.Fragment, null,
|
|
2145
|
+
React.createElement('div', { className: 'dsm-count' }, t('memory.trash.count', { count: trash.entries.length })),
|
|
2146
|
+
trash.entries.map(function (entry) {
|
|
2147
|
+
return React.createElement('div', { key: entry.trashId, className: 'dsm-trash-item' },
|
|
2148
|
+
React.createElement('div', { className: 'dsm-trash-main' },
|
|
2149
|
+
React.createElement('div', { className: 'dsm-name' }, entry.name),
|
|
2150
|
+
React.createElement('div', { className: 'dsm-note' },
|
|
2151
|
+
sceneLabel(entry.group) + ' · ' + t('memory.form.' + (entry.form === 'bundle' ? 'bundle' : 'flat')) + ' · ' + entry.bytes + ' B · '
|
|
2152
|
+
+ t('memory.trash.deletedAt', { time: entry.deletedAt ? new Date(entry.deletedAt).toLocaleString() : '' }))),
|
|
2153
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { submitRestore(entry) } }, t('memory.trash.restore')),
|
|
2154
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'trash-delete', entry: entry }) } }, t('memory.trash.purge')))
|
|
2155
|
+
}))
|
|
2156
|
+
: React.createElement('div', { className: 'dsm-empty' }, t('memory.trash.empty')))) : null,
|
|
2157
|
+
modal && modal.type === 'trash-delete' ? React.createElement(Modal, { key: 'trash-delete', title: t('memory.trash.confirmTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal({ type: 'trash' }) } },
|
|
2158
|
+
React.createElement('p', { className: 'dsm-help' }, t('memory.trash.confirmDesc', { name: modal.entry.name })),
|
|
2159
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
2160
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal({ type: 'trash' }) } }, t('btn.cancel')),
|
|
2161
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitTrashDelete(modal.entry) } }, t('memory.trash.purge')))) : null)
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
module.exports.DICT = DICT
|
|
2165
|
+
// Test-only export: page 组件 + i18n t,让 client 测试直接渲染 page(绕开
|
|
2166
|
+
// ToolsSection 的子组件嵌套——minimal React 不支持子组件 hook 隔离)。
|
|
2167
|
+
// 生产环境浏览器 React 会正确递归渲染 ToolsSection 内的 page。
|
|
2168
|
+
module.exports._pages = { MCPPage: MCPPage, SkillManagerSection: SkillManagerSection, AgentsMdPage: AgentsMdPage, HistoryPage: HistoryPage, SceneMemoryPage: SceneMemoryPage, t: t }
|
|
2169
|
+
},
|
|
2170
|
+
}
|
|
2171
|
+
return module.exports
|
|
2172
|
+
}
|
|
2173
|
+
})
|