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/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-4{grid-template-columns:repeat(4,minmax(0,1fr))}' +
15
- '@media(max-width:760px){.dsm-summary.dsm-summary-4{grid-template-columns:repeat(2,minmax(0,1fr))}}' +
16
- '@container(max-width:520px){.dsm-summary.dsm-summary-4{grid-template-columns:1fr}}' +
17
- '.dsm-version{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;letter-spacing:.3px}' +
18
- '.dsm-failed{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:nowrap}' +
19
- '.dsm-row-hint{grid-column:1/-1;margin:-4px 0 9px;color:#d49245;font-size:11px;line-height:16px}' +
20
- '.dsm-tools{display:flex;flex-direction:column;gap:8px}' +
21
- '.dsm-tool{padding:9px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
22
- '.dsm-tool-name{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;font-weight:600;word-break:break-all}' +
23
- '.dsm-tool-name-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
24
- '.dsm-tool-off .dsm-tool-name,.dsm-tool-off .dsm-tool-desc{opacity:.45}' +
25
- '.dsm-tool-desc{margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}' +
26
- '.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)}' +
27
- '.dsm-tool-param{display:flex;gap:8px;font-size:11px;align-items:baseline}' +
28
- '.dsm-tool-param-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-weight:600;min-width:80px;word-break:break-all}' +
29
- '.dsm-tool-param-type{color:var(--dsw-alias-label-tertiary);min-width:56px}' +
30
- '.dsm-tool-param-desc{flex:1;color:var(--dsw-alias-label-secondary);word-break:break-all}' +
31
- '.dsm-modal textarea.dsm-control.dsm-textarea-sm{min-height:88px}' +
32
- '.dsm-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
33
- '.dsm-note-user{color:var(--dsw-alias-label-tertiary)}' +
34
- `
35
- .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}
36
- .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}
37
- .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}
38
- .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)}
39
- .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{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}
40
- .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}
41
- .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)}
42
- .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}.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:14px;font-weight:620}.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}
43
- .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}
44
- @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}}
45
- `
46
-
47
- function ensureCss() {
48
- if (typeof document === 'undefined') return
49
- const id = 'dsh-plugin-tool-management'
50
- if (document.querySelector('style[data-plugin-css="' + id + '"]')) return
51
- const tag = document.createElement('style')
52
- tag.dataset.plugin = id
53
- tag.dataset.pluginCss = id
54
- tag.textContent = CSS
55
- document.head.appendChild(tag)
56
- }
57
-
58
- // Optional access token for write ops (host config.token or
59
- // DSH_SKILL_MCP_MANAGER_TOKEN). Read once from localStorage — there is no
60
- // UI row for it — and sent as `x-dsh-token` on every request (the host
61
- // ignores it when unset).
62
- let TOKEN = ''
63
- try {
64
- // 读取新键;旧版(dsh-skill-mcp-manager-token)数据一次性迁移到新键。
65
- TOKEN = window.localStorage.getItem('dsh-plugin-tool-management-token')
66
- || window.localStorage.getItem('dsh-skill-mcp-manager-token') || ''
67
- if (TOKEN && !window.localStorage.getItem('dsh-plugin-tool-management-token')) {
68
- window.localStorage.setItem('dsh-plugin-tool-management-token', TOKEN)
69
- }
70
- } catch (e) { /* storage unavailable */ }
71
-
72
- function apiCall(op, args) {
73
- // `x-dsh-plugin` is the cross-site (CSRF) gate header the host half
74
- // requires on every request; a cross-origin page cannot attach it
75
- // without a CORS preflight that this route never answers.
76
- const headers = { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-plugin-tool-management' }
77
- if (TOKEN) headers['x-dsh-token'] = TOKEN
78
- return fetch('/dsh-plugin-tool-management/api', {
79
- method: 'POST',
80
- headers,
81
- body: JSON.stringify({ op, args: args || {} }),
82
- }).then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }))
83
- }
84
-
85
- // Small version badge shown next to each settings-page title; reads the
86
- // package version from the host (plugin-version op) so it always matches
87
- // the installed release.
88
- function VersionBadge() {
89
- const [v, setV] = React.useState(null)
90
- React.useEffect(() => {
91
- let alive = true
92
- apiCall('plugin-version', {}).then((r) => { if (alive && r && r.ok) setV(r.version) }).catch(() => {})
93
- return () => { alive = false }
94
- }, [])
95
- return v ? React.createElement('span', { className: 'dsm-version' }, 'v' + v) : null
96
- }
97
-
98
- module.exports = {
99
- name: 'dsh-plugin-tool-management-client',
100
- inject: ['timer'],
101
- apply(ctx) {
102
- ensureCss()
103
- const slots = ctx.get('slots')
104
- if (slots === undefined) return
105
-
106
- const LEVEL_LABEL = { project: 'Profile 级', global: '全局', loader: '已加载' }
107
- const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'project' })
108
- const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
109
- const MCP_LEVEL_OPTIONS = [
110
- { value: '', label: '全部级别' },
111
- { value: 'project', label: 'Profile 级' },
112
- { value: 'global', label: '全局' },
113
- { value: 'loader', label: '已加载' },
114
- ]
115
- const MCP_TRANSPORT_OPTIONS = [
116
- { value: 'streamable-http', label: 'streamable-http' },
117
- { value: 'stdio', label: 'stdio' },
118
- ]
119
- const maskSecret = (value) => {
120
- const s = String(value == null ? '' : value)
121
- return s.length <= 4 ? '****' : s.slice(0, 4) + '****'
122
- }
123
-
124
- // 项目与问题反馈链接:MCP / Skills 两个设置页共用同一组地址。
125
- // 注意:引用了本作用域的 GithubMark16 / primitives,必须定义在 apply 里。
126
- function FeedbackLinks() {
127
- return React.createElement('div', { className: 'dsm-feedback-links' },
128
- React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management', target: '_blank', rel: 'noreferrer', 'aria-label': 'GitHub' },
129
- React.createElement(GithubMark16, null), 'GitHub'),
130
- React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management/issues', target: '_blank', rel: 'noreferrer', 'aria-label': '问题反馈' },
131
- React.createElement(primitives.IconListPenOutline16, null), '问题反馈'))
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
- running: rows.filter((row) => row.live && row.live.enabled && row.live.phase === 'active').length,
324
- tools: rows.reduce((sum, row) => sum + (typeof row.toolCount === 'number' ? row.toolCount : 0), 0),
325
- }
326
- const profilePath = state.paths && state.paths.profile ? 'profile: ' + state.paths.profile : null
327
- const groups = levelFilter === 'loader'
328
- ? [{ key: 'live', title: '已加载', path: profilePath, match: isRunning }]
329
- : [
330
- { key: 'project', title: 'Profile 级', path: state.paths ? state.paths.project : null, match: (row) => row.level === 'project' },
331
- { key: 'global', title: '全局', path: state.paths ? state.paths.global : null, match: (row) => row.level === 'global' },
332
- { key: 'loader', title: '已加载', path: profilePath, match: (row) => row.level === 'loader' && isRunning(row) },
333
- ]
334
-
335
- const liveStatus = (row) => {
336
- if (!row.live) return { text: '未加载', cls: 'dsm-shadowed' }
337
- if (row.live.phase === 'failed') return { text: '启动失败', cls: 'dsm-failed' }
338
- if (!row.live.enabled) return { text: '未运行', cls: 'dsm-shadowed' }
339
- if (row.live.phase && row.live.phase !== 'active') return { text: '加载中', cls: 'dsm-shadowed' }
340
- if (typeof row.toolCount === 'number' && row.toolCount === 0) return { text: '无工具', cls: 'dsm-disabled' }
341
- return { text: '运行中', cls: 'dsm-enabled' }
342
- }
343
- const liveHint = (row) => {
344
- if (!row.live) return null
345
- if (row.live.phase === 'failed') return 'loader 启动失败:请检查 URL / 命令 / 凭证 / 网络,然后点「重启」重试'
346
- if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) return '已连接但未注册任何工具:服务端可能未就绪或工具列表为空'
347
- return null
348
- }
349
-
350
- const renderRow = (row) => {
351
- const status = liveStatus(row)
352
- const hint = liveHint(row)
353
- const editable = row.level !== 'loader'
354
- return React.createElement('div', { key: row.id, className: 'dsm-row' },
355
- React.createElement('div', { className: 'dsm-main' },
356
- React.createElement('div', { className: 'dsm-name' }, row.serverName),
357
- row.notes ? React.createElement('div', { className: 'dsm-note dsm-note-user', title: row.notes }, '备注:' + row.notes) : null),
358
- React.createElement('div', { className: 'dsm-tags' },
359
- (levelFilter === 'loader' && row.level && row.level !== 'loader') ? React.createElement('span', { className: 'dsm-tag' }, LEVEL_LABEL[row.level] || row.level) : null,
360
- (typeof row.toolCount === 'number' && row.toolCount > 0) ? React.createElement('span', { className: 'dsm-tag' }, row.toolCount + ' 个工具') : null,
361
- row.duplicate ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, '重复 id') : null),
362
- React.createElement('div', { className: 'dsm-status ' + status.cls }, status.text),
363
- React.createElement('div', { className: 'dsm-row-actions' },
364
- editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null, label: '启停服务 ' + row.serverName, onClick: () => toggleRow(row) }) : null,
365
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () => openDetail(row) }, '详情'),
366
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => openEdit(row) }, '编辑') : null,
367
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => restartRow(row) }, '重启') : null,
368
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null, onClick: () => setConfirmRow(row) }, '删除') : null),
369
- hint ? React.createElement('div', { className: 'dsm-row-hint' }, '⚠ ' + hint) : null)
370
- }
371
-
372
- const renderGroup = (group) => {
373
- const groupRows = visibleRows.filter(group.match)
374
- if (groupRows.length === 0) return null
375
- const open = collapsed[group.key] !== true || !!normalizedQuery
376
- return React.createElement('section', { key: group.key, className: 'dsm-source' },
377
- React.createElement('div', { className: 'dsm-source-head' },
378
- React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: () => setCollapsed(Object.assign({}, collapsed, { [group.key]: !open })) },
379
- React.createElement('span', { className: 'dsm-source-title' }, group.title),
380
- React.createElement('span', { className: 'dsm-count' }, groupRows.length + ' 个服务'),
381
- group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
382
- open ? React.createElement('div', { className: 'dsm-source-body' },
383
- React.createElement(React.Fragment, null,
384
- React.createElement('div', { className: 'dsm-table-head' },
385
- React.createElement('span', null, '服务名称与地址'),
386
- React.createElement('span', null, '传输与工具'),
387
- React.createElement('span', null, '运行状态'),
388
- React.createElement('span', null, '')),
389
- groupRows.map(renderRow))) : null)
390
- }
391
-
392
- const configRow = (label, value) => React.createElement('div', { className: 'dsm-fm-row', key: label },
393
- React.createElement('div', { className: 'dsm-fm-key' }, label),
394
- React.createElement('div', { className: 'dsm-fm-val' }, value))
395
-
396
- const toolListNode = detail && (detail.loading
397
- ? React.createElement('div', { className: 'dsm-help' }, '正在获取工具列表…')
398
- : detail.error
399
- ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, '加载工具失败:' + detail.error)
400
- : (detail.tools || []).length === 0
401
- ? React.createElement('div', { className: 'dsm-help' }, '该服务暂无已注册工具。')
402
- : React.createElement(React.Fragment, null,
403
- React.createElement('div', { className: 'dsm-help' }, '停用的工具对模型不可见且不可调用,改动即时生效。'),
404
- 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 },
405
- React.createElement('div', { className: 'dsm-tool-name-row' },
406
- React.createElement('div', { className: 'dsm-tool-name' }, tool.name),
407
- React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null, label: '启停工具 ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
408
- tool.description ? React.createElement('div', { className: 'dsm-tool-desc' }, tool.description) : null,
409
- (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 },
410
- React.createElement('span', { className: 'dsm-tool-param-key' }, param.key + (param.required ? ' *' : '')),
411
- React.createElement('span', { className: 'dsm-tool-param-type' }, param.type || 'any'),
412
- React.createElement('span', { className: 'dsm-tool-param-desc' }, param.description || '')))) : null)))))
413
-
414
- const anyGroupVisible = groups.some((group) => visibleRows.some(group.match))
415
- const groupsNode = (state.loading && rows.length === 0)
416
- ? React.createElement('div', { className: 'dsm-empty' }, '正在加载 MCP 服务…')
417
- : anyGroupVisible
418
- ? React.createElement('div', { className: 'dsm-sources' }, groups.map(renderGroup))
419
- : React.createElement('div', { className: 'dsm-empty' }, (normalizedQuery || levelFilter) ? '没有匹配的服务。' : '暂无 MCP 服务。点击「新增服务」添加。')
420
-
421
- const formModalNode = formModal && React.createElement(Modal, {
422
- key: 'mcp-form',
423
- title: formModal.mode === 'edit' ? '编辑 MCP 服务:' + formModal.id : '新增 MCP 服务',
424
- closeLabel: '关闭',
425
- onClose: closeForm,
426
- },
427
- React.createElement('div', { className: 'dsm-form' },
428
- React.createElement('label', { className: 'dsm-field' },
429
- React.createElement('span', { className: 'dsm-label' }, '服务名称 serverName'),
430
- React.createElement('input', { className: 'dsm-control', value: formModal.serverName, placeholder: 'e.g. github', onChange: setFormField('serverName') }),
431
- React.createElement('span', { className: 'dsm-help' }, '唯一标识,1-32 位 [A-Za-z0-9_-];补丁中按该名称注册工具。')),
432
- React.createElement('label', { className: 'dsm-field' },
433
- React.createElement('span', { className: 'dsm-label' }, '传输方式'),
434
- React.createElement(SourceSelect, { value: formModal.transport, options: MCP_TRANSPORT_OPTIONS, onChange: (value) => setFormValue('transport', value) })),
435
- React.createElement('label', { className: 'dsm-field' },
436
- React.createElement('span', { className: 'dsm-label' }, '级别'),
437
- React.createElement(SourceSelect, {
438
- value: formModal.level,
439
- options: [
440
- { value: 'project', label: 'Profile 级(本应用:' + (state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml') + ')' },
441
- { value: 'global', label: '全局(跨 Profile:' + (state.paths ? state.paths.global : '~/.dsh/cordis.patch.yml') + ')' },
442
- ],
443
- onChange: (value) => setFormValue('level', value),
444
- })),
445
- formModal.transport === 'streamable-http'
446
- ? React.createElement('label', { className: 'dsm-field' },
447
- React.createElement('span', { className: 'dsm-label' }, '服务 URL'),
448
- React.createElement('input', { className: 'dsm-control', value: formModal.url, placeholder: 'https://host/mcp', onChange: setFormField('url') }),
449
- React.createElement('span', { className: 'dsm-help' }, '需以 http(s):// 开头。'))
450
- : React.createElement(React.Fragment, null,
451
- React.createElement('label', { className: 'dsm-field' },
452
- React.createElement('span', { className: 'dsm-label' }, '启动命令'),
453
- React.createElement('input', { className: 'dsm-control', value: formModal.command, placeholder: 'npx -y @modelcontextprotocol/server-github', onChange: setFormField('command') })),
454
- React.createElement('label', { className: 'dsm-field' },
455
- React.createElement('span', { className: 'dsm-label' }, '参数(空格或换行分隔)'),
456
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.args, placeholder: '-y\n@modelcontextprotocol/server-github', onChange: setFormField('args') })),
457
- React.createElement('label', { className: 'dsm-field' },
458
- React.createElement('span', { className: 'dsm-label' }, '环境变量(每行 key=value)'),
459
- React.createElement('span', { className: 'dsm-help' }, '路径按系统路径写法填(Windows 用 \\,macOS/Linux 用 /)。'),
460
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.env, placeholder: 'GITHUB_TOKEN=xxx', onChange: setFormField('env') }))),
461
- formModal.transport === 'streamable-http'
462
- ? React.createElement('label', { className: 'dsm-field' },
463
- React.createElement('span', { className: 'dsm-label' }, '请求头(每行 key=value)'),
464
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.headers, placeholder: 'Authorization=Bearer xxx', onChange: setFormField('headers') }))
465
- : null),
466
- React.createElement('div', { className: 'dsm-modal-actions' },
467
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: closeForm }, '取消'),
468
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy === 'form' || !formModal.serverName.trim(), onClick: submitForm }, formModal.mode === 'edit' ? '保存' : '添加')))
469
-
470
- const detailStatus = detail ? liveStatus(detail.row) : null
471
- const detailHint = detail ? liveHint(detail.row) : null
472
- const detailNode = detail && React.createElement(Modal, {
473
- key: 'mcp-detail',
474
- wide: true,
475
- title: '服务详情:' + detail.row.serverName,
476
- closeLabel: '关闭',
477
- onClose: () => setDetail(null),
478
- },
479
- React.createElement('div', { className: 'dsm-detail-section' },
480
- React.createElement('div', { className: 'dsm-detail-title dsm-detail-title-row' },
481
- '配置',
482
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: toggleReveal }, reveal ? '隐藏密钥' : '显示密钥')),
483
- React.createElement('div', { className: 'dsm-fm' },
484
- configRow('条目 ID', detail.row.id),
485
- configRow('级别', LEVEL_LABEL[detail.row.level] || detail.row.level),
486
- configRow('传输方式', detail.row.transport || '—'),
487
- detail.row.url ? configRow('服务 URL', detail.row.url) : null,
488
- detail.row.command ? configRow('启动命令', detail.row.command + ((detail.row.args && detail.row.args.length) ? ' ' + detail.row.args.join(' ') : '')) : null,
489
- (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,
490
- (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)),
491
- React.createElement('div', { className: 'dsm-detail-section' },
492
- React.createElement('div', { className: 'dsm-detail-title' }, '运行状态'),
493
- React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' : detailStatus.cls === 'dsm-disabled' ? ' dsm-warning' : '') },
494
- detailStatus.text + ':' + (detailHint || '该服务已登记在 Loader 中。'))),
495
- React.createElement('div', { className: 'dsm-detail-section' },
496
- React.createElement('div', { className: 'dsm-detail-title' }, '备注(仅本机可见,不会被配置更新覆盖)'),
497
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, placeholder: '例如:A 不可用时改用 B 兜底', onChange: (ev) => setNoteDraft(ev.target.value) }),
498
- React.createElement('div', { className: 'dsm-modal-actions' },
499
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, '保存备注'))),
500
- React.createElement('div', { className: 'dsm-detail-section' },
501
- React.createElement('div', { className: 'dsm-detail-title' }, '工具(' + (detail.loading ? '…' : (detail.tools || []).length) + ')'),
502
- toolListNode))
503
-
504
- const confirmNode = confirmRow && React.createElement(Modal, {
505
- key: 'mcp-remove',
506
- title: '删除 MCP 服务',
507
- closeLabel: '关闭',
508
- onClose: () => setConfirmRow(null),
509
- },
510
- React.createElement('p', { className: 'dsm-desc' }, '确定要删除「' + confirmRow.serverName + '」吗?删除后该服务的配置将从补丁文件中移除,相关工具立即下线,此操作不可撤销。'),
511
- React.createElement('div', { className: 'dsm-modal-actions' },
512
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: () => setConfirmRow(null) }, '取消'),
513
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', onClick: confirmRemove }, '删除')))
514
-
515
- return React.createElement('section', { className: 'dsm-section' },
516
- React.createElement('div', { className: 'dsm-head' },
517
- React.createElement('div', { className: 'dsm-title-block' },
518
- React.createElement('div', { className: 'dsm-title-row' },
519
- React.createElement('h2', { className: 'dsm-title' }, 'MCP'),
520
- React.createElement(VersionBadge, null),
521
- React.createElement(FeedbackLinks, null)),
522
- React.createElement('p', { className: 'dsm-desc' }, '统一管理本机的 MCP 服务与运行状态。')),
523
- React.createElement('div', { className: 'dsm-actions' },
524
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, '刷新'),
525
- React.createElement('button', { type: 'button', className: 'dsm-btn', onClick: openAdd }, '新增服务'),
526
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: true }, 'setall') }, '全部启用'),
527
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: false }, 'setall') }, '全部停用'))),
528
- React.createElement('div', { className: 'dsm-summary dsm-summary-4' },
529
- [[summary.total, '个服务'], [summary.enabled, '个已启用'], [summary.running, '个运行中'], [summary.tools, '个工具']].map((item) =>
530
- React.createElement('div', { key: item[1], className: 'dsm-stat' },
531
- React.createElement('strong', null, item[0]), item[1]))),
532
- React.createElement('div', { className: 'dsm-filters' },
533
- React.createElement('input', { className: 'dsm-control dsm-search', value: query, 'aria-label': '搜索服务', placeholder: '搜索服务名称、地址或命令', onChange: (ev) => setQuery(ev.target.value) }),
534
- React.createElement('div', { className: 'dsm-source-filter' },
535
- React.createElement(SourceSelect, { value: levelFilter, options: MCP_LEVEL_OPTIONS, onChange: setLevelFilter }))),
536
- restartInfo ? React.createElement('div', { className: 'dsm-feedback' }, '重启中… ' + restartInfo.name + '(已等待 ' + Math.max(0, Math.floor((Date.now() - restartInfo.startedAt) / 1000)) + ' 秒,完成后自动刷新)') : null,
537
- 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,
538
- state.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, state.error) : null,
539
- (state.errors && state.errors.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, '读取补丁告警:' + state.errors.join(';')) : null,
540
- (state.warnings && state.warnings.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, state.warnings.join(';')) : null,
541
- groupsNode,
542
- formModalNode,
543
- detailNode,
544
- confirmNode)
545
- }
546
-
547
- // 两个独立设置页:MCP 与 Skills,各自占一个侧栏项。
548
- slots.inject('settings.section', () => slots.register(
549
- { name: 'settings.section', id: 'dsm-mcp', order: 16, label: 'MCP' },
550
- () => React.createElement(MCPPage)
551
- ))
552
- slots.inject('settings.section', () => slots.register(
553
- { name: 'settings.section', id: 'dsm-skills', order: 17, label: 'Skills' },
554
- () => React.createElement(SkillManagerSection, { t: t })
555
- ))
556
-
557
-
558
- // ---------- Skills 页(移植自 dsh-skills-manager,Apache-2.0;类名前缀 dsm-) ----------
559
- var react = React // 上游组件使用小写 react,别名到本文件的 React
560
- // 上游 UI 依赖 @deepseek-ai/dsh-client-ui-primitives 的单个图标;
561
- // 这里用本地内联 SVG 替代,避免为客户端引入额外 peer 依赖。
562
- var primitives = { IconListPenOutline16: function () { return h('svg', { viewBox: '0 0 16 16', width: 16, height: 16, 'aria-hidden': 'true', focusable: 'false' }, h('path', { fill: 'currentColor', d: 'M3 2h10v12H3zM5 5h6M5 8h6M5 11h4' })) } }
563
- var h = React.createElement
564
-
565
- // SKILL.md frontmatter 常见键 → 中文标签(未知键原样显示,已知键附原始键名提示)。
566
- var FRONTMATTER_LABELS = {
567
- name: '名称', description: '描述', 'when-to-use': '适用场景', 'user-invocable': '用户可调用',
568
- 'disable-model-invocation': '禁止模型调用', 'allowed-tools': '允许的工具', license: '许可协议',
569
- version: '版本', author: '作者', category: '分类', tags: '标签', metadata: '附加信息', homepage: '主页',
570
- }
571
- function frontmatterValue(value) {
572
- if (Array.isArray(value)) return value.join('、')
573
- if (value === true) return '是'
574
- if (value === false) return '否'
575
- if (value === null || value === undefined) return ''
576
- if (typeof value === 'object') return JSON.stringify(value, null, 2)
577
- return String(value)
578
- }
579
- // 详情页把 frontmatter 渲染成中文键值表(原始 JSON 对使用者几乎不可读)。
580
- function renderFrontmatter(t, frontmatter) {
581
- var keys = frontmatter && typeof frontmatter === 'object' ? Object.keys(frontmatter) : []
582
- if (!keys.length) return h('div', { className: 'dsm-note' }, t('detail.noFrontmatter'))
583
- return h('div', { className: 'dsm-fm' }, keys.map(function (key) {
584
- var label = FRONTMATTER_LABELS[key] || key
585
- return h('div', { className: 'dsm-fm-row', key: key },
586
- h('div', { className: 'dsm-fm-key' }, label, label === key ? null : h('span', { className: 'dsm-fm-raw' }, key)),
587
- h('div', { className: 'dsm-fm-val' }, frontmatterValue(frontmatter[key])))
588
- }))
589
- }
590
-
591
- var DICT = {
592
- zh: {
593
- "title": "Skills", "desc": "统一加载和管理本机 Agent Skills。", "link.project": "GitHub", "link.feedback": "问题反馈",
594
- "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": "用系统编辑器打开",
595
- "btn.custom.add": "添加目录", "btn.custom.remove": "移除",
596
- "custom.add.title": "添加自定义技能目录", "custom.add.submit": "添加",
597
- "custom.add.path": "目录绝对路径", "custom.add.path.placeholder": "例如 D:\\skills\\my-skills 或 /home/me/skills",
598
- "custom.add.label": "显示名称(可选)", "custom.add.label.placeholder": "自定义目录",
599
- "custom.add.help": "以只读方式接入该目录:自动发现 SKILL.md 子目录与顶层 .md 单文件,可在来源卡片上启停整个目录或单个技能;与现有来源重叠的目录会被拒绝,目录与源文件都不会被修改。",
600
- "confirm.custom.remove.title": "移除自定义目录",
601
- "confirm.custom.remove.desc": "移除后该目录下的技能将不再出现在列表中(目录与技能文件本身不受影响,可随时重新添加):{name}",
602
- "result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录",
603
- "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": "单文件",
604
- "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": "调用状态",
605
- "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
606
- "empty.search": "没有匹配的技能。", "empty.source": "该来源目录不存在或暂时没有技能。", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
607
- "source.external.note": "通过 Skills Manager 接入,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站;启停只更新管理器状态。",
608
- "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
609
- "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 仍创建用户级 DSH Skill;项目 Skill 可在这里选择活动项目后创建。",
610
- "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}",
611
- "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
612
- "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从当前技能来源移入回收站,之后可以恢复到原位置。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
613
- "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}",
614
- "warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "技能管理器状态文件不可读;所有技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
615
- "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}",
616
- "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
617
- "error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中", "error.import.emptySource": "目录下未找到任何 skill 条目: {path}", "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
618
- "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 压缩包无法解压",
619
- "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
620
- "error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
621
- "error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
622
- "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})",
623
- "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} 覆盖",
624
- "action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
625
- "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"
626
- },
627
- en: {
628
- "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.", "link.project": "GitHub", "link.feedback": "Issues",
629
- "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",
630
- "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",
631
- "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",
632
- "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
633
- "empty.search": "No matching skills.", "empty.source": "This source does not exist or has no skills yet.", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
634
- "source.external.note": "Managed through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash; toggles update manager state only.",
635
- "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.",
636
- "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": "The conversational create_skill tool still creates a user-level DSH Skill; choose an active project here for a project Skill.",
637
- "import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .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}",
638
- "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}",
639
- "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of its current skill source and can be restored to the same location later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
640
- "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}",
641
- "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}",
642
- "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}",
643
- "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Skill sources containing symbolic links are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
644
- "error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory", "error.import.emptySource": "No skill entries found in the directory: {path}", "error.import.invalidName": "Cannot generate a valid kebab-case name (original: {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})",
645
- "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",
646
- "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})",
647
- "error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
648
- "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}",
649
- "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})",
650
- "diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name is not valid 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}",
651
- "action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
652
- "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"
653
- }
654
- };
655
-
656
-
657
- function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
658
- function translateError(t, payload) {
659
- if (payload instanceof Error && !payload.code) return payload.message;
660
- if (payload && typeof payload === "object") {
661
- 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; }
662
- if (payload.error !== undefined) return translateError(t, payload.error); if (payload.message !== undefined) return String(payload.message);
663
- }
664
- return String(payload == null ? "" : payload);
665
- }
666
- 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; }); }
667
- var OP_BY_PATH = { "/state": "skill-state", "/enable": "skill-enable", "/disable": "skill-disable",
668
- "/source-enable": "skill-source-enable", "/source-disable": "skill-source-disable",
669
- "/delete": "skill-delete", "/trash-restore": "skill-trash-restore", "/trash-delete": "skill-trash-delete",
670
- "/detail": "skill-detail", "/create": "skill-create", "/import": "skill-import",
671
- "/upload": "skill-upload", "/browse": "skill-browse" };
672
- function callApi(path, options) {
673
- var op = OP_BY_PATH[path];
674
- if (!op) return Promise.reject({ error: "unknown action: " + path });
675
- var args = {};
676
- if (options && typeof options.body === "string") { try { args = JSON.parse(options.body) || {}; } catch (e) { args = {}; } }
677
- return apiCall(op, args).then(function (payload) {
678
- if (!payload || payload.ok !== true) throw payload || { error: "request failed" };
679
- return payload.data;
680
- });
681
- }
682
- function isSkillEnabled(skill) { if (skill.enabled !== undefined) return skill.enabled === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
683
- function countKey(key, count) { return key + (Number(count) === 1 ? ".one" : ".other"); }
684
- function rootDisplayName(t, root) { var base = translateOrFallback(t, "root." + (root.localeKey || root.kind || root.key), root.label); return root.projectName ? base + " · " + root.projectName : base; }
685
- function summarizeImportResult(t, data) {
686
- var importedItems = data && data.imported || [];
687
- var imported = importedItems.map(function (item) { return item.name; });
688
- var skipped = (data && data.skipped || []).map(function (item) { return item.name; });
689
- var warnings = [];
690
- importedItems.forEach(function (item) {
691
- (item.warnings || []).forEach(function (warning) { warnings.push(translateError(t, warning)); });
692
- });
693
- var summary;
694
- if (imported.length && skipped.length) summary = { ok: true, warning: true, imported: true, text: t("result.importPartial", { imported: imported.join(", "), skipped: skipped.join(", ") }) };
695
- else if (imported.length) summary = { ok: true, warning: false, imported: true, text: t("result.imported", { names: imported.join(", ") }) };
696
- else if (skipped.length) summary = { ok: false, warning: true, imported: false, text: t("result.importSkipped", { names: skipped.join(", ") }) };
697
- else summary = { ok: false, warning: false, imported: false, text: t("result.importEmpty") };
698
- if (warnings.length) {
699
- summary.warning = true;
700
- summary.text = t("result.importWarnings", { result: summary.text, warnings: warnings.join(";") });
701
- }
702
- return summary;
703
- }
704
- function normalizeSkillQuery(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
705
- 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); }); }
706
- 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); }); }
707
- function visibleSkillRoots(roots) { return (roots || []).filter(function (root) { return root.scope !== "project" || (root.skills || []).length > 0; }); }
708
- var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
709
- function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
710
- function inspectUploadSelection(files) {
711
- var list = Array.prototype.slice.call(files || []); if (!list.length) return null;
712
- if (list.length > MAX_UPLOAD_ENTRIES) return { error: { code: "error.upload.tooMany", params: { limit: MAX_UPLOAD_ENTRIES } } };
713
- var relative = list.some(function (file) { return uploadFilePath(file).includes("/"); });
714
- if (relative) {
715
- if (!list.some(function (file) { return /(^|\/)skill\.md$/i.test(uploadFilePath(file)); })) return { error: { code: "select.folder.invalid" } };
716
- var oversized = list.find(function (file) { return Number(file.size || 0) > MAX_UPLOAD_ENTRY_BYTES; });
717
- if (oversized) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_ENTRY_BYTES } } };
718
- var total = list.reduce(function (sum, file) { return sum + Number(file.size || 0); }, 0);
719
- if (total > MAX_UPLOAD_TOTAL_BYTES) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_TOTAL_BYTES } } };
720
- return { kind: "folder", name: uploadFilePath(list[0]).split("/")[0], files: list, count: list.length, size: total };
721
- }
722
- if (list.length !== 1) return { error: { code: "select.file.invalid" } };
723
- var name = String(list[0].name || ""), lower = name.toLowerCase();
724
- var size = Number(list[0].size || 0);
725
- 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 };
726
- 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 };
727
- return { error: { code: "select.file.invalid" } };
728
- }
729
- 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); }
730
- 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 }; }); }
731
- 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(); }); }
732
- 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; }); }
733
- 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\"])") || []); }
734
- 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(); } }
735
- 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; }
736
-
737
- function SourceSelect(props) {
738
- 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);
739
- 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]);
740
- 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);
741
- }
742
- 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 }); }
743
- 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" })); }
744
- 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)); }
745
-
746
- function SkillManagerSection(props) {
747
- var t = props.t;
748
- var ss = react.useState({ loading: true, error: null, data: null }), snapshot = ss[0], setSnapshot = ss[1];
749
- var bs = react.useState(false), busy = bs[0], setBusy = bs[1];
750
- var qs = react.useState(""), query = qs[0], setQuery = qs[1];
751
- var fs = react.useState(""), source = fs[0], setSource = fs[1];
752
- var es = react.useState({}), expanded = es[0], setExpanded = es[1];
753
- var ms = react.useState(null), modal = ms[0], setModal = ms[1];
754
- var rs = react.useState(null), result = rs[0], setResult = rs[1];
755
- var fms = react.useState({ root: "dsh", name: "", description: "", body: "" }), form = fms[0], setForm = fms[1];
756
- var us = react.useState(null), upload = us[0], setUpload = us[1];
757
- var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
758
- var cs = react.useState({ path: "", label: "" }), customForm = cs[0], setCustomForm = cs[1];
759
- var inflightRef = react.useRef(false);
760
- var importInputRef = react.useRef(null);
761
- var folderInputRef = react.useRef(null);
762
- var pickerOpenRef = react.useRef(false);
763
- 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 }); }); }
764
- react.useEffect(function () { refresh(false); }, []);
765
- 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); }); }
766
- 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); }); }
767
- 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); }); }
768
- function updateForm(key, value) { setForm(Object.assign({}, form, { [key]: value })); }
769
- // 自定义技能目录:添加 / 移除(走 skill-custom-* ops,成功后静默刷新)。
770
- function submitCustomAdd() {
771
- var payload = { path: String(customForm.path || "").trim(), label: String(customForm.label || "").trim() };
772
- if (!payload.path) return;
773
- setBusy(true); setResult(null);
774
- apiCall("skill-custom-add", payload).then(function (res) {
775
- 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); }
776
- setResult({ ok: false, text: (res && res.error) || t("error.action", { error: "unknown" }) });
777
- }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
778
- }
779
- function submitCustomRemove(key, name) {
780
- setBusy(true); setResult(null);
781
- apiCall("skill-custom-remove", { key: key }).then(function (res) {
782
- if (res && res.ok) { setResult({ ok: true, text: t("result.custom.removed", { name: name }) }); setModal(null); return refresh(true); }
783
- setResult({ ok: false, text: (res && res.error) || t("error.action", { error: "unknown" }) });
784
- }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
785
- }
786
- 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 () {}); }
787
- 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); }
788
- 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); }
789
- 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 () {}); }
790
- 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 : "";
791
- 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") });
792
- function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
793
- function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
794
- 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 }) }; }));
795
-
796
- function renderSkill(root, skill) {
797
- 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";
798
- 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));
799
- }
800
- function renderRoot(root) {
801
- if (activeSource && activeSource !== root.key) return null;
802
- 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;
803
- var rootCount = root.count == null ? root.skills.length : root.count;
804
- 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);
805
- }
806
-
807
- var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
808
- 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(FeedbackLinks, 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", 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-4" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"], [summary.issues || 0, "summary.issues"]].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))]);
809
-
810
- 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")))));
811
- 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); } },
812
- h("input", { ref: importInputRef, className: "dsm-hidden-input", type: "file", accept: ".zip,.md", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
813
- h("input", { ref: folderInputRef, className: "dsm-hidden-input", type: "file", multiple: true, webkitdirectory: "", directory: "", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
814
- h("div", { className: "dsm-dropzone", 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) }); }); } },
815
- h("span", { className: "dsm-dropzone-title" }, t("upload.drop.title")),
816
- h("span", { className: "dsm-dropzone-copy" }, t("upload.drop.copy")),
817
- h("div", { className: "dsm-upload-choices" }, h("button", { type: "button", className: "dsm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(folderInputRef); } }, t("btn.folder.pick")), h("span", { className: "dsm-upload-divider" }, "/"), h("button", { type: "button", className: "dsm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(importInputRef); } }, t("btn.file.pick")))
818
- ),
819
- 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,
820
- result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null,
821
- 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")))),
822
- 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")))
823
- )); 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"))));
824
- 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"))));
825
- 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")))));
826
- 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")))));
827
- 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("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("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), 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")))));
828
- 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")))));
829
- return h("section", { className: "dsm-section" }, content);
830
- }
831
-
832
- var NS = 'dsh-plugin-tool-management'
833
- var t = function (key, params) {
834
- var s = (DICT.zh && DICT.zh[key]) || key
835
- if (params) Object.keys(params).forEach(function (k) { s = String(s).replace("{" + k + "}", params[k]) })
836
- return s
837
- }
838
- try {
839
- if (ctx.locale && typeof ctx.locale.register === 'function') {
840
- ctx.locale.register(NS, DICT)
841
- t = ctx.locale.bind(NS)
842
- }
843
- } catch (e) { /* locale 服务缺失时退回内置词典 */ }
844
-
845
- module.exports.DICT = DICT
846
- },
847
- }
848
- return module.exports
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
+ })