agent-purge 0.2.9 → 0.2.11
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/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -21
- package/README.md +176 -118
- package/backends/index.mjs +204 -244
- package/bench/bench-armor.mjs +844 -832
- package/bench/capture-hot.mjs +58 -0
- package/bench/capture-provider.mjs +66 -0
- package/bench/capture-report.mjs +15 -0
- package/bench/capture-server-hot.mjs +55 -0
- package/bench/capture-system.mjs +36 -0
- package/bench/judge-armor.mjs +119 -119
- package/bin/agent-purge.mjs +906 -772
- package/docs/LEGAL.md +27 -0
- package/docs/architecture.md +39 -0
- package/docs/compatibility.md +78 -0
- package/docs/images/console-desktop.png +0 -0
- package/docs/images/console-mobile.png +0 -0
- package/docs/releasing.md +87 -0
- package/docs/usage.md +158 -0
- package/lib/activation.mjs +38 -0
- package/lib/apply.mjs +227 -0
- package/lib/armed.mjs +38 -38
- package/lib/armor.mjs +225 -222
- package/lib/caps.mjs +182 -173
- package/lib/capture.mjs +324 -0
- package/lib/clean.mjs +124 -0
- package/lib/control.mjs +60 -0
- package/lib/detect.mjs +74 -70
- package/lib/doctor.mjs +7 -7
- package/lib/dsh.mjs +495 -453
- package/lib/environment.mjs +36 -0
- package/lib/evidence.mjs +41 -0
- package/lib/extension.mjs +140 -0
- package/lib/footprints.mjs +59 -0
- package/lib/home.mjs +47 -47
- package/lib/launch-conflicts.mjs +24 -0
- package/lib/launch.mjs +57 -0
- package/lib/opencode.mjs +62 -0
- package/lib/paths.mjs +25 -0
- package/lib/persist.mjs +439 -280
- package/lib/personas.mjs +17 -7
- package/lib/private-server.mjs +54 -0
- package/lib/project.mjs +134 -0
- package/lib/restore.mjs +131 -114
- package/lib/rewrite.mjs +124 -0
- package/lib/root.mjs +37 -0
- package/lib/shim.mjs +203 -160
- package/lib/slashes.mjs +227 -628
- package/lib/spawn.mjs +182 -126
- package/lib/state.mjs +85 -137
- package/lib/templates/personas/neutral.md +30 -30
- package/lib/update.mjs +137 -0
- package/lib/verification.mjs +23 -0
- package/lib/window.mjs +58 -0
- package/lib/wrappers.mjs +49 -0
- package/magpie-plugin/README.md +44 -0
- package/matrix.json +325 -341
- package/package.json +30 -4
- package/web/app.js +525 -0
- package/web/assets/agent-purge.ico +0 -0
- package/web/assets/claude.ico +0 -0
- package/web/assets/codebuddy.png +0 -0
- package/web/assets/codex.webp +0 -0
- package/web/assets/dsh.ico +0 -0
- package/web/assets/grok.ico +0 -0
- package/web/assets/hermes.ico +0 -0
- package/web/assets/omp.png +0 -0
- package/web/assets/opencode.ico +0 -0
- package/web/assets/opencode2.ico +0 -0
- package/web/assets/pi.png +0 -0
- package/web/assets/zcode.png +0 -0
- package/web/model.mjs +49 -0
- package/web/server.mjs +309 -194
- package/web/styles.css +917 -0
- package/web/ui.html +64 -1163
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-purge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "One persona, many coding agents: replace or inject system prompts across codex, claude, opencode (v1/v2), grok, omp, hermes, pi, zcode, dsh.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,21 +10,43 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
12
|
"backends",
|
|
13
|
-
"web",
|
|
13
|
+
"web/server.mjs",
|
|
14
|
+
"web/ui.html",
|
|
15
|
+
"web/app.js",
|
|
16
|
+
"web/model.mjs",
|
|
17
|
+
"web/styles.css",
|
|
18
|
+
"web/assets",
|
|
14
19
|
"bench",
|
|
15
20
|
"matrix.json",
|
|
16
21
|
"README.md",
|
|
22
|
+
"CONTRIBUTING.md",
|
|
23
|
+
"docs/usage.md",
|
|
24
|
+
"docs/compatibility.md",
|
|
25
|
+
"docs/architecture.md",
|
|
26
|
+
"docs/releasing.md",
|
|
27
|
+
"docs/LEGAL.md",
|
|
28
|
+
"docs/images",
|
|
29
|
+
"magpie-plugin/README.md",
|
|
17
30
|
"LICENSE"
|
|
18
31
|
],
|
|
19
32
|
"engines": {
|
|
20
33
|
"node": ">=20"
|
|
21
34
|
},
|
|
22
35
|
"scripts": {
|
|
36
|
+
"build": "node scripts/build-js.mjs",
|
|
37
|
+
"build:js": "node scripts/build-js.mjs",
|
|
38
|
+
"build:setup": "node scripts/build-js.mjs && node scripts/build-setup.mjs",
|
|
39
|
+
"build:exe": "node scripts/build-exe.mjs",
|
|
23
40
|
"web": "node bin/agent-purge.mjs web",
|
|
24
41
|
"matrix": "node bin/agent-purge.mjs matrix",
|
|
25
42
|
"detect": "node bin/agent-purge.mjs detect",
|
|
26
|
-
"test": "node
|
|
27
|
-
"
|
|
43
|
+
"test": "node scripts/test.mjs",
|
|
44
|
+
"check:repo": "node scripts/check-repo.mjs",
|
|
45
|
+
"test:harness": "node bench/capture-system.mjs --hot",
|
|
46
|
+
"test:ui": "playwright test",
|
|
47
|
+
"test:package": "node scripts/check-package.mjs",
|
|
48
|
+
"test:artifacts": "node scripts/check-artifacts.mjs",
|
|
49
|
+
"release:checksums": "node scripts/release-checksums.mjs"
|
|
28
50
|
},
|
|
29
51
|
"keywords": [
|
|
30
52
|
"system-prompt",
|
|
@@ -46,5 +68,9 @@
|
|
|
46
68
|
"homepage": "https://github.com/ZiChuanLan/agent-purge#readme",
|
|
47
69
|
"bugs": {
|
|
48
70
|
"url": "https://github.com/ZiChuanLan/agent-purge/issues"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@axe-core/playwright": "4.14.0",
|
|
74
|
+
"@playwright/test": "1.64.1"
|
|
49
75
|
}
|
|
50
76
|
}
|
package/web/app.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { MODE_LABELS, CHANNEL_LABELS, CAPTURE_LABELS, personaName, configurationOf, changed, needsApply, sortedHarnesses, effectLabel, scopeSummary } from './model.mjs';
|
|
2
|
+
import { verificationEvidence, captureScopeLabel } from './evidence.mjs';
|
|
3
|
+
const $ = id => document.getElementById(id);
|
|
4
|
+
// 数据只进入文本节点或固定属性,不解析来自人设/报告的 HTML。
|
|
5
|
+
function node(tag, attrs = {}, ...children) {
|
|
6
|
+
const el = ['svg', 'path'].includes(tag) ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag);
|
|
7
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
8
|
+
if (value === null || value === undefined || value === false) continue;
|
|
9
|
+
if (key === 'class') el.className = value;
|
|
10
|
+
else if (key === 'checked' || key === 'hidden' || key === 'disabled') el[key] = Boolean(value);
|
|
11
|
+
else el.setAttribute(key, String(value));
|
|
12
|
+
}
|
|
13
|
+
for (const child of children.flat(Infinity)) {
|
|
14
|
+
if (child !== null && child !== undefined && child !== false) el.append(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
15
|
+
}
|
|
16
|
+
return el;
|
|
17
|
+
}
|
|
18
|
+
const badge = (label, kind = '') => node('span', { class: `badge ${kind}` }, label);
|
|
19
|
+
// 抽屉头的「⚡/🔄」emoji 换成内联 SVG:emoji 字形在 Windows 上按彩色位图渲染,和 11px 文字挤在一起忽大忽小
|
|
20
|
+
const HOT_SVG = {
|
|
21
|
+
hot: node('svg', { viewBox: '0 0 16 16', width: '12', height: '12', 'aria-hidden': 'true' },
|
|
22
|
+
node('path', { d: 'M9 1.5 3.5 9h3l-1 5.5L12 7H8.8z', fill: 'currentColor' })),
|
|
23
|
+
restart: node('svg', { viewBox: '0 0 16 16', width: '12', height: '12', 'aria-hidden': 'true' },
|
|
24
|
+
node('path', { d: 'M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 1.5v3h-3', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })),
|
|
25
|
+
};
|
|
26
|
+
const hotBadge = (label, kind = '', icon = 'hot') =>
|
|
27
|
+
node('span', { class: `badge ${kind}` }, HOT_SVG[icon].cloneNode(true), label);
|
|
28
|
+
const empty = label => node('p', { class: 'empty' }, label);
|
|
29
|
+
const HARNESS_ICON_FILES = { pi: 'pi.png', claude: 'claude.ico', codex: 'codex.webp', omp: 'omp.png', dsh: 'dsh.ico', grok: 'grok.ico', hermes: 'hermes.ico', opencode: 'opencode.ico', opencode2: 'opencode2.ico', codebuddy: 'codebuddy.png', zcode: 'zcode.png' };
|
|
30
|
+
const harnessIcon = (id, size = 24) => {
|
|
31
|
+
// 真实图标在 web/assets/ 下(各官网/favicon 下载),不是手绘 SVG。
|
|
32
|
+
const file = HARNESS_ICON_FILES[id];
|
|
33
|
+
// ?v=2:旧版 server 对 /assets/ 发过 max-age=86400,浏览器里还留着换前的图;换 URL 绕开那份旧缓存(server 已改 no-store)。
|
|
34
|
+
const src = file ? `assets/${file}?v=2` : '';
|
|
35
|
+
const img = src
|
|
36
|
+
? node('img', {
|
|
37
|
+
class: 'harness-avatar',
|
|
38
|
+
'data-id': id,
|
|
39
|
+
src,
|
|
40
|
+
alt: id,
|
|
41
|
+
width: String(size),
|
|
42
|
+
height: String(size)
|
|
43
|
+
})
|
|
44
|
+
: node('span', { class: 'harness-avatar fallback', 'aria-hidden': 'true' }, id.slice(0, 1).toUpperCase());
|
|
45
|
+
if (size !== 24) {
|
|
46
|
+
// CSP 禁内联 style;尺寸档位用类名表达(抽屉头 34px)
|
|
47
|
+
img.classList.add(size === 34 ? 'harness-avatar-lg' : 'harness-avatar-md');
|
|
48
|
+
}
|
|
49
|
+
return img;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let state, base, draft, editor = null, editorBaseline = '', busy = false, activePage = 'console', expanded = null;
|
|
53
|
+
let capture = null, draftConflict = false;
|
|
54
|
+
// Process failure, no request, off-mode success and a real marker miss are distinct.
|
|
55
|
+
const captureFor = id => (capture?.report?.results ?? []).find(r => r.harness === id) || null;
|
|
56
|
+
function evidenceBadge(id) {
|
|
57
|
+
const evidence = verificationEvidence(captureFor(id));
|
|
58
|
+
return badge(evidence.label, evidence.kind);
|
|
59
|
+
}
|
|
60
|
+
function verifyNote(id) {
|
|
61
|
+
const r = captureFor(id);
|
|
62
|
+
if (!r) return '没有本机请求证据;能力说明不是实测结果。此按钮只验证隔离 CLI,不验证正在运行的桌面会话。';
|
|
63
|
+
return `${r.version || '版本未知'} · ${captureScopeLabel(r.scope)} · ${r.at || capture?.report?.at || '时间未记录'} · ${r.reason || r.evidence || verificationEvidence(r).label}。不代表当前会话已应用。`;
|
|
64
|
+
}
|
|
65
|
+
const mode = () => document.querySelector('[name=mode]:checked')?.value || 'replace';
|
|
66
|
+
const configDirty = () => draft && base && changed(draft, base);
|
|
67
|
+
const editorValue = () => ({ id: $('edit-id').value.trim(), meta: { name: $('edit-name').value.trim(), description: $('edit-desc').value.trim(), model: $('edit-model').value }, body: $('edit-body').value });
|
|
68
|
+
const editorDirty = () => editor !== null && JSON.stringify(editorValue()) !== editorBaseline;
|
|
69
|
+
function notify(message, kind = '') {
|
|
70
|
+
const el = $('notice'); el.textContent = message; el.className = `notice ${kind}`; el.hidden = false;
|
|
71
|
+
}
|
|
72
|
+
async function api(path, data, method = data ? 'POST' : 'GET') {
|
|
73
|
+
const response = await fetch(`/api/${path}`, { method, headers: data ? { 'content-type': 'application/json' } : {}, body: data ? JSON.stringify(data) : undefined });
|
|
74
|
+
let result; try { result = await response.json(); } catch { throw new Error('服务返回了无效内容,请确认本地服务仍在运行。'); }
|
|
75
|
+
if (!response.ok || result.error) throw new Error(result.error || `请求失败 (${response.status})`);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function announce(result, success) {
|
|
79
|
+
const report = result.applied || result;
|
|
80
|
+
if (report.ok === false) {
|
|
81
|
+
const failed = report.results?.filter(r => r.status === 'failed' || r.ok === false) || [];
|
|
82
|
+
notify(`${report.partial ? '部分变更已写入' : '操作未全部完成'}。${failed.map(r => `${r.harness}:${r.error}`).join(';') || '请查看诊断中的逐项结果。'}`, 'warn');
|
|
83
|
+
} else notify(success);
|
|
84
|
+
}
|
|
85
|
+
const lockedControls = new Set();
|
|
86
|
+
function lockControls() {
|
|
87
|
+
if (!busy) return;
|
|
88
|
+
document.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled)').forEach(el => { lockedControls.add(el); el.disabled = true; });
|
|
89
|
+
}
|
|
90
|
+
function focusKey(el) {
|
|
91
|
+
if (!el || el === document.body) return null;
|
|
92
|
+
if (el.id) return { id: el.id };
|
|
93
|
+
const keys = ['expand', 'hpersona', 'hmode', 'toggle', 'channel', 'verify', 'auto', 'profile', 'closeDrawer'];
|
|
94
|
+
const key = keys.find(key => el.dataset?.[key] !== undefined);
|
|
95
|
+
return key ? { key, value: el.dataset[key], drawer: Boolean(el.closest('.harness-drawer')) } : null;
|
|
96
|
+
}
|
|
97
|
+
function restoreFocus(key) {
|
|
98
|
+
if (!key) return;
|
|
99
|
+
const target = key.id ? $(key.id) : [...document.querySelectorAll('[data-' + key.key.replace(/[A-Z]/g, c => '-' + c.toLowerCase()) + ']')].find(el => el.dataset[key.key] === key.value && Boolean(el.closest('.harness-drawer')) === key.drawer);
|
|
100
|
+
if (target && !target.disabled) target.focus({ preventScroll: true });
|
|
101
|
+
}
|
|
102
|
+
async function operation(work) {
|
|
103
|
+
if (busy) return;
|
|
104
|
+
const focus = focusKey(document.activeElement);
|
|
105
|
+
busy = true;
|
|
106
|
+
lockControls();
|
|
107
|
+
document.body.setAttribute('aria-busy', 'true');
|
|
108
|
+
try { await work(); } catch (error) { notify(error.message, 'error'); }
|
|
109
|
+
finally {
|
|
110
|
+
busy = false;
|
|
111
|
+
lockedControls.forEach(el => { if (el.isConnected) el.disabled = false; }); lockedControls.clear();
|
|
112
|
+
document.body.removeAttribute('aria-busy');
|
|
113
|
+
if (state) { renderConfigControls(); renderHarnesses(); renderEditorStatus(); }
|
|
114
|
+
restoreFocus(focus);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function refresh({ resetDraft = false } = {}) {
|
|
118
|
+
const fresh = await api('state');
|
|
119
|
+
try { capture = await api('verification'); } catch { capture = { report: null }; }
|
|
120
|
+
const preserve = configDirty() && !resetDraft;
|
|
121
|
+
draftConflict = Boolean(preserve && base && changed(configurationOf(fresh), base));
|
|
122
|
+
state = { ...fresh, personas: Array.isArray(fresh.personas) ? fresh.personas : [], harnesses: Array.isArray(fresh.harnesses) ? fresh.harnesses : [], defaults: fresh.defaults || { harness: null, mode: 'replace' }, armed: fresh.armed || {}, modes: fresh.modes || Object.keys(MODE_LABELS) }; base = configurationOf(state);
|
|
123
|
+
if (!preserve) draft = { ...base };
|
|
124
|
+
$('connection').textContent = `本地服务已连接 · ${new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
|
|
125
|
+
render();
|
|
126
|
+
}
|
|
127
|
+
function render() {
|
|
128
|
+
$('version-badge').textContent = `v${state.version || '未知'}`;
|
|
129
|
+
const p = state.personas.find(p => p.id === state.active);
|
|
130
|
+
$('current-persona').textContent = personaName(p);
|
|
131
|
+
$('current-mode').textContent = `${MODE_LABELS[state.defaults.mode]} · ${p?.chars ?? 0} 字`;
|
|
132
|
+
$('power-status').textContent = state.paused ? '已暂停' : '配置启用';
|
|
133
|
+
$('power-status').className = `badge ${state.paused ? 'warn' : 'good'}`;
|
|
134
|
+
$('power').textContent = state.paused ? '恢复应用' : '暂停应用'; $('power').disabled = false;
|
|
135
|
+
const last = state.lastApply;
|
|
136
|
+
$('current-sync').textContent = last ? last.ok ? '同步记录已更新' : '有同步失败项' : '尚无应用记录';
|
|
137
|
+
$('persona-select').replaceChildren(...state.personas.map(p => new Option(`${personaName(p)} · ${p.chars} 字`, p.id)));
|
|
138
|
+
$('persona-select').value = draft.persona; $('persona-select').disabled = false;
|
|
139
|
+
document.querySelectorAll('[name=mode]').forEach(el => el.checked = el.value === draft.mode);
|
|
140
|
+
renderConfigControls(); renderHarnesses(); renderPersonas(); renderReport();
|
|
141
|
+
$('default-harness').replaceChildren(new Option('未设置', ''), ...state.harnesses.filter(h => h.oneshot && h.supported).map(h => new Option(`${h.id}${h.found ? '' : '(本机未发现)'}`, h.id)));
|
|
142
|
+
$('default-harness').value = state.defaultsSet ? state.defaults.harness : '';
|
|
143
|
+
$('default-summary').textContent = state.defaultsSet ? state.defaults.harness : '未设置';
|
|
144
|
+
$('diagnostic-state').textContent = JSON.stringify({ active: state.active, mode: state.defaults.mode, armed: state.armed, pausedInstalls: state.pausedInstalls, reconciled: state.reconciled, revision: last?.revision, runtimeConfirmed: last?.runtimeConfirmed }, null, 2);
|
|
145
|
+
renderEditorStatus(); lockControls();
|
|
146
|
+
}
|
|
147
|
+
function renderConfigControls() {
|
|
148
|
+
const dirty = configDirty();
|
|
149
|
+
const selected = state.personas.find(p => p.id === draft.persona);
|
|
150
|
+
$('persona-description').textContent = selected?.meta?.description || selected?.meta?.desc || '未填写说明';
|
|
151
|
+
$('draft-state').textContent = draftConflict ? '已保存配置发生变化;草稿保留,应用前请核对' : state.defaultsSet === false ? '首次设置尚未保存' : dirty ? '有待应用的选择' : '与已保存配置一致';
|
|
152
|
+
$('apply').textContent = state.defaultsSet === false ? '保存当前设置' : '应用变更';
|
|
153
|
+
$('apply').disabled = busy || !needsApply(state, draft, base) || !selected;
|
|
154
|
+
$('reset-config').disabled = busy || !dirty;
|
|
155
|
+
const count = Object.values(state.armed).filter(v => v.length).length;
|
|
156
|
+
$('apply-hint').textContent = state.paused ? '已暂停,保存只记录选择,不写入任何执行器。' : count ? `保存后同步到这 ${count} 个执行器。` : '还没接管任何 AI,先在下面选一个。';
|
|
157
|
+
}
|
|
158
|
+
function miniSelect(h, kind) {
|
|
159
|
+
const isPersona = kind === 'persona';
|
|
160
|
+
const globalPersona = state.personas.find(p => p.id === state.persona);
|
|
161
|
+
const options = isPersona
|
|
162
|
+
? [{ value: '', label: '人设·跟随' }, ...state.personas.map(p => ({ value: p.id, label: personaName(p) }))]
|
|
163
|
+
: [{ value: '', label: '模式·跟随' }, ...state.modes.map(m => ({ value: m, label: MODE_LABELS[m] }))];
|
|
164
|
+
// 只有真写了例外才选中覆盖值,否则选中“跟随全局”
|
|
165
|
+
const current = isPersona ? (h.personaFrom === 'override' ? h.persona || '' : '') : (h.modeFrom === 'override' ? h.mode || '' : '');
|
|
166
|
+
const globalNow = isPersona ? personaName(globalPersona) : MODE_LABELS[state.defaults?.mode || 'replace'];
|
|
167
|
+
const what = isPersona ? '人设' : '模式';
|
|
168
|
+
const shown = isPersona ? personaName(state.personas.find(p => p.id === h.persona)) : MODE_LABELS[h.mode] || h.mode;
|
|
169
|
+
const source = current ? '(单独指定)' : `(跟随全局:${globalNow})`;
|
|
170
|
+
const select = node('select', {
|
|
171
|
+
class: `mini-select ${current ? 'is-override' : ''}`,
|
|
172
|
+
[isPersona ? 'data-hpersona' : 'data-hmode']: h.id,
|
|
173
|
+
title: `${h.id} 的${what}:${shown}${source}`,
|
|
174
|
+
'aria-label': `${h.id} ${what}`
|
|
175
|
+
}, options.map(o => node('option', { value: o.value }, o.label)));
|
|
176
|
+
select.value = current; // 选项就位后才能设选中值
|
|
177
|
+
return select;
|
|
178
|
+
}
|
|
179
|
+
function renderHarnesses() {
|
|
180
|
+
$('scope-summary').textContent = scopeSummary(state);
|
|
181
|
+
const rows = sortedHarnesses(state, $('show-all').checked);
|
|
182
|
+
if (!rows.length) {
|
|
183
|
+
$('harness-list').replaceChildren(empty('没发现任何可配置的 AI。勾选“显示本机没装的”看看能配哪些。'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const capsules = rows.map(h => {
|
|
188
|
+
const on = h.armed.length > 0;
|
|
189
|
+
const failed = state.lastApply?.results?.some(r => r.harness === h.id && r.status === 'failed');
|
|
190
|
+
const installed = Boolean(h.found || h.homeExists);
|
|
191
|
+
const isSelected = expanded === h.id;
|
|
192
|
+
let statusText = on ? '已选择' : installed ? h.bin ? '已就绪' : '发现配置' : '未安装';
|
|
193
|
+
if (on && state.paused) statusText = '已暂停';
|
|
194
|
+
if (on && !state.paused && h.mode === 'off') statusText = '单独暂停';
|
|
195
|
+
if (on && failed) statusText = '需重试';
|
|
196
|
+
|
|
197
|
+
const capsuleClass = [
|
|
198
|
+
'harness-capsule',
|
|
199
|
+
on ? 'is-on' : '',
|
|
200
|
+
failed ? 'is-bad' : '',
|
|
201
|
+
isSelected ? 'is-selected' : '',
|
|
202
|
+
!installed && !on ? 'is-uninstalled' : ''
|
|
203
|
+
].filter(Boolean).join(' ');
|
|
204
|
+
|
|
205
|
+
return node('div', { class: capsuleClass },
|
|
206
|
+
node('button', { type: 'button', class: 'capsule-body', 'data-expand': h.id, 'aria-expanded': String(isSelected), ...(isSelected ? { 'aria-controls': `drawer-${h.id}` } : {}), title: `${h.id} (${statusText}) - 点击展开详情` },
|
|
207
|
+
harnessIcon(h.id, 24),
|
|
208
|
+
node('div', { class: 'capsule-info' },
|
|
209
|
+
node('div', { class: 'capsule-name-row' },
|
|
210
|
+
node('span', { class: 'capsule-title' }, h.id),
|
|
211
|
+
node('span', { class: `capsule-dot ${on ? 'is-on' : ''}` }),
|
|
212
|
+
state.defaultsSet && state.defaults.harness === h.id && node('span', { class: 'capsule-badge-default' }, '默认')
|
|
213
|
+
),
|
|
214
|
+
node('span', { class: 'capsule-status-text' }, statusText)
|
|
215
|
+
)
|
|
216
|
+
),
|
|
217
|
+
node('label', { class: 'switch switch-sm', title: `${on ? '关闭' : '开启'} ${h.id} 接管` },
|
|
218
|
+
node('input', {
|
|
219
|
+
type: 'checkbox',
|
|
220
|
+
'data-toggle': h.id,
|
|
221
|
+
checked: on,
|
|
222
|
+
disabled: !installed && !on,
|
|
223
|
+
'aria-label': `${on ? '关掉' : '打开'} ${h.id} 的人设接管`
|
|
224
|
+
}),
|
|
225
|
+
node('span', { class: 'switch-track', 'aria-hidden': 'true' })
|
|
226
|
+
),
|
|
227
|
+
// 单独占一行:胶囊水平空间只够放开关,塞在标题旁必被挤成“人设·”
|
|
228
|
+
node('div', { class: 'capsule-config' }, miniSelect(h, 'persona'), miniSelect(h, 'mode'))
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const gridNode = node('div', { class: 'harness-grid' }, capsules);
|
|
233
|
+
|
|
234
|
+
const expandedHarness = rows.find(h => h.id === expanded);
|
|
235
|
+
let drawerNode = null;
|
|
236
|
+
if (expandedHarness) {
|
|
237
|
+
const h = expandedHarness;
|
|
238
|
+
const on = h.armed.length > 0;
|
|
239
|
+
const failed = state.lastApply?.results?.some(r => r.harness === h.id && r.status === 'failed');
|
|
240
|
+
const installed = Boolean(h.found || h.homeExists);
|
|
241
|
+
let statusBadge = on ? badge('已选择', 'good') : badge(installed ? '未接管' : '未安装', installed ? '' : 'warn');
|
|
242
|
+
if (on && state.paused) statusBadge = badge('已暂停', 'warn');
|
|
243
|
+
if (on && !state.paused && h.mode === 'off') statusBadge = badge('单独暂停', 'warn');
|
|
244
|
+
if (on && failed) statusBadge = badge('需要重试', 'bad');
|
|
245
|
+
|
|
246
|
+
const effect = effectLabel(h, state.paused, state.dshRuntime);
|
|
247
|
+
|
|
248
|
+
drawerNode = node('div', { id: `drawer-${h.id}`, class: 'harness-drawer' },
|
|
249
|
+
node('div', { class: 'drawer-header' },
|
|
250
|
+
node('div', { class: 'drawer-title-group' },
|
|
251
|
+
harnessIcon(h.id, 34),
|
|
252
|
+
node('div', { class: 'drawer-meta' },
|
|
253
|
+
node('div', { class: 'drawer-name-row' },
|
|
254
|
+
node('span', { class: 'drawer-title' }, h.id),
|
|
255
|
+
statusBadge,
|
|
256
|
+
h.hotSwitch ? hotBadge('支持热切', 'good', 'hot') : hotBadge('需新会话', '', 'restart'),
|
|
257
|
+
evidenceBadge(h.id),
|
|
258
|
+
state.defaultsSet && state.defaults.harness === h.id && badge('默认启动项', 'blue')
|
|
259
|
+
),
|
|
260
|
+
Boolean(effect) && node('div', { class: 'drawer-effect' }, effect)
|
|
261
|
+
)
|
|
262
|
+
),
|
|
263
|
+
node('div', { class: 'drawer-actions' },
|
|
264
|
+
node('label', { class: 'switch', title: `${on ? '关闭' : '开启'}接管` },
|
|
265
|
+
node('input', {
|
|
266
|
+
type: 'checkbox',
|
|
267
|
+
'data-toggle': h.id,
|
|
268
|
+
checked: on,
|
|
269
|
+
disabled: !installed && !on,
|
|
270
|
+
'aria-label': `${on ? '关掉' : '打开'} ${h.id} 的人设接管`
|
|
271
|
+
}),
|
|
272
|
+
node('span', { class: 'switch-track', 'aria-hidden': 'true' })
|
|
273
|
+
),
|
|
274
|
+
node('button', {
|
|
275
|
+
type: 'button',
|
|
276
|
+
class: 'drawer-close',
|
|
277
|
+
'data-close-drawer': 'true',
|
|
278
|
+
'aria-label': '收起详情',
|
|
279
|
+
title: '收起详情'
|
|
280
|
+
}, '✕')
|
|
281
|
+
)
|
|
282
|
+
),
|
|
283
|
+
node('div', { class: 'drawer-body' }, harnessDetail(h))
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const focus = focusKey(document.activeElement);
|
|
288
|
+
$('harness-list').replaceChildren(gridNode, ...(drawerNode ? [drawerNode] : []));
|
|
289
|
+
lockControls();
|
|
290
|
+
if (!busy) restoreFocus(focus);
|
|
291
|
+
}
|
|
292
|
+
function harnessDetail(h) {
|
|
293
|
+
const channels = ['install', 'slash', 'shim', 'project'];
|
|
294
|
+
// 项目块的真相在磁盘上(projectOn),armed 只是缓存
|
|
295
|
+
const channelOn = c => c === 'project' ? Boolean(h.projectOn) : h.armed.includes(c);
|
|
296
|
+
const supported = channels.filter(c => h.caps?.[c]?.ok || channelOn(c));
|
|
297
|
+
const desc = {
|
|
298
|
+
install: ['dsh', 'opencode', 'opencode2'].includes(h.id) ? '全局生效:装成宿主插件,新会话自动读取(无需每次带参数);首次安装或更新代码需重启。' : '全局生效:写入这家自己的全局配置文件(人格/指令落点),新会话读取;不改命令本身。',
|
|
299
|
+
slash: h.id === 'dsh' ? '装上 /purge 命令插件后,在 dsh 会话里输 /purge on 开启当前会话。注意:它只是个开关,注入靠「常驻配置」,两样都要启用;首次安装要重启 dsh。' : '安装扩展后重新加载宿主,用 /purge on 开启当前会话:同一会话内热切换人设,不重开不重启。',
|
|
300
|
+
shim: '接管终端裸命令:在 PATH 前面放一层同名壳,以后你直接敲 codex/pi 就等于敲 agent-purge codex/pi,启动时自动注入;不影响已运行会话与桌面图标。',
|
|
301
|
+
project: `写进当前目录的 ${h.caps?.project?.file || 'AGENTS.md'};其他原生执行器也可能读取这个同名文件。每个文件只登记一个管理归属,同时开全局通道会有两份。`,
|
|
302
|
+
};
|
|
303
|
+
const installed = Boolean(h.found || h.homeExists);
|
|
304
|
+
return [
|
|
305
|
+
node('div', { class: 'capsule-config drawer-config' }, miniSelect(h, 'persona'), miniSelect(h, 'mode')),
|
|
306
|
+
node('div', { class: 'verify-row' },
|
|
307
|
+
node('button', { class: 'secondary', 'data-verify': h.id, disabled: !installed, title: '临时 HOME + 测试人设 + 本地 provider;不检查或修改当前桌面会话' }, '隔离验证一次'),
|
|
308
|
+
node('div', {}, node('p', { class: 'help' }, verifyNote(h.id)))),
|
|
309
|
+
!supported.length && node('p', { class: 'detail-note' }, h.reason || h.caps?.install?.why || '没有可配置的通道'),
|
|
310
|
+
...supported.map(channel => {
|
|
311
|
+
const isOn = channelOn(channel);
|
|
312
|
+
const blocked = !h.caps?.[channel]?.ok && !isOn;
|
|
313
|
+
return node('div', { class: 'channel-row' }, node('div', {}, node('strong', {}, CHANNEL_LABELS[channel]), node('p', { class: 'help' }, h.caps?.[channel]?.why || desc[channel])), node('button', { class: 'secondary', 'data-channel': channel, 'data-target': h.id, 'data-enabled': String(!isOn), disabled: blocked }, isOn ? '关闭' : '启用'));
|
|
314
|
+
}),
|
|
315
|
+
['pi', 'omp'].includes(h.id) && h.armed.includes('slash') && node('label', { class: 'check-label advanced' }, node('input', { type: 'checkbox', 'data-auto': h.id, checked: state[`${h.id}Auto`] }), ' 新会话自动应用(关闭时须 /purge on)'),
|
|
316
|
+
h.id === 'dsh' && h.homeExists && node('div', { class: 'profile-options' }, state.dshAll.map(p => node('label', {}, node('input', { type: 'checkbox', 'data-profile': p, checked: state.dshProfiles.includes(p) }), p))),
|
|
317
|
+
node('details', { class: 'advanced' }, node('summary', {}, '技术详情与不可用通道'), node('p', { class: 'help' }, `热切能力:${h.caps?.hot?.how || h.caps?.hot?.why || '待核实'}`), node('p', { class: 'help' }, '配置路径:', node('code', { class: 'path' }, h.persistFile || h.home || '—')), node('p', { class: 'help' }, '命令入口:', node('code', { class: 'path' }, h.bin || '未发现')), channels.filter(c => !h.caps?.[c]?.ok).map(c => node('p', { class: 'help' }, `${CHANNEL_LABELS[c]}:${h.caps?.[c]?.why || '不可用'}`))),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
function renderPersonas() {
|
|
321
|
+
const query = $('persona-search').value.toLocaleLowerCase();
|
|
322
|
+
const list = state.personas.filter(p => `${p.id} ${personaName(p)} ${p.meta.description || ''}`.toLocaleLowerCase().includes(query));
|
|
323
|
+
$('persona-list').replaceChildren(...(list.length ? list.map(p => node('button', { class: `persona-item ${editor === p.id ? 'selected' : ''}`, 'data-persona': p.id }, node('strong', {}, personaName(p)), node('small', {}, `${p.id} · ${p.chars} 字`), state.active === p.id && badge('当前人设', 'blue'))) : [empty('没有匹配的人设')]));
|
|
324
|
+
}
|
|
325
|
+
function confirmDiscard() { return !editorDirty() || window.confirm('这份人设有未保存修改,放弃修改?'); }
|
|
326
|
+
function openEditor(id) {
|
|
327
|
+
if (!confirmDiscard()) return;
|
|
328
|
+
editor = id || '';
|
|
329
|
+
const p = state.personas.find(p => p.id === id);
|
|
330
|
+
$('editor-empty').hidden = true; $('editor-form').hidden = false;
|
|
331
|
+
$('edit-id').value = p?.id || ''; $('edit-id').disabled = Boolean(p);
|
|
332
|
+
$('edit-name').value = p?.meta?.name || ''; $('edit-desc').value = p?.meta?.description || p?.meta?.desc || '';
|
|
333
|
+
$('edit-body').value = p?.body || ''; $('edit-model').value = (p?.meta?.model || []).join(', ');
|
|
334
|
+
editorBaseline = JSON.stringify(editorValue());
|
|
335
|
+
renderEditorStatus(); renderPersonas();
|
|
336
|
+
}
|
|
337
|
+
function renderEditorStatus() {
|
|
338
|
+
if (editor === null) return;
|
|
339
|
+
const current = editor === state.active;
|
|
340
|
+
$('editor-title').textContent = editor ? current ? '编辑当前人设' : '编辑人设' : '新建人设';
|
|
341
|
+
$('editor-status').textContent = editorDirty() ? '未保存' : '已保存';
|
|
342
|
+
$('char-count').textContent = `${$('edit-body').value.trim().length} 字`;
|
|
343
|
+
$('save-persona').textContent = current ? '保存并同步' : '保存';
|
|
344
|
+
$('save-persona').disabled = busy || !editorDirty();
|
|
345
|
+
$('save-apply').hidden = current;
|
|
346
|
+
$('save-apply').disabled = busy;
|
|
347
|
+
$('delete-persona').disabled = busy || !editor || state.personas.length <= 1;
|
|
348
|
+
$('edit-id').disabled = busy || Boolean(editor);
|
|
349
|
+
$('editor-hint').textContent = current ? '当前人设的正文保存后会同步到已选范围;暂停时不会恢复应用。' : '“保存”只更新人设库;“保存并应用”会切换当前人设并同步。';
|
|
350
|
+
}
|
|
351
|
+
async function saveEditor(apply) {
|
|
352
|
+
if (!$('editor-form').reportValidity()) return;
|
|
353
|
+
const value = editorValue();
|
|
354
|
+
if (!editor && state.personas.some(p => p.id === value.id)) { notify('这个标识已存在,请换一个标识,避免覆盖已有的人设。', 'error'); return; }
|
|
355
|
+
await operation(async () => {
|
|
356
|
+
const result = await api('persona', { ...value, apply });
|
|
357
|
+
editor = result.persona.id;
|
|
358
|
+
editorBaseline = JSON.stringify(value);
|
|
359
|
+
await refresh({ resetDraft: apply });
|
|
360
|
+
announce(result, apply ? '人设已保存并应用。' : '人设已保存。');
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function renderReport() {
|
|
364
|
+
const r = state.lastApply;
|
|
365
|
+
$('retry-failed').hidden = !r?.results?.some(x => x.status === 'failed');
|
|
366
|
+
$('report-meta').textContent = r ? `${new Date(r.at).toLocaleString('zh-CN')} · 版本 ${r.revision} · ${MODE_LABELS[r.mode]}` : '应用后会记录每个通道的写入结果。';
|
|
367
|
+
$('apply-results').className = '';
|
|
368
|
+
$('apply-results').replaceChildren(...(!r ? [empty('尚无应用记录')] : !r.results.length ? [empty('配置已保存,当前没有需要写入的通道。')] : r.results.map(x => node('div', { class: 'result-row' }, node('div', {}, node('strong', {}, x.harness), node('p', { class: 'muted' }, CHANNEL_LABELS[x.channel] || x.channel)), badge(x.status === 'failed' ? '写入失败' : '配置已处理', x.status === 'failed' ? 'bad' : 'good'), node('p', { class: x.status === 'failed' ? 'error-text' : 'muted' }, x.error || x.message || '', x.status !== 'failed' && [node('br'), '运行中会话:尚未确认'])))));
|
|
369
|
+
}
|
|
370
|
+
function captureRow(result) {
|
|
371
|
+
const modes = result.modes || [];
|
|
372
|
+
const hot = result.hotSwitch?.status || 'not-tested';
|
|
373
|
+
const strict = modes.find(m => m.mode === 'strict');
|
|
374
|
+
const roles = [...new Set(modes.flatMap(m => (m.requests || []).flatMap(r => r.roles || [])))].join(' + ');
|
|
375
|
+
const residual = strict?.requests?.some(r => r.identityLines?.length);
|
|
376
|
+
const assessment = verificationEvidence(result);
|
|
377
|
+
const details = modes.length ? node('details', {}, node('summary', {}, `${modes.length} 种模式的请求证据`), modes.map(m => node('p', { class: 'help' }, `${MODE_LABELS[m.mode] || m.mode} · ${CAPTURE_LABELS[m.status] || m.status}:`, m.reason || '', (m.requests || []).map(r => `${(r.roles || []).join('+')} / ${r.chars}字 / 人设${r.markerCount}次 / 身份句${r.identityLines?.length || 0} / 工具${r.toolDefinitions}`).join(';')))) : null;
|
|
378
|
+
return node('tr', {},
|
|
379
|
+
node('td', {}, node('strong', {}, result.harness), node('p', { class: 'muted' }, result.version || '版本未知'), node('p', { class: 'help' }, captureScopeLabel(result.scope))),
|
|
380
|
+
node('td', {}, roles || '—'),
|
|
381
|
+
node('td', {}, badge(assessment.label, assessment.kind), residual && node('p', { class: 'help' }, '仍有固定身份句')),
|
|
382
|
+
node('td', {}, badge(CAPTURE_LABELS[hot] || hot, hot === 'passed' ? 'good' : ''), hot === 'passed' && node('p', { class: 'help' }, `${captureScopeLabel(result.hotSwitch?.scope)} · A → B → ${MODE_LABELS.strict} → ${MODE_LABELS.off}`)),
|
|
383
|
+
node('td', {}, result.reason || (!modes.length ? result.evidence || '' : ''), result.hotSwitch?.reason && node('p', { class: 'help' }, result.hotSwitch.reason), details));
|
|
384
|
+
}
|
|
385
|
+
async function loadCapture() {
|
|
386
|
+
try {
|
|
387
|
+
capture = await api('verification');
|
|
388
|
+
const report = capture.report;
|
|
389
|
+
const command = node('p', { class: 'help' }, '重新验证:', node('code', {}, 'npm run test:harness'));
|
|
390
|
+
$('capture-results').replaceChildren(...(!report ? [node('p', { class: 'muted' }, '暂无请求捕获报告。测试使用隔离配置和本地服务,不以模型自报身份作为证据。'), command] : [
|
|
391
|
+
node('p', { class: 'help path' }, `${report.at} · 历史独立测试,不代表当前会话 · ${report.reportPath || ''}`),
|
|
392
|
+
node('p', { class: 'help' }, '这是隔离测试,不是当前桌面会话。未捕获请求不等于人设未命中;命中也不证明模型行为符合人设。暂停模式单独判定。'),
|
|
393
|
+
node('div', { class: 'table-scroll' }, node('table', { class: 'capture-table' }, node('thead', {}, node('tr', {}, ['执行器 / 版本', '实际角色', '请求捕获', '同会话热切', '证据 / 原因'].map(label => node('th', {}, label)))), node('tbody', {}, report.results.map(captureRow)))), command]));
|
|
394
|
+
} catch (error) { $('capture-results').textContent = `报告读取失败:${error.message}`; }
|
|
395
|
+
}
|
|
396
|
+
const pageFromHash = () => ({ personas: 'library', overview: 'console' }[location.hash.slice(1)] || location.hash.slice(1) || 'console');
|
|
397
|
+
async function navigate(page, { replace = false, focus = true } = {}) {
|
|
398
|
+
if (!['console', 'library', 'diagnostics'].includes(page)) page = 'console';
|
|
399
|
+
if (activePage === 'library' && page !== 'library' && !confirmDiscard()) {
|
|
400
|
+
history.replaceState(null, '', '#library');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// 放弃的编辑不应在下次进入时悄悄复活。
|
|
404
|
+
if (activePage === 'library' && page !== 'library' && editorDirty()) { editor = null; $('editor-form').hidden = true; $('editor-empty').hidden = false; }
|
|
405
|
+
activePage = page;
|
|
406
|
+
document.querySelectorAll('.page').forEach(el => el.hidden = el.id !== `page-${page}`);
|
|
407
|
+
document.querySelectorAll('[data-page]').forEach(el => el.setAttribute('aria-current', el.dataset.page === page ? 'page' : 'false'));
|
|
408
|
+
if (replace || location.hash !== `#${page}`) history[replace ? 'replaceState' : 'pushState'](null, '', `#${page}`);
|
|
409
|
+
if (focus) $('main').focus({ preventScroll: true });
|
|
410
|
+
if (page === 'diagnostics') await loadCapture();
|
|
411
|
+
}
|
|
412
|
+
window.addEventListener('hashchange', () => navigate(pageFromHash(), { replace: true }));
|
|
413
|
+
document.querySelectorAll('[data-page]').forEach(el => el.addEventListener('click', () => navigate(el.dataset.page)));
|
|
414
|
+
$('current-sync').addEventListener('click', e => { e.preventDefault(); navigate('diagnostics'); });
|
|
415
|
+
document.querySelector('.brand').addEventListener('click', e => { e.preventDefault(); navigate('console'); });
|
|
416
|
+
$('persona-select').addEventListener('change', () => { draft.persona = $('persona-select').value; renderConfigControls(); });
|
|
417
|
+
document.querySelectorAll('[name=mode]').forEach(el => el.addEventListener('change', () => { draft.mode = mode(); renderConfigControls(); }));
|
|
418
|
+
$('reset-config').addEventListener('click', () => { draft = { ...base }; draftConflict = false; render(); });
|
|
419
|
+
$('config-form').addEventListener('submit', e => { e.preventDefault(); operation(async () => { const result = await api('apply', { persona: draft.persona, mode: state.paused ? 'off' : draft.mode, ...(state.defaultsSet === false ? { harness: state.defaults.harness } : {}), ...(state.paused ? { resumeMode: draft.mode } : {}) }); await refresh({ resetDraft: result.ok === true }); announce(result, state.paused ? '选择已保存,仍保持暂停。' : '变更已应用。请按各通道提示检查生效时机。'); }); });
|
|
420
|
+
$('power').addEventListener('click', () => operation(async () => { const on = state.paused; const result = await api('power', { on }); await refresh(); announce(result, on ? '已恢复原选择范围。' : '已暂停;静态内容已撤下,旧会话可能需要重开。'); }));
|
|
421
|
+
$('disarm-all').addEventListener('click', () => {
|
|
422
|
+
if (!window.confirm('确定撤销所有已登记的接管吗?\n\n- 暂停:撤下静态内容并保留通道选择;\n- 完全还原:撤销托管配置、插件与同名命令,保留人设和用户修改;恢复冲突会保留凭据并报错。\n\n这不会卸载 agent-purge 程序本身。')) return;
|
|
423
|
+
operation(async () => {
|
|
424
|
+
const res = await api('clean', {});
|
|
425
|
+
await refresh();
|
|
426
|
+
if (res.ok) notify('已撤销登记的接管,人设和用户修改已保留。', 'good');
|
|
427
|
+
else notify(`部分项目撤销失败:${(res.leftovers || []).join(';')}`, 'warn');
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
$('refresh').addEventListener('click', () => operation(async () => { await refresh(); notify('状态已刷新,未保存的草稿保持不变。'); }));
|
|
431
|
+
$('show-all').addEventListener('change', renderHarnesses);
|
|
432
|
+
const toggleExpand = id => { expanded = expanded === id ? null : id; renderHarnesses(); };
|
|
433
|
+
$('harness-list').addEventListener('click', e => {
|
|
434
|
+
const closeBtn = e.target.closest('[data-close-drawer]');
|
|
435
|
+
if (closeBtn) { toggleExpand(null); return; }
|
|
436
|
+
if (e.target.closest('.switch')) return;
|
|
437
|
+
const verifyBtn = e.target.closest('[data-verify]');
|
|
438
|
+
if (verifyBtn) {
|
|
439
|
+
const id = verifyBtn.dataset.verify;
|
|
440
|
+
operation(async () => {
|
|
441
|
+
const r = await api('verify', { harness: id });
|
|
442
|
+
try { capture = await api('verification'); } catch { /* 报告读不到就保持旧的 */ }
|
|
443
|
+
await refresh();
|
|
444
|
+
const assessment = r.assessment || verificationEvidence(captureFor(id));
|
|
445
|
+
const where = r.roles?.join('+') || '角色未记录';
|
|
446
|
+
notify(`${id}:${assessment.label} · ${captureScopeLabel(r.scope)}${assessment.state === 'verified' ? ` · ${where},${r.chars ?? 0} 字` : ''}${r.reason ? `;${r.reason}` : ''}。不代表当前会话已应用。`, ['verified', 'off-verified'].includes(assessment.state) ? '' : 'warn');
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const expand = e.target.closest('[data-expand]');
|
|
451
|
+
// 行内下拉自己处理点击,不能连带展开抽屉
|
|
452
|
+
if (e.target.closest('.capsule-config')) return;
|
|
453
|
+
if (expand) { toggleExpand(expand.dataset.expand); return; }
|
|
454
|
+
const btn = e.target.closest('[data-channel]'); if (!btn) return;
|
|
455
|
+
operation(async () => { const result = await api('arm', { harness: btn.dataset.target, channel: btn.dataset.channel, action: btn.dataset.enabled === 'true' ? 'on' : 'off' }); await refresh(); announce(result, '应用范围已更新。首次加载或更新扩展后请重新加载宿主。'); });
|
|
456
|
+
});
|
|
457
|
+
$('harness-list').addEventListener('change', e => {
|
|
458
|
+
const el = e.target;
|
|
459
|
+
const hp = el.dataset.hpersona, hm = el.dataset.hmode;
|
|
460
|
+
if (hp) {
|
|
461
|
+
const pick = el.value;
|
|
462
|
+
operation(async () => { const result = await api('harness', { harness: hp, persona: pick || 'follow' }); await refresh(); announce(result, pick ? `${hp} 换用 ${pick}。` : `${hp} 恢复跟随全局人设。`); });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (hm) {
|
|
466
|
+
const pick = el.value;
|
|
467
|
+
operation(async () => { const result = await api('harness', { harness: hm, mode: pick || 'follow' }); await refresh(); announce(result, pick ? `${hm} 模式 → ${MODE_LABELS[pick] || pick}。` : `${hm} 恢复跟随全局模式。`); });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (el.dataset.toggle) {
|
|
471
|
+
const id = el.dataset.toggle, on = el.checked;
|
|
472
|
+
operation(async () => { const result = await api('arm', { harness: id, channel: 'auto', action: on ? 'on' : 'off' }); await refresh(); announce(result, on ? `${id} 开始使用当前人设。` : `${id} 已恢复原样。`); });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (el.dataset.auto) operation(async () => { await api(`${el.dataset.auto}-auto`, { on: el.checked }); await refresh(); notify('自动应用设置已更新。'); });
|
|
476
|
+
if (el.dataset.profile) operation(async () => { await api('dsh-profile', { profile: el.dataset.profile, on: el.checked }); await refresh(); notify('dsh 生效形态已更新。'); });
|
|
477
|
+
});
|
|
478
|
+
// 标准 button/select/input 使用浏览器原生键盘语义,不拦截子控件的 Enter/Space。
|
|
479
|
+
$('default-form').addEventListener('submit', e => { e.preventDefault(); operation(async () => { const result = await api('defaults', { harness: $('default-harness').value || null }); await refresh(); announce(result, '默认启动设置已保存,应用范围未改变。'); }); });
|
|
480
|
+
$('edit-current').addEventListener('click', () => { openEditor(draft.persona); navigate('library'); });
|
|
481
|
+
$('new-persona').addEventListener('click', () => openEditor(''));
|
|
482
|
+
$('persona-search').addEventListener('input', renderPersonas);
|
|
483
|
+
$('persona-list').addEventListener('click', e => { const el = e.target.closest('[data-persona]'); if (el) openEditor(el.dataset.persona); });
|
|
484
|
+
$('editor-form').addEventListener('input', renderEditorStatus);
|
|
485
|
+
$('editor-form').addEventListener('submit', e => { e.preventDefault(); saveEditor(false); });
|
|
486
|
+
$('save-apply').addEventListener('click', () => saveEditor(true));
|
|
487
|
+
$('delete-persona').addEventListener('click', () => { if (!confirm(`删除人设“${editor}”?${editor === state.active ? '当前人设将回退并同步到已选范围。' : ''}`)) return; operation(async () => { const result = await api(`persona?id=${encodeURIComponent(editor)}`, null, 'DELETE'); editor = null; editorBaseline = ''; $('editor-form').hidden = true; $('editor-empty').hidden = false; await refresh({ resetDraft: true }); announce(result, '人设已删除。'); }); });
|
|
488
|
+
$('sync-all').addEventListener('click', () => operation(async () => { const r = await api('sync', {}); await refresh(); announce(r, '已重新同步原范围。'); }));
|
|
489
|
+
$('retry-failed').addEventListener('click', () => operation(async () => { const r = await api('sync', { onlyFailed: true }); await refresh(); announce(r, '失败项已重试。'); }));
|
|
490
|
+
async function checkAppUpdate(force = false) {
|
|
491
|
+
const vb = $('version-badge');
|
|
492
|
+
if (!vb) return;
|
|
493
|
+
try {
|
|
494
|
+
const res = await api(`update${force ? '?force=1' : ''}`);
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
vb.textContent = `v${res.current || state?.version || '未知'}`;
|
|
497
|
+
if (force) notify(`检查更新失败:${res.error}`, 'warn');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
vb.textContent = `v${res.current}`;
|
|
501
|
+
if (res.hasUpdate) {
|
|
502
|
+
vb.className = 'badge good';
|
|
503
|
+
vb.textContent = `v${res.current} (发现 v${res.latest})`;
|
|
504
|
+
vb.title = `发现新版本 v${res.latest},点击查看并前往下载`;
|
|
505
|
+
if (force) {
|
|
506
|
+
if (confirm(`发现新版本 v${res.latest}!\n\n${(res.body || '无更新说明').slice(0, 300)}\n\n是否前往 GitHub Release 发布页面?`)) {
|
|
507
|
+
window.open('https://github.com/ZiChuanLan/agent-purge/releases/latest', '_blank', 'noopener,noreferrer');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else if (res.published === false) {
|
|
511
|
+
vb.className = 'badge';
|
|
512
|
+
vb.title = '当前没有公开 Release;npm 安装请通过 npm 检查更新';
|
|
513
|
+
if (force) notify('尚无公开 Release。npm 安装可使用 npm outdated -g agent-purge 检查版本。');
|
|
514
|
+
} else {
|
|
515
|
+
vb.className = 'badge';
|
|
516
|
+
vb.title = `当前版本 v${res.current} 已是最新,点击可重新检查`;
|
|
517
|
+
if (force) notify('当前已是最新版本!', 'good');
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
/* 离线或后台探测失败不阻碍主界面 */
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
$('version-badge')?.addEventListener('click', () => checkAppUpdate(true));
|
|
524
|
+
window.addEventListener('beforeunload', e => { if (configDirty() || editorDirty()) { e.preventDefault(); e.returnValue = ''; } });
|
|
525
|
+
refresh().then(() => navigate(pageFromHash(), { replace: true, focus: false })).then(() => checkAppUpdate(false)).catch(error => { $('connection').textContent = '本地服务未连接'; notify(`${error.message} 使用右上角“刷新状态”重试。`, 'error'); });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|