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/web/ui.html
CHANGED
|
@@ -1,1172 +1,73 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="utf-8"
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1"
|
|
6
|
-
<meta name="color-scheme" content="light"
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light">
|
|
7
7
|
<title>agent-purge · 提示词控制台</title>
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='
|
|
9
|
-
<link rel="
|
|
10
|
-
<
|
|
11
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Host+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&display=swap" />
|
|
12
|
-
<style>
|
|
13
|
-
/* ── Design system: Software(浅色 / 电光蓝 / Grotesk + Instrument) ── */
|
|
14
|
-
:root{
|
|
15
|
-
/* 权威令牌 */
|
|
16
|
-
--ground:#fcfcfd;
|
|
17
|
-
--ink:#16191f;
|
|
18
|
-
--ink-2:#454c58;
|
|
19
|
-
--ground-muted:#f3f4f6;
|
|
20
|
-
--rule:#dde0e6;
|
|
21
|
-
--accent:#074ce0;
|
|
22
|
-
--field:#16191f;
|
|
23
|
-
--on-field:#fcfcfd;
|
|
24
|
-
--on-field-2:#d0d4dd;
|
|
25
|
-
--panel:#ffffff;
|
|
26
|
-
--on-accent:#ffffff;
|
|
27
|
-
--r-panel:12px;
|
|
28
|
-
--hair:1px;
|
|
29
|
-
--motion:160ms;
|
|
30
|
-
--ease:ease-out;
|
|
31
|
-
--gap:24px;
|
|
32
|
-
|
|
33
|
-
/* 派生 */
|
|
34
|
-
--accent-soft:rgba(7,76,224,.08);
|
|
35
|
-
--accent-line:rgba(7,76,224,.32);
|
|
36
|
-
--on-field-soft:rgba(252,252,253,.10);
|
|
37
|
-
--danger:#b42318;
|
|
38
|
-
--danger-soft:rgba(180,35,24,.06);
|
|
39
|
-
--shadow:0 1px 2px rgba(22,25,31,.04),0 12px 24px -18px rgba(22,25,31,.35);
|
|
40
|
-
|
|
41
|
-
--font-display:"Host Grotesk","Instrument Sans","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;
|
|
42
|
-
--font-ui:"Instrument Sans","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,"Microsoft YaHei",sans-serif;
|
|
43
|
-
--font-mono:"Cascadia Code","JetBrains Mono",ui-monospace,SFMono-Regular,Consolas,monospace;
|
|
44
|
-
--t:var(--motion) var(--ease);
|
|
45
|
-
}
|
|
46
|
-
*{box-sizing:border-box}
|
|
47
|
-
html{color-scheme:light}
|
|
48
|
-
body{margin:0;background:var(--ground);color:var(--ink);min-height:100vh;display:flex;flex-direction:column;
|
|
49
|
-
font:16px/1.6 var(--font-ui);-webkit-font-smoothing:antialiased;overflow-x:clip}
|
|
50
|
-
button{font:inherit;color:inherit;cursor:pointer;background:none;border:none}
|
|
51
|
-
input,select,textarea{font:15px/1.5 var(--font-ui);color:var(--ink);background:var(--panel);
|
|
52
|
-
border:var(--hair) solid var(--rule);border-radius:8px;padding:9px 11px;outline:none;width:100%;
|
|
53
|
-
transition:border-color var(--t),box-shadow var(--t)}
|
|
54
|
-
input::placeholder,textarea::placeholder{color:var(--ink-2);opacity:.72}
|
|
55
|
-
input:hover,select:hover,textarea:hover{border-color:#c3c8d2}
|
|
56
|
-
select{cursor:pointer;appearance:none;padding-right:32px;
|
|
57
|
-
background-image:linear-gradient(45deg,transparent 50%,var(--ink-2) 50%),linear-gradient(135deg,var(--ink-2) 50%,transparent 50%);
|
|
58
|
-
background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
|
|
59
|
-
input:focus-visible,select:focus-visible,textarea:focus-visible,button:focus-visible,[tabindex]:focus-visible{
|
|
60
|
-
outline:2px solid var(--accent);outline-offset:2px;border-color:var(--accent);box-shadow:none}
|
|
61
|
-
code,pre,kbd{font-family:var(--font-mono);font-size:.92em}
|
|
62
|
-
::selection{background:var(--accent-soft)}
|
|
63
|
-
:focus:not(:focus-visible){outline:none}
|
|
64
|
-
|
|
65
|
-
/* ── 顶栏 ─────────────────────────────────────────────────────── */
|
|
66
|
-
header.top{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:16px;
|
|
67
|
-
min-height:64px;padding:12px 24px;border-bottom:var(--hair) solid var(--rule);
|
|
68
|
-
background:rgba(252,252,253,.92);backdrop-filter:blur(14px)}
|
|
69
|
-
.brand{display:flex;align-items:center;gap:11px;padding-right:20px;border-right:var(--hair) solid var(--rule)}
|
|
70
|
-
.brand .mark{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;flex:0 0 auto;
|
|
71
|
-
background:var(--accent);color:var(--on-accent)}
|
|
72
|
-
.brand .txt{display:flex;flex-direction:column;line-height:1.25}
|
|
73
|
-
.brand b{font-family:var(--font-display);font-size:15px;font-weight:700;letter-spacing:-.01em}
|
|
74
|
-
.brand s{text-decoration:none;font-size:12px;color:var(--ink-2)}
|
|
75
|
-
.spacer{flex:1}
|
|
76
|
-
.facts{display:flex;gap:8px;flex-wrap:wrap;min-width:0}
|
|
77
|
-
.fact{display:flex;flex-direction:column;gap:1px;padding:6px 12px;border-radius:8px;
|
|
78
|
-
border:var(--hair) solid var(--rule);background:var(--panel);min-width:0;max-width:190px}
|
|
79
|
-
.fact s{text-decoration:none;font-size:11px;color:var(--ink-2);letter-spacing:.04em;text-transform:uppercase}
|
|
80
|
-
.fact em{font-style:normal;font-family:var(--font-mono);font-size:13px;color:var(--ink);
|
|
81
|
-
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
82
|
-
.fact.warn{border-color:var(--accent-line);background:var(--accent-soft)}
|
|
83
|
-
.fact.warn em{color:var(--accent)}
|
|
84
|
-
.fact.off{opacity:.72}
|
|
85
|
-
.fact.off em{color:var(--ink-2)}
|
|
86
|
-
.power{display:flex;align-items:center;gap:12px;padding:8px 12px 8px 14px;border-radius:10px;
|
|
87
|
-
border:var(--hair) solid var(--rule);background:var(--panel);transition:border-color var(--t),background var(--t)}
|
|
88
|
-
.power:hover{border-color:#c3c8d2}
|
|
89
|
-
.power .txt{display:flex;flex-direction:column;line-height:1.3;text-align:right}
|
|
90
|
-
.power .txt b{font-size:13px;font-weight:600;display:flex;align-items:center;gap:6px;justify-content:flex-end}
|
|
91
|
-
.power .txt s{text-decoration:none;font-size:11px;color:var(--ink-2)}
|
|
92
|
-
.power .live{width:6px;height:6px;border-radius:50%;background:var(--ink-2);transition:background var(--t)}
|
|
93
|
-
.switch{position:relative;width:46px;height:26px;border-radius:99px;flex:0 0 auto;
|
|
94
|
-
background:#dfe2e8;border:var(--hair) solid var(--rule);transition:background var(--t),border-color var(--t)}
|
|
95
|
-
.switch::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;
|
|
96
|
-
background:var(--panel);box-shadow:0 1px 2px rgba(22,25,31,.28);transition:left var(--t),background var(--t)}
|
|
97
|
-
.switch:active::after{width:22px}
|
|
98
|
-
.power.on{border-color:var(--accent-line);background:var(--accent-soft)}
|
|
99
|
-
.power.on .switch{background:var(--accent);border-color:var(--accent)}
|
|
100
|
-
.power.on .switch::after{left:23px;background:var(--panel)}
|
|
101
|
-
.power.on .live{background:var(--accent)}
|
|
102
|
-
.power.on .txt b{color:var(--accent)}
|
|
103
|
-
|
|
104
|
-
/* ── 骨架 ─────────────────────────────────────────────────────── */
|
|
105
|
-
.shell{display:grid;grid-template-columns:236px minmax(0,1fr);flex:1 0 auto;width:100%;
|
|
106
|
-
align-items:start;max-width:1340px;margin:0 auto;min-width:0}
|
|
107
|
-
nav.rail{position:sticky;top:64px;display:flex;flex-direction:column;gap:2px;padding:24px 18px 48px;min-width:0}
|
|
108
|
-
nav.rail .lab{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2);padding:0 12px 10px}
|
|
109
|
-
nav.rail button{position:relative;display:flex;align-items:center;gap:11px;padding:10px 12px;
|
|
110
|
-
border-radius:8px;color:var(--ink-2);font-size:15px;text-align:left;
|
|
111
|
-
transition:background var(--t),color var(--t)}
|
|
112
|
-
nav.rail button svg{width:18px;height:18px;flex:0 0 auto;opacity:.8}
|
|
113
|
-
nav.rail button:hover{background:var(--ground-muted);color:var(--ink)}
|
|
114
|
-
nav.rail button.cur{background:var(--accent);color:var(--on-accent);font-weight:600}
|
|
115
|
-
nav.rail button.cur svg{color:var(--on-accent);opacity:1}
|
|
116
|
-
nav.rail button.cur::before{display:none}
|
|
117
|
-
nav.rail button kbd{margin-left:auto;font-size:11px;color:var(--ink-2);border:var(--hair) solid var(--rule);
|
|
118
|
-
border-radius:5px;padding:0 5px;line-height:16px;background:var(--panel)}
|
|
119
|
-
nav.rail button.cur kbd{color:var(--on-accent);border-color:rgba(255,255,255,.45);background:transparent}
|
|
120
|
-
main{padding:26px 32px 96px;min-width:0}
|
|
121
|
-
section{display:none}
|
|
122
|
-
section.cur{display:block}
|
|
123
|
-
|
|
124
|
-
/* ── 标题 ─────────────────────────────────────────────────────── */
|
|
125
|
-
h2{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:32px 0 14px;
|
|
126
|
-
font-family:var(--font-display);font-size:20px;font-weight:700;letter-spacing:-.015em;line-height:1.25}
|
|
127
|
-
h2:first-child{margin-top:0}
|
|
128
|
-
h2 .n{font-family:var(--font-mono);font-size:11px;color:var(--accent);background:var(--accent-soft);
|
|
129
|
-
border:var(--hair) solid var(--accent-line);border-radius:5px;padding:1px 6px;line-height:16px;
|
|
130
|
-
font-weight:500;transform:translateY(-2px)}
|
|
131
|
-
h2>span:last-child{font-size:14px;font-weight:400;color:var(--ink-2);letter-spacing:0;
|
|
132
|
-
font-family:var(--font-ui)}
|
|
133
|
-
h2 span code{background:var(--ground-muted);border:var(--hair) solid var(--rule);border-radius:5px;
|
|
134
|
-
padding:1px 5px;font-size:12px}
|
|
135
|
-
h3{margin:0 0 10px;font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-.01em}
|
|
136
|
-
|
|
137
|
-
/* ── 卡片 ─────────────────────────────────────────────────────── */
|
|
138
|
-
.card{background:var(--panel);border:var(--hair) solid var(--rule);border-radius:var(--r-panel);
|
|
139
|
-
padding:20px 22px;margin-bottom:16px;box-shadow:var(--shadow)}
|
|
140
|
-
.card>.lead{color:var(--ink-2);font-size:15px;margin:-2px 0 16px}
|
|
141
|
-
.card-dark{background:var(--field);color:var(--on-field);border-color:var(--field)}
|
|
142
|
-
.card-dark .lead{color:var(--on-field-2)}
|
|
143
|
-
.card-dark h3{color:var(--on-field)}
|
|
144
|
-
.row{display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap}
|
|
145
|
-
.field{display:flex;flex-direction:column;gap:6px;min-width:200px;flex:1}
|
|
146
|
-
.field>label{font-size:13px;color:var(--ink-2);font-weight:500}
|
|
147
|
-
.field>small{font-size:13px;color:var(--ink-2);line-height:1.5}
|
|
148
|
-
.tiny{font-size:13px;color:var(--ink-2)}
|
|
149
|
-
.mono{font-family:var(--font-mono);font-size:13px}
|
|
150
|
-
|
|
151
|
-
.btn{display:inline-flex;align-items:center;gap:8px;padding:9px 16px;min-height:40px;border-radius:8px;
|
|
152
|
-
border:var(--hair) solid var(--rule);background:var(--panel);color:var(--ink);font-size:15px;
|
|
153
|
-
font-weight:500;white-space:nowrap;transition:transform var(--t),background var(--t),border-color var(--t),color var(--t),box-shadow var(--t)}
|
|
154
|
-
.btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 2px 8px -4px rgba(7,76,224,.4);transform:translateY(-1px)}
|
|
155
|
-
.btn:active{transform:none;box-shadow:none}
|
|
156
|
-
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent);font-weight:600}
|
|
157
|
-
.btn.primary:hover{background:#0539ad;border-color:#0539ad;color:var(--on-accent)}
|
|
158
|
-
.btn:disabled{cursor:not-allowed;opacity:.5}
|
|
159
|
-
.btn.primary:disabled{background:var(--ground-muted);border-color:var(--rule);color:var(--ink-2);opacity:1}
|
|
160
|
-
.btn.danger{color:var(--danger);border-color:rgba(180,35,24,.34);background:var(--panel)}
|
|
161
|
-
.btn.danger:hover{background:var(--danger-soft);border-color:var(--danger);color:var(--danger);box-shadow:none}
|
|
162
|
-
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
|
|
163
|
-
/* 深底卡里的按钮 */
|
|
164
|
-
.card-dark .btn{border-color:rgba(255,255,255,.28);background:transparent;color:var(--on-field)}
|
|
165
|
-
.card-dark .btn:hover{border-color:var(--on-field);color:var(--on-field);background:var(--on-field-soft);box-shadow:none}
|
|
166
|
-
.card-dark .btn.primary{background:var(--on-field);border-color:var(--on-field);color:var(--field)}
|
|
167
|
-
.card-dark .btn.primary:hover{background:var(--on-field-2);border-color:var(--on-field-2);color:var(--field)}
|
|
168
|
-
.card-dark .btn.danger{border-color:rgba(255,255,255,.28);color:#ffb4ab;background:transparent}
|
|
169
|
-
.card-dark .btn.danger:hover{background:rgba(255,180,171,.12);border-color:#ffb4ab}
|
|
170
|
-
/* 忙态 */
|
|
171
|
-
.btn.is-busy,.switch.is-busy,.ch.is-busy{position:relative;pointer-events:none;opacity:.66}
|
|
172
|
-
.btn.is-busy::after,.ch.is-busy::after{content:"";position:absolute;right:10px;top:50%;
|
|
173
|
-
width:11px;height:11px;margin-top:-6px;border-radius:50%;border:1.5px solid currentColor;
|
|
174
|
-
border-right-color:transparent;animation:ap-spin .7s linear infinite}
|
|
175
|
-
@keyframes ap-spin{to{transform:rotate(360deg)}}
|
|
176
|
-
|
|
177
|
-
/* ── 总览:现在会发生什么 ─────────────────────────────────────── */
|
|
178
|
-
.hero{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--gap);align-items:start;margin-bottom:16px}
|
|
179
|
-
.hero .card{margin-bottom:0;height:100%}
|
|
180
|
-
.now{display:grid;min-width:0}
|
|
181
|
-
.nowline{display:grid;grid-template-columns:78px minmax(0,1fr);gap:16px;padding:14px 0;border-top:var(--hair) solid var(--rule)}
|
|
182
|
-
.nowline:first-child{border-top:none;padding-top:0}
|
|
183
|
-
.nowline>s{text-decoration:none;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-2);padding-top:4px}
|
|
184
|
-
.nowline .big{font-family:var(--font-display);font-size:18px;font-weight:700;letter-spacing:-.015em;
|
|
185
|
-
display:flex;align-items:center;gap:9px;flex-wrap:wrap;line-height:1.3}
|
|
186
|
-
.nowline>*{min-width:0}
|
|
187
|
-
.nowline .sub{font-size:15px;color:var(--ink-2);margin-top:4px;min-width:0}
|
|
188
|
-
.pinline{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:9px}
|
|
189
|
-
.cmdbox{display:flex;align-items:center;gap:10px;margin-top:10px;background:var(--ground-muted);min-width:0;
|
|
190
|
-
border:var(--hair) solid var(--rule);border-radius:8px;padding:8px 10px 8px 13px}
|
|
191
|
-
.cmdbox code{flex:1;min-width:0;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;
|
|
192
|
-
color:var(--ink);font-size:13px;line-height:1.55;max-height:4.8em}
|
|
193
|
-
.cmdbox .btn{min-height:32px;padding:5px 12px;font-size:13px}
|
|
194
|
-
|
|
195
|
-
/* 执行器选择(原「执行器」页合并进来,只展示选中的那一个) */
|
|
196
|
-
.hpick{display:flex;gap:7px;flex-wrap:wrap;margin:0 0 12px}
|
|
197
|
-
.hchip{display:inline-flex;align-items:center;gap:7px;padding:7px 12px;border-radius:99px;
|
|
198
|
-
border:var(--hair) solid var(--rule);background:var(--panel);color:var(--ink);font-size:13px;
|
|
199
|
-
font-family:var(--font-mono);cursor:pointer;transition:border-color var(--t),background var(--t),color var(--t)}
|
|
200
|
-
.hchip:hover{border-color:var(--accent);color:var(--accent)}
|
|
201
|
-
.hchip .pdot{width:7px;height:7px;border-radius:50%;background:#c3c8d2;flex:0 0 auto;transition:background var(--t)}
|
|
202
|
-
.hchip.on .pdot{background:var(--accent)}
|
|
203
|
-
.hchip.miss{color:var(--ink-2);opacity:.62;border-style:dashed}
|
|
204
|
-
.hchip.sel{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
|
|
205
|
-
.hchip.sel .pdot{background:var(--on-accent)}
|
|
206
|
-
.selbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px}
|
|
207
|
-
.selbar .grow{flex:1 1 auto;min-width:0;font-size:13px;color:var(--ink-2)}
|
|
208
|
-
#hsel>.hc{box-shadow:var(--shadow)}
|
|
209
|
-
.hdead{margin-top:18px;border-top:var(--hair) solid var(--rule);padding-top:4px}
|
|
210
|
-
.hdead[hidden]{display:none}
|
|
211
|
-
.hdead>summary{cursor:pointer;padding:11px 0;font-size:14px;color:var(--ink-2);display:flex;gap:10px;
|
|
212
|
-
align-items:baseline;flex-wrap:wrap;list-style-position:inside}
|
|
213
|
-
.hdead>summary:hover{color:var(--ink)}
|
|
214
|
-
.hdead>summary::marker{color:var(--ink-2)}
|
|
215
|
-
.hdead .hgrid{margin-top:10px}
|
|
216
|
-
|
|
217
|
-
/* ── 模式选择 ─────────────────────────────────────────────────── */
|
|
218
|
-
.modes{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;align-items:stretch}
|
|
219
|
-
.mode{position:relative;display:flex;flex-direction:column;gap:2px;min-height:132px;height:100%;padding:13px 14px;
|
|
220
|
-
border:var(--hair) solid var(--rule);border-radius:10px;background:var(--panel);cursor:pointer;text-align:left;
|
|
221
|
-
transition:border-color var(--t),background var(--t),box-shadow var(--t)}
|
|
222
|
-
.mode:hover{border-color:var(--accent);background:var(--accent-soft)}
|
|
223
|
-
.mode b{display:flex;align-items:center;gap:6px;font-size:15px;font-weight:600}
|
|
224
|
-
.mode i{display:block;font-style:normal;font-family:var(--font-mono);font-size:11px;color:var(--ink-2);margin-top:2px}
|
|
225
|
-
.mode small{display:block;font-size:13px;color:var(--ink-2);margin-top:auto;padding-top:8px;line-height:1.5}
|
|
226
|
-
.mode.on{border-color:var(--accent);background:var(--accent);color:var(--on-accent);
|
|
227
|
-
box-shadow:0 12px 24px -18px rgba(7,76,224,.95)}
|
|
228
|
-
.mode.on b{color:var(--on-accent)}
|
|
229
|
-
.mode.on i,.mode.on small{color:rgba(255,255,255,.84)}
|
|
230
|
-
.mode.on::after{content:"✓";position:absolute;top:10px;right:11px;font-size:12px;font-weight:700;
|
|
231
|
-
color:var(--accent);background:var(--on-accent);border-radius:50%;width:18px;height:18px;
|
|
232
|
-
display:grid;place-items:center;line-height:1}
|
|
233
|
-
.mode.cur{border-color:var(--ink)}
|
|
234
|
-
.card-dark .mode{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.18);color:var(--on-field)}
|
|
235
|
-
.card-dark .mode small,.card-dark .mode i{color:var(--on-field-2)}
|
|
236
|
-
.card-dark .mode:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.4)}
|
|
237
|
-
.card-dark .mode.on{background:rgba(252,252,253,.14);border-color:rgba(252,252,253,.6);color:var(--on-field)}
|
|
238
|
-
.card-dark .mode.on b{color:var(--on-field)}
|
|
239
|
-
.card-dark .mode.on small,.card-dark .mode.on i{color:var(--on-field-2)}
|
|
240
|
-
.card-dark .mode.on::after{background:var(--on-field);color:var(--field)}
|
|
241
|
-
|
|
242
|
-
/* ── 通道 ─────────────────────────────────────────────────────── */
|
|
243
|
-
.chans{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:10px;min-width:0}
|
|
244
|
-
.chan{display:flex;gap:11px;align-items:flex-start;padding:13px 14px;border:var(--hair) solid var(--rule);
|
|
245
|
-
border-radius:10px;background:var(--panel);text-align:left;min-width:0;overflow:hidden;
|
|
246
|
-
transition:border-color var(--t),background var(--t)}
|
|
247
|
-
.chan:hover{border-color:var(--accent);background:var(--accent-soft)}
|
|
248
|
-
.chan .dot{width:9px;height:9px;border-radius:50%;background:#c3c8d2;margin-top:6px;flex:0 0 auto;
|
|
249
|
-
transition:background var(--t)}
|
|
250
|
-
.chan > span:last-of-type{min-width:0;flex:1 1 auto}
|
|
251
|
-
.chan b{display:flex;align-items:center;gap:7px;font-size:14px;font-weight:600}
|
|
252
|
-
.chan b u{text-decoration:none;font-family:var(--font-mono);font-size:11px;color:var(--ink-2);
|
|
253
|
-
border:var(--hair) solid var(--rule);border-radius:5px;padding:0 5px;line-height:16px}
|
|
254
|
-
.chan b i.lay{font-style:normal;font-family:var(--font-mono);font-size:10px;letter-spacing:.07em;
|
|
255
|
-
color:var(--ink-2);border:var(--hair) solid var(--rule);border-radius:4px;padding:0 4px;line-height:15px}
|
|
256
|
-
.chan b i.lay.warn{color:#8a5a00;border-color:#e6cfa4;background:#fff7e8}
|
|
257
|
-
.chan small{display:block;font-size:12px;color:var(--ink-2);margin-top:3px;line-height:1.45;
|
|
258
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
259
|
-
.chan small.path{display:block;-webkit-line-clamp:none;
|
|
260
|
-
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left}
|
|
261
|
-
.chan.on{border-color:var(--accent);background:var(--accent-soft)}
|
|
262
|
-
.chan.on .dot{background:var(--accent)}
|
|
263
|
-
.dprof{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap}
|
|
264
|
-
.dprof-k{font-size:11.5px;color:var(--ink-2);letter-spacing:.02em}
|
|
265
|
-
.chan.mini{padding:7px 11px;align-items:center;gap:7px}
|
|
266
|
-
.chan.mini .dot{margin-top:0}
|
|
267
|
-
.chan.mini b{font-size:12px;font-weight:600}
|
|
268
|
-
.chan.on b u{color:var(--accent);border-color:var(--accent-line)}
|
|
269
|
-
.chan.dis{opacity:.62;cursor:not-allowed}
|
|
270
|
-
/* 结构性做不到(不是“没开”):虚线 + 不跟手,和“关了”区分开 */
|
|
271
|
-
.chan.na{border-style:dashed;opacity:.55;cursor:not-allowed;background:transparent}
|
|
272
|
-
.chan.na:hover{border-color:var(--line);background:transparent}
|
|
273
|
-
.card-dark .chan{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.18);color:var(--on-field)}
|
|
274
|
-
.card-dark .chan small{-webkit-line-clamp:3}
|
|
275
|
-
.card-dark .chan small,.card-dark .chan b u{color:var(--on-field-2)}
|
|
276
|
-
.card-dark .chan b u{border-color:rgba(255,255,255,.24)}
|
|
277
|
-
.card-dark .chan:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.42)}
|
|
278
|
-
.card-dark .chan .dot{background:rgba(255,255,255,.4)}
|
|
279
|
-
.card-dark .chan.on{background:rgba(252,252,253,.13);border-color:rgba(252,252,253,.6);color:var(--on-field)}
|
|
280
|
-
.card-dark .chan.on small{color:var(--on-field-2)}
|
|
281
|
-
.card-dark .chan.on .dot{background:#7ea6ff}
|
|
282
|
-
.card-dark .chan.on b u{border-color:rgba(126,166,255,.5);color:#a9c3ff}
|
|
283
|
-
.card-dark .tiny{color:var(--on-field-2)}
|
|
284
|
-
|
|
285
|
-
/* ── 执行器卡 ─────────────────────────────────────────────────── */
|
|
286
|
-
.hgrid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));align-items:start}
|
|
287
|
-
.hc{background:var(--panel);border:var(--hair) solid var(--rule);border-radius:var(--r-panel);
|
|
288
|
-
padding:18px 20px;transition:border-color var(--t),box-shadow var(--t);min-width:0}
|
|
289
|
-
.hc:hover{border-color:#c3c8d2;box-shadow:var(--shadow)}
|
|
290
|
-
.hc.dead{background:var(--ground-muted)}
|
|
291
|
-
.hhead{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
|
|
292
|
-
.hname{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600;font-family:var(--font-display);
|
|
293
|
-
letter-spacing:-.01em}
|
|
294
|
-
.hname .st{width:8px;height:8px;border-radius:50%;background:#c3c8d2;flex:0 0 auto}
|
|
295
|
-
.hname .st.on{background:var(--accent)}
|
|
296
|
-
.badge{font-size:12px;padding:2px 9px;border-radius:99px;border:var(--hair) solid var(--rule);
|
|
297
|
-
color:var(--ink-2);white-space:nowrap;display:inline-flex;align-items:center;gap:5px;background:var(--panel)}
|
|
298
|
-
.badge.ok{border-color:var(--accent-line);color:var(--accent);background:var(--accent-soft)}
|
|
299
|
-
.badge.warn{border-color:var(--rule);color:var(--ink-2);background:var(--ground-muted)}
|
|
300
|
-
.badge.bad{border-color:rgba(180,35,24,.4);color:var(--danger);background:var(--danger-soft)}
|
|
301
|
-
.badge.acc{border-color:var(--accent-line);color:var(--accent);background:var(--accent-soft)}
|
|
302
|
-
.hpath{flex:0 0 100%;margin:0;font-family:var(--font-mono);font-size:11px;color:var(--ink-2);
|
|
303
|
-
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
|
|
304
|
-
.rec{display:flex;align-items:center;gap:7px;flex-wrap:wrap;font-size:13px;color:var(--ink);background:var(--accent-soft);border-radius:8px;padding:7px 11px;margin:10px 0 12px}
|
|
305
|
-
.rec b{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);font-weight:600}
|
|
306
|
-
.rec .sep{color:var(--ink-3)}
|
|
307
|
-
.hmeta{display:flex;gap:20px;flex-wrap:wrap;margin:12px 0 14px;font-size:13px;color:var(--ink-2)}
|
|
308
|
-
.hmeta k{color:var(--ink-2);margin-right:5px;font-weight:500;font-size:11px;letter-spacing:.04em;text-transform:uppercase}
|
|
309
|
-
.hmeta .hint{display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;border-radius:50%;
|
|
310
|
-
border:var(--hair) solid var(--rule);color:var(--ink-2);font-size:9px;font-style:normal;margin-left:5px;
|
|
311
|
-
cursor:help;vertical-align:1px;background:var(--panel)}
|
|
312
|
-
.hnote{font-size:13px;color:var(--ink);background:var(--ground-muted);border-left:2px solid var(--accent);
|
|
313
|
-
padding:9px 12px;border-radius:0 8px 8px 0;margin:0 0 12px;line-height:1.55}
|
|
314
|
-
.hwhy{font-size:13px;color:var(--ink-2);background:var(--ground-muted);border:var(--hair) solid var(--rule);
|
|
315
|
-
border-radius:8px;padding:10px 12px;margin-bottom:12px;line-height:1.6}
|
|
316
|
-
|
|
317
|
-
/* ── 表格 ─────────────────────────────────────────────────────── */
|
|
318
|
-
.tblwrap{overflow-x:auto;margin:-4px 0;min-width:0}
|
|
319
|
-
.tbl{width:100%;min-width:900px;border-collapse:collapse;font-size:13px}
|
|
320
|
-
.tbl th{text-align:left;font-weight:500;color:var(--ink-2);font-size:11px;letter-spacing:.04em;
|
|
321
|
-
text-transform:uppercase;padding:0 12px 9px;border-bottom:var(--hair) solid var(--rule);white-space:nowrap}
|
|
322
|
-
.tbl td{padding:10px 12px;border-bottom:var(--hair) solid var(--rule);vertical-align:middle;white-space:nowrap}
|
|
323
|
-
.tbl tbody tr:hover td{background:var(--ground-muted)}
|
|
324
|
-
.tbl tr:last-child td{border-bottom:none}
|
|
325
|
-
.tbl .m{font-family:var(--font-mono);font-weight:500;color:var(--ink)}
|
|
326
|
-
.yes{color:var(--accent);font-weight:600}
|
|
327
|
-
.no{color:var(--ink-2)}
|
|
328
|
-
.na{color:var(--ink-2);opacity:.6}
|
|
329
|
-
.warncell{color:#7a5200;background:#fdf6e3;font-weight:500}
|
|
330
|
-
.tbl .g{display:inline-block;width:12px;font-size:12px;margin-right:2px}
|
|
331
|
-
.tbl .v{display:inline-flex;align-items:baseline;gap:1px;justify-content:flex-end}
|
|
332
|
-
|
|
333
|
-
/* 芯片 */
|
|
334
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
335
|
-
.chip{font-family:var(--font-mono);font-size:12px;color:var(--accent);background:var(--accent-soft);
|
|
336
|
-
border:var(--hair) solid var(--accent-line);border-radius:6px;padding:1px 7px;line-height:18px;white-space:nowrap}
|
|
337
|
-
|
|
338
|
-
/* ── 人设 ─────────────────────────────────────────────────────── */
|
|
339
|
-
.split{display:grid;grid-template-columns:300px minmax(0,1fr);gap:16px;align-items:start}
|
|
340
|
-
.plist{display:flex;flex-direction:column;gap:8px;max-height:70vh;overflow:auto;padding:2px 4px 2px 2px}
|
|
341
|
-
.pitem{padding:12px 14px;border:var(--hair) solid var(--rule);border-radius:10px;background:var(--panel);
|
|
342
|
-
text-align:left;transition:border-color var(--t),background var(--t)}
|
|
343
|
-
.pitem:hover{border-color:var(--accent);background:var(--accent-soft)}
|
|
344
|
-
.pitem.cur{border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}
|
|
345
|
-
.pitem b{display:flex;align-items:center;gap:7px;font-size:14px;font-weight:600}
|
|
346
|
-
.pitem .tag{font-size:10px;padding:1px 7px;border-radius:99px;background:var(--accent);color:var(--on-accent);
|
|
347
|
-
border:var(--hair) solid var(--accent);font-weight:600}
|
|
348
|
-
.pitem small{display:block;color:var(--ink-2);font-size:11px;margin-top:4px;font-family:var(--font-mono)}
|
|
349
|
-
.pitem p{margin:6px 0 0;font-size:12px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:2;
|
|
350
|
-
-webkit-box-orient:vertical;overflow:hidden}
|
|
351
|
-
textarea{resize:vertical;min-height:340px;font-family:var(--font-mono);font-size:13px;line-height:1.7;tab-size:2}
|
|
352
|
-
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
353
|
-
|
|
354
|
-
/* ── 页脚(第二处深色块) ─────────────────────────────────────── */
|
|
355
|
-
.foot{position:relative;z-index:1;background:var(--field);color:var(--on-field);
|
|
356
|
-
padding:28px 32px;margin-top:8px}
|
|
357
|
-
.foot .fin{max-width:1040px;margin:0 auto;display:flex;gap:var(--gap);align-items:baseline;flex-wrap:wrap}
|
|
358
|
-
.foot b{font-family:var(--font-display);font-size:16px;font-weight:700;letter-spacing:-.01em}
|
|
359
|
-
.foot p{margin:0;font-size:13px;color:var(--on-field-2);line-height:1.6;max-width:62ch}
|
|
360
|
-
.foot .end{margin-left:auto;font-family:var(--font-mono);font-size:12px;color:var(--on-field-2)}
|
|
361
|
-
|
|
362
|
-
/* ── toast ────────────────────────────────────────────────────── */
|
|
363
|
-
#toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);display:flex;flex-direction:column;
|
|
364
|
-
gap:8px;align-items:center;pointer-events:none;z-index:200}
|
|
365
|
-
.toast{padding:11px 18px;border-radius:8px;background:var(--field);border:var(--hair) solid var(--field);
|
|
366
|
-
font-size:14px;color:var(--on-field);box-shadow:0 18px 40px -18px rgba(22,25,31,.7);opacity:0;
|
|
367
|
-
transform:translateY(10px);transition:opacity 200ms var(--ease),transform 200ms var(--ease)}
|
|
368
|
-
.toast.in{opacity:1;transform:none}
|
|
369
|
-
.toast.err{border-color:var(--danger);background:#3d1414;color:#ffdad6}
|
|
370
|
-
.empty{padding:28px;text-align:center;color:var(--ink-2);font-size:14px}
|
|
371
|
-
|
|
372
|
-
/* ── 响应式(桌面优先;≤1050 收紧,≤760 堆叠) ───────────────── */
|
|
373
|
-
@media (max-width:1050px){
|
|
374
|
-
.hgrid{grid-template-columns:minmax(0,1fr)}
|
|
375
|
-
.hero{gap:16px}
|
|
376
|
-
.split{grid-template-columns:260px minmax(0,1fr)}
|
|
377
|
-
main{padding:22px 22px 80px}
|
|
378
|
-
}
|
|
379
|
-
@media (max-width:900px){
|
|
380
|
-
.shell{grid-template-columns:minmax(0,1fr)}
|
|
381
|
-
nav.rail{position:sticky;top:64px;z-index:40;flex-direction:row;overflow-x:auto;min-width:0;max-width:100%;gap:4px;
|
|
382
|
-
padding:10px 18px;background:rgba(252,252,253,.94);backdrop-filter:blur(12px);
|
|
383
|
-
border-bottom:var(--hair) solid var(--rule);scrollbar-width:none}
|
|
384
|
-
nav.rail .lab,nav.rail button kbd{display:none}
|
|
385
|
-
nav.rail button{white-space:nowrap}
|
|
386
|
-
main{padding:20px 18px 72px}
|
|
387
|
-
.facts{display:none}
|
|
388
|
-
}
|
|
389
|
-
@media (max-width:760px){
|
|
390
|
-
.split{grid-template-columns:minmax(0,1fr)}
|
|
391
|
-
.plist{max-height:none;flex-direction:row;overflow-x:auto;max-width:100%;padding-bottom:6px}
|
|
392
|
-
.pitem{min-width:230px}
|
|
393
|
-
.grid2{grid-template-columns:1fr}
|
|
394
|
-
.nowline{grid-template-columns:minmax(0,1fr);gap:4px}
|
|
395
|
-
.nowline>s{padding-top:0}
|
|
396
|
-
.hpath{max-width:100%;margin-left:0}
|
|
397
|
-
header.top{flex-wrap:wrap;gap:10px}
|
|
398
|
-
.brand{padding-right:0;border-right:none}
|
|
399
|
-
.cmdbox code{white-space:normal;word-break:break-all}
|
|
400
|
-
.foot{padding:24px 18px}
|
|
401
|
-
.foot .end{margin-left:0}
|
|
402
|
-
}
|
|
403
|
-
@media (prefers-reduced-motion:reduce){
|
|
404
|
-
*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;
|
|
405
|
-
transform:none!important}
|
|
406
|
-
}
|
|
407
|
-
.pirow{display:flex;align-items:center;justify-content:space-between;gap:18px;border:1px solid var(--line);border-radius:12px;padding:11px 14px;margin:12px 0 4px;background:var(--field)}
|
|
408
|
-
.pirow b{display:block;font-size:13px;font-weight:600}
|
|
409
|
-
.pirow .tiny{display:block;margin-top:3px;line-height:1.5}
|
|
410
|
-
.pirow .switch{background:var(--line);border:1px solid var(--line)}
|
|
411
|
-
.pirow.on .switch{background:var(--accent);border-color:var(--accent)}
|
|
412
|
-
.pirow.on .switch::after{left:23px;background:#fff}</style>
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%232459cf'/%3E%3Cpath d='m8 23 8-15 8 15M11 18h10' fill='none' stroke='white' stroke-width='2.5'/%3E%3C/svg%3E">
|
|
9
|
+
<link rel="stylesheet" href="/styles.css">
|
|
10
|
+
<script type="module" src="/app.js"></script>
|
|
413
11
|
</head>
|
|
414
12
|
<body>
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
<div class="
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
<div class="row" style="margin-top:16px">
|
|
475
|
-
<button class="btn primary" id="saveModeBtn" disabled>保存设置</button>
|
|
476
|
-
<span class="tiny" id="dirtyHint"></span>
|
|
477
|
-
</div>
|
|
478
|
-
|
|
479
|
-
<div class="cmdbox">
|
|
480
|
-
<code id="cmdPreview">—</code>
|
|
481
|
-
<button class="btn" id="copyCmd">复制</button>
|
|
482
|
-
</div>
|
|
483
|
-
<div class="tiny" style="margin-top:8px" id="modeHint"></div>
|
|
484
|
-
</div>
|
|
485
|
-
|
|
486
|
-
<h2><span class="n">03</span>执行器细节 <span>只显示选中的那一个:注入层、实测、三个通道;点名字换一家</span></h2>
|
|
487
|
-
<div class="hpick" id="hpick"></div>
|
|
488
|
-
<div class="selbar" id="selbar"></div>
|
|
489
|
-
<div id="hsel"></div>
|
|
490
|
-
<details class="hdead" id="hdeadWrap" hidden>
|
|
491
|
-
<summary>注入不进去的那几个 <span class="tiny">读不进去是有原因的,不是没做</span></summary>
|
|
492
|
-
<div class="hgrid" id="hdeadBox"></div>
|
|
493
|
-
</details>
|
|
494
|
-
</section>
|
|
495
|
-
|
|
496
|
-
<!-- ═══ 3 人设 ═══ -->
|
|
497
|
-
<section id="v-personas">
|
|
498
|
-
<h2><span class="n">04</span>人设库 <span><code>personas/<id>.md</code>;派生文件 <code>active.md</code> 自动生成,别手改</span></h2>
|
|
499
|
-
<div class="split">
|
|
500
|
-
<div>
|
|
501
|
-
<div class="plist" id="plist"></div>
|
|
502
|
-
<button class="btn" id="pNew" style="width:100%;margin-top:9px">+ 新建空白人设</button>
|
|
503
|
-
</div>
|
|
504
|
-
<div class="card" style="margin:0">
|
|
505
|
-
<div class="row" style="margin-bottom:12px">
|
|
506
|
-
<div class="field"><label for="pId">id(文件名)</label><input id="pId" placeholder="xiaomajiang" /></div>
|
|
507
|
-
<div class="field"><label for="pName">名字</label><input id="pName" placeholder="示例:项目助手" /></div>
|
|
508
|
-
</div>
|
|
509
|
-
<div class="row" style="margin-bottom:12px">
|
|
510
|
-
<div class="field"><label for="pDesc">说明</label><input id="pDesc" placeholder="一句话:这个身份是干嘛的" /></div>
|
|
511
|
-
<div class="field" style="max-width:250px"><label for="pModel">建议模型</label><input id="pModel" placeholder="留空 = 不限制" /></div>
|
|
512
|
-
</div>
|
|
513
|
-
<div class="field" style="margin-bottom:12px">
|
|
514
|
-
<label for="pBody">正文 <span class="tiny" id="pChars">0 字</span></label>
|
|
515
|
-
<textarea id="pBody" placeholder="这段文字会被塞进系统提示词层(sys)或用户消息层(user)……"></textarea>
|
|
516
|
-
<small>写的是“身份”而不是“任务”。带拒绝倾向的模型看到它就开始直接干活,这就是注入的全部内容。</small>
|
|
517
|
-
</div>
|
|
518
|
-
<div class="row">
|
|
519
|
-
<button class="btn primary" id="pSave">保存</button>
|
|
520
|
-
<button class="btn" id="pUse">设为当前</button>
|
|
521
|
-
<div class="spacer" style="flex:1"></div>
|
|
522
|
-
<button class="btn danger" id="pDel">删除</button>
|
|
523
|
-
</div>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
526
|
-
</section>
|
|
527
|
-
|
|
528
|
-
<!-- ═══ 4 自检 ═══ -->
|
|
529
|
-
<section id="v-check">
|
|
530
|
-
<h2><span class="n">05</span>本机真值 <span>state 说装了 ≠ 盘上有文件,这里两个都看</span></h2>
|
|
531
|
-
<div class="card" style="padding:16px 8px 10px">
|
|
532
|
-
<div class="tblwrap"><table class="tbl" id="tbl"></table></div>
|
|
533
|
-
</div>
|
|
534
|
-
<div class="card">
|
|
535
|
-
<h3>对账</h3>
|
|
536
|
-
<div class="tiny" id="recon" style="font-size:12px;color:var(--ink-2)">—</div>
|
|
537
|
-
<div class="row" style="margin-top:14px">
|
|
538
|
-
<button class="btn" id="reload">重新读盘</button>
|
|
539
|
-
<button class="btn" id="withVersions">连版本号一起查(慢一点)</button>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
</section>
|
|
543
|
-
</main>
|
|
13
|
+
<a class="skip" href="#main">跳到主要内容</a>
|
|
14
|
+
<div class="app-shell">
|
|
15
|
+
<aside class="sidebar">
|
|
16
|
+
<a class="brand" href="#console"><svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7"/><path d="m8 23 8-15 8 15M11 18h10"/></svg><span><strong>agent-purge</strong><small>提示词控制台</small></span></a>
|
|
17
|
+
<nav aria-label="主要导航">
|
|
18
|
+
<button data-page="console" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>控制台</button>
|
|
19
|
+
<button data-page="library"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a8 8 0 0 1 16 0v2"/></svg>人设库</button>
|
|
20
|
+
<button data-page="diagnostics"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6z"/><path d="m8 12 3 3 5-6"/></svg>验证与诊断</button>
|
|
21
|
+
</nav>
|
|
22
|
+
<div class="sidebar-foot"><span class="dot"></span> 本地配置 · 127.0.0.1<small>变更保存在这台电脑</small></div>
|
|
23
|
+
</aside>
|
|
24
|
+
<div class="workspace">
|
|
25
|
+
<header class="topbar"><span id="connection">正在连接本地服务…</span><div class="topbar-actions"><button type="button" id="version-badge" class="badge" title="检查 GitHub Release 更新">版本加载中</button><button id="refresh" class="quiet">刷新状态</button></div></header>
|
|
26
|
+
<main id="main" tabindex="-1">
|
|
27
|
+
<div id="notice" class="notice" role="status" aria-live="polite" hidden></div>
|
|
28
|
+
<section id="page-console" class="page" aria-labelledby="console-title">
|
|
29
|
+
<div class="page-heading"><div><h1 id="console-title">控制台</h1><p>选好人设,明确范围,再应用。</p></div><div class="actions"><button id="power" class="secondary" disabled>读取状态中…</button><button id="disarm-all" class="quiet danger" title="撤销已登记的通道,保留人设和用户修改;不卸载程序">撤销全部接管</button></div></div>
|
|
30
|
+
<div class="current-strip"><span id="power-status" class="badge">正在加载</span><div><strong id="current-persona">—</strong><span id="current-mode">等待读取当前配置</span></div><a href="#diagnostics" id="current-sync">尚无应用记录</a></div>
|
|
31
|
+
<section class="panel settings" aria-labelledby="config-title">
|
|
32
|
+
<div class="section-heading"><h2 id="config-title">人设与模式</h2><span id="draft-state" class="muted">加载中</span></div>
|
|
33
|
+
<form id="config-form">
|
|
34
|
+
<div class="config-grid"><div><label for="persona-select">应用人设</label><select id="persona-select" disabled></select><p id="persona-description" class="help"></p><button type="button" id="edit-current" class="text-button">编辑这份人设</button></div>
|
|
35
|
+
<fieldset><legend>提示词处理方式</legend><div class="mode-options">
|
|
36
|
+
<label class="mode-option"><input type="radio" name="mode" value="replace"><span><strong>换身份(默认)</strong><small>优先替换身份行;无法读取宿主文本的启动入口按「只留人设」处理</small></span></label>
|
|
37
|
+
<label class="mode-option"><input type="radio" name="mode" value="strict"><span><strong>只留人设</strong><small>替换可控入口中的宿主文本;宿主仍可能额外添加身份、工具或运行说明</small></span></label>
|
|
38
|
+
<label class="mode-option"><input type="radio" name="mode" value="append"><span><strong>追加</strong><small>宿主提示词原样保留,人设另加一份(两份身份可能打架)</small></span></label>
|
|
39
|
+
</div><p class="help">实际层级取决于通道,可能是 system、developer 或项目上下文。不会直接修改工具定义、权限和模型参数。</p></fieldset></div>
|
|
40
|
+
<div class="apply-bar"><span id="apply-hint" class="help">配置写入不代表现有会话已生效。</span><div class="actions"><button type="button" id="reset-config" class="quiet" disabled>撤销选择</button><button type="submit" id="apply" class="primary" disabled>应用变更</button></div></div>
|
|
41
|
+
</form>
|
|
42
|
+
</section>
|
|
43
|
+
<section class="panel" aria-labelledby="scope-title">
|
|
44
|
+
<div class="section-heading"><div><h2 id="scope-title">接管哪些 AI</h2><p id="scope-summary" class="help">正在检查本机执行器…</p></div><label class="check-label"><input type="checkbox" id="show-all">显示本机没装的</label></div>
|
|
45
|
+
<div id="harness-list"><p class="empty">正在检查本机执行器…</p></div>
|
|
46
|
+
</section>
|
|
47
|
+
<details class="panel launch-settings"><summary>默认命令启动 <span id="default-summary" class="muted">未设置</span></summary><div class="detail-body"><p class="help">只决定裸敲 <code>agent-purge</code> 时启动谁,不改变上面的应用范围。</p><form id="default-form" class="inline-form"><label for="default-harness">默认执行器</label><select id="default-harness"></select><button class="secondary" type="submit">保存启动设置</button></form></div></details>
|
|
48
|
+
</section>
|
|
49
|
+
<section id="page-library" class="page" aria-labelledby="library-title" hidden>
|
|
50
|
+
<div class="page-heading"><div><h1 id="library-title">人设库</h1><p>正文用于提示词,名称与说明只用于管理。</p></div><button id="new-persona" class="primary">新建人设</button></div>
|
|
51
|
+
<div class="library-layout"><aside class="panel persona-sidebar"><label class="sr-only" for="persona-search">搜索人设</label><input id="persona-search" type="search" placeholder="搜索名称或说明"><div id="persona-list"></div></aside>
|
|
52
|
+
<section class="panel editor-panel"><div id="editor-empty" class="empty">选择一份人设,查看或编辑正文。</div><form id="editor-form" hidden>
|
|
53
|
+
<div class="section-heading"><h2 id="editor-title">编辑人设</h2><span id="editor-status" class="muted">未修改</span></div>
|
|
54
|
+
<div class="two-fields"><div><label for="edit-id">标识</label><input id="edit-id" required pattern="[a-zA-Z0-9][a-zA-Z0-9_\-]{0,63}" maxlength="64" autocomplete="off"><span class="help">字母、数字、短横线、下划线</span></div><div><label for="edit-name">显示名称</label><input id="edit-name" autocomplete="off"></div></div>
|
|
55
|
+
<label for="edit-desc">说明</label><input id="edit-desc" autocomplete="off">
|
|
56
|
+
<label for="edit-body">人设正文 <span id="char-count" class="muted"></span></label><textarea id="edit-body" required spellcheck="false" rows="17"></textarea>
|
|
57
|
+
<details><summary>高级元数据</summary><label for="edit-model">建议模型(逗号分隔,留空不限制)</label><input id="edit-model"></details>
|
|
58
|
+
<div class="editor-actions"><button id="delete-persona" class="danger quiet" type="button">删除</button><div class="actions"><button id="save-persona" class="secondary" type="submit">保存</button><button id="save-apply" class="primary" type="button">保存并应用</button></div></div>
|
|
59
|
+
<p id="editor-hint" class="help"></p>
|
|
60
|
+
</form></section></div>
|
|
61
|
+
</section>
|
|
62
|
+
<section id="page-diagnostics" class="page" aria-labelledby="diagnostics-title" hidden>
|
|
63
|
+
<div class="page-heading"><div><h1 id="diagnostics-title">验证与诊断</h1><p>磁盘同步与真实请求验证分开记录。</p></div><button id="sync-all" class="secondary">重新同步已选范围</button></div>
|
|
64
|
+
<section class="panel"><div class="section-heading"><div><h2>最近一次应用</h2><p id="report-meta" class="help"></p></div><button id="retry-failed" class="secondary" hidden>重试失败项</button></div><div id="apply-results" class="empty">尚无应用记录</div></section>
|
|
65
|
+
<section class="panel"><div class="section-heading"><h2>真实 harness 请求验证</h2><span class="badge">独立测试</span></div><div id="capture-results"><p class="muted">尚未加载验证报告。</p></div></section>
|
|
66
|
+
<details class="panel"><summary>状态与路径</summary><div class="detail-body"><p class="help">运行时未确认的项目不会标为当前会话已生效。首次安装或更新插件代码需要重新加载宿主。</p><pre id="diagnostic-state"></pre></div></details>
|
|
67
|
+
</section>
|
|
68
|
+
</main>
|
|
69
|
+
<footer>配置写入与会话生效是两件事。以执行器回调和请求证据为准。</footer>
|
|
70
|
+
</div>
|
|
544
71
|
</div>
|
|
545
|
-
|
|
546
|
-
<footer class="foot">
|
|
547
|
-
<div class="fin">
|
|
548
|
-
<b>agent-purge</b>
|
|
549
|
-
<p>人设写得对,模型就没有拒绝的理由。所有注入都落在本机文件里,关掉总电源即全部撤回。</p>
|
|
550
|
-
<span class="end">127.0.0.1 · 本地控制台</span>
|
|
551
|
-
</div>
|
|
552
|
-
</footer>
|
|
553
|
-
|
|
554
|
-
<div id="toast"></div>
|
|
555
|
-
|
|
556
|
-
<script>
|
|
557
|
-
const $ = (s, r = document) => r.querySelector(s);
|
|
558
|
-
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
559
|
-
|
|
560
|
-
const MODE_INFO = {
|
|
561
|
-
strict: { name: "只留人设", desc: "宿主原有的系统提示整段丢掉,只剩人设。" },
|
|
562
|
-
replace: { name: "整段替换", desc: "默认。用带层的替换把宿主提示换成“人设 + 运行时约束”。" },
|
|
563
|
-
append: { name: "追加", desc: "宿主提示照留,只在末尾续一段人设(最温和)。" },
|
|
564
|
-
off: { name: "不注入", desc: "原样启动,等于没打开过这个工具。" },
|
|
565
|
-
};
|
|
566
|
-
const ARM_INFO = {
|
|
567
|
-
shim: { name: "接管裸命令", desc: (h) => h?.id === "pi"
|
|
568
|
-
? "PATH 最前放个同名壳:敲 pi 就注入。想要“裸启动也带人设”,开这个。"
|
|
569
|
-
: `往 PATH 前置一个同名小壳:直接敲 ${h?.id || "<harness>"} 也是接管版。` },
|
|
570
|
-
slash: { name: "会话内热切", desc: (h) => h?.id === "pi" || h?.id === "omp"
|
|
571
|
-
? `写 ${h.id} 扩展,只挂 /purge:不敲 /purge 就原样跑,动手才改写。`
|
|
572
|
-
: "写 harness 自己支持的扩展/命令,会话里 /purge 切换。" },
|
|
573
|
-
install: { name: "配置文件常驻", desc: () => "写进 harness 自己的指令文件:连别人启动、GUI 里点开都生效。" },
|
|
574
|
-
};
|
|
575
|
-
const armDesc = (key, h) => ARM_INFO[key].desc(h);
|
|
576
|
-
/* 热切:caps.hot 是唯一裁决(ok + kind)。kind=command 会话内 /purge;kind=state 改设置下一轮生效 */
|
|
577
|
-
const hotOf = (h) => h?.caps?.hot ?? { ok: false, kind: null, why: null, how: null };
|
|
578
|
-
const hotBadge = (h) => {
|
|
579
|
-
const x = hotOf(h);
|
|
580
|
-
if (!x.ok) return null;
|
|
581
|
-
return x.kind === "command" ? { text: "会话内热切", tip: x.how } : { text: "热切", tip: x.how };
|
|
582
|
-
};
|
|
583
|
-
const CLASS_INFO = {
|
|
584
|
-
"native-extension": "原生扩展",
|
|
585
|
-
"plugin": "插件",
|
|
586
|
-
"launch-arg": "启动参数",
|
|
587
|
-
"launch-arg + binary-patch": "启动参数 + 补丁",
|
|
588
|
-
"file-swap": "文件替换",
|
|
589
|
-
"binary-patch": "二进制补丁",
|
|
590
|
-
"config-or-hook": "配置 / 钩子",
|
|
591
|
-
"unsupported": "做不到",
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
let S = null, A = null, view = "overview", selPersona = null, draftMode = null, selHarness = null;
|
|
595
|
-
let lastPop = null, pDirty = false;
|
|
596
|
-
let withVersions = new URLSearchParams(location.search).get("versions") === "1";
|
|
597
|
-
|
|
598
|
-
/* 说明字段两套键名:README/模板写 description,旧 UI 写 desc。两套都认 */
|
|
599
|
-
const descOf = (p) => p?.meta?.description || p?.meta?.desc || "";
|
|
600
|
-
/* 前端值统一转成 input 可编辑的一行字:数组用「, 」拼(model: ["a","b"] → "a, b") */
|
|
601
|
-
const fieldText = (v) => (Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v));
|
|
602
|
-
|
|
603
|
-
async function api(path, opts) {
|
|
604
|
-
const r = await fetch(path, opts);
|
|
605
|
-
const txt = await r.text();
|
|
606
|
-
let j; try { j = JSON.parse(txt); } catch { throw new Error(txt.slice(0, 200)); }
|
|
607
|
-
if (!r.ok || j.error) throw new Error(j.error || r.status);
|
|
608
|
-
return j;
|
|
609
|
-
}
|
|
610
|
-
function toast(msg, err) {
|
|
611
|
-
const el = document.createElement("div");
|
|
612
|
-
el.className = "toast" + (err ? " err" : "");
|
|
613
|
-
el.textContent = msg;
|
|
614
|
-
$("#toast").appendChild(el);
|
|
615
|
-
requestAnimationFrame(() => el.classList.add("in"));
|
|
616
|
-
setTimeout(() => { el.classList.remove("in"); setTimeout(() => el.remove(), 250); }, err ? 4200 : 2200);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/* ── 数据 ─────────────────────────────────────────── */
|
|
620
|
-
async function load() {
|
|
621
|
-
const [s, a] = await Promise.all([
|
|
622
|
-
api("/api/state?versions=" + (withVersions ? "1" : "0")),
|
|
623
|
-
api("/api/armor"),
|
|
624
|
-
]);
|
|
625
|
-
S = s; A = a;
|
|
626
|
-
/* 旧实例/坏盘可能回 null:这里是页面的唯一入口,先兜底再渲染,
|
|
627
|
-
否则 renderHeader 里 S.defaults.mode 一抛,整页就停在静态 HTML 上了 */
|
|
628
|
-
if (!S || typeof S !== "object") throw new Error("state 接口返回异常");
|
|
629
|
-
if (!Array.isArray(S.harnesses)) S.harnesses = [];
|
|
630
|
-
if (!Array.isArray(S.personas)) S.personas = [];
|
|
631
|
-
const dflt = S.defaults && typeof S.defaults === "object" ? S.defaults : {};
|
|
632
|
-
const fallbackH = S.harnesses.find((h) => h.found && h.supported && h.inject) || S.harnesses.find((h) => h.supported) || S.harnesses[0];
|
|
633
|
-
S.defaults = {
|
|
634
|
-
harness: dflt.harness ?? fallbackH?.id ?? null,
|
|
635
|
-
mode: (S.modes || []).includes(dflt.mode) ? dflt.mode : "replace",
|
|
636
|
-
};
|
|
637
|
-
if (!selPersona) selPersona = S.active || (S.personas[0] && S.personas[0].id) || null;
|
|
638
|
-
if (draftMode !== null && !(S.modes || []).includes(draftMode)) draftMode = null;
|
|
639
|
-
if (draftMode === null) draftMode = S.defaults.mode;
|
|
640
|
-
render();
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/* ── 渲染:顶栏 ───────────────────────────────────── */
|
|
644
|
-
function renderHeader() {
|
|
645
|
-
const mode = S.defaults.mode;
|
|
646
|
-
const h = S.harnesses.find((x) => x.id === S.defaults.harness);
|
|
647
|
-
const armedCount = S.harnesses.filter((x) => (x.armed || []).length).length;
|
|
648
|
-
const persona = S.personas.find((p) => p.id === S.persona);
|
|
649
|
-
const diverge = S.active && S.persona && S.active !== S.persona;
|
|
650
|
-
// defaultsSet=false:state.json 里还没存过默认,这里显示的只是建议值(见 load() 里的兜底)
|
|
651
|
-
const unset = S.defaultsSet === false;
|
|
652
|
-
const unsetTag = unset ? `<span class="tiny"> · 未保存</span>` : "";
|
|
653
|
-
$("#facts").innerHTML = `
|
|
654
|
-
<div class="fact"><s>默认执行器</s><em>${esc(h ? h.id : S.defaults.harness || "未设")}${unsetTag}</em></div>
|
|
655
|
-
<div class="fact ${mode === "off" ? "off" : ""}"><s>注入模式</s><em>${esc(MODE_INFO[mode]?.name || mode)}</em></div>
|
|
656
|
-
<div class="fact ${diverge ? "warn" : ""}"><s>人设</s><em>${esc(persona ? persona.id : S.persona || "未设")}</em></div>
|
|
657
|
-
<div class="fact ${mode === "off" ? "off" : ""}"><s>已武装</s><em>${mode === "off" ? "全部停用" : armedCount + " 个执行器"}</em></div>`;
|
|
658
|
-
// 未设默认时总电源不可点:applyArmAll 没有 default.harness 就写不回落点,点了也没反应
|
|
659
|
-
$("#power").classList.toggle("on", mode !== "off" && !unset);
|
|
660
|
-
const pb = $("#powerBtn");
|
|
661
|
-
pb.setAttribute("aria-checked", String(mode !== "off" && !unset));
|
|
662
|
-
pb.disabled = unset;
|
|
663
|
-
$("#powerSub").textContent = unset ? "未设置 · 先在 01 保存" : mode === "off" ? "已关 · 原样启动" : "注入已开";
|
|
664
|
-
pb.title = unset
|
|
665
|
-
? "还没保存过默认执行器:在 01 里选好模式点“保存设置”,总开关才有落点"
|
|
666
|
-
: mode === "off"
|
|
667
|
-
? "当前关:任何入口(agent-purge / 裸命令 / --ask)都不注入。点一下恢复"
|
|
668
|
-
: "当前开:点一下关掉全部注入(保留各通道原状,开回来自动恢复)";
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/* ── 渲染:总览 ───────────────────────────────────── */
|
|
672
|
-
function layerText(h) {
|
|
673
|
-
if (!h) return "—";
|
|
674
|
-
return (h.layer || "—").toUpperCase();
|
|
675
|
-
}
|
|
676
|
-
/* C:\Users\xxx → ~ :通道路径太长,缩成家目录开头 */
|
|
677
|
-
function shortPath(p) {
|
|
678
|
-
return String(p || "").replace(/^[A-Za-z]:\\Users\\[^\\]+/i, "~").replace(/^\/home\/[^/]+|^\/Users\/[^/]+/, "~");
|
|
679
|
-
}
|
|
680
|
-
/* 尾巴优先:路径太长时砍开头保结尾(不能靠 direction:rtl——会把 . 和 \ 排反) */
|
|
681
|
-
function tailPath(p, max = 46) {
|
|
682
|
-
const s = shortPath(p);
|
|
683
|
-
if (!s) return "";
|
|
684
|
-
return s.length <= max ? s : "…" + s.slice(s.length - (max - 1));
|
|
685
|
-
}
|
|
686
|
-
function renderOverview() {
|
|
687
|
-
const mode = S.defaults.mode;
|
|
688
|
-
const hid = S.defaults.harness;
|
|
689
|
-
const h = S.harnesses.find((x) => x.id === hid) || S.harnesses[0];
|
|
690
|
-
const persona = S.personas.find((p) => p.id === (selPersona || S.persona)) || S.personas[0];
|
|
691
|
-
const hot = S.harnesses.filter((h) => (h.armed || []).length).map((h) => h.id + "(" + h.armed.join("/") + (((h.id === "pi" && S.piAuto) || (h.id === "omp" && S.ompAuto)) ? "+裸启动" : "") + ")");
|
|
692
|
-
|
|
693
|
-
const why = mode === "off"
|
|
694
|
-
? "总电源关着 —— 下面写的都只是配置,实际一条都不会注入。"
|
|
695
|
-
: h
|
|
696
|
-
? (() => {
|
|
697
|
-
const chans = h.armed || [];
|
|
698
|
-
if (!chans.length) {
|
|
699
|
-
const c = pickChannel(h);
|
|
700
|
-
return c
|
|
701
|
-
? "没武装任何入口:现在裸启动是原样的,要用就武装 " + c[1] + "。"
|
|
702
|
-
: h.id + " 没有任何可用的注入通道,实际是原样启动(03 里写了为什么)。";
|
|
703
|
-
}
|
|
704
|
-
const say = (c) => c === "shim"
|
|
705
|
-
? (h.id === "pi" ? "敲 pi 会注入(PATH 刚加的前置壳)" : "敲 " + h.id + " 会注入(PATH 前置壳)")
|
|
706
|
-
: c === "slash"
|
|
707
|
-
? (h.id === "pi" || h.id === "omp" ? ((h.id === "pi" ? S.piAuto : S.ompAuto) ? `裸敲 ${h.id} 也注入(扩展常驻)` : `裸敲 ${h.id} 不注入,只有会话里 /purge 才改写`) : "会话里 /purge 展开成用户消息")
|
|
708
|
-
: "改了它的配置文件,连别人启动的也生效";
|
|
709
|
-
return chans.map(say).join(";") + "。";
|
|
710
|
-
})()
|
|
711
|
-
: "还没选执行器。";
|
|
712
|
-
|
|
713
|
-
$("#nowBox").innerHTML = `
|
|
714
|
-
<div class="nowline"><s>身份</s><div>
|
|
715
|
-
<div class="big">${esc(persona ? (persona.meta?.name || persona.id) : "未选人设")}</div>
|
|
716
|
-
<div class="sub">${esc(persona ? (descOf(persona) || persona.id) : "去“人设”页挑一个")} · ${persona ? persona.chars : 0} 字</div>
|
|
717
|
-
</div></div>
|
|
718
|
-
<div class="nowline"><s>怎么进</s><div>
|
|
719
|
-
<div class="big">${esc(MODE_INFO[mode]?.name || mode)} <span class="tiny">· ${esc(mode)}</span></div>
|
|
720
|
-
<div class="sub">${esc(MODE_INFO[mode]?.desc || "")}</div>
|
|
721
|
-
<div class="rec" style="margin:8px 0 0"><b>推荐</b><code class="mono">${esc(mode)}</code> ${esc(MODE_INFO[mode]?.name || "")}${mode === "off" ? "" : (() => { const c = pickChannel(h); return c ? `<span class="sep">·</span>${esc(c[1])}` : `<span class="sep">·</span>没有可用通道,原样启动`; })()}</div>
|
|
722
|
-
</div></div>
|
|
723
|
-
<div class="nowline"><s>灌进哪</s><div>
|
|
724
|
-
<div class="big">${esc(h ? h.id : "—")} <span class="badge acc">${esc(CLASS_INFO[h?.class] || h?.class || "")}</span></div>
|
|
725
|
-
<div class="sub">${mode === "off" ? "关着,不注入" : "注入层:" + layerText(h) + " · " + (hotOf(h).ok ? (hotOf(h).kind === "command" ? "支持会话内 /purge" : "改设置下一轮生效(不用重启)") : "只能启动时生效")}</div>
|
|
726
|
-
</div></div>
|
|
727
|
-
<div class="nowline"><s>哪些入口</s><div>
|
|
728
|
-
<div class="sub chips">${hot.length ? hot.map((x) => `<span class="chip">${esc(x)}</span>`).join("") : "还没有任何执行器被武装"}</div>
|
|
729
|
-
<div class="cmdbox"><code>${esc(previewCmd(h, mode, persona))}</code><button class="btn" data-copy="${esc(previewCmd(h, mode, persona))}">复制</button></div>
|
|
730
|
-
</div></div>
|
|
731
|
-
<div class="nowline"><s>一句话</s><div class="sub" style="color:var(--ink-2)">${esc(why)}</div></div>`;
|
|
732
|
-
|
|
733
|
-
// 人设下拉
|
|
734
|
-
$("#dPersona").innerHTML = S.personas.map((p) =>
|
|
735
|
-
`<option value="${p.id}" ${p.id === S.persona ? "selected" : ""}>${esc(p.meta?.name || p.id)}(${p.chars} 字)</option>`).join("");
|
|
736
|
-
|
|
737
|
-
// 模式
|
|
738
|
-
$("#modeBox").innerHTML = S.modes.map((m) => `
|
|
739
|
-
<div class="mode ${m === draftMode ? "on" : ""} ${m === S.defaults.mode ? "cur" : ""}" data-mode="${m}">
|
|
740
|
-
<b>${MODE_INFO[m].name}</b><i>${m}${m === S.defaults.mode ? " · 当前" : ""}</i>
|
|
741
|
-
<small>${MODE_INFO[m].desc}</small>
|
|
742
|
-
</div>`).join("");
|
|
743
|
-
$("#modeHint").textContent = S.defaultsSet === false
|
|
744
|
-
? "还没设过默认(上面显示的是建议值):选一个模式 + “保存设置”,裸敲 agent-purge 才真的有落点。"
|
|
745
|
-
: draftMode !== S.defaults.mode
|
|
746
|
-
? "改过还没保存(点上面的“保存设置”才会生效)"
|
|
747
|
-
: mode === "off" ? "现在是 off:保存别的模式并武装之后才会真正注入。" : "";
|
|
748
|
-
$("#saveModeBtn").disabled = draftMode === S.defaults.mode && S.defaultsSet !== false;
|
|
749
|
-
$("#dirtyHint").textContent = S.defaultsSet === false
|
|
750
|
-
? "默认还没保存"
|
|
751
|
-
: draftMode === S.defaults.mode
|
|
752
|
-
? (mode === "off" ? "当前是 off(原样启动)" : "已保存")
|
|
753
|
-
: "未保存:" + (MODE_INFO[draftMode]?.name || draftMode);
|
|
754
|
-
$("#cmdPreview").textContent = previewCmd(h, draftMode, persona);
|
|
755
|
-
$("#copyCmd").dataset.copy = previewCmd(h, draftMode, persona);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function previewCmd(h, mode, persona) {
|
|
759
|
-
if (!h) return "agent-purge";
|
|
760
|
-
const p = persona ? persona.id : "ACTIVE";
|
|
761
|
-
return `agent-purge ${h.id} --persona ${p} --mode ${mode}`;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/* ── 渲染:执行器 ─────────────────────────────────── */
|
|
765
|
-
function harnessCard(h) {
|
|
766
|
-
const row = A.rows.find((r) => r.id === h.id);
|
|
767
|
-
const cls = CLASS_INFO[h.class] || h.class;
|
|
768
|
-
const pct = row ? (row.persona?.sys ?? 0) : null;
|
|
769
|
-
const lyr = (h.layer || "sys").toUpperCase();
|
|
770
|
-
const verdict = pct == null ? null : `${lyr} ${pct >= 100 ? "通过" : pct > 0 ? "部分通过" : "未通过"}`;
|
|
771
|
-
const vcls = pct == null ? "" : pct >= 100 ? "ok" : pct > 0 ? "warn" : "bad";
|
|
772
|
-
const armBadges = (h.armed || []).length
|
|
773
|
-
? h.armed.map((a) => `<span class="badge ok">${ARM_INFO[a]?.name || a}</span>`).join("")
|
|
774
|
-
: `<span class="badge">未武装</span>`;
|
|
775
|
-
const CHAN_ORDER = ["shim", "slash", "install"];
|
|
776
|
-
const isPi = h.id === "pi";
|
|
777
|
-
// 只摆真能用的:能点但没反应的按钮 = 误导,理由改用一行字写在卡片下方
|
|
778
|
-
// pi 例外:它的“裸命令接管”是扩展常驻(piAuto),不是 PATH 壳,所以照样给按钮
|
|
779
|
-
const hiddenChans = CHAN_ORDER.filter((c) => !(isPi && c === "shim") && h.caps?.[c] && !h.caps[c].ok);
|
|
780
|
-
const hiddenText = hiddenChans.map((c) => {
|
|
781
|
-
const cap = h.caps[c];
|
|
782
|
-
if (cap.hiddenLabel) return `${cap.hiddenLabel}(压不住系统提示词)`;
|
|
783
|
-
// 从来就没有 /purge 的,不提:提了只是噪音
|
|
784
|
-
if (c === "slash") return null;
|
|
785
|
-
return `${ARM_INFO[c].name}:${String(cap.why || "").replace(/\s+/g, " ").split(/[;。]/)[0]}`;
|
|
786
|
-
}).filter(Boolean);
|
|
787
|
-
const chans = CHAN_ORDER.filter((c) => (isPi && c === "shim") || !h.caps?.[c] || h.caps[c].ok).map((c) => {
|
|
788
|
-
const on = isPi && c === "shim" ? Boolean(S.piAuto) : h.armed?.includes(c);
|
|
789
|
-
// pi 的裸命令接管=扩展常驻:点它直接切 piAuto,而不是装 PATH 壳
|
|
790
|
-
if (isPi && c === "shim") {
|
|
791
|
-
return `<button class="chan ${on ? "on" : ""}" data-piauto="${on ? "off" : "on"}" type="button" title="扩展常驻,不改 PATH:开关写在 state.json 的 piAuto">
|
|
792
|
-
<span class="dot"></span><span><b>裸命令接管${on ? " · 开" : ""}</b>
|
|
793
|
-
<small>${on ? "扩展常驻:敲 pi 就注入(每次启动)" : "关着:只有 /purge 或 agent-purge pi 才注入"}</small></span></button>`;
|
|
794
|
-
}
|
|
795
|
-
// 能力由后端(lib/caps.mjs)给;UI 不再自己猜一遍,否则两处判断迟早分叉
|
|
796
|
-
const cap = h.caps?.[c] ?? null;
|
|
797
|
-
const avail = cap ? cap.ok : c === "install" ? !!h.persistable : c === "slash" ? !!h.slash : true;
|
|
798
|
-
const enabled = h.found && h.supported && avail;
|
|
799
|
-
const path = c === "install" ? h.persistFile : c === "slash" ? h.slash?.file : (S.shimDir ? S.shimDir + "\\" + h.id + ".cmd" : null);
|
|
800
|
-
const why = !h.found ? "本机没装"
|
|
801
|
-
: cap && !cap.ok ? cap.why
|
|
802
|
-
: c === "install" && !h.persistable ? "它没有可写的配置文件"
|
|
803
|
-
: c === "slash" && !h.slash ? "它没有 /purge 通道" : "";
|
|
804
|
-
const na = Boolean(h.found && cap && !cap.ok);
|
|
805
|
-
// 常驻通道要额外说清楚两件事:写进哪一层(SYS/USER)、实测过还是只有文档
|
|
806
|
-
const lay = c === "install" && cap?.layer ? ` <i class="lay">${cap.layer.toUpperCase()}</i>` : "";
|
|
807
|
-
const unverified = c === "install" && cap?.ok && cap.verified === false ? `<i class="lay warn">未实测</i>` : "";
|
|
808
|
-
const smallText = why || (c === "install" && cap?.note ? cap.note : (path ? tailPath(path) : armDesc(c, h)));
|
|
809
|
-
const smallCls = why || (c === "install" && cap?.note) ? "" : "path";
|
|
810
|
-
return `<button class="chan ${on ? "on" : ""} ${enabled ? "" : "dis"} ${na ? "na" : ""}" ${enabled ? `data-chan="${c}" data-h="${h.id}"` : "disabled"}>
|
|
811
|
-
<span class="dot"></span><span><b>${ARM_INFO[c].name}${lay}${unverified}${on ? " · 开" : ""}${na ? " · 不可用" : ""}</b>
|
|
812
|
-
<small class="${smallCls}" ${path && !why ? `title="${esc(path)}"` : ""}>${esc(smallText)}</small></span></button>`;
|
|
813
|
-
}).join("");
|
|
814
|
-
|
|
815
|
-
// omp 的裸启动注入靠扩展常驻(ompAuto);只有扩展已装(slash 已武装)才给这个开关
|
|
816
|
-
const ompAutoBtn = h.id === "omp" && h.armed?.includes("slash")
|
|
817
|
-
? `<button class="chan ${S.ompAuto ? "on" : ""}" data-ompauto="${S.ompAuto ? "off" : "on"}" type="button" title="扩展常驻,不改 PATH:开关写在 state.json 的 ompAuto">
|
|
818
|
-
<span class="dot"></span><span><b>裸命令接管${S.ompAuto ? " · 开" : ""}</b>
|
|
819
|
-
<small>${S.ompAuto ? "扩展常驻:敲 omp 就注入(每次启动)" : "关着:只有 /purge 或 agent-purge omp 才注入"}</small></span></button>`
|
|
820
|
-
: "";
|
|
821
|
-
|
|
822
|
-
const notes = []
|
|
823
|
-
// 卡片上的徽章已经写了“SYS 通过/部分通过”,同义的行就不再重复
|
|
824
|
-
.concat((row?.warnings || []).filter((w) => !/^(sys|user) (通过|部分通过|未通过)$/.test(w)))
|
|
825
|
-
// 只存在于 user 层的落点:不再给按钮,只留一行实话,免得以为“忘了做”
|
|
826
|
-
.concat(hiddenText.length ? [`不提供:${hiddenText.join(";")}`] : [])
|
|
827
|
-
.concat(h.residue ? ["关掉后仍残留:" + h.residue] : [])
|
|
828
|
-
.concat(h.reason ? [h.reason] : []);
|
|
829
|
-
|
|
830
|
-
const recMode = recommendMode(h);
|
|
831
|
-
const recChan = pickChannel(h);
|
|
832
|
-
|
|
833
|
-
const noChannel = h.caps ? !(h.caps.shim.ok || h.caps.install.ok || h.caps.slash.ok) : false;
|
|
834
|
-
|
|
835
|
-
// dsh 的 desktop / web / headless 是三个独立 profile,插件分别装在三处;
|
|
836
|
-
// 这里只改 state.json,插件每次 assemble 现读,下一轮就生效,不用重启。
|
|
837
|
-
const dshList = S.dshAll || ["desktop", "web", "headless"];
|
|
838
|
-
const profileRow = h.id === "dsh"
|
|
839
|
-
? `<div class="dprof">
|
|
840
|
-
<span class="dprof-k">生效形态 <i class="hint" title="dsh 三个形态各用一个 profile 目录,插件装在三处。勾选只改 state.json 的 dshProfiles,下一轮即生效">?</i></span>
|
|
841
|
-
${dshList.map((p) => {
|
|
842
|
-
const on = (S.dshProfiles || []).includes(p);
|
|
843
|
-
return `<button class="chan mini ${on ? "on" : ""}" data-dshprofile="${esc(p)}" data-on="${on ? "off" : "on"}" type="button"><span class="dot"></span><span><b>${esc(p)}</b></span></button>`;
|
|
844
|
-
}).join("")}
|
|
845
|
-
</div>`
|
|
846
|
-
: "";
|
|
847
|
-
|
|
848
|
-
return `<div class="hc ${h.found ? "" : "dead"}">
|
|
849
|
-
<div class="hhead">
|
|
850
|
-
<span class="hname"><span class="st ${h.armed && h.armed.length ? "on" : ""}"></span>${esc(h.id)}</span>
|
|
851
|
-
<span class="badge acc">${esc(cls)}</span>
|
|
852
|
-
${verdict ? `<span class="badge ${vcls}">${verdict}</span>` : ""}
|
|
853
|
-
${hotBadge(h) ? `<span class="badge" title="${esc(hotBadge(h).tip || "")}">${esc(hotBadge(h).text)}</span>` : ""}
|
|
854
|
-
${h.oneshot ? `<span class="badge">支持 --ask</span>` : `<span class="badge warn">无 --ask</span>`}
|
|
855
|
-
${noChannel ? `<span class="badge bad">只能原样启动</span>` : ""}
|
|
856
|
-
${h.found ? "" : `<span class="badge bad">本机未安装</span>`}
|
|
857
|
-
<span class="hpath" title="${esc(h.bin || "")}">${esc(h.bin ? tailPath(h.bin, 58) : "—")}</span>
|
|
858
|
-
</div>
|
|
859
|
-
${recMode !== "—" ? `<div class="rec"><b>推荐</b><code class="mono">${esc(recMode)}</code> ${esc(MODE_INFO[recMode]?.name || "")}${recChan ? `<span class="sep">·</span>${esc(recChan[1])}` : ""}</div>` : ""}
|
|
860
|
-
<div class="hmeta">
|
|
861
|
-
<span><k>注入层</k>${h.layer ? h.layer.toUpperCase() : "—"}<i class="hint" title="层不是可选项:每家只有它自己暴露的那一个写入点。sys = 系统提示 / 配置 / 插件 splice;user = 只落在用户消息里">?</i></span>
|
|
862
|
-
<span><k>版本</k>${esc(h.version || "—")}${h.version ? "" : `<i class="hint" title="默认不跑各家的 --version(会 spawn 九个进程)。想看就开“自检”页的“连版本号一起查”">?</i>`}</span>
|
|
863
|
-
<span><k>home</k>${esc(h.home || "—")}${h.homeExists === false ? "(不存在)" : ""}</span>
|
|
864
|
-
</div>
|
|
865
|
-
${notes.length ? `<div class="hnote">${notes.map(esc).join("<br>")}</div>` : ""}
|
|
866
|
-
<div class="chans">${chans}${ompAutoBtn}</div>
|
|
867
|
-
${profileRow}
|
|
868
|
-
</div>`;
|
|
869
|
-
}
|
|
870
|
-
function recommendMode(h) {
|
|
871
|
-
const row = A.rows.find((r) => r.id === h.id);
|
|
872
|
-
return row?.mode || "—";
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/* 推荐哪条通道:只认 caps 的裁决(本机真能不能用),不猜 h.found / h.slash */
|
|
876
|
-
function pickChannel(h) {
|
|
877
|
-
if (!h) return null;
|
|
878
|
-
const c = h.caps;
|
|
879
|
-
if (c) {
|
|
880
|
-
if (c.hot?.ok && c.hot.kind === "state" && c.install?.ok && !c.slash?.ok) return ["install", "配置文件常驻(装了就能热切)"];
|
|
881
|
-
if (c.slash?.ok) return ["slash", c.hot?.ok ? "会话内热切(/purge)" : "会话内命令"];
|
|
882
|
-
if (c.shim?.ok) return ["shim", "裸命令接管(敲 " + h.id + ")"];
|
|
883
|
-
if (c.install?.ok) return ["install", "配置文件常驻"];
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
return h.slash ? ["slash", "会话内热切(/purge)"]
|
|
887
|
-
: h.found ? ["shim", "裸命令接管(敲 " + h.id + ")"]
|
|
888
|
-
: h.persistable ? ["install", "配置文件常驻"] : null;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function renderHarness() {
|
|
892
|
-
const list = S.harnesses.filter((h) => h.supported && h.inject);
|
|
893
|
-
const dead = S.harnesses.filter((h) => !(h.supported && h.inject));
|
|
894
|
-
if (!selHarness || !S.harnesses.some((x) => x.id === selHarness)) {
|
|
895
|
-
selHarness = (S.defaults.harness && S.harnesses.some((x) => x.id === S.defaults.harness) ? S.defaults.harness : null)
|
|
896
|
-
|| list[0]?.id || dead[0]?.id || null;
|
|
897
|
-
}
|
|
898
|
-
$("#hpick").innerHTML = list.map((h) => {
|
|
899
|
-
const on = (h.armed || []).length > 0;
|
|
900
|
-
return `<button class="hchip ${h.found ? "" : "miss"} ${on ? "on" : ""} ${h.id === selHarness ? "sel" : ""}"
|
|
901
|
-
data-hpick="${h.id}" title="${esc(h.bin || "")}"><span class="pdot"></span>${esc(h.id)}</button>`;
|
|
902
|
-
}).join("");
|
|
903
|
-
const cur = S.harnesses.find((h) => h.id === selHarness);
|
|
904
|
-
const isDefault = !!cur && cur.id === S.defaults.harness;
|
|
905
|
-
$("#selbar").innerHTML = cur
|
|
906
|
-
? `<span class="grow">${isDefault
|
|
907
|
-
? `<span class="badge ok">默认执行器</span> 裸敲 <code class="mono">agent-purge</code> 就用它`
|
|
908
|
-
: `当前默认是 <code class="mono">${esc(S.defaults.harness || "未设")}</code>`}</span>
|
|
909
|
-
<button class="btn" id="setDefaultH" ${isDefault ? "disabled" : ""}>设为默认执行器</button>`
|
|
910
|
-
: "";
|
|
911
|
-
$("#hsel").innerHTML = cur ? harnessCard(cur) : `<div class="empty">没有可选的执行器</div>`;
|
|
912
|
-
$("#hdeadBox").innerHTML = dead.map(harnessCard).join("");
|
|
913
|
-
$("#hdeadWrap").hidden = dead.length === 0;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/* ── 渲染:人设 ───────────────────────────────────── */
|
|
917
|
-
function renderPersonas() {
|
|
918
|
-
$("#plist").innerHTML = S.personas.map((p) => `
|
|
919
|
-
<button class="pitem ${p.id === selPersona ? "cur" : ""}" data-p="${p.id}">
|
|
920
|
-
<b>${esc(p.meta?.name || p.id)}${p.id === S.persona ? '<span class="tag">当前</span>' : ""}</b>
|
|
921
|
-
<small>${esc(p.id)} · ${p.chars} 字</small>
|
|
922
|
-
<p>${esc(descOf(p) || p.body.slice(0, 90))}</p>
|
|
923
|
-
</button>`).join("") || `<div class="empty">还没有人设,点下面新建</div>`;
|
|
924
|
-
const p = S.personas.find((x) => x.id === selPersona);
|
|
925
|
-
if (!p) return;
|
|
926
|
-
// 正在编辑的草稿不能被 8 秒轮询刷掉:只在“换了选中项”且没改过时才回填
|
|
927
|
-
if (pDirty || lastPop === p.id) return;
|
|
928
|
-
lastPop = p.id; pDirty = false;
|
|
929
|
-
$("#pId").value = p.id;
|
|
930
|
-
$("#pName").value = p.meta?.name || "";
|
|
931
|
-
$("#pDesc").value = descOf(p);
|
|
932
|
-
$("#pModel").value = fieldText(p.meta?.model);
|
|
933
|
-
$("#pBody").value = p.body || "";
|
|
934
|
-
$("#pChars").textContent = (p.body || "").length + " 字";
|
|
935
|
-
// 已有 id 的 persona 不能直接改文件名(否则会写出第二份),要换 id 请点“新建”
|
|
936
|
-
$("#pId").disabled = true;
|
|
937
|
-
$("#pId").title = "要换 id 请点下面「新建空白人设」";
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/* ── 渲染:自检 ───────────────────────────────────── */
|
|
941
|
-
function renderCheck() {
|
|
942
|
-
const head = `<tr><th>执行器</th><th>类别</th><th>装</th><th>版本</th><th>实测</th><th>接管</th><th>热切</th><th>常驻</th><th>state</th></tr>`;
|
|
943
|
-
$("#tbl").innerHTML = head + S.harnesses.map((h) => {
|
|
944
|
-
const cell = (b, t, l) => b === null || b === undefined ? `<td class="na" data-l="${l}">—</td>` : b ? `<td class="yes" data-l="${l}"><span class="v"><span class="g">✓</span>${t}</span></td>` : `<td class="no" data-l="${l}"><span class="v"><span class="g">✕</span>${t}</span></td>`;
|
|
945
|
-
const row = A.rows.find((r) => r.id === h.id);
|
|
946
|
-
const pct = row ? row.persona?.sys : null;
|
|
947
|
-
const pctCell = pct == null
|
|
948
|
-
? `<td class="na" data-l="实测注入">未测</td>`
|
|
949
|
-
: `<td class="${pct >= 100 ? "yes" : pct > 0 ? "warncell" : "no"}" data-l="实测注入"><span class="v"><span class="g">${pct >= 100 ? "✓" : pct > 0 ? "~" : "✕"}</span>${pct}%</span></td>`;
|
|
950
|
-
const arms = (h.armed || []).map((a) => ARM_INFO[a]?.name || a);
|
|
951
|
-
return `<tr>
|
|
952
|
-
<td class="m" data-l="执行器">${esc(h.id)}</td>
|
|
953
|
-
<td class="tiny" data-l="类别">${esc(CLASS_INFO[h.class] || h.class)}</td>
|
|
954
|
-
${cell(h.found, h.found ? "装好" : "没装", "本机装了")}
|
|
955
|
-
<td class="tiny m" data-l="版本">${esc(h.version || "—")}</td>
|
|
956
|
-
${pctCell}
|
|
957
|
-
${(() => {
|
|
958
|
-
const ok = h.caps?.shim?.ok;
|
|
959
|
-
if (!ok) return `<td class="na" data-l="裸命令接管">—</td>`;
|
|
960
|
-
return h.shim ? `<td class="yes" data-l="裸命令接管"><span class="v"><span class="g">✓</span>已接管</span></td>` : `<td class="warncell" data-l="裸命令接管"><span class="v"><span class="g">·</span>可用</span></td>`;
|
|
961
|
-
})()}
|
|
962
|
-
${(() => {
|
|
963
|
-
const hot = h.caps?.hot;
|
|
964
|
-
if (!hot?.ok) return `<td class="na" data-l="会话内热切">—</td>`;
|
|
965
|
-
const on = (h.armed || []).includes(hot.needs);
|
|
966
|
-
const label = hot.kind === "command" ? "会话内热切" : "热切(改设置即生效)";
|
|
967
|
-
return on
|
|
968
|
-
? `<td class="yes" data-l="${label}"><span class="v"><span class="g">✓</span>已开</span></td>`
|
|
969
|
-
: `<td class="warncell" data-l="${label}"><span class="v"><span class="g">·</span>可用</span></td>`;
|
|
970
|
-
})()}
|
|
971
|
-
${(() => {
|
|
972
|
-
// 常驻不再写“无”:能装就是一条可用通道,没装只是没拧开(和 CLI status 同口径)
|
|
973
|
-
const cap = h.caps?.install;
|
|
974
|
-
if (!cap || !cap.ok) return `<td class="na" data-l="配置常驻">—</td>`;
|
|
975
|
-
if (h.installed) return `<td class="yes" data-l="配置常驻"><span class="v"><span class="g">✓</span>已写</span></td>`;
|
|
976
|
-
return `<td class="warncell" data-l="配置常驻"><span class="v"><span class="g">·</span>可用${cap.verified === false ? "(未实测)" : ""}</span></td>`;
|
|
977
|
-
})()}
|
|
978
|
-
<td class="tiny" data-l="state 记录">${arms.length ? esc(arms.join("、")) : "—"}</td>
|
|
979
|
-
</tr>`;
|
|
980
|
-
}).join("");
|
|
981
|
-
const r = S.reconciled || {};
|
|
982
|
-
const lines = Object.entries(r).map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
983
|
-
$("#recon").innerHTML = lines.length ? lines.map(esc).join("<br>") : "state 与磁盘一致,没发现残留。";
|
|
984
|
-
$("#withVersions").textContent = withVersions ? "已连版本号(点这里用缓存)" : "连版本号一起查(慢一点)";
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/* ── 主渲染 ───────────────────────────────────────── */
|
|
988
|
-
function render() {
|
|
989
|
-
renderHeader(); renderOverview(); renderHarness(); renderPersonas(); renderCheck();
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/* ── 事件 ─────────────────────────────────────────── */
|
|
993
|
-
/* ── 视图切换(带 #hash,可直链、可后退)─────────────── */
|
|
994
|
-
function setView(v, sync) {
|
|
995
|
-
if (v === "targets") v = "overview"; // 旧链接兼容:执行器已合并进总览
|
|
996
|
-
view = v;
|
|
997
|
-
document.querySelectorAll("nav.rail button").forEach((x) => x.classList.toggle("cur", x.dataset.v === v));
|
|
998
|
-
document.querySelectorAll("main section").forEach((s) => s.classList.toggle("cur", s.id === "v-" + v));
|
|
999
|
-
if (sync !== false && location.hash.slice(1) !== v) history.replaceState(null, "", "#" + v);
|
|
1000
|
-
}
|
|
1001
|
-
window.addEventListener("hashchange", () => {
|
|
1002
|
-
let v = location.hash.slice(1) || "overview";
|
|
1003
|
-
if (v === "targets") v = "overview";
|
|
1004
|
-
if (v !== view) setView(v, false);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
$("nav.rail").addEventListener("click", (e) => {
|
|
1008
|
-
const b = e.target.closest("button[data-v]"); if (!b) return;
|
|
1009
|
-
setView(b.dataset.v);
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
async function withBusy(el, fn) {
|
|
1013
|
-
if (!el || el.dataset.busy) return undefined;
|
|
1014
|
-
el.dataset.busy = "1"; el.classList.add("is-busy");
|
|
1015
|
-
try { return await fn(); }
|
|
1016
|
-
finally { delete el.dataset.busy; el.classList.remove("is-busy"); }
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
$("#powerBtn").addEventListener("click", () => withBusy($("#powerBtn"), async () => {
|
|
1020
|
-
const on = S.defaults.mode !== "off";
|
|
1021
|
-
try {
|
|
1022
|
-
const r = await api("/api/arm-all", {
|
|
1023
|
-
method: "POST", headers: { "content-type": "application/json" },
|
|
1024
|
-
body: JSON.stringify({ action: on ? "off" : "on" }),
|
|
1025
|
-
});
|
|
1026
|
-
toast(on ? "已关闭全部注入" : "已恢复注入(模式 " + r.mode + ")");
|
|
1027
|
-
draftMode = null; await load();
|
|
1028
|
-
} catch (e) { toast("失败:" + e.message, true); }
|
|
1029
|
-
}));
|
|
1030
|
-
|
|
1031
|
-
$("#modeBox").addEventListener("click", (e) => {
|
|
1032
|
-
const m = e.target.closest("[data-mode]"); if (!m) return;
|
|
1033
|
-
draftMode = m.dataset.mode; renderOverview();
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
$("#dPersona").addEventListener("change", async (e) => {
|
|
1037
|
-
await api("/api/use", { method: "POST", headers: { "content-type": "application/json" },
|
|
1038
|
-
body: JSON.stringify({ id: e.target.value }) });
|
|
1039
|
-
selPersona = e.target.value; lastPop = null; await load(); toast("人设 → " + e.target.value);
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
$("#saveModeBtn").addEventListener("click", async () => {
|
|
1043
|
-
if (draftMode === S.defaults.mode) return;
|
|
1044
|
-
try {
|
|
1045
|
-
await api("/api/defaults", { method: "POST", headers: { "content-type": "application/json" },
|
|
1046
|
-
body: JSON.stringify({ harness: S.defaults.harness, mode: draftMode }) });
|
|
1047
|
-
toast("默认模式 → " + MODE_INFO[draftMode].name);
|
|
1048
|
-
draftMode = null; await load();
|
|
1049
|
-
} catch (e) { toast("失败:" + e.message, true); }
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
$("#hpick").addEventListener("click", (e) => {
|
|
1053
|
-
const b = e.target.closest("[data-hpick]"); if (!b) return;
|
|
1054
|
-
selHarness = b.dataset.hpick; renderHarness();
|
|
1055
|
-
});
|
|
1056
|
-
$("#hsel").addEventListener("click", async (e) => {
|
|
1057
|
-
const b = e.target.closest("[data-piauto], [data-ompauto]"); if (!b) return;
|
|
1058
|
-
const isPi = Boolean(b.dataset.piauto);
|
|
1059
|
-
const want = (isPi ? b.dataset.piauto : b.dataset.ompauto) === "on";
|
|
1060
|
-
await withBusy(b, async () => {
|
|
1061
|
-
try {
|
|
1062
|
-
await api(isPi ? "/api/pi-auto" : "/api/omp-auto", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ on: want }) });
|
|
1063
|
-
toast((isPi ? "pi" : "omp") + " 裸启动自动注入 → " + (want ? "开" : "关"));
|
|
1064
|
-
await load();
|
|
1065
|
-
} catch (err) { toast("失败:" + err.message, true); }
|
|
1066
|
-
});
|
|
1067
|
-
});
|
|
1068
|
-
$("#hsel").addEventListener("click", onChan);
|
|
1069
|
-
$("#hdeadBox").addEventListener("click", onChan);
|
|
1070
|
-
$("#selbar").addEventListener("click", async (e) => {
|
|
1071
|
-
const b = e.target.closest("#setDefaultH"); if (!b) return;
|
|
1072
|
-
await withBusy(b, async () => {
|
|
1073
|
-
try {
|
|
1074
|
-
await api("/api/defaults", { method: "POST", headers: { "content-type": "application/json" },
|
|
1075
|
-
body: JSON.stringify({ harness: selHarness, mode: S.defaults.mode }) });
|
|
1076
|
-
toast("默认执行器 → " + selHarness);
|
|
1077
|
-
await load();
|
|
1078
|
-
} catch (err) { toast("失败:" + err.message, true); }
|
|
1079
|
-
});
|
|
1080
|
-
});
|
|
1081
|
-
async function onChan(e) {
|
|
1082
|
-
// dsh 的三个形态:勾/取消只改 state.json,不动磁盘,所以是即时可逆的
|
|
1083
|
-
const pb = e.target.closest("[data-dshprofile]");
|
|
1084
|
-
if (pb) {
|
|
1085
|
-
const profile = pb.dataset.dshprofile;
|
|
1086
|
-
const on = pb.dataset.on === "on";
|
|
1087
|
-
try {
|
|
1088
|
-
await api("/api/dsh-profile", { method: "POST", headers: { "content-type": "application/json" },
|
|
1089
|
-
body: JSON.stringify({ profile, on }) });
|
|
1090
|
-
toast(`dsh · ${profile} ${on ? "已开" : "已关"}`);
|
|
1091
|
-
await load();
|
|
1092
|
-
} catch (err) { toast("失败:" + err.message, true); }
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
const b = e.target.closest("[data-chan]"); if (!b) return;
|
|
1096
|
-
const { chan, h } = b.dataset;
|
|
1097
|
-
const on = S.harnesses.find((x) => x.id === h)?.armed?.includes(chan);
|
|
1098
|
-
try {
|
|
1099
|
-
await api("/api/arm", { method: "POST", headers: { "content-type": "application/json" },
|
|
1100
|
-
body: JSON.stringify({ harness: h, channel: chan, action: on ? "uninstall" : "install" }) });
|
|
1101
|
-
toast(`${h} · ${ARM_INFO[chan].name} ${on ? "已撤" : "已装"}`);
|
|
1102
|
-
await load();
|
|
1103
|
-
} catch (err) { toast("失败:" + err.message, true); }
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
document.addEventListener("click", async (e) => {
|
|
1107
|
-
const c = e.target.closest("[data-copy]");
|
|
1108
|
-
if (c) { try { await navigator.clipboard.writeText(c.dataset.copy); toast("已复制"); } catch { toast("复制失败", true); } }
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
$("#plist").addEventListener("click", (e) => {
|
|
1112
|
-
const b = e.target.closest("[data-p]"); if (!b) return;
|
|
1113
|
-
selPersona = b.dataset.p; pDirty = false; lastPop = null; renderPersonas();
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
$("#pBody").addEventListener("input", (e) => {
|
|
1117
|
-
pDirty = true; $("#pChars").textContent = e.target.value.length + " 字";
|
|
1118
|
-
});
|
|
1119
|
-
["#pId", "#pName", "#pDesc", "#pModel"].forEach((s) => $(s).addEventListener("input", () => { pDirty = true; }));
|
|
1120
|
-
|
|
1121
|
-
$("#pSave").addEventListener("click", async () => {
|
|
1122
|
-
const id = $("#pId").value.trim();
|
|
1123
|
-
if (!id) return toast("id 不能为空", true);
|
|
1124
|
-
try {
|
|
1125
|
-
await api("/api/persona", { method: "POST", headers: { "content-type": "application/json" },
|
|
1126
|
-
body: JSON.stringify({ id, meta: { name: $("#pName").value, description: $("#pDesc").value, model: $("#pModel").value }, body: $("#pBody").value }) });
|
|
1127
|
-
toast("已保存 " + id); selPersona = id; pDirty = false; lastPop = null; await load();
|
|
1128
|
-
} catch (e) { toast("失败:" + e.message, true); }
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
$("#pUse").addEventListener("click", async () => {
|
|
1132
|
-
const id = $("#pId").value.trim() || selPersona;
|
|
1133
|
-
try {
|
|
1134
|
-
await api("/api/use", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id }) });
|
|
1135
|
-
toast("当前人设 → " + id); selPersona = id; await load();
|
|
1136
|
-
} catch (e) { toast("失败:" + e.message, true); }
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
$("#pDel").addEventListener("click", async () => {
|
|
1140
|
-
const id = $("#pId").value.trim() || selPersona;
|
|
1141
|
-
if (!id) return;
|
|
1142
|
-
try {
|
|
1143
|
-
await api("/api/persona?id=" + encodeURIComponent(id), { method: "DELETE" });
|
|
1144
|
-
toast("已删除 " + id); selPersona = null; await load();
|
|
1145
|
-
} catch (e) { toast("失败:" + e.message, true); }
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
$("#pNew").addEventListener("click", () => {
|
|
1149
|
-
selPersona = null; pDirty = true; lastPop = null;
|
|
1150
|
-
$("#pId").disabled = false;
|
|
1151
|
-
$("#pId").title = "";
|
|
1152
|
-
$("#pId").value = ""; $("#pName").value = ""; $("#pDesc").value = ""; $("#pModel").value = "";
|
|
1153
|
-
$("#pBody").value = "你是这个项目的编码助手。\n\n"; $("#pChars").textContent = $("#pBody").value.length + " 字";
|
|
1154
|
-
document.querySelectorAll(".pitem").forEach((x) => x.classList.remove("cur"));
|
|
1155
|
-
$("#pId").focus();
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
$("#reload").addEventListener("click", async () => { await load(); toast("已重新读盘"); });
|
|
1159
|
-
$("#withVersions").addEventListener("click", async () => {
|
|
1160
|
-
withVersions = !withVersions;
|
|
1161
|
-
const u = new URL(location.href); withVersions ? u.searchParams.set("versions", "1") : u.searchParams.delete("versions");
|
|
1162
|
-
history.replaceState(null, "", u);
|
|
1163
|
-
await load();
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
load().then(() => setView(location.hash.slice(1) || "overview", false)).catch((e) => { console.error("[agent-purge] 首次加载失败", e); toast("加载失败:" + e.message, true); });
|
|
1167
|
-
// 轮询不静默吞错:页面渲染失败以前只表现为“永远空白”,DevTools 里都看不到是谁在抛
|
|
1168
|
-
setInterval(() => load().catch((e) => console.error("[agent-purge] 轮询失败", e)), 8000);
|
|
1169
|
-
</script>
|
|
1170
|
-
|
|
1171
72
|
</body>
|
|
1172
73
|
</html>
|