dsh-tool-adapt 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +841 -0
- package/lib/config.js +145 -0
- package/lib/index.js +512 -0
- package/package.json +40 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// dsh-tool-adapt — client half (official __ModuleLoader__ web bundle)
|
|
2
|
+
//
|
|
3
|
+
// Browser-only bundle: consumed by the DSH web loader (window.__ModuleLoader__).
|
|
4
|
+
// Not importable in Node. Renders the ADAPT status pill and an official-style
|
|
5
|
+
// expandable Settings Card under settings.plugin.item / key tool-adapt.
|
|
6
|
+
//
|
|
7
|
+
// The pill is hidden by DEFAULT: the ui.pill switch in the Settings Card
|
|
8
|
+
// (设置 → 插件 → ADAPT「显示状态胶囊」) owns visibility; the polled /status
|
|
9
|
+
// config is the hot source of truth the pill follows.
|
|
10
|
+
//
|
|
11
|
+
// Lifecycle contract: every client side effect — the pill mounted flag, root
|
|
12
|
+
// element, shadow DOM, plugin-card style tag, MutationObserver, ResizeObserver,
|
|
13
|
+
// both intervals, the pending animation frame, and every window / document /
|
|
14
|
+
// element / pointer / drag listener — is owned by the Cordis fiber through
|
|
15
|
+
// ctx.effect() and released by the returned disposer. Cleanup is idempotent,
|
|
16
|
+
// so stop / update / HMR can dispose the bundle and a later apply re-mounts
|
|
17
|
+
// the pill from scratch.
|
|
18
|
+
|
|
19
|
+
window.__ModuleLoader__.load({
|
|
20
|
+
id: 'dsh-tool-adapt',
|
|
21
|
+
factory: (require) => {
|
|
22
|
+
const module = { exports: {} }
|
|
23
|
+
const exports = module.exports
|
|
24
|
+
const React = require('react')
|
|
25
|
+
|
|
26
|
+
const NS = 'tool-adapt'
|
|
27
|
+
const API = '/api/tool-adapt'
|
|
28
|
+
const POLL_MS = 5000
|
|
29
|
+
const e = React.createElement
|
|
30
|
+
|
|
31
|
+
const FIELDS = [
|
|
32
|
+
{ path: ['ui', 'pill'], kind: 'bool', label: '显示状态胶囊', hint: '默认关闭。开启后输入框旁显示 ADAPT 胶囊;关闭只隐藏 UI,守卫逻辑不受影响。' },
|
|
33
|
+
{ path: ['guard', 'enabled'], kind: 'bool', label: '守卫启用', hint: '提权死状态下移除 schema 参数,并剥除仍携带的字段。' },
|
|
34
|
+
{ path: ['l2', 'enabled'], kind: 'bool', label: 'L2 惯例预装', hint: '向非排除模型注入 DSH 工具调用惯例。' },
|
|
35
|
+
{ path: ['l2', 'excludeModels'], kind: 'models', label: '排除模型', hint: '逗号分隔的 * 通配模式,默认 deepseek-*。' },
|
|
36
|
+
{ path: ['l2', 'block'], kind: 'textarea', label: '惯例块' },
|
|
37
|
+
{ path: ['l0', 'enabled'], kind: 'bool', label: 'L0 失败保险丝' },
|
|
38
|
+
{ path: ['l0', 'remindAfter'], kind: 'num', label: '提醒阈值', min: 2, max: 20 },
|
|
39
|
+
{ path: ['l0', 'vetoAfter'], kind: 'num', label: 'veto 阈值', min: 0, max: 20, hint: '0 表示关闭 veto。' },
|
|
40
|
+
{ path: ['l0', 'reminderText'], kind: 'textarea', label: '提醒文案' },
|
|
41
|
+
{ path: ['l0', 'vetoText'], kind: 'textarea', label: 'veto 文案' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function isPlainObject(v) {
|
|
45
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getAt(obj, path) {
|
|
49
|
+
let cur = obj
|
|
50
|
+
for (const key of path) {
|
|
51
|
+
if (!isPlainObject(cur) || !(key in cur)) return undefined
|
|
52
|
+
cur = cur[key]
|
|
53
|
+
}
|
|
54
|
+
return cur
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatValue(field, value) {
|
|
58
|
+
if (field.kind === 'bool') return value ? 'true' : 'false'
|
|
59
|
+
if (field.kind === 'models') return Array.isArray(value) ? value.join(',') : ''
|
|
60
|
+
if (value === undefined || value === null) return ''
|
|
61
|
+
return String(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseValue(field, text) {
|
|
65
|
+
if (field.kind === 'bool') return text === true || text === 'true'
|
|
66
|
+
if (field.kind === 'models') {
|
|
67
|
+
const list = String(text || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
68
|
+
if (list.length > 50) return undefined
|
|
69
|
+
for (const entry of list) {
|
|
70
|
+
if (!/^[A-Za-z0-9.*_-]{1,64}$/.test(entry)) return undefined
|
|
71
|
+
}
|
|
72
|
+
return list
|
|
73
|
+
}
|
|
74
|
+
if (field.kind === 'num') {
|
|
75
|
+
const n = Number.parseInt(String(text), 10)
|
|
76
|
+
if (!Number.isInteger(n)) return undefined
|
|
77
|
+
if (field.min !== undefined && n < field.min) return undefined
|
|
78
|
+
if (field.max !== undefined && n > field.max) return undefined
|
|
79
|
+
return n
|
|
80
|
+
}
|
|
81
|
+
const s = String(text || '')
|
|
82
|
+
if (!s) return undefined
|
|
83
|
+
if (field.path[1] === 'block' && s.length > 4000) return undefined
|
|
84
|
+
if ((field.path[1] === 'reminderText' || field.path[1] === 'vetoText') && s.length > 2000) return undefined
|
|
85
|
+
return s
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fieldKey(path) {
|
|
89
|
+
return path.join('.')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Official PluginCard / field chrome cannot be imported by an out-of-repo
|
|
93
|
+
// plugin (bundle purity). Recreate the same disclosure card so ADAPT sits
|
|
94
|
+
// in the Plugins list as one expandable <li> beside Shell / Agent loop.
|
|
95
|
+
const CARD_CSS_ID = 'dsh-tool-adapt/plugin-card'
|
|
96
|
+
const CARD_CSS = [
|
|
97
|
+
'.dtaCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}',
|
|
98
|
+
'.dtaCard:hover{border-color:var(--dsw-alias-label-dimmed)}',
|
|
99
|
+
'.dtaCardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
|
|
100
|
+
'.dtaHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}',
|
|
101
|
+
'.dtaHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
|
|
102
|
+
'.dtaHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}',
|
|
103
|
+
'.dtaName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
|
|
104
|
+
'.dtaDescription{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}',
|
|
105
|
+
'.dtaChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}',
|
|
106
|
+
'.dtaChevronOpen{transform:rotate(180deg)}',
|
|
107
|
+
'.dtaBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}',
|
|
108
|
+
'.dtaReadOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}',
|
|
109
|
+
'.dtaPending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}',
|
|
110
|
+
'.dtaFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}',
|
|
111
|
+
'.dtaFailed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}',
|
|
112
|
+
'.dtaDiscard,.dtaSave{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}',
|
|
113
|
+
'.dtaDiscard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}',
|
|
114
|
+
'.dtaDiscard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}',
|
|
115
|
+
'.dtaSave{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}',
|
|
116
|
+
'.dtaDiscard:disabled,.dtaSave:disabled{opacity:.4;cursor:default}',
|
|
117
|
+
'.dtaDiscard:focus-visible,.dtaSave:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
118
|
+
'.dtaField{flex-direction:column;gap:6px;padding:12px 0;display:flex}',
|
|
119
|
+
'.dtaField+.dtaField{border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
120
|
+
'.dtaFieldHead{align-items:center;gap:8px;display:flex}',
|
|
121
|
+
'.dtaLabel{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}',
|
|
122
|
+
'.dtaBadges{align-items:center;gap:8px;display:inline-flex}',
|
|
123
|
+
'.dtaBadge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}',
|
|
124
|
+
'.dtaReset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}',
|
|
125
|
+
'.dtaReset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}',
|
|
126
|
+
'.dtaReset:disabled{cursor:default}',
|
|
127
|
+
'.dtaInput,.dtaTextarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;font-size:13px;line-height:1.5;width:100%;box-sizing:border-box}',
|
|
128
|
+
'.dtaInput{height:34px;padding:0 12px}',
|
|
129
|
+
'.dtaTextarea{min-height:88px;padding:8px 12px;resize:vertical}',
|
|
130
|
+
'.dtaInput:focus-visible,.dtaTextarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}',
|
|
131
|
+
'.dtaInput:disabled,.dtaTextarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}',
|
|
132
|
+
'.dtaInputInvalid{border-color:var(--dsw-alias-label-error)}',
|
|
133
|
+
'.dtaInvalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}',
|
|
134
|
+
'.dtaHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}',
|
|
135
|
+
'.dtaSwitch{appearance:none;width:36px;height:20px;margin:0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-3);position:relative;cursor:pointer;flex:none}',
|
|
136
|
+
'.dtaSwitch::after{content:"";width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-tertiary);position:absolute;top:2px;left:2px;transition:transform .16s,background .16s}',
|
|
137
|
+
'.dtaSwitch:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}',
|
|
138
|
+
'.dtaSwitch:checked::after{background:var(--dsw-alias-bg-layer-3);transform:translateX(16px)}',
|
|
139
|
+
'.dtaSwitch:disabled{opacity:.4;cursor:default}',
|
|
140
|
+
'.dtaSwitch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
|
|
141
|
+
].join('')
|
|
142
|
+
|
|
143
|
+
function ensureCardStyles() {
|
|
144
|
+
if (typeof document === 'undefined') return
|
|
145
|
+
if (document.querySelector('style[data-plugin-css=' + JSON.stringify(CARD_CSS_ID) + ']')) return
|
|
146
|
+
const tag = document.createElement('style')
|
|
147
|
+
tag.dataset.plugin = 'dsh-tool-adapt'
|
|
148
|
+
tag.dataset.pluginCss = CARD_CSS_ID
|
|
149
|
+
tag.textContent = CARD_CSS
|
|
150
|
+
document.head.appendChild(tag)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Idempotent inverse of ensureCardStyles(): the plugin disposer removes the
|
|
154
|
+
// card style tag so stop / update / HMR leave no style behind. A later
|
|
155
|
+
// SettingsCard render re-creates it via ensureCardStyles().
|
|
156
|
+
function removeCardStyles() {
|
|
157
|
+
if (typeof document === 'undefined') return
|
|
158
|
+
const tag = document.querySelector('style[data-plugin-css=' + JSON.stringify(CARD_CSS_ID) + ']')
|
|
159
|
+
if (tag && tag.parentNode) tag.parentNode.removeChild(tag)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Chevron(props) {
|
|
163
|
+
return e('svg', {
|
|
164
|
+
width: 14,
|
|
165
|
+
height: 14,
|
|
166
|
+
className: props.className,
|
|
167
|
+
viewBox: '0 0 14 14',
|
|
168
|
+
fill: 'none',
|
|
169
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
170
|
+
'aria-hidden': true,
|
|
171
|
+
}, e('path', {
|
|
172
|
+
d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z',
|
|
173
|
+
fill: 'currentColor',
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function FieldRow(props) {
|
|
178
|
+
const id = props.id
|
|
179
|
+
const head = [
|
|
180
|
+
e('label', { key: 'lab', className: 'dtaLabel', htmlFor: id }, props.label),
|
|
181
|
+
]
|
|
182
|
+
if (props.overridden) {
|
|
183
|
+
head.push(e('span', { key: 'badges', className: 'dtaBadges' },
|
|
184
|
+
e('span', { className: 'dtaBadge' }, '已覆盖'),
|
|
185
|
+
e('button', {
|
|
186
|
+
type: 'button',
|
|
187
|
+
className: 'dtaReset',
|
|
188
|
+
disabled: props.disabled,
|
|
189
|
+
onClick: props.onReset,
|
|
190
|
+
}, '恢复默认'),
|
|
191
|
+
))
|
|
192
|
+
}
|
|
193
|
+
const control = props.kind === 'bool'
|
|
194
|
+
? e('input', {
|
|
195
|
+
id,
|
|
196
|
+
className: 'dtaSwitch',
|
|
197
|
+
type: 'checkbox',
|
|
198
|
+
checked: props.text === 'true',
|
|
199
|
+
disabled: props.disabled,
|
|
200
|
+
onChange: (ev) => props.onEdit(ev.target.checked ? 'true' : 'false'),
|
|
201
|
+
})
|
|
202
|
+
: props.kind === 'textarea'
|
|
203
|
+
? e('textarea', {
|
|
204
|
+
id,
|
|
205
|
+
className: props.invalid ? 'dtaTextarea dtaInputInvalid' : 'dtaTextarea',
|
|
206
|
+
value: props.text,
|
|
207
|
+
disabled: props.disabled,
|
|
208
|
+
'aria-invalid': props.invalid || undefined,
|
|
209
|
+
onChange: (ev) => props.onEdit(ev.target.value),
|
|
210
|
+
})
|
|
211
|
+
: e('input', {
|
|
212
|
+
id,
|
|
213
|
+
className: props.invalid ? 'dtaInput dtaInputInvalid' : 'dtaInput',
|
|
214
|
+
type: 'text',
|
|
215
|
+
inputMode: props.kind === 'num' ? 'numeric' : undefined,
|
|
216
|
+
value: props.text,
|
|
217
|
+
disabled: props.disabled,
|
|
218
|
+
'aria-invalid': props.invalid || undefined,
|
|
219
|
+
onChange: (ev) => props.onEdit(ev.target.value),
|
|
220
|
+
})
|
|
221
|
+
return e('div', { className: 'dtaField' },
|
|
222
|
+
e('div', { className: 'dtaFieldHead' }, head),
|
|
223
|
+
control,
|
|
224
|
+
e('p', { className: props.invalid ? 'dtaInvalid' : 'dtaHint' },
|
|
225
|
+
props.invalid ? '当前草稿无法保存' : (props.hint || null),
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function SettingsCard(props) {
|
|
231
|
+
ensureCardStyles()
|
|
232
|
+
const scope = props.scope
|
|
233
|
+
const api = props.api
|
|
234
|
+
const [tick, setTick] = React.useState(0)
|
|
235
|
+
const [open, setOpen] = React.useState(false)
|
|
236
|
+
const [staged, setStaged] = React.useState({})
|
|
237
|
+
const [saving, setSaving] = React.useState(false)
|
|
238
|
+
const [failed, setFailed] = React.useState(false)
|
|
239
|
+
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
if (!scope || typeof scope.subscribe !== 'function') return undefined
|
|
242
|
+
return scope.subscribe(() => setTick((n) => n + 1))
|
|
243
|
+
}, [scope])
|
|
244
|
+
|
|
245
|
+
const snap = scope && typeof scope.getSnapshot === 'function'
|
|
246
|
+
? scope.getSnapshot()
|
|
247
|
+
: { status: 'unavailable', value: undefined, base: undefined, user: undefined, revision: undefined, writable: false }
|
|
248
|
+
|
|
249
|
+
const available = snap.status === 'ready'
|
|
250
|
+
const writable = !!snap.writable
|
|
251
|
+
const value = snap.value || {}
|
|
252
|
+
const base = snap.base || {}
|
|
253
|
+
const user = snap.user || {}
|
|
254
|
+
|
|
255
|
+
const plan = []
|
|
256
|
+
for (const field of FIELDS) {
|
|
257
|
+
const key = fieldKey(field.path)
|
|
258
|
+
const draft = staged[key]
|
|
259
|
+
if (!draft) continue
|
|
260
|
+
if (draft.clear) {
|
|
261
|
+
if (getAt(user, field.path) !== undefined) plan.push({ field, op: 'unset', path: field.path })
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
const parsed = parseValue(field, draft.text)
|
|
265
|
+
if (parsed === undefined) {
|
|
266
|
+
plan.push({ field, invalid: true })
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
if (formatValue(field, getAt(value, field.path)) === formatValue(field, parsed)) continue
|
|
270
|
+
plan.push({ field, op: 'set', path: field.path, value: parsed })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const dirty = plan.length > 0
|
|
274
|
+
const invalid = plan.some((item) => item.invalid)
|
|
275
|
+
const blocked = !dirty || invalid || saving
|
|
276
|
+
|
|
277
|
+
function stage(field, next) {
|
|
278
|
+
setFailed(false)
|
|
279
|
+
setStaged((prev) => Object.assign({}, prev, { [fieldKey(field.path)]: next }))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function discard() {
|
|
283
|
+
if (!dirty && !failed) return
|
|
284
|
+
setStaged({})
|
|
285
|
+
setFailed(false)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function save() {
|
|
289
|
+
if (!api || !api.settings || saving || !dirty || invalid || !writable) return
|
|
290
|
+
setSaving(true)
|
|
291
|
+
setFailed(false)
|
|
292
|
+
try {
|
|
293
|
+
const ops = plan.filter((item) => !item.invalid).map((item) => (
|
|
294
|
+
item.op === 'unset'
|
|
295
|
+
? { op: 'unset', path: item.path }
|
|
296
|
+
: { op: 'set', path: item.path, value: item.value }
|
|
297
|
+
))
|
|
298
|
+
const payload = { ns: NS, ops }
|
|
299
|
+
if (snap.revision !== undefined) payload.expectedRevision = snap.revision
|
|
300
|
+
const response = await api.settings.mutate(payload)
|
|
301
|
+
const ok = !!(response && response.result && response.result.ok)
|
|
302
|
+
if (ok) setStaged({})
|
|
303
|
+
else setFailed(true)
|
|
304
|
+
} catch (_) {
|
|
305
|
+
setFailed(true)
|
|
306
|
+
}
|
|
307
|
+
setSaving(false)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void tick
|
|
311
|
+
if (!available) return null
|
|
312
|
+
|
|
313
|
+
const fields = FIELDS.map((field) => {
|
|
314
|
+
const key = fieldKey(field.path)
|
|
315
|
+
const draft = staged[key]
|
|
316
|
+
const current = getAt(value, field.path)
|
|
317
|
+
const stored = getAt(user, field.path) !== undefined
|
|
318
|
+
const overridden = draft
|
|
319
|
+
? !draft.clear
|
|
320
|
+
: stored
|
|
321
|
+
const text = draft ? draft.text : formatValue(field, current)
|
|
322
|
+
const parsed = draft && !draft.clear ? parseValue(field, draft.text) : current
|
|
323
|
+
const invalidField = !!(draft && !draft.clear && parsed === undefined)
|
|
324
|
+
return e(FieldRow, {
|
|
325
|
+
key,
|
|
326
|
+
id: 'plugin-config-tool-adapt-' + key.replace(/\./g, '-'),
|
|
327
|
+
kind: field.kind,
|
|
328
|
+
label: field.label,
|
|
329
|
+
hint: field.hint,
|
|
330
|
+
text,
|
|
331
|
+
overridden,
|
|
332
|
+
invalid: invalidField,
|
|
333
|
+
disabled: !writable || saving,
|
|
334
|
+
onEdit: (next) => stage(field, { text: next, clear: false }),
|
|
335
|
+
onReset: () => stage(field, { text: formatValue(field, getAt(base, field.path)), clear: true }),
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const body = open ? e('div', { className: 'dtaBody' },
|
|
340
|
+
writable ? null : e('p', { className: 'dtaReadOnly', role: 'status' }, '本部署的设置为只读。'),
|
|
341
|
+
fields,
|
|
342
|
+
e('div', { className: 'dtaFooter' },
|
|
343
|
+
failed ? e('p', { className: 'dtaFailed', role: 'status' }, '本部署没有接受这些值,已保留供你修改。') : null,
|
|
344
|
+
e('button', {
|
|
345
|
+
type: 'button',
|
|
346
|
+
className: 'dtaDiscard',
|
|
347
|
+
disabled: !dirty || saving,
|
|
348
|
+
onClick: discard,
|
|
349
|
+
}, '放弃修改'),
|
|
350
|
+
e('button', {
|
|
351
|
+
type: 'button',
|
|
352
|
+
className: 'dtaSave',
|
|
353
|
+
disabled: blocked || !writable,
|
|
354
|
+
onClick: save,
|
|
355
|
+
}, saving ? '保存中…' : '保存'),
|
|
356
|
+
),
|
|
357
|
+
) : null
|
|
358
|
+
|
|
359
|
+
return e('li', { className: open ? 'dtaCard dtaCardOpen' : 'dtaCard' },
|
|
360
|
+
e('button', {
|
|
361
|
+
type: 'button',
|
|
362
|
+
className: 'dtaHeader',
|
|
363
|
+
'aria-expanded': open,
|
|
364
|
+
'aria-label': (open ? '收起设置' : '展开设置') + ': ADAPT',
|
|
365
|
+
onClick: () => setOpen(!open),
|
|
366
|
+
},
|
|
367
|
+
e('span', { className: 'dtaHeadText' },
|
|
368
|
+
e('span', { className: 'dtaName' }, 'ADAPT'),
|
|
369
|
+
e('span', { className: 'dtaDescription' }, '模型适配守卫。配置热生效,作用于下一次工具调用与提示词组装。'),
|
|
370
|
+
),
|
|
371
|
+
dirty ? e('span', { className: 'dtaPending' }, '未保存') : null,
|
|
372
|
+
e(Chevron, { className: open ? 'dtaChevron dtaChevronOpen' : 'dtaChevron' }),
|
|
373
|
+
),
|
|
374
|
+
body,
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── composer pill ───────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
const PILL_ROOT_ID = 'dsh-tool-adapt-root'
|
|
381
|
+
const ANCHOR_KEY = 'dsh.toolAdapt.anchor'
|
|
382
|
+
const ANCHORS = ['tl', 'tr', 'bl', 'br']
|
|
383
|
+
const ENSURE_MS = 2000
|
|
384
|
+
|
|
385
|
+
// Mounted flag owned by the plugin lifecycle: set when the pill starts and
|
|
386
|
+
// reset by the idempotent disposer, so a later apply (update / HMR / re-run)
|
|
387
|
+
// mounts a fresh pill instead of being blocked by a stale flag.
|
|
388
|
+
let pillMounted = false
|
|
389
|
+
let pillDispose = null
|
|
390
|
+
|
|
391
|
+
function startPill(scope) {
|
|
392
|
+
if (typeof document === 'undefined') return
|
|
393
|
+
if (pillMounted) {
|
|
394
|
+
// Previous mount leaked (disposal was skipped). Dispose it first, then
|
|
395
|
+
// mount again: every apply must end with exactly one live pill.
|
|
396
|
+
const previous = pillDispose
|
|
397
|
+
pillDispose = null
|
|
398
|
+
if (typeof previous === 'function') previous()
|
|
399
|
+
return startPill()
|
|
400
|
+
}
|
|
401
|
+
pillMounted = true
|
|
402
|
+
|
|
403
|
+
// Idempotent re-mount: drop any root a previous bundle left behind
|
|
404
|
+
// before creating a fresh one.
|
|
405
|
+
const stale = document.getElementById(PILL_ROOT_ID)
|
|
406
|
+
if (stale && stale.parentNode) stale.parentNode.removeChild(stale)
|
|
407
|
+
|
|
408
|
+
const cleanups = []
|
|
409
|
+
const track = (fn) => cleanups.push(fn)
|
|
410
|
+
|
|
411
|
+
let disposed = false
|
|
412
|
+
let root = null
|
|
413
|
+
let dragCleanup = null
|
|
414
|
+
|
|
415
|
+
// Mount target is ONLY the composer seat. There is deliberately no
|
|
416
|
+
// document.body fallback (and no composer-card parent fallback): the pill
|
|
417
|
+
// must stay inside the composer stacking context so DSH overlays can
|
|
418
|
+
// cover it. When the seat has not appeared yet, the document observer
|
|
419
|
+
// below waits for [data-composer-seat] and mounts as soon as it exists.
|
|
420
|
+
function findSeat() {
|
|
421
|
+
return document.querySelector('[data-composer-seat]')
|
|
422
|
+
}
|
|
423
|
+
function ensureMounted() {
|
|
424
|
+
const seat = findSeat()
|
|
425
|
+
if (!seat) return false
|
|
426
|
+
if (root.parentNode !== seat || !root.isConnected) seat.appendChild(root)
|
|
427
|
+
return true
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
root = document.createElement('div')
|
|
431
|
+
root.id = PILL_ROOT_ID
|
|
432
|
+
// Hidden by default (ui.pill defaults to false): the visibility gate
|
|
433
|
+
// below flips this to '' only once the settings snapshot or a status
|
|
434
|
+
// poll reports ui.pill === true.
|
|
435
|
+
root.style.cssText = 'position:fixed;z-index:1;display:none;font-family:ui-sans-serif,system-ui,sans-serif;'
|
|
436
|
+
|
|
437
|
+
const shadow = root.attachShadow({ mode: 'open' })
|
|
438
|
+
shadow.innerHTML = '<style>' + [
|
|
439
|
+
':host{all:initial}',
|
|
440
|
+
'.pill{display:flex;align-items:center;gap:7px;padding:7px 14px;border:1px solid var(--dsw-alias-border-l1,#444);border-radius:999px;background:var(--dsw-alias-bg-layer-1,#222);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#222) 82%, transparent);backdrop-filter:blur(8px);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-weight:600;letter-spacing:.04em;cursor:grab;box-shadow:0 2px 10px rgba(0,0,0,.28);user-select:none;touch-action:none;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease}',
|
|
441
|
+
'.pill:active{cursor:grabbing}',
|
|
442
|
+
'.pill:hover{border-color:var(--dsw-alias-brand-primary,#4a9eff);box-shadow:0 4px 16px rgba(0,0,0,.38);transform:translateY(-1px)}',
|
|
443
|
+
'.pill:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4a9eff);outline-offset:2px}',
|
|
444
|
+
'.glyph{display:flex;width:14px;height:14px;color:var(--dsw-alias-brand-primary,#4a9eff)}',
|
|
445
|
+
'.glyph svg{width:14px;height:14px;fill:currentColor}',
|
|
446
|
+
'.dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-secondary,#999);transition:background .3s ease}',
|
|
447
|
+
'.dot.on{background:var(--dsw-alias-state-success-primary,#3fb950);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary,#3fb950) 22%, transparent)}',
|
|
448
|
+
'.dot.off{background:var(--dsw-alias-label-secondary,#999)}',
|
|
449
|
+
'.panel{position:fixed;right:0;bottom:calc(100% + 10px);width:360px;max-height:50vh;overflow:auto;display:none;flex-direction:column;gap:10px;padding:14px;border:1px solid var(--dsw-alias-border-l1,#444);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#222);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#222) 92%, transparent);backdrop-filter:blur(14px);color:var(--dsw-alias-label-primary,#eee);font-size:12px;box-shadow:0 12px 40px rgba(0,0,0,.45)}',
|
|
450
|
+
'.panel.open{display:flex}',
|
|
451
|
+
'.head{display:flex;align-items:baseline;gap:8px}',
|
|
452
|
+
'.head .t{font-size:14px;font-weight:700}',
|
|
453
|
+
'.head .sub{color:var(--dsw-alias-label-secondary,#999);font-size:11px}',
|
|
454
|
+
'.close{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-label-secondary,#999);cursor:pointer;font-size:14px}',
|
|
455
|
+
'.hint{color:var(--dsw-alias-label-secondary,#999);font-size:11px;line-height:1.5}',
|
|
456
|
+
'.status{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary,#999);font-size:11px}',
|
|
457
|
+
'.status .sdot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#3fb950)}',
|
|
458
|
+
'.status .sdot.off{background:var(--dsw-alias-label-secondary,#999)}',
|
|
459
|
+
'.err{color:var(--dsw-alias-state-error-primary,#f85149);font-size:11px;word-break:break-all}',
|
|
460
|
+
].join('') + '</style>'
|
|
461
|
+
|
|
462
|
+
const SHIELD = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l8 3.5v5.2c0 5-3.4 9.3-8 11.3-4.6-2-8-6.3-8-11.3V5.5L12 2zm0 2.2L6 6.4v4.3c0 3.9 2.6 7.4 6 9.1 3.4-1.7 6-5.2 6-9.1V6.4l-6-2.2z"/><path d="M10.8 14.6l-2.3-2.3-1.4 1.4 3.7 3.7 6.1-6.1-1.4-1.4-4.7 4.7z" opacity=".9"/></svg>'
|
|
463
|
+
const pill = document.createElement('button')
|
|
464
|
+
pill.className = 'pill'
|
|
465
|
+
pill.title = 'ADAPT 适配守卫(点击查看状态;拖动可吸附到输入框四角)'
|
|
466
|
+
const glyph = document.createElement('span')
|
|
467
|
+
glyph.className = 'glyph'
|
|
468
|
+
glyph.innerHTML = SHIELD
|
|
469
|
+
const dot = document.createElement('span')
|
|
470
|
+
dot.className = 'dot'
|
|
471
|
+
const label = document.createElement('span')
|
|
472
|
+
label.textContent = 'ADAPT'
|
|
473
|
+
pill.append(glyph, label, dot)
|
|
474
|
+
|
|
475
|
+
const cluster = document.createElement('div')
|
|
476
|
+
cluster.style.cssText = 'display:flex;align-items:center'
|
|
477
|
+
cluster.append(pill)
|
|
478
|
+
|
|
479
|
+
const panel = document.createElement('div')
|
|
480
|
+
panel.className = 'panel'
|
|
481
|
+
const head = document.createElement('div')
|
|
482
|
+
head.className = 'head'
|
|
483
|
+
const title = document.createElement('span')
|
|
484
|
+
title.className = 't'
|
|
485
|
+
title.textContent = 'ADAPT 模型适配守卫'
|
|
486
|
+
const sub = document.createElement('span')
|
|
487
|
+
sub.className = 'sub'
|
|
488
|
+
sub.textContent = 'dsh-tool-adapt'
|
|
489
|
+
const closeBtn = document.createElement('button')
|
|
490
|
+
closeBtn.className = 'close'
|
|
491
|
+
closeBtn.textContent = '✕'
|
|
492
|
+
head.append(title, sub, closeBtn)
|
|
493
|
+
const statusEl = document.createElement('div')
|
|
494
|
+
statusEl.className = 'status'
|
|
495
|
+
const hint = document.createElement('div')
|
|
496
|
+
hint.className = 'hint'
|
|
497
|
+
hint.textContent = '完整配置请到 设置 → 插件 → ADAPT。这里只显示当前守卫状态。'
|
|
498
|
+
const errEl = document.createElement('div')
|
|
499
|
+
errEl.className = 'err'
|
|
500
|
+
panel.append(head, statusEl, hint, errEl)
|
|
501
|
+
shadow.append(cluster, panel)
|
|
502
|
+
|
|
503
|
+
let anchor = 'br'
|
|
504
|
+
try {
|
|
505
|
+
const saved = localStorage.getItem(ANCHOR_KEY)
|
|
506
|
+
if (saved === 'left') anchor = 'bl'
|
|
507
|
+
else if (saved === 'right') anchor = 'br'
|
|
508
|
+
else if (ANCHORS.indexOf(saved) !== -1) anchor = saved
|
|
509
|
+
} catch (_) {}
|
|
510
|
+
|
|
511
|
+
root.style.right = '16px'
|
|
512
|
+
root.style.bottom = '64px'
|
|
513
|
+
let lastX = null
|
|
514
|
+
let lastY = null
|
|
515
|
+
let dragging = false
|
|
516
|
+
|
|
517
|
+
function findComposer() {
|
|
518
|
+
return document.querySelector('[data-composer-card]') || findSeat() || null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function place() {
|
|
522
|
+
if (dragging || !root.isConnected) return
|
|
523
|
+
const seat = findComposer()
|
|
524
|
+
if (!seat) return
|
|
525
|
+
const r = seat.getBoundingClientRect()
|
|
526
|
+
const pr = cluster.getBoundingClientRect()
|
|
527
|
+
const gap = 8
|
|
528
|
+
const leftSide = anchor === 'tl' || anchor === 'bl'
|
|
529
|
+
const topSide = anchor === 'tl' || anchor === 'tr'
|
|
530
|
+
let x = leftSide ? r.left - pr.width - gap : r.right + gap
|
|
531
|
+
let y = topSide ? r.top - pr.height - gap : r.bottom + gap
|
|
532
|
+
x = Math.max(4, Math.min(window.innerWidth - pr.width - 4, x))
|
|
533
|
+
y = Math.max(4, Math.min(window.innerHeight - pr.height - 4, y))
|
|
534
|
+
if (x === lastX && y === lastY) return
|
|
535
|
+
lastX = x
|
|
536
|
+
lastY = y
|
|
537
|
+
root.style.left = x + 'px'
|
|
538
|
+
root.style.top = y + 'px'
|
|
539
|
+
root.style.right = 'auto'
|
|
540
|
+
root.style.bottom = 'auto'
|
|
541
|
+
if (panel.classList.contains('open')) placePanel()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let rafId = 0
|
|
545
|
+
let rafPending = false
|
|
546
|
+
function schedulePlace() {
|
|
547
|
+
if (disposed || rafPending) return
|
|
548
|
+
rafPending = true
|
|
549
|
+
rafId = requestAnimationFrame(function () {
|
|
550
|
+
rafPending = false
|
|
551
|
+
rafId = 0
|
|
552
|
+
place()
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Wait for [data-composer-seat]: observe the whole document so the pill
|
|
557
|
+
// mounts whenever the seat first appears and follows later re-inserts.
|
|
558
|
+
const domObserver = new MutationObserver(function () {
|
|
559
|
+
if (ensureMounted()) schedulePlace()
|
|
560
|
+
})
|
|
561
|
+
domObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
562
|
+
track(function () { domObserver.disconnect() })
|
|
563
|
+
|
|
564
|
+
window.addEventListener('scroll', schedulePlace, { capture: true, passive: true })
|
|
565
|
+
track(function () { window.removeEventListener('scroll', schedulePlace, { capture: true }) })
|
|
566
|
+
|
|
567
|
+
window.addEventListener('resize', schedulePlace)
|
|
568
|
+
track(function () { window.removeEventListener('resize', schedulePlace) })
|
|
569
|
+
|
|
570
|
+
const seatRo = new ResizeObserver(schedulePlace)
|
|
571
|
+
let seatObserved = null
|
|
572
|
+
function watchSeat() {
|
|
573
|
+
const seat = findComposer()
|
|
574
|
+
if (seat && seat !== seatObserved) {
|
|
575
|
+
if (seatObserved) seatRo.unobserve(seatObserved)
|
|
576
|
+
seatRo.observe(seat)
|
|
577
|
+
seatObserved = seat
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
watchSeat()
|
|
581
|
+
track(function () { seatRo.disconnect() })
|
|
582
|
+
|
|
583
|
+
const ensureTimer = setInterval(function () {
|
|
584
|
+
ensureMounted() // React may have re-rendered the composer seat away
|
|
585
|
+
watchSeat()
|
|
586
|
+
place()
|
|
587
|
+
}, ENSURE_MS)
|
|
588
|
+
track(function () { clearInterval(ensureTimer) })
|
|
589
|
+
|
|
590
|
+
let lastDragAt = 0
|
|
591
|
+
function onPointerDown(ev) {
|
|
592
|
+
if (ev.button !== 0 || disposed) return
|
|
593
|
+
ev.preventDefault()
|
|
594
|
+
dragging = true
|
|
595
|
+
panel.classList.remove('open')
|
|
596
|
+
const startX = ev.clientX
|
|
597
|
+
const startY = ev.clientY
|
|
598
|
+
const startLeft = root.getBoundingClientRect().left
|
|
599
|
+
const startTop = root.getBoundingClientRect().top
|
|
600
|
+
let moved = false
|
|
601
|
+
const prevSelect = document.body.style.userSelect
|
|
602
|
+
document.body.style.userSelect = 'none'
|
|
603
|
+
function onMove(mv) {
|
|
604
|
+
const dx = mv.clientX - startX
|
|
605
|
+
const dy = mv.clientY - startY
|
|
606
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) moved = true
|
|
607
|
+
root.style.left = (startLeft + dx) + 'px'
|
|
608
|
+
root.style.top = (startTop + dy) + 'px'
|
|
609
|
+
root.style.right = 'auto'
|
|
610
|
+
root.style.bottom = 'auto'
|
|
611
|
+
}
|
|
612
|
+
function onUp() {
|
|
613
|
+
dragCleanup = null
|
|
614
|
+
document.removeEventListener('pointermove', onMove)
|
|
615
|
+
document.removeEventListener('pointerup', onUp)
|
|
616
|
+
document.body.style.userSelect = prevSelect
|
|
617
|
+
dragging = false
|
|
618
|
+
if (!moved) return
|
|
619
|
+
lastDragAt = Date.now()
|
|
620
|
+
const rect = root.getBoundingClientRect()
|
|
621
|
+
const cx = rect.left + rect.width / 2
|
|
622
|
+
const cy = rect.top + rect.height / 2
|
|
623
|
+
const seat = findComposer()
|
|
624
|
+
if (seat) {
|
|
625
|
+
const r = seat.getBoundingClientRect()
|
|
626
|
+
const leftSide = cx < r.left + r.width / 2
|
|
627
|
+
const topSide = cy < r.top + r.height / 2
|
|
628
|
+
anchor = (topSide ? 't' : 'b') + (leftSide ? 'l' : 'r')
|
|
629
|
+
try { localStorage.setItem(ANCHOR_KEY, anchor) } catch (_) {}
|
|
630
|
+
}
|
|
631
|
+
lastX = null
|
|
632
|
+
lastY = null
|
|
633
|
+
place()
|
|
634
|
+
}
|
|
635
|
+
// Disposal during an active drag must still release the document-level
|
|
636
|
+
// listeners and restore the body selection style.
|
|
637
|
+
dragCleanup = function () {
|
|
638
|
+
document.removeEventListener('pointermove', onMove)
|
|
639
|
+
document.removeEventListener('pointerup', onUp)
|
|
640
|
+
document.body.style.userSelect = prevSelect
|
|
641
|
+
dragging = false
|
|
642
|
+
}
|
|
643
|
+
document.addEventListener('pointermove', onMove)
|
|
644
|
+
document.addEventListener('pointerup', onUp)
|
|
645
|
+
}
|
|
646
|
+
cluster.addEventListener('pointerdown', onPointerDown)
|
|
647
|
+
track(function () { cluster.removeEventListener('pointerdown', onPointerDown) })
|
|
648
|
+
|
|
649
|
+
function placePanel() {
|
|
650
|
+
panel.style.position = 'fixed'
|
|
651
|
+
const r = cluster.getBoundingClientRect()
|
|
652
|
+
const pw = panel.offsetWidth || 360
|
|
653
|
+
const ph = panel.offsetHeight || 220
|
|
654
|
+
let left = r.left
|
|
655
|
+
const maxLeft = Math.max(4, window.innerWidth - pw - 4)
|
|
656
|
+
if (left > maxLeft) left = maxLeft
|
|
657
|
+
if (left < 4) left = 4
|
|
658
|
+
const below = window.innerHeight - r.bottom - 8 >= ph || r.top < ph + 8
|
|
659
|
+
if (below) {
|
|
660
|
+
panel.style.left = left + 'px'
|
|
661
|
+
panel.style.top = (r.bottom + 8) + 'px'
|
|
662
|
+
panel.style.right = 'auto'
|
|
663
|
+
panel.style.bottom = 'auto'
|
|
664
|
+
} else {
|
|
665
|
+
panel.style.left = left + 'px'
|
|
666
|
+
panel.style.top = 'auto'
|
|
667
|
+
panel.style.right = 'auto'
|
|
668
|
+
panel.style.bottom = (window.innerHeight - r.top + 8) + 'px'
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function renderPill(cfg) {
|
|
673
|
+
const on = !!(cfg && cfg.guard && cfg.guard.enabled)
|
|
674
|
+
dot.className = 'dot ' + (on ? 'on' : 'off')
|
|
675
|
+
pill.title = on ? 'ADAPT 适配守卫:已启用' : 'ADAPT 适配守卫:已停用'
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function renderStatus(data) {
|
|
679
|
+
statusEl.textContent = ''
|
|
680
|
+
const sdot = document.createElement('span')
|
|
681
|
+
sdot.className = data && data.ok ? 'sdot' : 'sdot off'
|
|
682
|
+
const txt = document.createElement('span')
|
|
683
|
+
if (data && data.ok) {
|
|
684
|
+
const via = data.settingsReady ? '官方 Settings(tool-adapt)' : '兼容 JSON 文件'
|
|
685
|
+
txt.textContent = '守卫 ' + (data.config && data.config.guard && data.config.guard.enabled ? '已启用' : '已停用') + ' · ' + via
|
|
686
|
+
} else {
|
|
687
|
+
txt.textContent = (data && data.error) || '状态获取失败'
|
|
688
|
+
}
|
|
689
|
+
statusEl.append(sdot, txt)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function refresh() {
|
|
693
|
+
try {
|
|
694
|
+
const res = await fetch(API + '/status', { cache: 'no-store' })
|
|
695
|
+
const data = await res.json()
|
|
696
|
+
if (data && data.ok) {
|
|
697
|
+
renderPill(data.config)
|
|
698
|
+
renderStatus(data)
|
|
699
|
+
noteStatusConfig(data.config)
|
|
700
|
+
errEl.textContent = data.fileError ? '配置警告: ' + data.fileError : ''
|
|
701
|
+
} else {
|
|
702
|
+
renderStatus(data)
|
|
703
|
+
errEl.textContent = (data && data.error) || '状态获取失败'
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
renderStatus(null)
|
|
707
|
+
errEl.textContent = String((err && err.message) || err)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function pollPill() {
|
|
712
|
+
try {
|
|
713
|
+
const res = await fetch(API + '/status', { cache: 'no-store' })
|
|
714
|
+
const data = await res.json()
|
|
715
|
+
if (data && data.ok) {
|
|
716
|
+
renderPill(data.config)
|
|
717
|
+
noteStatusConfig(data.config)
|
|
718
|
+
}
|
|
719
|
+
} catch (_) {}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function onPillClick() {
|
|
723
|
+
if (Date.now() - lastDragAt < 350) return
|
|
724
|
+
panel.classList.toggle('open')
|
|
725
|
+
if (panel.classList.contains('open')) {
|
|
726
|
+
placePanel()
|
|
727
|
+
refresh()
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function onCloseClick() {
|
|
731
|
+
panel.classList.remove('open')
|
|
732
|
+
}
|
|
733
|
+
pill.addEventListener('click', onPillClick)
|
|
734
|
+
track(function () { pill.removeEventListener('click', onPillClick) })
|
|
735
|
+
closeBtn.addEventListener('click', onCloseClick)
|
|
736
|
+
track(function () { closeBtn.removeEventListener('click', onCloseClick) })
|
|
737
|
+
|
|
738
|
+
refresh()
|
|
739
|
+
pollPill()
|
|
740
|
+
const pollTimer = setInterval(pollPill, POLL_MS)
|
|
741
|
+
track(function () { clearInterval(pollTimer) })
|
|
742
|
+
|
|
743
|
+
// ── visibility gate: 设置 → 插件 → ADAPT「显示状态胶囊」(ui.pill) ─────
|
|
744
|
+
// Default OFF: the root is created display:none and only the polled
|
|
745
|
+
// /status config (hot, host-authoritative, also correct for the legacy
|
|
746
|
+
// JSON file mode) may reveal it. Placed after every binding above is
|
|
747
|
+
// initialized because applyVisibility() can call schedulePlace().
|
|
748
|
+
let pillVisible = false
|
|
749
|
+
|
|
750
|
+
function applyVisibility() {
|
|
751
|
+
if (!root) return
|
|
752
|
+
root.style.display = pillVisible ? '' : 'none'
|
|
753
|
+
if (!pillVisible) {
|
|
754
|
+
if (panel.classList.contains('open')) panel.classList.remove('open')
|
|
755
|
+
} else {
|
|
756
|
+
schedulePlace() // cluster rect was 0 while hidden — reposition now
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function noteStatusConfig(cfg) {
|
|
761
|
+
const show = !!(cfg && isPlainObject(cfg.ui) && cfg.ui.pill === true)
|
|
762
|
+
if (show === pillVisible) return
|
|
763
|
+
pillVisible = show
|
|
764
|
+
applyVisibility()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (scope && typeof scope.subscribe === 'function') {
|
|
768
|
+
// A settings save reaches the host config immediately; re-fetch right
|
|
769
|
+
// away so the pill flips without waiting up to POLL_MS. The 5s poll
|
|
770
|
+
// below remains the safety net (and covers other tabs).
|
|
771
|
+
const unsubscribe = scope.subscribe(function () { refresh() })
|
|
772
|
+
track(function () { unsubscribe() })
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Mount immediately when the seat already exists; otherwise the document
|
|
776
|
+
// observer mounts the pill as soon as [data-composer-seat] appears.
|
|
777
|
+
ensureMounted()
|
|
778
|
+
place()
|
|
779
|
+
|
|
780
|
+
function disposePill() {
|
|
781
|
+
if (disposed) return
|
|
782
|
+
disposed = true
|
|
783
|
+
pillMounted = false
|
|
784
|
+
if (rafPending && rafId !== 0) cancelAnimationFrame(rafId)
|
|
785
|
+
rafPending = false
|
|
786
|
+
rafId = 0
|
|
787
|
+
if (dragCleanup) {
|
|
788
|
+
const cleanupDrag = dragCleanup
|
|
789
|
+
dragCleanup = null
|
|
790
|
+
cleanupDrag()
|
|
791
|
+
}
|
|
792
|
+
for (let i = cleanups.length - 1; i >= 0; i--) {
|
|
793
|
+
try { cleanups[i]() } catch (_) {}
|
|
794
|
+
}
|
|
795
|
+
cleanups.length = 0
|
|
796
|
+
if (root && root.parentNode) root.parentNode.removeChild(root)
|
|
797
|
+
root = null
|
|
798
|
+
pillDispose = null
|
|
799
|
+
}
|
|
800
|
+
pillDispose = disposePill
|
|
801
|
+
return disposePill
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function apply(ctx) {
|
|
805
|
+
// Settings card first: pill mounting waits for the composer seat
|
|
806
|
+
// asynchronously and must never delay or break the Settings Slot.
|
|
807
|
+
const scope = ctx.settingsScope.bind({ namespace: NS })
|
|
808
|
+
// Compatibility layer: prefer DSH 0.1.2 fine-grained remote settings;
|
|
809
|
+
// retain the legacy RC connection API for older hosts.
|
|
810
|
+
const remote = ctx.get('remote')
|
|
811
|
+
const api = remote && remote.settings
|
|
812
|
+
? { settings: { mutate: (payload) => remote.settings.mutate(payload.ns, payload.ops, payload.expectedRevision).then((result) => ({ result })) } }
|
|
813
|
+
: (ctx.get('connection') && ctx.get('connection').api)
|
|
814
|
+
const disposeSlot = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
815
|
+
name: 'settings.plugin.item',
|
|
816
|
+
key: NS,
|
|
817
|
+
label: 'ADAPT',
|
|
818
|
+
}, function ToolAdaptCard() {
|
|
819
|
+
return e(SettingsCard, { scope, api })
|
|
820
|
+
}))
|
|
821
|
+
|
|
822
|
+
// Pill: fully lifecycle-owned. The effect runs startPill() now and the
|
|
823
|
+
// returned disposer on stop / update / HMR, idempotently releasing the
|
|
824
|
+
// mounted flag, root, shadow DOM, observers, both intervals, the rAF,
|
|
825
|
+
// and every listener so the next apply re-mounts from scratch. The pill
|
|
826
|
+
// starts hidden: startPill gates visibility on ui.pill (default false)
|
|
827
|
+
// via the settings scope plus the /status poll fallback.
|
|
828
|
+
ctx.effect(() => startPill(scope), 'dsh-tool-adapt: composer pill')
|
|
829
|
+
|
|
830
|
+
// Plugin-card style tag: created lazily by SettingsCard renders and
|
|
831
|
+
// removed here so no style is left behind after stop / update.
|
|
832
|
+
ctx.effect(() => () => removeCardStyles(), 'dsh-tool-adapt: plugin card style')
|
|
833
|
+
|
|
834
|
+
return disposeSlot
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
exports.apply = apply
|
|
838
|
+
exports.inject = ['slots', 'settingsScope', 'connection']
|
|
839
|
+
return module.exports
|
|
840
|
+
},
|
|
841
|
+
})
|