agent-purge 0.2.1 → 0.2.3
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/README.md +193 -76
- package/backends/index.mjs +63 -0
- package/bin/agent-purge.mjs +736 -729
- package/lib/caps.mjs +5 -1
- package/lib/detect.mjs +2 -0
- package/lib/dsh.mjs +177 -0
- package/lib/persist.mjs +80 -1
- package/lib/personas.mjs +207 -180
- package/matrix.json +24 -0
- package/package.json +51 -48
- package/web/server.mjs +375 -331
- package/web/ui.html +1131 -1103
package/lib/caps.mjs
CHANGED
|
@@ -46,6 +46,8 @@ const DESKTOP_FIRST = new Set(["zcode", "antigravity", "qoder", "workbuddy"]);
|
|
|
46
46
|
},
|
|
47
47
|
qoder: { shim: "提示词在服务端,本地拦不到" },
|
|
48
48
|
workbuddy: { shim: "提示词在服务端,本地拦不到" },
|
|
49
|
+
// dsh 的 CLI 只能起 headless:接管裸命令会把桌面/网页入口静默改道
|
|
50
|
+
dsh: { shim: "dsh 命令行只能起 headless:接管裸命令会让双击/网页入口变成终端会话;要三形态就装常驻插件(install)" },
|
|
49
51
|
codebuddy: {
|
|
50
52
|
install: "只有 settings.json(插件开关/模型),没有能把文本塞进提示的词条;实测只能走启动参数或壳",
|
|
51
53
|
},
|
|
@@ -62,6 +64,8 @@ const BLOCKED = { antigravity: "二进制补丁才能改它的系统提示,配
|
|
|
62
64
|
const HOT = {
|
|
63
65
|
pi: { kind: "command", needs: "slash", how: "扩展钩子 before_agent_start:/purge 或改设置,下一轮生效" },
|
|
64
66
|
opencode: { kind: "state", needs: "install", how: "插件钩子 chat.system.transform 每轮读 state.json + active.md:改设置下一轮生效" },
|
|
67
|
+
dsh: { kind: "state", needs: "install", how: "cordis 插件每轮 assemble 读 state.json + active.md:改设置下一轮生效(desktop 装完要重启一次)" },
|
|
68
|
+
opencode2: { kind: "state", needs: "install", how: "v2 插件 ctx.session.hook('context') 每轮读 state.json + active.md:改设置下一轮生效" },
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
/** 没有每轮拼装点的:说明为什么只能重启 */
|
|
@@ -105,7 +109,7 @@ export function capsFor(id, ctx = {}) {
|
|
|
105
109
|
: `本机 PATH 里找不到 ${id}(先把它装到 PATH 上)`
|
|
106
110
|
: desktop
|
|
107
111
|
? `只对终端里敲 \`${id}\` 生效;桌面图标启动绕开 PATH`
|
|
108
|
-
:
|
|
112
|
+
: reason(id, "shim");
|
|
109
113
|
|
|
110
114
|
const t = persistTargets[id];
|
|
111
115
|
// user 层不算能力:写下去也压不住 harness 自己的身份,装了等于白装
|
package/lib/detect.mjs
CHANGED
|
@@ -14,6 +14,8 @@ export const specs = {
|
|
|
14
14
|
codebuddy: { bins: ["codebuddy"], homes: [["CODEBUDDY_HOME", ".codebuddy"]], version: ["--version"], marker: "history.jsonl" },
|
|
15
15
|
claude: { bins: ["claude"], homes: [["CLAUDE_CONFIG_DIR", ".claude"]], version: ["--version"], marker: "settings.json" },
|
|
16
16
|
opencode: { bins: ["opencode"], homes: [["XDG_CONFIG_HOME", ".config/opencode"]], version: ["--version"], marker: "opencode.json" },
|
|
17
|
+
opencode2: { bins: ["opencode2"], homes: [["XDG_CONFIG_HOME", ".config/opencode"]], version: ["--version"], marker: "opencode.json" },
|
|
18
|
+
dsh: { bins: ["dsh"], homes: [["DSH_HOME", ".dsh"]], version: ["--version"], marker: "profiles" },
|
|
17
19
|
grok: { bins: ["grok"], homes: [["GROK_HOME", ".grok"]], version: ["--version"], marker: "config.toml" },
|
|
18
20
|
omp: { bins: ["omp"], homes: [["OMP_HOME", ".omp"]], version: ["--version"], marker: "agent/config.yml" },
|
|
19
21
|
hermes: { bins: ["hermes"], homes: [["HERMES_HOME", "AppData/Local/hermes"]], version: ["--version"], marker: "hermes-agent" },
|
package/lib/dsh.mjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// dsh(DeepSeek Harness)的插件通道,三形态(desktop / web / headless)共用一套写法。
|
|
2
|
+
//
|
|
3
|
+
// 两个插件名,互不覆盖:
|
|
4
|
+
// agent-purge 常驻:每次 assemble 读 state.json + active.md,off 即时生效
|
|
5
|
+
// agent-purge-session 一次性:persona 快照编译进源码,进程退出即删
|
|
6
|
+
//
|
|
7
|
+
// 三个 profile 都要装:paths 推导自 profile 的 dirname(src/index.js → package root),
|
|
8
|
+
// 所以包必须放在 <profile>/node_modules/<name> 下。
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export const DSH_PROFILES = ["desktop", "web", "headless"];
|
|
14
|
+
export const RESIDENT = "agent-purge";
|
|
15
|
+
export const SESSION = "agent-purge-session";
|
|
16
|
+
|
|
17
|
+
export const dshHome = () => process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
18
|
+
export const profileDir = (p) => join(dshHome(), "profiles", p);
|
|
19
|
+
export const pluginDir = (p, name = RESIDENT) => join(profileDir(p), "node_modules", name);
|
|
20
|
+
const patchFile = (p) => join(profileDir(p), "cordis.patch.yml");
|
|
21
|
+
|
|
22
|
+
const mark = (name) => [`# >>> agent-purge:${name}`, `# <<< agent-purge:${name}`];
|
|
23
|
+
|
|
24
|
+
/** 这个 package.json 是 agent-purge 写的吗(别碰同名外来目录)。 */
|
|
25
|
+
const isOurs = (meta) => existsSync(meta) && /"?agentPurge"?\s*:\s*true/.test(readFileSync(meta, "utf8"));
|
|
26
|
+
|
|
27
|
+
/** 常驻插件:不编译人设,每次请求现读 state;off 就原样放过宿主的结果。 */
|
|
28
|
+
export function residentSource(root, activePath) {
|
|
29
|
+
const state = join(root, "state.json").replace(/\\/g, "/");
|
|
30
|
+
// active.md 是派生物,实际位置是 <root>/personas/active.md(由 createStore().materialize 给出)
|
|
31
|
+
const active = String(activePath).replace(/\\/g, "/");
|
|
32
|
+
return `// 由 agent-purge 生成;删掉本目录 + patch 段即取消。
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
const STATE = ${JSON.stringify(state)};
|
|
35
|
+
const ACTIVE = ${JSON.stringify(active)};
|
|
36
|
+
function cfg() {
|
|
37
|
+
try {
|
|
38
|
+
const st = JSON.parse(readFileSync(STATE, "utf8"));
|
|
39
|
+
// 磁盘卸载后内存里的回调还在:armed 里没有 install 就当成已关闭
|
|
40
|
+
const armed = st && st.armed && st.armed.dsh;
|
|
41
|
+
if (!Array.isArray(armed) || !armed.includes("install")) return null;
|
|
42
|
+
const mode = (st && st.default && st.default.mode) || "replace";
|
|
43
|
+
if (mode === "off") return null;
|
|
44
|
+
const body = readFileSync(ACTIVE, "utf8").trim();
|
|
45
|
+
return body ? { mode, body } : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export default {
|
|
51
|
+
name: ${JSON.stringify(RESIDENT)},
|
|
52
|
+
inject: ["systemPrompt"],
|
|
53
|
+
apply(ctx) {
|
|
54
|
+
const prompt = ctx.systemPrompt;
|
|
55
|
+
const orig = prompt.assemble.bind(prompt);
|
|
56
|
+
prompt.assemble = async (...rest) => {
|
|
57
|
+
const asm = await orig(...rest);
|
|
58
|
+
const c = cfg();
|
|
59
|
+
if (!c) return asm;
|
|
60
|
+
if (c.mode === "append") return { ...asm, sections: [...(asm.sections || []), { name: ${JSON.stringify(RESIDENT)}, text: c.body }] };
|
|
61
|
+
return { ...asm, sections: [{ name: ${JSON.stringify(RESIDENT)}, text: c.body }] };
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 一次性插件:人设快照内嵌,进程结束就没。 */
|
|
69
|
+
export function sessionSource(personaText, mode) {
|
|
70
|
+
const body =
|
|
71
|
+
mode === "append"
|
|
72
|
+
? `\t\tctx.systemPrompt.section({ name: ${JSON.stringify(SESSION)}, order: 5100, interpolate: false, text: PERSONA });`
|
|
73
|
+
: `\t\tconst prompt = ctx.systemPrompt;\n\t\tconst orig = prompt.assemble.bind(prompt);\n\t\tprompt.assemble = async (...rest) => {\n\t\t\tconst asm = await orig(...rest);\n\t\t\treturn { ...asm, sections: [{ name: ${JSON.stringify(SESSION)}, text: PERSONA }] };\n\t\t};`;
|
|
74
|
+
return `// 由 agent-purge 生成(一次性,进程退出即删)。
|
|
75
|
+
const PERSONA = ${JSON.stringify(personaText.trim())};
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
name: ${JSON.stringify(SESSION)},
|
|
79
|
+
inject: ["systemPrompt"],
|
|
80
|
+
apply(ctx) {
|
|
81
|
+
${body}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 装一个插件名到三个 profile,并在 patch 里挂上(同一段 marker 幂等)。 */
|
|
88
|
+
export function addPlugin(name, source) {
|
|
89
|
+
const pkg = JSON.stringify({ name, version: "0.0.0", type: "module", main: "index.js", agentPurge: true }, null, 1);
|
|
90
|
+
const [begin, end] = mark(name);
|
|
91
|
+
const block = `\n${begin}\n- insert:\n - id: ${name}\n name: ${name}\n${end}\n`;
|
|
92
|
+
const files = [];
|
|
93
|
+
const touched = []; // { dir, patch, before }:中途抛错时按相反顺序回滚
|
|
94
|
+
// 预检:同名目录要么不存在,要么是我们的(package.json 带 agentPurge 标记),否则不碰
|
|
95
|
+
for (const p of DSH_PROFILES) {
|
|
96
|
+
const dir = pluginDir(p, name);
|
|
97
|
+
const meta = join(dir, "package.json");
|
|
98
|
+
if (existsSync(dir) && !isOurs(meta)) throw new Error(`${dir} 已存在且不是 agent-purge 装的,拒绝覆盖`);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
for (const p of DSH_PROFILES) {
|
|
102
|
+
const dir = pluginDir(p, name);
|
|
103
|
+
const patch = patchFile(p);
|
|
104
|
+
const before = existsSync(patch) ? readFileSync(patch, "utf8") : null;
|
|
105
|
+
// 先登记再动手:下面任何一步抛错,这一轮的产物都能被回滚
|
|
106
|
+
touched.push({ dir, patch, before });
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
writeFileSync(join(dir, "package.json"), pkg);
|
|
109
|
+
writeFileSync(join(dir, "index.js"), source);
|
|
110
|
+
files.push(dir);
|
|
111
|
+
const old = before ?? "# agent-purge 新建的 patch 层\n";
|
|
112
|
+
if (old.includes(begin)) continue;
|
|
113
|
+
// `[]` 是合法空数组,后面接 `- insert:` 就是非法 YAML:整份换成我们的段
|
|
114
|
+
// 不存在也算“空”:走 stash 分支才能把“原本没有这个文件”记下来,卸载后才不会留个空壳
|
|
115
|
+
const empty = before === null || /^\s*(\[\s*\]\s*)?$/.test(old);
|
|
116
|
+
const next = empty ? block.slice(1) : old + block;
|
|
117
|
+
if (empty) {
|
|
118
|
+
// 整份替换会把原文端掉:原文 + 我们写出的成品都存下,卸载时对得上才整份还原
|
|
119
|
+
// (对不上 = 期间被人改过,那就只剔自己那段)
|
|
120
|
+
writeFileSync(join(dir, ".patch.before"), JSON.stringify({ before, after: next }));
|
|
121
|
+
}
|
|
122
|
+
writeFileSync(patch, next);
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// 回滚:撤销本次已写的文件,宁可完全没装也不留半套
|
|
126
|
+
for (const { dir, patch, before } of touched.reverse()) {
|
|
127
|
+
rmSync(dir, { recursive: true, force: true });
|
|
128
|
+
if (before === null) rmSync(patch, { force: true });
|
|
129
|
+
else writeFileSync(patch, before);
|
|
130
|
+
}
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
return { dirs: files, profiles: DSH_PROFILES };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 只撤自己那一段:目录 + patch marker,别的配置原样。 */
|
|
137
|
+
export function removePlugin(name) {
|
|
138
|
+
const [begin, end] = mark(name);
|
|
139
|
+
// 删 install 写入的那一段:前导 \n 可有可无(整份替换的形态没有),尾随 \n 一并吃掉
|
|
140
|
+
const re = new RegExp(`(?:^|\\n)${begin}[\\s\\S]*?${end}\\n?`, "g");
|
|
141
|
+
for (const p of DSH_PROFILES) {
|
|
142
|
+
const dir = pluginDir(p, name);
|
|
143
|
+
const meta = join(dir, "package.json");
|
|
144
|
+
// 只删自己装的;同名但不是我们的目录留着(可能被用户或其他包占了)
|
|
145
|
+
const ours = isOurs(meta);
|
|
146
|
+
const stashFile = join(dir, ".patch.before");
|
|
147
|
+
// 当初整份替换过的:记录躲在插件目录里,先读出来,不然删目录就丢了
|
|
148
|
+
let stash = null;
|
|
149
|
+
if (ours && existsSync(stashFile)) {
|
|
150
|
+
try {
|
|
151
|
+
stash = JSON.parse(readFileSync(stashFile, "utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
stash = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (ours) rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
const patch = patchFile(p);
|
|
158
|
+
if (!existsSync(patch)) continue;
|
|
159
|
+
const text = readFileSync(patch, "utf8");
|
|
160
|
+
// 我们整份写的、且此后没人动过 → 整份还原(原本没这个文件就删掉它)
|
|
161
|
+
if (stash && text === stash.after) {
|
|
162
|
+
if (stash.before === null) rmSync(patch, { force: true });
|
|
163
|
+
else writeFileSync(patch, stash.before);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (text.includes(begin)) writeFileSync(patch, text.replace(re, ""));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** patch 里还挂着这个名字吗(status 用;不许在有其他 insert 时谎报干净)。 */
|
|
171
|
+
export function hasPlugin(name) {
|
|
172
|
+
const [begin] = mark(name);
|
|
173
|
+
return DSH_PROFILES.every((p) => {
|
|
174
|
+
const patch = patchFile(p);
|
|
175
|
+
return existsSync(join(pluginDir(p, name), "index.js")) && existsSync(patch) && readFileSync(patch, "utf8").includes(begin);
|
|
176
|
+
});
|
|
177
|
+
}
|
package/lib/persist.mjs
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// 不动二进制——这里只做可逆的配置改动。
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { RESIDENT as DSH_RESIDENT, SESSION as DSH_SESSION, addPlugin, hasPlugin, pluginDir, residentSource, removePlugin } from "./dsh.mjs";
|
|
6
7
|
|
|
7
8
|
const BEGIN = "# >>> agent-purge";
|
|
8
9
|
const END = "# <<< agent-purge";
|
|
9
10
|
const home = process.env.USERPROFILE || process.env.HOME;
|
|
11
|
+
// opencode 认 XDG_CONFIG_HOME(lib/detect.mjs 也是这么探测的):两处必须同一个落点,否则装了白装
|
|
12
|
+
export const opencodePlugins = () => join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "opencode", "plugins");
|
|
10
13
|
|
|
11
14
|
export const targets = {
|
|
12
15
|
pi: {
|
|
@@ -67,8 +70,10 @@ export const targets = {
|
|
|
67
70
|
},
|
|
68
71
|
opencode: {
|
|
69
72
|
// 写入用全局单一位置;读取/删除要扫全部历史位置,否则老版本留下的项目级插件会继续注入
|
|
70
|
-
file: () => join(
|
|
73
|
+
file: () => join(opencodePlugins(), "agent-purge.ts"),
|
|
71
74
|
files: () => [
|
|
75
|
+
join(opencodePlugins(), "agent-purge.ts"),
|
|
76
|
+
// 旧落点:设了 XDG 的机器上,老版本写在 home/.config 的那份也得能被扫到并清掉
|
|
72
77
|
join(home, ".config", "opencode", "plugins", "agent-purge.ts"),
|
|
73
78
|
join(home, ".config", "opencode", "plugin", "agent-purge.ts"),
|
|
74
79
|
join(process.cwd(), ".opencode", "plugin", "agent-purge.ts"),
|
|
@@ -99,6 +104,62 @@ export const AgentPurge = async () => ({
|
|
|
99
104
|
else output.system.splice(0, output.system.length, c.body);
|
|
100
105
|
},
|
|
101
106
|
});
|
|
107
|
+
`,
|
|
108
|
+
},
|
|
109
|
+
dsh: {
|
|
110
|
+
// cordis 插件挂 systemPrompt 层;三个 profile 各一份(desktop 由 Electron 独占,装完要重启一次)
|
|
111
|
+
file: () => join(pluginDir("desktop"), "index.js"),
|
|
112
|
+
kind: "file",
|
|
113
|
+
layer: "sys",
|
|
114
|
+
note: "cordis 插件包;运行时读 state.json,off 即时生效;desktop 需重启一次",
|
|
115
|
+
probe: () => hasPlugin(DSH_RESIDENT),
|
|
116
|
+
install: ({ root, active }) => {
|
|
117
|
+
const r = addPlugin(DSH_RESIDENT, residentSource(root, active));
|
|
118
|
+
return { file: join(pluginDir("desktop"), "index.js"), note: `三个 profile:${r.profiles.join("/")};desktop 重启 DeepSeek Harness 生效` };
|
|
119
|
+
},
|
|
120
|
+
uninstall: () => {
|
|
121
|
+
removePlugin(DSH_RESIDENT);
|
|
122
|
+
// 一次性插件被 SIGKILL 时不会自己清,一并扫掉
|
|
123
|
+
removePlugin(DSH_SESSION);
|
|
124
|
+
return { file: join(pluginDir("desktop"), "index.js") };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
opencode2: {
|
|
128
|
+
// v2 新版插件 API(V1 的 experimental.chat.system.transform 在 2.x 已废),与 V1 插件文件物理分开
|
|
129
|
+
file: () => join(opencodePlugins(), "agent-purge-v2.ts"),
|
|
130
|
+
files: () => [join(opencodePlugins(), "agent-purge-v2.ts"), join(opencodePlugins(), "agent-purge-v2-session.ts")],
|
|
131
|
+
kind: "file",
|
|
132
|
+
layer: "sys",
|
|
133
|
+
note: "v2 插件 ctx.session.hook(\"context\");运行时读 state.json,热切",
|
|
134
|
+
body: (active, root) => `import { readFileSync } from "node:fs";
|
|
135
|
+
// agent-purge 托管文件:删除即可取消
|
|
136
|
+
const STATE = ${JSON.stringify(join(root, "state.json").replace(/\\/g, "/"))};
|
|
137
|
+
const ACTIVE = ${JSON.stringify(String(active).replace(/\\/g, "/"))};
|
|
138
|
+
function cfg() {
|
|
139
|
+
try {
|
|
140
|
+
const st = JSON.parse(readFileSync(STATE, "utf8"));
|
|
141
|
+
// 磁盘卸载后服务里已加载的回调还在:armed 没了就当成已关闭
|
|
142
|
+
const armed = st && st.armed && st.armed.opencode2;
|
|
143
|
+
if (!Array.isArray(armed) || !armed.includes("install")) return null;
|
|
144
|
+
const mode = (st && st.default && st.default.mode) || "replace";
|
|
145
|
+
if (mode === "off") return null;
|
|
146
|
+
const body = readFileSync(ACTIVE, "utf8").trim();
|
|
147
|
+
return body ? { mode, body } : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export default {
|
|
153
|
+
id: "agent-purge",
|
|
154
|
+
async setup(ctx) {
|
|
155
|
+
await ctx.session.hook("context", (event) => {
|
|
156
|
+
const c = cfg();
|
|
157
|
+
if (!c) return;
|
|
158
|
+
if (c.mode === "append") event.system.push({ type: "text", text: c.body });
|
|
159
|
+
else event.system.splice(0, event.system.length, { type: "text", text: c.body });
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
102
163
|
`,
|
|
103
164
|
},
|
|
104
165
|
};
|
|
@@ -106,6 +167,8 @@ export const AgentPurge = async () => ({
|
|
|
106
167
|
export function status(_root) {
|
|
107
168
|
return Object.entries(targets).map(([name, t]) => {
|
|
108
169
|
const file = t.file();
|
|
170
|
+
// 自定义探针(多文件目标用),优先于默认的存在性判断
|
|
171
|
+
if (t.probe) return { name, installed: t.probe(), file };
|
|
109
172
|
const candidates = t.files ? t.files() : [file];
|
|
110
173
|
const present = candidates.filter((f) => existsSync(f));
|
|
111
174
|
if (!present.length) return { name, installed: false, file };
|
|
@@ -121,6 +184,15 @@ export function status(_root) {
|
|
|
121
184
|
export function install(name, { root, active }) {
|
|
122
185
|
const t = targets[name];
|
|
123
186
|
if (!t) return { error: `${name} 没有持久化后端(见 notes:grok/omp 只有 CLI 开关,claude/zcode/antigravity 要打补丁)` };
|
|
187
|
+
// 多文件/自定义布局的 harness 自带安装函数(dsh 三个 profile、opencode2 v2 插件)
|
|
188
|
+
// 它们的失败信号是异常(同名外来目录拒绝覆盖等),这里转成调用方约好的 {error}
|
|
189
|
+
if (t.install) {
|
|
190
|
+
try {
|
|
191
|
+
return { ...t.install({ root, active }), action: "custom" };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { error: e.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
124
196
|
const file = t.file();
|
|
125
197
|
mkdirSync(dirname(file), { recursive: true });
|
|
126
198
|
if (t.kind === "file") {
|
|
@@ -136,6 +208,13 @@ export function install(name, { root, active }) {
|
|
|
136
208
|
export function uninstall(name) {
|
|
137
209
|
const t = targets[name];
|
|
138
210
|
if (!t) return { error: `${name} 没有持久化后端` };
|
|
211
|
+
if (t.uninstall) {
|
|
212
|
+
try {
|
|
213
|
+
return { ...t.uninstall(), action: "custom" };
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return { error: e.message };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
139
218
|
if (t.kind === "file") {
|
|
140
219
|
// 清掉所有历史位置,保证“关掉”之后没有任何残留插件还在注入
|
|
141
220
|
const candidates = t.files ? t.files() : [t.file()];
|