agent-purge 0.2.4 → 0.2.6
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 +738 -736
- package/lib/caps.mjs +4 -4
- package/lib/dsh.mjs +346 -240
- package/lib/persist.mjs +2 -2
- package/lib/slashes.mjs +23 -2
- package/package.json +1 -1
package/lib/caps.mjs
CHANGED
|
@@ -23,8 +23,8 @@ function readMatrix() {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** 有 /purge 机制的 harness ——
|
|
27
|
-
export const SLASH_IDS = new Set(["pi"]);
|
|
26
|
+
/** 有 /purge 机制的 harness —— **只算能把内容写进系统提示词的**(pi 的扩展钩子、dsh 的 cordis 插件) */
|
|
27
|
+
export const SLASH_IDS = new Set(["pi", "dsh"]);
|
|
28
28
|
/** 有可写配置文件/插件的 harness(其余家只能点名启动或打补丁) */
|
|
29
29
|
export const PERSIST_IDS = new Set(Object.keys(persistTargets));
|
|
30
30
|
/** 能把内容真的送进 system 数组的层;user 层(上下文块/用户消息)一律不对外提供 */
|
|
@@ -64,7 +64,7 @@ const BLOCKED = { antigravity: "二进制补丁才能改它的系统提示,配
|
|
|
64
64
|
const HOT = {
|
|
65
65
|
pi: { kind: "command", needs: "slash", how: "扩展钩子 before_agent_start:/purge 或改设置,下一轮生效" },
|
|
66
66
|
opencode: { kind: "state", needs: "install", how: "插件钩子 chat.system.transform 每轮读 state.json + active.md:改设置下一轮生效" },
|
|
67
|
-
dsh: { kind: "
|
|
67
|
+
dsh: { kind: "command", needs: "slash", how: "cordis 插件注册 /purge:改 state.json,下一句生效,不产生模型消息" },
|
|
68
68
|
opencode2: { kind: "state", needs: "install", how: "v2 插件 ctx.session.hook('context') 每轮读 state.json + active.md:改设置下一轮生效" },
|
|
69
69
|
};
|
|
70
70
|
|
|
@@ -122,7 +122,7 @@ export function capsFor(id, ctx = {}) {
|
|
|
122
122
|
const slashWhy = blocked
|
|
123
123
|
? blocked
|
|
124
124
|
: !SLASH_IDS.has(id)
|
|
125
|
-
? `${id} 没有能改系统提示词的 /purge(/purge 展开成用户消息,实测无效;只有 pi
|
|
125
|
+
? `${id} 没有能改系统提示词的 /purge(/purge 展开成用户消息,实测无效;只有 pi 的扩展和 dsh 的 cordis 插件能做到)`
|
|
126
126
|
: null;
|
|
127
127
|
|
|
128
128
|
return {
|
package/lib/dsh.mjs
CHANGED
|
@@ -1,240 +1,346 @@
|
|
|
1
|
-
// dsh(DeepSeek Harness)的插件通道,三形态(desktop / web / headless)共用一套写法。
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// agent-purge
|
|
5
|
-
// agent-purge-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
assembled =
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
+ "
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
prompt
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const NAME = ${JSON.stringify(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
// dsh(DeepSeek Harness)的插件通道,三形态(desktop / web / headless)共用一套写法。
|
|
2
|
+
//
|
|
3
|
+
// 三个插件名,互不覆盖:
|
|
4
|
+
// agent-purge 常驻注入:每次 assemble 读 state.json + active.md,off 即时生效
|
|
5
|
+
// agent-purge-slash 会话内热切:只注册 /purge 命令,不碰系统提示词
|
|
6
|
+
// agent-purge-session 一次性:persona 快照编译进源码,进程退出即删
|
|
7
|
+
//
|
|
8
|
+
// 三个 profile 都要装:paths 推导自 profile 的 dirname(src/index.js → package root),
|
|
9
|
+
// 所以包必须放在 <profile>/node_modules/<name> 下。
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
export const DSH_PROFILES = ["desktop", "web", "headless"];
|
|
15
|
+
export const RESIDENT = "agent-purge";
|
|
16
|
+
export const SLASH_RESIDENT = "agent-purge-slash";
|
|
17
|
+
export const SESSION = "agent-purge-session";
|
|
18
|
+
|
|
19
|
+
export const dshHome = () => process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
20
|
+
export const profileDir = (p) => join(dshHome(), "profiles", p);
|
|
21
|
+
export const pluginDir = (p, name = RESIDENT) => join(profileDir(p), "node_modules", name);
|
|
22
|
+
const patchFile = (p) => join(profileDir(p), "cordis.patch.yml");
|
|
23
|
+
|
|
24
|
+
const mark = (name) => [`# >>> agent-purge:${name}`, `# <<< agent-purge:${name}`];
|
|
25
|
+
|
|
26
|
+
/** 这个 package.json 是 agent-purge 写的吗(别碰同名外来目录)。 */
|
|
27
|
+
const isOurs = (meta) => existsSync(meta) && /"?agentPurge"?\s*:\s*true/.test(readFileSync(meta, "utf8"));
|
|
28
|
+
|
|
29
|
+
/** 常驻插件:不编译人设,每次请求现读 state;off 就把宿主的返回值原样透传。 */
|
|
30
|
+
export function residentSource(root, activePath) {
|
|
31
|
+
const state = join(root, "state.json").replace(/\\/g, "/");
|
|
32
|
+
// active.md 是派生物,实际位置是 <root>/personas/active.md(由 createStore().materialize 给出)
|
|
33
|
+
const active = String(activePath).replace(/\\/g, "/");
|
|
34
|
+
return `// 由 agent-purge 生成;删掉本目录 + patch 段即取消。
|
|
35
|
+
import { readFileSync, appendFileSync } from "node:fs";
|
|
36
|
+
const STATE = ${JSON.stringify(state)};
|
|
37
|
+
const ACTIVE = ${JSON.stringify(active)};
|
|
38
|
+
const NAME = ${JSON.stringify(RESIDENT)};
|
|
39
|
+
// 一行一条的运行日志,出问题先看它。写失败静默。
|
|
40
|
+
const LOG = STATE.replace(/state\.json$/, "dsh.log");
|
|
41
|
+
function log(ev, extra) {
|
|
42
|
+
try {
|
|
43
|
+
appendFileSync(LOG, new Date().toISOString() + " " + PROFILE + " " + ev + (extra ? " " + extra : "") + "\\n");
|
|
44
|
+
} catch { /* 只读环境不抛 */ }
|
|
45
|
+
}
|
|
46
|
+
// 自己在哪个 profile:<dsh>/profiles/<name>/node_modules/<pkg>/index.js
|
|
47
|
+
const PROFILE = (() => {
|
|
48
|
+
try {
|
|
49
|
+
const m = /[\\\\/]profiles[\\\\/]([^\\\\/]+)[\\\\/]/.exec(new URL(import.meta.url).pathname);
|
|
50
|
+
return m ? decodeURIComponent(m[1]) : "";
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
function cfg() {
|
|
56
|
+
try {
|
|
57
|
+
const st = JSON.parse(readFileSync(STATE, "utf8"));
|
|
58
|
+
// 磁盘卸载后内存里的回调还在:armed 里没有 install 就当成已关闭
|
|
59
|
+
const armed = st && st.armed && st.armed.dsh;
|
|
60
|
+
if (!Array.isArray(armed) || !armed.includes("install")) return null;
|
|
61
|
+
// dshProfiles:没设置过 = desktop/web/headless 全开;设了 = 只认列出的那几个(可热改)
|
|
62
|
+
const on = st && Array.isArray(st.dshProfiles) ? st.dshProfiles : null;
|
|
63
|
+
if (on && (!PROFILE || !on.includes(PROFILE))) return null;
|
|
64
|
+
const mode = (st && st.default && st.default.mode) || "replace";
|
|
65
|
+
if (mode === "off") return null;
|
|
66
|
+
const body = readFileSync(ACTIVE, "utf8").trim();
|
|
67
|
+
return body ? { mode, body } : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function put(asm, c) {
|
|
73
|
+
if (!asm || typeof asm !== "object") return asm;
|
|
74
|
+
const sections = Array.isArray(asm.sections) ? asm.sections : [];
|
|
75
|
+
if (c.mode === "append") return { ...asm, sections: [...sections, { name: NAME, text: c.body }] };
|
|
76
|
+
return { ...asm, sections: [{ name: NAME, text: c.body }] };
|
|
77
|
+
}
|
|
78
|
+
export default {
|
|
79
|
+
name: NAME,
|
|
80
|
+
inject: ["systemPrompt"],
|
|
81
|
+
apply(ctx) {
|
|
82
|
+
log("apply", "inject=" + JSON.stringify(["systemPrompt"]));
|
|
83
|
+
// 宿主每轮 assemble 走 system-prompt/assemble 瀑布。必须 global:agent 作用域的
|
|
84
|
+
// systemPrompt 是另一个实例,且切预设时宿主会整只换代理,实例上的 monkey-patch 会丢。
|
|
85
|
+
const handler = async (assembly, _context, next) => {
|
|
86
|
+
let assembled = assembly;
|
|
87
|
+
if (typeof next === "function") {
|
|
88
|
+
try {
|
|
89
|
+
assembled = await next();
|
|
90
|
+
} catch {
|
|
91
|
+
assembled = assembly; // 下游抛错也要让上游兜底,不能把整轮发消息弄死
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const c = cfg();
|
|
95
|
+
const out = c ? put(assembled, c) : assembled;
|
|
96
|
+
log("assemble", (c ? "mode=" + c.mode : "off")
|
|
97
|
+
+ " in=" + (Array.isArray(assembled && assembled.sections) ? assembled.sections.length : -1)
|
|
98
|
+
+ " out=" + (Array.isArray(out && out.sections) ? out.sections.length : -1)
|
|
99
|
+
+ " names=" + (Array.isArray(out && out.sections) ? out.sections.map((s) => s && s.name).join(",") : "-"));
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
ctx.on("system-prompt/assemble", handler, { global: true, prepend: true });
|
|
104
|
+
log("hook", "system-prompt/assemble global");
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// 老版本没有这个瀑布:退回包 assemble(关闭时同 this、同参数、同返回类型)
|
|
107
|
+
const prompt = ctx.systemPrompt;
|
|
108
|
+
const orig = prompt.assemble;
|
|
109
|
+
prompt.assemble = function (...rest) {
|
|
110
|
+
const c = cfg();
|
|
111
|
+
if (!c) return orig.apply(this, rest);
|
|
112
|
+
const out = orig.apply(this, rest);
|
|
113
|
+
if (out && typeof out.then === "function") return out.then((asm) => put(asm, c));
|
|
114
|
+
return put(out, c);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 会话内热切插件:只注册 /purge,改 state.json,不产生模型消息、不碰系统提示词。 */
|
|
123
|
+
export function slashSource(root) {
|
|
124
|
+
const state = join(root, "state.json").replace(/\\/g, "/");
|
|
125
|
+
return `// 由 agent-purge 生成(/purge 命令);删掉本目录 + patch 段即取消。
|
|
126
|
+
import { readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
127
|
+
const STATE = ${JSON.stringify(state)};
|
|
128
|
+
const NAME = ${JSON.stringify(SLASH_RESIDENT)};
|
|
129
|
+
const LOG = STATE.replace(/state\.json$/, "dsh.log");
|
|
130
|
+
function log(ev, extra) {
|
|
131
|
+
try {
|
|
132
|
+
appendFileSync(LOG, new Date().toISOString() + " " + PROFILE + " " + ev + (extra ? " " + extra : "") + "\\n");
|
|
133
|
+
} catch { /* 只读环境不抛 */ }
|
|
134
|
+
}
|
|
135
|
+
const PROFILE = (() => {
|
|
136
|
+
try {
|
|
137
|
+
const m = /[\\\\/]profiles[\\\\/]([^\\\\/]+)[\\\\/]/.exec(new URL(import.meta.url).pathname);
|
|
138
|
+
return m ? decodeURIComponent(m[1]) : "";
|
|
139
|
+
} catch {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
function readState() {
|
|
144
|
+
try { return JSON.parse(readFileSync(STATE, "utf8")) || {}; } catch { return {}; }
|
|
145
|
+
}
|
|
146
|
+
function saveState(st) {
|
|
147
|
+
writeFileSync(STATE, JSON.stringify(st, null, 2) + "\\n");
|
|
148
|
+
}
|
|
149
|
+
function apPatch(fn) {
|
|
150
|
+
const st = readState();
|
|
151
|
+
if (!st.armed || typeof st.armed !== "object") st.armed = {};
|
|
152
|
+
if (!Array.isArray(st.armed.dsh)) st.armed.dsh = [];
|
|
153
|
+
fn(st, st.armed.dsh);
|
|
154
|
+
saveState(st);
|
|
155
|
+
}
|
|
156
|
+
function handleAp(raw) {
|
|
157
|
+
const parts = String(raw || "").trim().split(/\\s+/).filter(Boolean);
|
|
158
|
+
const cmd = (parts[0] || "status").toLowerCase();
|
|
159
|
+
const arg = parts.slice(1).join(" ").trim();
|
|
160
|
+
if (cmd === "off") {
|
|
161
|
+
apPatch((st, a) => {
|
|
162
|
+
if (a.includes("install")) {
|
|
163
|
+
st.dshPrevArmed = a.slice();
|
|
164
|
+
st.armed.dsh = a.filter((x) => x !== "install");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return { kind: "success", text: "agent-purge 已关闭(dsh),下一句起不注入" };
|
|
168
|
+
}
|
|
169
|
+
if (cmd === "on") {
|
|
170
|
+
apPatch((st, a) => {
|
|
171
|
+
if (!a.includes("install")) a.push("install");
|
|
172
|
+
delete st.dshPrevArmed;
|
|
173
|
+
});
|
|
174
|
+
return { kind: "success", text: "agent-purge 已开启(dsh),下一句起生效" };
|
|
175
|
+
}
|
|
176
|
+
if (cmd === "mode") {
|
|
177
|
+
const m = arg.toLowerCase();
|
|
178
|
+
if (m !== "strict" && m !== "replace" && m !== "append") {
|
|
179
|
+
return { kind: "error", text: "用法:/purge mode strict|replace|append" };
|
|
180
|
+
}
|
|
181
|
+
apPatch((st) => { if (!st.default || typeof st.default !== "object") st.default = {}; st.default.mode = m; });
|
|
182
|
+
return { kind: "success", text: "模式已切到 " + m };
|
|
183
|
+
}
|
|
184
|
+
if (cmd === "status" || cmd === "help") {
|
|
185
|
+
const st = readState();
|
|
186
|
+
const a = st.armed && Array.isArray(st.armed.dsh) ? st.armed.dsh : [];
|
|
187
|
+
const mode = (st.default && st.default.mode) || "replace";
|
|
188
|
+
return { kind: "success", text: "agent-purge / dsh:" + (a.includes("install") ? "开" : "关") + " · 模式 " + mode + " · 人设 " + (st.persona || "-") + "(可用:on | off | status | mode <strict|replace|append>)" };
|
|
189
|
+
}
|
|
190
|
+
return { kind: "error", text: "用法:/purge on | off | status | mode <strict|replace|append>" };
|
|
191
|
+
}
|
|
192
|
+
export default {
|
|
193
|
+
name: NAME,
|
|
194
|
+
apply(ctx) {
|
|
195
|
+
log("apply", "slash-only");
|
|
196
|
+
try {
|
|
197
|
+
if (typeof ctx.inject === "function") {
|
|
198
|
+
ctx.inject(["commands"], (c) => {
|
|
199
|
+
const spec = {
|
|
200
|
+
description: "agent-purge 人设注入:开关 / 模式",
|
|
201
|
+
input: { hint: "on | off | status | mode <strict|replace|append>" },
|
|
202
|
+
handler: ({ rawInput }) => handleAp(rawInput),
|
|
203
|
+
};
|
|
204
|
+
// 主名 /purge(和 pi 等其他 harness 统一),/ap 留作别名;
|
|
205
|
+
// 与已有同名命令(如 dsh-purge)撞车时只跳过那一个
|
|
206
|
+
for (const n of ["purge", "ap"]) {
|
|
207
|
+
try {
|
|
208
|
+
c.commands.register({ name: n, ...spec });
|
|
209
|
+
log("command", "registered /" + n);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
log("command", "skip /" + n + " " + (e && e.message));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
log("command", "no inject api");
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
log("command", "err " + (e && e.message));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 一次性插件:人设快照内嵌,进程结束就没。 */
|
|
227
|
+
export function sessionSource(personaText, mode) {
|
|
228
|
+
const put =
|
|
229
|
+
mode === "append"
|
|
230
|
+
? `return { ...asm, sections: [...(Array.isArray(asm.sections) ? asm.sections : []), { name: NAME, text: PERSONA }] };`
|
|
231
|
+
: `return { ...asm, sections: [{ name: NAME, text: PERSONA }] };`;
|
|
232
|
+
return `// 由 agent-purge 生成(一次性,进程退出即删)。
|
|
233
|
+
const PERSONA = ${JSON.stringify(personaText.trim())};
|
|
234
|
+
const NAME = ${JSON.stringify(SESSION)};
|
|
235
|
+
export default {
|
|
236
|
+
name: NAME,
|
|
237
|
+
inject: ["systemPrompt"],
|
|
238
|
+
apply(ctx) {
|
|
239
|
+
const prompt = ctx.systemPrompt;
|
|
240
|
+
const orig = prompt.assemble;
|
|
241
|
+
// 同常驻插件:不包 async,保持宿主的返回类型
|
|
242
|
+
prompt.assemble = function (...rest) {
|
|
243
|
+
const out = orig.apply(this, rest);
|
|
244
|
+
const put = (asm) => {
|
|
245
|
+
if (!asm || typeof asm !== "object") return asm;
|
|
246
|
+
${put}
|
|
247
|
+
};
|
|
248
|
+
if (out && typeof out.then === "function") return out.then(put);
|
|
249
|
+
return put(out);
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 装一个插件名到三个 profile,并在 patch 里挂上(同一段 marker 幂等)。 */
|
|
257
|
+
export function addPlugin(name, source) {
|
|
258
|
+
const pkg = JSON.stringify({ name, version: "0.0.0", type: "module", main: "index.js", agentPurge: true }, null, 1);
|
|
259
|
+
const [begin, end] = mark(name);
|
|
260
|
+
const block = `\n${begin}\n- insert:\n - id: ${name}\n name: ${name}\n${end}\n`;
|
|
261
|
+
const files = [];
|
|
262
|
+
const touched = []; // { dir, patch, before }:中途抛错时按相反顺序回滚
|
|
263
|
+
// 预检:同名目录要么不存在,要么是我们的(package.json 带 agentPurge 标记),否则不碰
|
|
264
|
+
for (const p of DSH_PROFILES) {
|
|
265
|
+
const dir = pluginDir(p, name);
|
|
266
|
+
const meta = join(dir, "package.json");
|
|
267
|
+
if (existsSync(dir) && !isOurs(meta)) throw new Error(`${dir} 已存在且不是 agent-purge 装的,拒绝覆盖`);
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
for (const p of DSH_PROFILES) {
|
|
271
|
+
const dir = pluginDir(p, name);
|
|
272
|
+
const patch = patchFile(p);
|
|
273
|
+
const before = existsSync(patch) ? readFileSync(patch, "utf8") : null;
|
|
274
|
+
// 先登记再动手:下面任何一步抛错,这一轮的产物都能被回滚
|
|
275
|
+
touched.push({ dir, patch, before });
|
|
276
|
+
mkdirSync(dir, { recursive: true });
|
|
277
|
+
writeFileSync(join(dir, "package.json"), pkg);
|
|
278
|
+
writeFileSync(join(dir, "index.js"), source);
|
|
279
|
+
files.push(dir);
|
|
280
|
+
const old = before ?? "# agent-purge 新建的 patch 层\n";
|
|
281
|
+
if (old.includes(begin)) continue;
|
|
282
|
+
// `[]` 是合法空数组,后面接 `- insert:` 就是非法 YAML:整份换成我们的段
|
|
283
|
+
// 不存在也算“空”:走 stash 分支才能把“原本没有这个文件”记下来,卸载后才不会留个空壳
|
|
284
|
+
const empty = before === null || /^\s*(\[\s*\]\s*)?$/.test(old);
|
|
285
|
+
const next = empty ? block.slice(1) : old + block;
|
|
286
|
+
if (empty) {
|
|
287
|
+
// 整份替换会把原文端掉:原文 + 我们写出的成品都存下,卸载时对得上才整份还原
|
|
288
|
+
// (对不上 = 期间被人改过,那就只剔自己那段)
|
|
289
|
+
writeFileSync(join(dir, ".patch.before"), JSON.stringify({ before, after: next }));
|
|
290
|
+
}
|
|
291
|
+
writeFileSync(patch, next);
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
// 回滚:撤销本次已写的文件,宁可完全没装也不留半套
|
|
295
|
+
for (const { dir, patch, before } of touched.reverse()) {
|
|
296
|
+
rmSync(dir, { recursive: true, force: true });
|
|
297
|
+
if (before === null) rmSync(patch, { force: true });
|
|
298
|
+
else writeFileSync(patch, before);
|
|
299
|
+
}
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
return { dirs: files, profiles: DSH_PROFILES };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 只撤自己那一段:目录 + patch marker,别的配置原样。 */
|
|
306
|
+
export function removePlugin(name) {
|
|
307
|
+
const [begin, end] = mark(name);
|
|
308
|
+
// 删 install 写入的那一段:前导 \n 可有可无(整份替换的形态没有),尾随 \n 一并吃掉
|
|
309
|
+
const re = new RegExp(`(?:^|\\n)${begin}[\\s\\S]*?${end}\\n?`, "g");
|
|
310
|
+
for (const p of DSH_PROFILES) {
|
|
311
|
+
const dir = pluginDir(p, name);
|
|
312
|
+
const meta = join(dir, "package.json");
|
|
313
|
+
// 只删自己装的;同名但不是我们的目录留着(可能被用户或其他包占了)
|
|
314
|
+
const ours = isOurs(meta);
|
|
315
|
+
const stashFile = join(dir, ".patch.before");
|
|
316
|
+
// 当初整份替换过的:记录躲在插件目录里,先读出来,不然删目录就丢了
|
|
317
|
+
let stash = null;
|
|
318
|
+
if (ours && existsSync(stashFile)) {
|
|
319
|
+
try {
|
|
320
|
+
stash = JSON.parse(readFileSync(stashFile, "utf8"));
|
|
321
|
+
} catch {
|
|
322
|
+
stash = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (ours) rmSync(dir, { recursive: true, force: true });
|
|
326
|
+
const patch = patchFile(p);
|
|
327
|
+
if (!existsSync(patch)) continue;
|
|
328
|
+
const text = readFileSync(patch, "utf8");
|
|
329
|
+
// 我们整份写的、且此后没人动过 → 整份还原(原本没这个文件就删掉它)
|
|
330
|
+
if (stash && text === stash.after) {
|
|
331
|
+
if (stash.before === null) rmSync(patch, { force: true });
|
|
332
|
+
else writeFileSync(patch, stash.before);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (text.includes(begin)) writeFileSync(patch, text.replace(re, ""));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** patch 里还挂着这个名字吗(status 用;不许在有其他 insert 时谎报干净)。 */
|
|
340
|
+
export function hasPlugin(name) {
|
|
341
|
+
const [begin] = mark(name);
|
|
342
|
+
return DSH_PROFILES.every((p) => {
|
|
343
|
+
const patch = patchFile(p);
|
|
344
|
+
return existsSync(join(pluginDir(p, name), "index.js")) && existsSync(patch) && readFileSync(patch, "utf8").includes(begin);
|
|
345
|
+
});
|
|
346
|
+
}
|