agent-purge 0.2.0 → 0.2.1
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/bin/agent-purge.mjs +14 -2
- package/lib/slashes.mjs +53 -2
- package/lib/state.mjs +7 -1
- package/package.json +1 -1
- package/web/server.mjs +3 -1
- package/web/ui.html +29 -6
package/bin/agent-purge.mjs
CHANGED
|
@@ -115,10 +115,22 @@ async function launch(name) {
|
|
|
115
115
|
console.log(`[auto] 裸敲 pi ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// pi 的人设落在哪一层:sys(默认,真系统提示词)或 user(塞进用户消息,给忽略 system 的模型)
|
|
119
|
+
const layerFlag = name === "pi" ? flag("--layer", null) : null;
|
|
120
|
+
if (layerFlag !== null) {
|
|
121
|
+
if (layerFlag !== "sys" && layerFlag !== "user") die("--layer 只能是 sys / user");
|
|
122
|
+
const { setPiUserLayer } = await import("../lib/state.mjs");
|
|
123
|
+
setPiUserLayer(DATA, layerFlag === "user");
|
|
124
|
+
console.log(
|
|
125
|
+
layerFlag === "user"
|
|
126
|
+
? "[layer] 人设额外塞进用户消息(给忽略 system 的模型用;代价是每条消息多一段)"
|
|
127
|
+
: "[layer] 人设走 system 层(默认)",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
// 实测策略:该 harness 的哪一层真的能注入(数据来自 matrix.json armor 段)
|
|
119
132
|
const { plan } = await import("../lib/armor.mjs");
|
|
120
|
-
const strategy = plan(matrix, name);
|
|
121
|
-
if (!strategy.error) {
|
|
133
|
+
const strategy = plan(matrix, name); if (!strategy.error) {
|
|
122
134
|
if (strategy.requiresSeed) console.log(`[策略] ${name}:裸注入 sys 层实测 ${strategy.bare.sys}%,带人设词种子 ${strategy.persona.sys}% —— 需要 seed 层`);
|
|
123
135
|
const noSys = strategy.blocked.find((b) => b.layer === "sys");
|
|
124
136
|
if (noSys && mode !== "off") console.log(`[策略] ${name}:sys 层不可用(${noSys.reason}),本次只能靠 user 层`);
|
package/lib/slashes.mjs
CHANGED
|
@@ -186,7 +186,7 @@ function loadPersona(): { id: string | null; body: string } {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function loadState(): { persona: string | null; mode: string; shim: boolean } {
|
|
189
|
+
function loadState(): { persona: string | null; mode: string; shim: boolean; piAuto: boolean; piUserLayer: boolean } {
|
|
190
190
|
// 启动器会用环境变量告知本次运行用的 persona/mode(--persona / --mode 覆盖);
|
|
191
191
|
// 否则读 state.json —— 注意 mode 存在 default.mode,不是顶层。
|
|
192
192
|
const envPersona = process.env.AGENT_PURGE_PERSONA;
|
|
@@ -197,15 +197,17 @@ function loadState(): { persona: string | null; mode: string; shim: boolean } {
|
|
|
197
197
|
default?: { harness?: string; mode?: string };
|
|
198
198
|
armed?: Record<string, string[]>;
|
|
199
199
|
piAuto?: boolean;
|
|
200
|
+
piUserLayer?: boolean;
|
|
200
201
|
};
|
|
201
202
|
return {
|
|
202
203
|
persona: envPersona ?? st.persona ?? null,
|
|
203
204
|
mode: envMode ?? st.default?.mode ?? "replace",
|
|
204
205
|
shim: Array.isArray(st.armed?.pi) && st.armed.pi.includes("shim"),
|
|
205
206
|
piAuto: st.piAuto === true,
|
|
207
|
+
piUserLayer: st.piUserLayer === true,
|
|
206
208
|
};
|
|
207
209
|
} catch {
|
|
208
|
-
return { persona: envPersona ?? null, mode: envMode ?? "replace", shim: false, piAuto: process.env.AGENT_PURGE_FORCE === "1" };
|
|
210
|
+
return { persona: envPersona ?? null, mode: envMode ?? "replace", shim: false, piAuto: process.env.AGENT_PURGE_FORCE === "1", piUserLayer: false };
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -248,6 +250,55 @@ export default function agentPurge(pi: ExtensionAPI) {
|
|
|
248
250
|
return { systemPrompt: host.length > 0 ? cache.body + "\\n\\n" + host : cache.body };
|
|
249
251
|
});
|
|
250
252
|
|
|
253
|
+
// 真正保证“会话中途热切也生效”的是这个拦截面:每个模型请求前重写 transcript 里的
|
|
254
|
+
// system 消恵。before_agent_start 的返回值在部分 pi 版本上只对首轮生效(会话已有上下文时
|
|
255
|
+
// 会被 transcript 覆盖),所以这里再兜一层;两层同时生效时靠 body 前缀去重。
|
|
256
|
+
try {
|
|
257
|
+
pi.on("context_with_system", async (event: any) => {
|
|
258
|
+
const st = loadState();
|
|
259
|
+
const m = st.mode === "strict" || st.mode === "append" ? st.mode : "replace";
|
|
260
|
+
const on = st.mode === "off" ? "off" : (forced ?? (st.persona && autoOn(st) ? "on" : "off"));
|
|
261
|
+
if (on !== "on") return;
|
|
262
|
+
const p = loadPersona();
|
|
263
|
+
if (!p.body) return;
|
|
264
|
+
const messages: any[] = Array.isArray(event?.messages) ? event.messages : [];
|
|
265
|
+
if (messages.length === 0) return;
|
|
266
|
+
const textOf = (msg: any): string => {
|
|
267
|
+
const c = msg?.content;
|
|
268
|
+
if (typeof c === "string") return c;
|
|
269
|
+
if (Array.isArray(c)) return c.map((b: any) => (b && typeof b.text === "string" ? b.text : "")).join("");
|
|
270
|
+
return "";
|
|
271
|
+
};
|
|
272
|
+
// pi 1.4 约定:这个钩子的返回值会被忽略,必须原地改 event.messages
|
|
273
|
+
const out = messages;
|
|
274
|
+
const at = out.findIndex((msg: any) => msg?.role === "system");
|
|
275
|
+
const host = at >= 0 ? textOf(out[at]) : "";
|
|
276
|
+
if (!host.includes(p.body.slice(0, 40))) {
|
|
277
|
+
let next: string;
|
|
278
|
+
if (m === "strict") next = p.body;
|
|
279
|
+
else if (m === "append") next = (host ? host + "\\n\\n" : "") + p.body;
|
|
280
|
+
else next = host ? p.body + "\\n\\n" + cutHostIdentity(host) : p.body;
|
|
281
|
+
if (at >= 0) out[at].content = next;
|
|
282
|
+
else out.unshift({ role: "system", content: next });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 有些模型(实测 deepseek-v4.1-flash 走 mg 网关)直接无视 system 角色:
|
|
286
|
+
// 再往最后一条用户消息前面贴一份,让指令一定被看到。
|
|
287
|
+
if (st.piUserLayer) {
|
|
288
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
289
|
+
if (out[i]?.role !== "user") continue;
|
|
290
|
+
const shown = textOf(out[i]);
|
|
291
|
+
if (shown.includes(p.body.slice(0, 40))) break;
|
|
292
|
+
out[i].content = "<agent-purge>\\n" + p.body + "\\n</agent-purge>\\n\\n" + shown;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
// 老版本 pi 没有 context_with_system:退回 before_agent_start 的返回值
|
|
300
|
+
}
|
|
301
|
+
|
|
251
302
|
pi.registerCommand("purge", {
|
|
252
303
|
description: "会话级接管开关(人设跟随 agent-purge 的 state.json)",
|
|
253
304
|
getArgumentCompletions: (prefix: string) =>
|
package/lib/state.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export function statePath(root) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function emptyState() {
|
|
36
|
-
return { v: 1, persona: null, default: null, armed: {}, piAuto: false };
|
|
36
|
+
return { v: 1, persona: null, default: null, armed: {}, piAuto: false, piUserLayer: false };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -115,6 +115,12 @@ export function setPiAuto(root, on) {
|
|
|
115
115
|
return saveState(root, { ...state, piAuto: on === true }).piAuto;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** pi 的人设走哪一层:false = system(默认),true = 额外塞进用户消息(给忽略 system 的模型用) */
|
|
119
|
+
export function setPiUserLayer(root, on) {
|
|
120
|
+
const { state } = loadState(root);
|
|
121
|
+
return saveState(root, { ...state, piUserLayer: on === true }).piUserLayer;
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
/** armed 变成 {codex: ["shim","install"]};空数组即删键 */
|
|
119
125
|
export function setArmed(root, id, arm, on) {
|
|
120
126
|
const { state } = loadState(root);
|
package/package.json
CHANGED
package/web/server.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { detect } from "../lib/detect.mjs";
|
|
7
7
|
import { oneshots } from "../backends/index.mjs";
|
|
8
8
|
import { createStore } from "../lib/personas.mjs";
|
|
9
|
-
import { MODES, armedOf, loadState, patchState, setArmed, setDefault, setPersona } from "../lib/state.mjs";
|
|
9
|
+
import { MODES, armedOf, loadState, patchState, setArmed, setDefault, setPersona, setPiAuto } from "../lib/state.mjs";
|
|
10
10
|
import { install, status as persistStatus, uninstall } from "../lib/persist.mjs";
|
|
11
11
|
import { plans, summary as armorSummary } from "../lib/armor.mjs";
|
|
12
12
|
import { installShim, shimStatus, uninstallShim } from "../lib/shim.mjs";
|
|
@@ -54,6 +54,7 @@ async function handle(req, res) {
|
|
|
54
54
|
if (req.method === "GET" && p === "/api/state") return send(res, 200, await state(url.searchParams.get("versions") === "1"));
|
|
55
55
|
if (req.method === "GET" && p === "/api/armor") return send(res, 200, armorState());
|
|
56
56
|
if (req.method === "POST" && p === "/api/defaults") return send(res, 200, saveDefaults(await body(req)));
|
|
57
|
+
if (req.method === "POST" && p === "/api/pi-auto") return send(res, 200, { piAuto: setPiAuto(DATA, (await body(req)).on === true) });
|
|
57
58
|
if (req.method === "POST" && p === "/api/persona") return send(res, 200, savePersona(await body(req)));
|
|
58
59
|
if (req.method === "DELETE" && p === "/api/persona") return send(res, 200, deletePersona(url.searchParams.get("id")));
|
|
59
60
|
if (req.method === "POST" && p === "/api/use") return send(res, 200, usePersona(await body(req)));
|
|
@@ -134,6 +135,7 @@ async function state(withVersions) {
|
|
|
134
135
|
defaults: st.default,
|
|
135
136
|
armed: st.armed ?? {},
|
|
136
137
|
modes: MODES,
|
|
138
|
+
piAuto: st.piAuto === true,
|
|
137
139
|
shimDir: shimStatus()[0]?.dir ?? null,
|
|
138
140
|
slashes: slashes,
|
|
139
141
|
};
|
package/web/ui.html
CHANGED
|
@@ -399,7 +399,12 @@ textarea{resize:vertical;min-height:340px;font-family:var(--font-mono);font-size
|
|
|
399
399
|
*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;
|
|
400
400
|
transform:none!important}
|
|
401
401
|
}
|
|
402
|
-
|
|
402
|
+
.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)}
|
|
403
|
+
.pirow b{display:block;font-size:13px;font-weight:600}
|
|
404
|
+
.pirow .tiny{display:block;margin-top:3px;line-height:1.5}
|
|
405
|
+
.pirow .switch{background:var(--line);border:1px solid var(--line)}
|
|
406
|
+
.pirow.on .switch{background:var(--accent);border-color:var(--accent)}
|
|
407
|
+
.pirow.on .switch::after{left:23px;background:#fff}</style>
|
|
403
408
|
</head>
|
|
404
409
|
<body>
|
|
405
410
|
|
|
@@ -655,7 +660,7 @@ function renderOverview() {
|
|
|
655
660
|
const hid = S.defaults.harness;
|
|
656
661
|
const h = S.harnesses.find((x) => x.id === hid) || S.harnesses[0];
|
|
657
662
|
const persona = S.personas.find((p) => p.id === (selPersona || S.persona)) || S.personas[0];
|
|
658
|
-
const hot = S.harnesses.filter((h) => (h.armed || []).length).map((h) => h.id + "(" + h.armed.join("/") + ")");
|
|
663
|
+
const hot = S.harnesses.filter((h) => (h.armed || []).length).map((h) => h.id + "(" + h.armed.join("/") + (h.id === "pi" && S.piAuto ? "+裸启动" : "") + ")");
|
|
659
664
|
|
|
660
665
|
const why = mode === "off"
|
|
661
666
|
? "总电源关着 —— 下面写的都只是配置,实际一条都不会注入。"
|
|
@@ -671,7 +676,7 @@ function renderOverview() {
|
|
|
671
676
|
const say = (c) => c === "shim"
|
|
672
677
|
? (h.id === "pi" ? "敲 pi 会注入(PATH 刚加的前置壳)" : "敲 " + h.id + " 会注入(PATH 前置壳)")
|
|
673
678
|
: c === "slash"
|
|
674
|
-
? (h.id === "pi" ? "裸敲 pi 不注入,只有会话里 /purge 才改写" : "会话里 /purge 展开成用户消息")
|
|
679
|
+
? (h.id === "pi" ? (S.piAuto ? "裸敲 pi 也注入(扩展常驻)" : "裸敲 pi 不注入,只有会话里 /purge 才改写") : "会话里 /purge 展开成用户消息")
|
|
675
680
|
: "改了它的配置文件,连别人启动的也生效";
|
|
676
681
|
return chans.map(say).join(";") + "。";
|
|
677
682
|
})()
|
|
@@ -736,8 +741,10 @@ function harnessCard(h) {
|
|
|
736
741
|
? h.armed.map((a) => `<span class="badge ok">${ARM_INFO[a]?.name || a}</span>`).join("")
|
|
737
742
|
: `<span class="badge">未武装</span>`;
|
|
738
743
|
const CHAN_ORDER = ["shim", "slash", "install"];
|
|
744
|
+
const isPi = h.id === "pi";
|
|
739
745
|
// 只摆真能用的:能点但没反应的按钮 = 误导,理由改用一行字写在卡片下方
|
|
740
|
-
|
|
746
|
+
// pi 例外:它的“裸命令接管”是扩展常驻(piAuto),不是 PATH 壳,所以照样给按钮
|
|
747
|
+
const hiddenChans = CHAN_ORDER.filter((c) => !(isPi && c === "shim") && h.caps?.[c] && !h.caps[c].ok);
|
|
741
748
|
const hiddenText = hiddenChans.map((c) => {
|
|
742
749
|
const cap = h.caps[c];
|
|
743
750
|
if (cap.hiddenLabel) return `${cap.hiddenLabel}(压不住系统提示词)`;
|
|
@@ -745,8 +752,14 @@ function harnessCard(h) {
|
|
|
745
752
|
if (c === "slash") return null;
|
|
746
753
|
return `${ARM_INFO[c].name}:${String(cap.why || "").replace(/\s+/g, " ").split(/[;。]/)[0]}`;
|
|
747
754
|
}).filter(Boolean);
|
|
748
|
-
const chans = CHAN_ORDER.filter((c) => !h.caps?.[c] || h.caps[c].ok).map((c) => {
|
|
749
|
-
const on = h.armed?.includes(c);
|
|
755
|
+
const chans = CHAN_ORDER.filter((c) => (isPi && c === "shim") || !h.caps?.[c] || h.caps[c].ok).map((c) => {
|
|
756
|
+
const on = isPi && c === "shim" ? Boolean(S.piAuto) : h.armed?.includes(c);
|
|
757
|
+
// pi 的裸命令接管=扩展常驻:点它直接切 piAuto,而不是装 PATH 壳
|
|
758
|
+
if (isPi && c === "shim") {
|
|
759
|
+
return `<button class="chan ${on ? "on" : ""}" data-piauto="${on ? "off" : "on"}" type="button" title="扩展常驻,不改 PATH:开关写在 state.json 的 piAuto">
|
|
760
|
+
<span class="dot"></span><span><b>裸命令接管${on ? " · 开" : ""}</b>
|
|
761
|
+
<small>${on ? "扩展常驻:敲 pi 就注入(每次启动)" : "关着:只有 /purge 或 agent-purge pi 才注入"}</small></span></button>`;
|
|
762
|
+
}
|
|
750
763
|
// 能力由后端(lib/caps.mjs)给;UI 不再自己猜一遍,否则两处判断迟早分叉
|
|
751
764
|
const cap = h.caps?.[c] ?? null;
|
|
752
765
|
const avail = cap ? cap.ok : c === "install" ? !!h.persistable : c === "slash" ? !!h.slash : true;
|
|
@@ -987,6 +1000,16 @@ $("#hpick").addEventListener("click", (e) => {
|
|
|
987
1000
|
const b = e.target.closest("[data-hpick]"); if (!b) return;
|
|
988
1001
|
selHarness = b.dataset.hpick; renderHarness();
|
|
989
1002
|
});
|
|
1003
|
+
$("#hsel").addEventListener("click", async (e) => {
|
|
1004
|
+
const b = e.target.closest("[data-piauto]"); if (!b) return;
|
|
1005
|
+
await withBusy(b, async () => {
|
|
1006
|
+
try {
|
|
1007
|
+
await api("/api/pi-auto", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ on: b.dataset.piauto === "on" }) });
|
|
1008
|
+
toast("裸启动自动注入 → " + (b.dataset.piauto === "on" ? "开" : "关"));
|
|
1009
|
+
await load();
|
|
1010
|
+
} catch (err) { toast("失败:" + err.message, true); }
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
990
1013
|
$("#hsel").addEventListener("click", onChan);
|
|
991
1014
|
$("#hdeadBox").addEventListener("click", onChan);
|
|
992
1015
|
$("#selbar").addEventListener("click", async (e) => {
|