Files
dsh-purge/lib/index.js
T
叫我小杨同学 d6c97a49ed fix(desktop): PowerShell 立即 kill Electron 消除 #64 crash dialog 根因
- ASAR_SWAP_PS1 不再等 WaitPid 优雅退出,改为立即 Stop-HarnessInstall;
  Electron main 和 host 一起被 kill,UI 来不及收 host 退出事件,
  main.js:3725 的"host stopped"对话框根本没机会显示。
  PowerShell 是 detached+stdio:ignore 独立进程,杀 Electron 不影响自己。
- scheduleAsarSwapRestart 接 ctx fallback,启动期自愈路径也能解析
  desktopRuntime 走 requestRestart 做双保险。
- gracefulExit timeout 从 4500/3000 缩到 2000/1500ms:PS 杀得快,
  不需要那么长的 handoff 窗口兜底。
- core.restartSealedDesktopIfNeeded 透传 input.ctx 给桌面层两条路径。
- index.js:scheduleApplyRestart / handlePurgeCommand 全链路加 ctx 参数,
  commands.register handler 把 effect 作用域 ctx 交到最深处的
  restartSealedDesktopIfNeeded 和 scheduleRestart。
- identity.js:isHardcodedExecRule 长度闸 800→1500,用户 rules 文件偶尔
  引用写死 preamble + 本体 >800 字节时不再被误剥。
2026-10-09 14:41:44 +08:00

2292 lines
91 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import { fsFor } from "./extract-asar.js";
import * as core from "./core.js";
import * as hostctl from "./host.js";
import * as rules from "./rules.js";
import * as skills from "./skills.js";
import * as updater from "./update.js";
import * as reapply from "./reapply.js";
import * as officialUpdate from "./official-update.js";
import {
GLOBAL_PROMPT_ORDER,
adaptInjectForMinimalPreset,
isMnemonPluginMessage,
isRedteamSession,
installAssembleGuard,
modePromptText,
rewritePromptAssembly,
} from "./identity.js";
import { capabilityProtocolText, CAPABILITY_SECTION } from "./capability-protocol.js";
import { installSubagentRoleCapture, resolveSubagentRoleText } from "./redteam/role-section.js";
import * as rewind from "./rewind.js";
import * as continueRetry from "./continue-retry.js";
import * as uninstaller from "./uninstall.js";
import { readDrillGrant, writeDrillGrant } from "./drill-grant.js";
import { installRegionGate, readAllowText, writeAllowText } from "./net-scope.js";
import * as refusalRecover from "./refusal-recover.js";
import {
applyShouldRestart,
isInactiveContextError,
settleDelayMs,
settleMayReapply,
settleMayRestart,
watchContextLife,
} from "./settle-policy.js";
export const name = "dsh-purge";
export const inject = ["systemPrompt", "tools"];
let bootFullQuit = false;
const DEFAULTS = {
enabled: true,
autoApplyOnStart: true,
autoUpdateOnStart: false,
autoRevertOnMissing: false,
injectOnce: false,
stripMnemon: true,
postPrompt: "",
postPromptOrder: 5100,
verbose: false,
autoRetry: true,
retryMax: 3,
autoContinue: true,
continueMax: 3,
continueText: "继续",
refusalRecover: true,
refusalRecoverMax: 2,
refusalRecoverText: "",
// Codex 风格:教模型主动发现 skill / MCP;不改官方注册与工具面。
capabilityProtocol: true,
hooksDenyBypass: true,
};
const log = (config, ...args) => {
if (config?.verbose) console.log("[dsh-purge]", ...args);
};
let webTreeReady = false;
function watchWebTreeReady(ctx) {
// 必须挂在 connection 上再 await,与官方打印 URL 同一时刻。
// apply() 里立刻 await 会在后续入口尚未入树时提前结束,session.list 仍是空的。
// 入口复位:热重载 / disposed 后再 apply 会复用上一次 ready=true,第二次 inject 之前跑的
// /status 把旧标志当准,这里强制重置。
webTreeReady = false;
const mark = () => {
webTreeReady = true;
};
if (typeof ctx?.inject !== "function") {
mark();
return;
}
try {
ctx.inject(["connection"], (host) => {
const settled = host.get?.("loader")?.await?.();
if (!settled || typeof settled.then !== "function") {
mark();
return;
}
settled.then(() => {
if (host.get?.("webServer") !== undefined && host.get?.("connection") !== undefined) mark();
}, mark);
});
} catch {
mark();
}
}
function fsExists(fp) {
const list = Array.isArray(fp) ? fp : [fp];
return list.some((p) => {
if (!p || typeof p !== "string") return false;
try {
return fsFor(p).existsSync(p);
} catch {
return false;
}
});
}
function line(title, value, ok = null) {
const mark = ok === null ? "·" : ok ? "✓" : "✗";
return `${mark} ${title}: ${value}`;
}
function renderStatus(state) {
const out = [];
out.push("dsh-purge 状态 / Status");
out.push(line("宿主 / surface", state.surface || hostctl.currentSurface()));
out.push(line("DSH_HOME", state.dsh_home));
if (state.desktop_install) out.push(line("桌面安装 / desktop", state.desktop_install));
if (state.ai_base) {
out.push(line("插件根 / plugin root", state.ai_base));
for (const [key, fp] of Object.entries(state.files)) {
const list = Array.isArray(fp) ? fp : [fp];
const shown = list.length <= 1 ? String(list[0] || "") : `${list[0]} (+${list.length - 1})`;
out.push(` ${fsExists(fp) ? "✓" : "✗"} ${key.padEnd(22)} ${shown}`);
}
} else {
out.push(line("插件根 / plugin root", core.missingAiBaseMessage(), false));
}
out.push(line("shim 目录", state.shim_dir || "未定位 / not found", !!state.shim_dir));
if (state.desktop_runtimes?.length) {
out.push(line("Desktop runtime", `${state.desktop_runtimes.length} sealed (shim OK; bak external)`, true));
for (const d of state.desktop_runtimes) out.push(` · ${d}`);
}
out.push(line("备份 / backup", state.has_backup ? "有 / yes" : "无 / no", state.has_backup));
out.push(line("注入文件 / inject", state.override_path));
out.push(line("注入状态 / override", state.override_status));
if (state.asar_still_sealed) {
out.push(line("app.asar", "还在。补丁写在解开目录,当前进程读不到", false));
}
out.push("");
out.push(`补丁 / patches: ${state.patches_applied}/${state.patches_total || 0} applied, ${state.patches_pending} pending`);
for (const p of core.ALL_PATCHES) {
const s = state.patch_status[p.id] || "n/a";
const mark = s === "applied" ? "✓" : s === "pending" ? "✗" : "·";
out.push(` ${mark} [#${String(p.id).padEnd(2)}] ${p.name.padEnd(30)} ${p.layer}/${p.layer_en} — ${s}`);
}
out.push("");
out.push(`shim: dsh.cmd=${state.shim_cmd} dsh.ps1=${state.shim_ps1} dsh=${state.shim_bin}`);
return out.join("\n");
}
function injectAllowedNow(config) {
return config.enabled !== false;
}
/** 提示词框自改 > prompt-inject.md > 加密默认(见 rules.resolveInjectText)。 */
// mtime 节流:400ms setInterval + 每 pre-step + 每 preset-selected + 每 readInjectFallback
// 的高频触发下,disk sync + 3 次 hashOverride + 2 次模块级赋值一轮能点 20 次。
// 短窗复用上次结果:home 没换 + 文件 mtime 没变 + 距上次 < 200ms,直接返回缓存。
let hungCache = { home: "", mtime: 0, at: 0, resolved: null };
const HUNG_CACHE_MS = 200;
function statInjectMtimeMs(home) {
if (!home) return 0;
try {
return Math.floor(fs.statSync(core.findOverrideFile(home)).mtimeMs);
} catch { return 0; }
}
function refreshHungOperator(dshHome = core.findDshHome()) {
const home = core.pinDshHome() || dshHome || core.findDshHome();
const now = Date.now();
const mtimeBefore = statInjectMtimeMs(home);
if (
hungCache.resolved
&& hungCache.home === home
&& hungCache.mtime === mtimeBefore
&& now - hungCache.at < HUNG_CACHE_MS
) {
return hungCache.resolved;
}
try { core.syncPromptInjectOnDiskSync(home); } catch { /* 同步失败仍按 resolve 走 */ }
const resolved = rules.resolveInjectText(home);
// 兜底顺序:resolved.text > 加密默认 > 永不空串(避免二轮 fallbackInject 为空走 purge[] 残留)。
const fallback = core.normalizeOverride(core.defaultOverrideText()) || "";
const text = resolved.text || fallback;
if (text) {
core.hangOperatorPrompt(text);
// 与点「保存」同一钉钉:不点保存,组装也要读到这一份全文。
core.rememberCommittedOverride(home, text);
}
const out = { ...resolved, text: text || resolved.text };
// 用 sync 之后的 mtime 存 cache,下次高频轮询(文件没被外部动)直接命中。
hungCache = { home, mtime: statInjectMtimeMs(home), at: now, resolved: out };
return out;
}
function installHungPromptGlobal() {
const fn = () => {
try {
return refreshHungOperator(core.findDshHome()).text || "";
} catch {
return core.hungOperatorPrompt() || "";
}
};
globalThis.__dshPurgeHungPrompt = fn;
return fn;
}
function injectSnapshot(dshHome = core.findDshHome()) {
const resolved = rules.resolveInjectText(dshHome);
return {
injectSource: resolved.source,
injectRuleId: resolved.ruleId,
needPrompt: resolved.source === "none",
};
}
/** 框里正在显示的字优先;框空则 resolveInjectText(含 prompt-inject.md)。 */
function injectSnapshotFromBox(dshHome, boxText) {
if (typeof boxText === "string" && core.normalizeOverride(boxText)) {
return { injectSource: "prompt", injectRuleId: null, needPrompt: false };
}
const resolved = rules.resolveInjectText(dshHome);
if (resolved.text) {
return { injectSource: resolved.source, injectRuleId: null, needPrompt: false };
}
return { injectSource: "none", injectRuleId: null, needPrompt: true };
}
const NEED_PROMPT_ERROR = "提示词和规则集都是空的,必须先添加提示词";
function rejectIfNeedPrompt(dshHome, boxText) {
const snap = injectSnapshotFromBox(dshHome, boxText);
if (!snap.needPrompt) return null;
return { ...snap, error: NEED_PROMPT_ERROR };
}
async function ensureRestartReady() {
return core.ensureHostClean();
}
/** 提示词服务自己的 ctx 多半没有 scope,段会进全局层。插件若挂在某个 scope 上,预设代理的父链看不到那一层。 */
function addPromptSection(ctx, section) {
const hosts = [];
const push = (host) => {
if (host && !hosts.includes(host)) hosts.push(host);
};
try { push(ctx?.systemPrompt?.ctx); } catch { /* 代理未注入时点属性会抛 */ }
push(ctx);
const disposers = [];
let registered = false;
let lastError = null;
for (const host of hosts) {
try {
const d = host.systemPrompt.section(section);
registered = true;
if (typeof d === "function") disposers.push(d);
} catch (error) {
const msg = String(error && error.message || error);
if (msg.includes("already registered")) registered = true;
else lastError = error;
}
}
if (!registered && lastError) throw lastError;
// 让 ctx.effect 在 reload / dispose 时把 section 一并撤掉;host.systemPrompt.section
// 当前多半不返回 disposer,这段只对返回 function 的实现生效,不改变旧行为。
if (disposers.length && typeof ctx?.effect === "function") {
try {
ctx.effect(() => () => {
for (const d of disposers) {
try { d(); } catch { /* ignore */ }
}
});
} catch { /* 死 ctx */ }
}
}
export function registerPromptSections(ctx, config, _injectFile) {
addPromptSection(ctx, {
name: "dsh-purge",
order: GLOBAL_PROMPT_ORDER + 1,
interpolate: false,
text: (context) => {
if (!injectAllowedNow(config)) return "";
// 标准、极简都注入同一份正文。极简只改工具句,不决定注不注入。
return adaptInjectForMinimalPreset(readInjectFallback(context), context);
},
});
if (config.capabilityProtocol !== false) {
addPromptSection(ctx, {
name: CAPABILITY_SECTION,
order: GLOBAL_PROMPT_ORDER,
interpolate: false,
text: (context) => {
if (!injectAllowedNow(config)) return "";
const redteam = isRedteamSession(context);
return capabilityProtocolText({
redteam,
redteamChild: redteam && context?.agent?.session?.header?.origin === "subagent",
enabled: true,
});
},
});
}
const postPrompt = typeof config.postPrompt === "string" ? config.postPrompt.trim() : "";
if (!postPrompt) return;
if (!Number.isFinite(config.postPromptOrder) || config.postPromptOrder <= GLOBAL_PROMPT_ORDER) {
// 旧版 throw 会把整个 apply 炸掉;postPrompt 只是附加段,降级 skip 不影响核心注入。
console.warn(
`[dsh-purge] postPromptOrder (${config.postPromptOrder}) 需要是 > ${GLOBAL_PROMPT_ORDER} 的有限数;跳过 postPrompt 段`,
);
return;
}
addPromptSection(ctx, {
name: "dsh-purge:post",
order: config.postPromptOrder,
text: () => (injectAllowedNow(config) ? postPrompt : ""),
});
}
function readInjectFallback(context) {
let home = "";
try { home = core.findDshHome(); } catch { home = ""; }
const raw = refreshHungOperator(home).text || rules.ensureOperatorBody(home);
if (!raw) return "";
let redteam = false;
try { redteam = isRedteamSession(context); } catch { redteam = false; }
return modePromptText(raw, redteam);
}
function installMnemonMessageStrip(ctx, config = {}) {
if (typeof ctx.on !== "function") return;
if (ctx.__dshPurgeMnemonStrip) return;
ctx.__dshPurgeMnemonStrip = true;
const strip = config.stripMnemon !== false;
const wrap = async (_payload, next) => {
const decision = await next();
if (!decision) return decision;
// agent-loop 会读 decision.messages.length;缺数组时第二轮直接 gateway/internal 炸掉。
const incoming = Array.isArray(decision.messages) ? decision.messages : [];
const messages = strip ? incoming.filter((message) => !isMnemonPluginMessage(message)) : incoming;
if (messages.length === incoming.length && Array.isArray(decision.messages)) return decision;
return { ...decision, messages };
};
// 旧版 late() + queueMicrotask(late) + setTimeout(late, 1500) 三连都成功时,
// 同一个 wrap 会在 agent/pre-step 瀑布上叠 3 层;中间插件返回的 decision 每轮被过滤 3 次。
// 这里用 mounted 闸:第一次成功挂上就不再重试,拿回 disposer 走 ctx.effect 回收。
let mounted = false;
let dispose = null;
const late = () => {
if (mounted) return;
try {
const d = ctx.on("agent/pre-step", wrap, { global: true, prepend: true });
mounted = true;
if (typeof d === "function") dispose = d;
} catch { /* 下一次重试 */ }
};
late();
if (!mounted) queueMicrotask(late);
let retryTimer = null;
if (!mounted) {
retryTimer = setTimeout(late, 1500);
if (typeof retryTimer.unref === "function") retryTimer.unref();
}
try {
ctx.effect(() => () => {
if (retryTimer) clearTimeout(retryTimer);
try { dispose?.(); } catch { /* ignore */ }
});
} catch { /* 死 ctx 时无 effect,靠下一次 apply 覆盖 */ }
}
function rewriteLiveAssembly(config, assembled, context) {
const enabled = config.enabled !== false;
const redteam = enabled && isRedteamSession(context);
const inject = enabled ? readInjectFallback(context) : "";
return rewritePromptAssembly(assembled, {
fallbackInject: inject,
dropMnemon: config.stripMnemon !== false,
dropPurgeAfterFold: false,
capabilityProtocol: config.capabilityProtocol !== false,
context,
roleText: redteam ? resolveSubagentRoleText(context) : "",
});
}
/** 注册表若已经按旧文件算过 rev,立刻 rebuilt,让页面拿到的地址和缓存一致(#63)。 */
function publishClientBundleRevs(ctx, packageNames) {
if (!Array.isArray(packageNames) || packageNames.length === 0) return;
let mods = null;
try {
mods = ctx?.get?.("clientModules", false);
} catch {
mods = null;
}
if (!mods || typeof mods.rebuilt !== "function") return;
for (const id of packageNames) {
try { mods.rebuilt(id); } catch { /* 这一包还没进注册表,下次快照会读到新文件 */ }
}
}
/** Cordis 的 ctx 是 Proxy:未声明 inject 时点属性会抛 "cannot get property ... without inject",所以两段都要包 try。 */
function permissionPresetsOf(ctx) {
try {
const direct = ctx?.permissionPresets;
if (direct && typeof direct.set === "function") return direct;
} catch {
/* 未 inject 这项服务时,Cordis 代理会抛,不能让新会话的全开权限钉死失败。 */
}
try {
const svc = ctx?.get?.("permissionPresets");
if (svc && typeof svc.set === "function") return svc;
} catch {
/* 没有这项服务。 */
}
return null;
}
function shouldKeepFreshFullAccess(session, svc) {
if (session?.header?.origin === "subagent") return false;
const state = typeof svc.permissionState === "function" ? svc.permissionState(session) : null;
const preset = state?.preset;
if (preset === "danger-full-access" || preset === "auto" || preset === "custom") return false;
const events = typeof session?.snapshotEvents === "function" ? session.snapshotEvents() : [];
const pins = Array.isArray(events) ? events.filter((event) => event?.type === "permission/preset") : [];
if (pins.length > 1) return false;
return preset == null || preset === "workspace-write" || preset === "read-only";
}
function upgradeFreshSessionAccess(ctx, session) {
try {
const svc = permissionPresetsOf(ctx);
if (!svc || !shouldKeepFreshFullAccess(session, svc)) return;
svc.set(session, "danger-full-access");
} catch (error) {
console.warn("[dsh-purge] full-access pin:", error instanceof Error ? error.message : String(error));
}
}
/** 空会话界面按部署默认显示完全权限;建会话时若钉成 workspace-write,发消息后芯片会掉下去。 */
function installFreshFullAccess(ctx) {
if (typeof ctx.on !== "function" || ctx.__dshPurgeFullPin) return;
ctx.__dshPurgeFullPin = true;
try {
ctx.on("session/created", (session) => {
queueMicrotask(() => upgradeFreshSessionAccess(ctx, session));
});
} catch {}
}
function installRewindDeriveFilter(ctx) {
if (!ctx || ctx.__dshPurgeRewindDerive) return;
ctx.__dshPurgeRewindDerive = true;
const wrapOne = (session) => {
try { rewind.wrapSessionDeriveMessages(session); } catch { /* ignore */ }
};
try {
const store = ctx.get?.("sessions");
const all = typeof store?.all === "function" ? store.all()
: typeof store?.list === "function" ? store.list()
: [];
for (const session of all || []) wrapOne(session);
} catch { /* ignore */ }
try {
ctx.on("session/created", (session) => wrapOne(session));
} catch { /* ignore */ }
}
function installIdentityOverride(ctx, config = {}) {
if (typeof ctx.on !== "function") return;
installSubagentRoleCapture(ctx);
// installAssembleGuard 幂等:slot.rewrite 已挂的 prompt 对象只刷新 rewriteFn,
// 不再 return 新 dispose。原实现每 400ms + 每 pre-step push 一个 noop closure 入 Set,
// 2 小时 session 累积上万条。改为:只 track 首次真正挂上的 disposer。
const wrapDisposers = new Set();
const wrappedPrompts = new WeakSet();
const trackDisposer = (d, prompt) => {
if (typeof d !== "function") return d;
if (prompt) {
if (wrappedPrompts.has(prompt)) return d;
wrappedPrompts.add(prompt);
}
wrapDisposers.add(d);
return d;
};
const rewriteFn = (assembled, context) => rewriteLiveAssembly(config, assembled, context);
const wrapAssemble = () => {
try {
const prompt = ctx.systemPrompt;
return trackDisposer(installAssembleGuard(prompt, rewriteFn), prompt);
} catch {
return false;
}
};
if (!ctx.__dshPurgeAssembleHook) {
ctx.__dshPurgeAssembleHook = true;
// cordis 不同版本的 waterfall 可能是 (assembly, context) 或 (assembly, context, next):
// 旧写法把 `next` 当函数直接取第 3 位,老宿主就拿到 context 当 next,await next() 立刻抛。
ctx.on("system-prompt/assemble", async (...args) => {
const [assembly, context] = args;
const next = args.length >= 3 ? args[args.length - 1] : null;
let assembled = assembly;
if (typeof next === "function") {
try {
assembled = await next();
} catch (err) {
// 下游 waterfall 任一环节抛就会把整条 assemble 弄死,UI 卡住发不出去。
// 吞掉改用上游装配兜底,inject 保底能挂上。
try { console.warn("[dsh-purge] system-prompt/assemble waterfall rejected:", err?.message || err); } catch {}
assembled = assembly;
}
}
try {
return rewriteLiveAssembly(config, assembled, context);
} catch (err) {
try { console.warn("[dsh-purge] rewriteLiveAssembly failed:", err?.message || err); } catch {}
return assembled;
}
}, { global: true, prepend: true });
}
// 切换预设会换掉 assemble。pre-step 瀑布在 assemble 之后才跑,第一次发送包不上。
// 宿主在 assemble 之前调用这个函数,用的就是当前模式自己的 systemPrompt。
const prepareAssemble = (loopCtx) => {
try { refreshHungOperator(core.findDshHome()); } catch { /* 主目录还没有提示词 */ }
// loopCtx 可能是宿主的 Proxy,访问 .systemPrompt 自身就会抛(代理层尚未注入服务)。
let prompt = null;
try { prompt = loopCtx && loopCtx.systemPrompt; } catch { prompt = null; }
if (prompt) {
try {
trackDisposer(installAssembleGuard(prompt, rewriteFn), prompt);
} catch { /* 这一层代理包不上时,下面再包插件自己的 */ }
}
wrapAssemble();
return prompt || null;
};
globalThis.__dshPurgePrepareAssemble = prepareAssemble;
const hungFn = installHungPromptGlobal();
// complete:true 会在 waterfall 之后盖掉 sections;包住 assemble 返回值,每轮都补回注入。
wrapAssemble();
// guard 需要在每次 cordis 换 systemPrompt 代理时重挂(session 内切预设、subagent fork、
// live-mount)。原本 20 秒后 clearInterval,之后换代理就包不上 → 第二轮 inject 走原生
// assemble → 宿主按 token 预算裁剪 → 用户看到"只截取了部分"。installAssembleGuard 幂等
// (wrappedPrompts WeakSet + 对象 identity 校验),400ms 持续跑到 ctx dispose 的 CPU 代价可忽略。
const timer = setInterval(wrapAssemble, 400);
if (typeof timer.unref === "function") timer.unref();
// 下面三个 ctx.on 必须加 flag:installIdentityOverride 若被同一 ctx 二次 apply
// (dispose 后重 mount / 热重载),listener 叠加会让 inject 跑 N 遍,也让 wrap 挂 N 层。
if (!ctx.__dshPurgeSessionHook) {
ctx.__dshPurgeSessionHook = true;
try { ctx.on("session/created", wrapAssemble); } catch { /* ignore */ }
}
if (!ctx.__dshPurgePresetHook) {
ctx.__dshPurgePresetHook = true;
try {
ctx.on("agent-preset/selected", () => {
try { refreshHungOperator(core.findDshHome()); } catch { /* 主目录还没有提示词 */ }
wrapAssemble();
}, { global: true });
} catch { /* ignore */ }
}
if (!ctx.__dshPurgePreStepHook) {
ctx.__dshPurgePreStepHook = true;
try {
ctx.on("agent/pre-step", (payload, next) => {
wrapAssemble();
return typeof next === "function" ? next() : payload;
}, { global: true, prepend: true });
} catch { /* ignore */ }
}
// 热重载/卸载时统一回收:轮询 timer、guard 代理层、暴露给 agent loop 的全局钩子,
// 都在同一个 effect 里扫掉;避免下次 apply 再绑一层造成多次写入。
try {
ctx.effect(() => () => {
clearInterval(timer);
for (const d of wrapDisposers) {
try { d(); } catch { /* ignore */ }
}
wrapDisposers.clear();
// 清 flag,下次 apply 允许重新挂 listener;不然 ctx 复用时沉默无响应。
delete ctx.__dshPurgeSessionHook;
delete ctx.__dshPurgePresetHook;
delete ctx.__dshPurgePreStepHook;
if (globalThis.__dshPurgePrepareAssemble === prepareAssemble) {
delete globalThis.__dshPurgePrepareAssemble;
}
// identity 校验再删,免得热重载抢到新实例又被旧 cleanup 挖掉。
if (globalThis.__dshPurgeHungPrompt === hungFn) {
delete globalThis.__dshPurgeHungPrompt;
}
});
} catch { /* 无 effect 时靠 unref + 下次 apply 自覆盖 */ }
}
function shimNeedsPatch(state) {
if (hostctl.currentSurface() === "desktop") return false;
return (
state.shim_cmd === "original" ||
state.shim_ps1 === "original" ||
state.shim_bin === "original"
);
}
function currentSettleScope() {
let sealed = false;
try { sealed = core.desktopAsarStillSealed(); } catch { /* 非桌面或还没定位安装目录 */ }
return { surface: hostctl.currentSurface(), sealed };
}
async function settleInstalledPatches(config, ctx, life) {
if (life && !life.alive) return "skip:inactive";
try { core.rememberUnpackedArchive(); } catch { /* 没有官方安装目录时不必记归档 */ }
try {
reapply.ensureInstalledRevSeed();
} catch (e) {
log(config, "seed installed-rev failed:", String(e && e.message ? e.message : e));
}
try {
if (reapply.healEmptyAppliedStamp()) log(config, "healed empty applied stamp");
} catch (e) {
log(config, "heal applied stamp failed:", String(e && e.message ? e.message : e));
}
const scope = currentSettleScope();
if (!reapply.patchesMatchInstall()) {
if (!settleMayReapply(scope)) {
log(config, "sealed desktop: skip startup reapply (wait for Apply)");
return "skip:sealed-wait-apply";
}
if (await reapply.hostCleanEnoughToKeep()) {
reapply.markPatchesApplied();
log(config, "host already clean; kept patches and refreshed stamp");
return "already-clean";
}
if (officialUpdate.upgradeMarkerExists(process.execPath)) {
const upgraded = await reapply.reapplyInstalled({ scheduleSwap: false, upgrade: true });
if (life && !life.alive) return "skip:inactive";
if (!upgraded.ok) {
log(config, "official upgrade reapply failed:", upgraded.error || "");
return "reapply-failed";
}
officialUpdate.clearUpgradeMarker(process.execPath);
officialUpdate.clearStaleExtractedApps(process.execPath);
if (!settleMayRestart(currentSettleScope()) || reapply.shouldSkipSettleRestart()) {
if (upgraded.needsFullQuit) bootFullQuit = true;
log(config, "official upgrade reapplied; skip settle restart");
return "reapplied-no-restart";
}
reapply.markSettleRestart();
log(config, "official upgrade reapplied; restarting onto the new build");
// ctx 可能在 reapply.reapplyInstalled 的 await 中被 dispose;重启前再查一次。
if (life && !life.alive) return "skip:inactive";
try {
hostctl.scheduleRestart(null, ctx, { openBrowser: false });
} catch (e) {
log(config, "restart after official upgrade failed:", String(e && e.message ? e.message : e));
return "restart-failed";
}
return "restarting";
}
// 启动自愈只写盘,不 swap asar / 不杀进程,避免官方 Host Fiber._reload 撞 INACTIVE_EFFECT。
const result = await reapply.reapplyInstalled({ scheduleSwap: false });
if (life && !life.alive) return "skip:inactive";
if (!result.ok) {
log(config, "reapply failed:", result.error || "");
return "reapply-failed";
}
// reapply 成功后强制保证非空戳,避免 clean 路径漏写又空转(#59/#60)。
try { reapply.markPatchesApplied(); } catch { /* ignore */ }
if (result.needsFullQuit) {
bootFullQuit = true;
log(config, "reapply wrote patches; official host needs a full quit");
return "full-quit";
}
if (!settleMayRestart(currentSettleScope())) {
log(config, "reapply wrote patches; desktop settle does not restart");
return "reapplied-no-restart";
}
if (reapply.shouldSkipSettleRestart()) {
log(config, "reapply wrote patches; skip settle restart (cooldown)");
return "reapplied-no-restart";
}
// 已对齐则不必再重启;只在 stamp/宿主仍不一致时重启一次。
if (reapply.patchesMatchInstall()) {
log(config, "reapply wrote patches; already aligned, no restart");
return "reapplied";
}
reapply.markSettleRestart();
log(config, "reapply wrote patches; restarting");
// reapplyInstalled 自带 await;重启前再查一次 life,避免对 disposed ctx 发起 scheduleRestart。
if (life && !life.alive) return "skip:inactive";
try {
// web:自愈重启不弹浏览器,关窗后不应再被反复拉起新窗口(#59/#61)。
hostctl.scheduleRestart(null, ctx, { openBrowser: false });
} catch (e) {
log(config, "restart after reapply failed:", String(e && e.message ? e.message : e));
return "restart-failed";
}
return "restarting";
}
const applied = await autoApply(config);
if (config.autoUpdateOnStart === true) {
updater.autoUpdateIfNeeded()
.then((r) => {
// ctx 可能在 autoUpdate 等待网络的过程中被 dispose(热重载 / 卸载)。
// 回调里再碰 ctx.scheduleRestart 会命中 INACTIVE_EFFECT。
if (life && !life.alive) return;
log(config, "auto-update:", JSON.stringify(r));
if (r && r.needsFullQuit) bootFullQuit = true;
if (r && r.restartAfter && settleMayRestart(currentSettleScope())) {
if (life && !life.alive) return;
try { hostctl.scheduleRestart(null, ctx, { openBrowser: false }); } catch (e) {
console.warn("[dsh-purge] auto-update restart failed:", e);
}
}
})
.catch((e) => console.warn("[dsh-purge] auto-update error:", String(e)));
}
return applied;
}
async function autoApply(config) {
try {
core.applyRuntimeEnv();
const sanitized = core.sanitizeDesktopCommandRuntimes();
if (sanitized.length) log(config, "desktop runtime sanitized:", JSON.stringify(sanitized));
core.refreshHooksDenySettings();
if (core.getHooksDenySettings().hooksDenyBypass === false) {
const restored = await core.restoreHookDenyForBases(core.enumeratePatchAiBases());
if (restored.failed.length) {
log(config, "hooks-deny restore leftover:", restored.failed.join(","));
}
}
const state = await core.gatherState();
if (!state.ai_base) {
log(config, "skip auto-apply: plugin root not found");
return "skip:plugin_root_not_found";
}
try {
const flash = core.silenceCmdFlash(state.ai_base);
log(config, "cmd-flash:", JSON.stringify(flash));
if (!flash.ok) log(config, "cmd-flash incomplete:", flash.entry);
} catch (e) {
log(config, "cmd-flash failed:", String(e));
}
if (state.patches_pending === 0) {
if (config.autoApplyOnStart && shimNeedsPatch(state)) {
await core.patchAllShims();
core.silenceCmdFlash(state.ai_base);
log(config, "auto-apply: patches clean, shim refreshed");
return "already_clean:shim_patched";
}
log(config, "auto-apply: already clean");
return "already_clean";
}
if (!config.autoApplyOnStart) {
log(config, "auto-apply disabled");
return "disabled";
}
const autoMulti = await core.applyPatchesAllHosts(core.ALL_PATCHES, { preserveClientBundles: true });
await core.patchAllShims();
const report = autoMulti.report;
const flash = core.silenceCmdFlash(autoMulti.hosts[0]?.aiBase || state.ai_base);
const applied = report.filter((r) => r.status === "applied").length;
log(config, `auto-apply done: ${applied} applied; cmd-flash=${flash.entry} ok=${flash.ok}`);
return flash.ok ? `applied:${applied}` : `applied:${applied}:cmd_flash_incomplete`;
} catch (e) {
console.warn("[dsh-purge] auto-apply error:", String(e));
return `error:${e}`;
}
}
/** 应用之后的那一次重启。密封包退出再打开,不再干等用户自己退出。 */
function scheduleApplyRestart(shouldRestart, ctx = null) {
if (!shouldRestart) return { restarting: false };
if (core.desktopAsarStillSealed()) {
// 透传 ctx 给核心层,让密封包重启也能走 PowerShell 立即 kill Electron + requestRestart
// 双保险路径,不再弹"host stopped"崩溃对话框 (#64)。
const planned = core.restartSealedDesktopIfNeeded({ ctx });
return {
...(planned || {}),
restarting: Boolean(planned && (planned.restarting || planned.asarSwap)),
};
}
try {
const planned = hostctl.scheduleRestart(null, ctx);
if (planned && (planned.restarting === false || planned.clean === false)) return planned;
return planned || { restarting: true };
} catch (error) {
return { restarting: false, error: String(error && error.message ? error.message : error) };
}
}
async function handlePurgeCommand(rawInput, config, ctx = null) {
const args = (rawInput || "").trim().split(/\s+/).filter(Boolean);
const sub = (args[0] || "status").toLowerCase();
switch (sub) {
case "status":
case "s": {
const state = await core.gatherState();
return { kind: "success", text: renderStatus(state) };
}
case "apply":
case "a": {
core.applyRuntimeEnv();
core.clearDesktopLocateCache();
const home = core.pinDshHome();
const scrubbed = core.sanitizeDesktopCommandRuntimes();
const state = await core.gatherState();
if (home) state.dsh_home = home;
let located;
try {
located = await core.resolveOfficialAiBase(state.ai_base, { forApply: true });
} catch (e) {
return { kind: "error", text: String(e && e.message ? e.message : e) };
}
if (!located.aiBase) return { kind: "error", text: core.missingAiBaseMessage() };
state.ai_base = located.aiBase;
if (located.extracted && located.handoff?.archive) {
core.writeUnpackedAsarStamp(located.handoff.archive);
}
const blocked = rejectIfNeedPrompt(state.dsh_home);
if (blocked) return { kind: "error", text: blocked.error };
const applyMulti = await core.applyPatchesAllHosts(core.ALL_PATCHES, {
preserveClientBundles: false,
forApply: true,
});
const report = applyMulti.report;
const shimResult = await core.patchAllShims();
const flash = core.silenceCmdFlash(applyMulti.hosts[0]?.aiBase || state.ai_base);
const lines = [];
for (const h of applyMulti.hosts) lines.push(` 宿主 / host → ${h.aiBase}`);
lines.push(...report.map((r) => {
const m = r.status === "applied" ? "✓ 已清洗" : r.status === "already" ? "- 已是最新" : r.status === "missing_file" ? "⚠ 文件缺失" : `✗ ${r.status}`;
return ` ${m} patch #${String(r.patch_id).padEnd(2)} ${r.name}`;
}));
lines.push("", "shim:");
for (const [dir, st] of Object.entries(shimResult)) {
lines.push(` · ${dir} → ${typeof st === "object" ? JSON.stringify(st) : st}`);
}
lines.push(` cmd-flash=${flash.entry} phase-1=${flash.phase1} ok=${flash.ok}`);
if (scrubbed.length) {
lines.push("", "Desktop in-bin .bak scrub:");
for (const row of scrubbed) lines.push(` ✓ ${row.dir} → ${row.cleaned.join(", ")}`);
}
lines.push("Desktop: host-commands 密封目录只清理不注入;bak 禁止写进 bin(issue #9)。");
const summary = core.summarizeApplyReport(report);
const hostWritten = core.allPatchHostsWritten(applyMulti.hosts) || core.hostCleanMarkersPresent(state.ai_base);
const hookDenyRestoreFailed = (applyMulti.hookDenyRestoreFailed || []).length > 0;
const complete = summary.failed.length === 0 && flash.ok === true && hostWritten && !hookDenyRestoreFailed;
const promptReady = summary.failed.length > 0 && summary.failed.every((item) => item.status === "pattern_not_found");
const shouldRestart = applyShouldRestart({
complete: complete || (promptReady && hostWritten),
sealed: core.desktopAsarStillSealed(),
appliedCount: summary.applied,
promptReady: promptReady && hostWritten,
});
if (shouldRestart) reapply.markSettleRestart();
const planned = scheduleApplyRestart(shouldRestart, ctx);
if (planned && planned.clean === false) {
lines.push("", "清洗没有写进当前宿主,已取消重启。");
} else if (planned && planned.restarting === false && planned.error) {
lines.push("", String(planned.error));
} else if (shouldRestart && planned && planned.restarting !== false) {
lines.push("", "正在重启。");
} else if (!complete) {
lines.push("", "清洗没有完成,没有重启。");
} else {
lines.push("", "补丁已经在宿主里,不再重启。");
}
return { kind: "success", text: lines.join("\n") };
}
case "revert":
case "r": {
const state = await core.gatherState();
const lines = [];
if (state.ai_base) {
const { reverted, errors } = await core.revertAll(state.ai_base);
lines.push(...reverted.map((p) => ` ✓ 已还原 / Reverted ${p}`));
for (const [p, e] of errors) lines.push(` ⚠ 还原失败 ${p}: ${e}`);
if (reverted.length === 0) lines.push(" - 没有补丁备份可还原 / no patch backup");
}
const shimRevert = await core.revertAllShims();
for (const [dir, st] of Object.entries(shimRevert)) {
lines.push(` shim ${dir}: ${typeof st === "object" ? JSON.stringify(st) : st}`);
}
lines.push("", "回滚完成 / Revert done。重启 dsh 后恢复 / restart to restore.");
lines.push("注: prompt-inject.md 保留(用户文件)/ prompt-inject.md kept (user file)");
return { kind: "success", text: lines.join("\n") };
}
case "uninstall":
case "remove": {
const result = await uninstaller.uninstallPurge();
const lines = [];
if (result.applied) lines.push("已检测到补丁,已还原回原版。");
else lines.push("未检测到已应用补丁,仍会清除插件文件。");
if (result.patches) {
lines.push(` 还原文件 ${result.patches.reverted?.length || 0} 个`);
}
if (result.override?.removed) lines.push(` 已删除 ${result.override.path}`);
if (result.stripped?.length) lines.push(` 已从 profile 移除: ${result.stripped.join(", ")}`);
if (result.errors?.length) {
for (const e of result.errors) lines.push(` ⚠ ${e}`);
}
lines.push("", "请重启 dsh。重启后插件不再加载。");
return { kind: result.ok ? "success" : "error", text: lines.join("\n") };
}
case "edit":
case "e": {
const state = await core.gatherState();
let r = core.editOverride(state.dsh_home);
let created = false;
if (!r.ok && r.needCreate) {
await core.installOverride(state.dsh_home, true);
r = core.editOverride(state.dsh_home);
created = true;
}
// 旧实现:r.ok 为 false 时仍然返回 kind:"success",用户看到"已打开"但进程根本没起来。
if (!r.ok) {
return { kind: "error", text: r.error || "打开编辑器失败" };
}
return {
kind: "success",
text: created
? `已创建默认注入文件并打开编辑器:${r.path}\n重启 dsh 生效。`
: `已打开 ${r.editor} 编辑注入文件:${r.path}\n编辑完成后重启 dsh 生效。`,
};
}
case "help":
case "h":
default:
return {
kind: "success",
text: "dsh-purge 命令:\n" +
" /purge status 显示状态\n" +
" /purge apply 应用全部清洗(提示词+代码+shim+override)\n" +
" /purge revert 回滚还原\n" +
" /purge uninstall 卸载插件(已应用则先还原)\n" +
" /purge edit 编辑注入文件 prompt-inject.md\n" +
" /purge help 显示帮助\n" +
"规则集切换见 /rules(list | use <id> | create | delete | reset)\n" +
"官方 Skill 安装见 /skills(list | import <路径> | create <id> [说明] | delete <id>)",
};
}
}
function renderRulesStatus(st) {
const out = [];
out.push("规则集 / Rule Sets");
out.push(` 存储目录 / store ${st.rules_dir}`);
const targetLine = st.active
? `${st.agents_path} (${st.active_target})`
: "(无激活)";
out.push(` 目标文件 / target ${targetLine} — exists=${st.agents_exists}, synced=${st.agents_synced}`);
out.push("");
if (st.rules.length === 0) {
out.push(" (暂无规则 / no rules — 用 /rules create <id> 或设置页新建)");
}
for (const r of st.rules) {
const isActive = r.id === st.active;
const label = r.name !== r.id ? `${r.name} (${r.id})` : r.id;
out.push(` ${isActive ? "▶" : "·"} ${label}${isActive ? " ★ 当前" : ""} [${r.target}] (${rules.formatSize(r.size)})`);
}
return out.join("\n");
}
async function handleRulesCommand(rawInput) {
const args = (rawInput || "").trim().split(/\s+/).filter(Boolean);
const sub = (args[0] || "list").toLowerCase();
const dshHome = core.findDshHome();
switch (sub) {
case "list":
case "ls":
case "s":
return { kind: "success", text: renderRulesStatus(await rules.rulesStatus(dshHome)) };
case "use":
case "activate":
case "u": {
const id = args[1];
if (!id) return { kind: "error", text: "用法: /rules use <id>" };
try {
const meta = await rules.activateRule(dshHome, id);
refreshHungOperator(dshHome);
return {
kind: "success",
text: `✓ 已激活规则 ${meta.name}(${id})→ 写入 ${rules.targetPath(dshHome, meta.target)},并注入 systemPrompt。新会话生效。`,
};
} catch (e) {
return { kind: "error", text: `激活失败: ${e.message}` };
}
}
case "create":
case "new":
case "c": {
const id = args[1];
if (!id) return { kind: "error", text: "用法: /rules create <id> [别名] [AGENTS.md|CLAUDE.md]" };
const name = args[2] || id;
const target = args[3] || "AGENTS.md";
if (!rules.validTarget(target)) {
return { kind: "error", text: `无效目标: ${target}(只能是 AGENTS.md 或 CLAUDE.md)` };
}
try {
await rules.saveRule(dshHome, id, "", { name, target });
return {
kind: "success",
text: `✓ 已创建规则 ${name}(${id} → ${target})。在设置页「规则设定」编辑内容后保存。`,
};
} catch (e) {
return { kind: "error", text: `创建失败: ${e.message}` };
}
}
case "delete":
case "rm":
case "d": {
const id = args[1];
if (!id) return { kind: "error", text: "用法: /rules delete <id>" };
try {
await rules.deleteRule(dshHome, id);
refreshHungOperator(dshHome);
return { kind: "success", text: `✓ 已删除规则 ${id}。` };
} catch (e) {
return { kind: "error", text: `删除失败: ${e.message}` };
}
}
case "reset":
case "restore":
case "还原": {
try {
const r = await rules.resetToOriginal(dshHome);
refreshHungOperator(dshHome);
const lines = [];
if (r.removed.length > 0) lines.push(`✓ 已删除插件写入的目标文件: ${r.removed.join(", ")}`);
if (r.skipped.length > 0) {
lines.push(`⚠ 跳过(文件内容与规则不一致,可能被手动修改): ${r.skipped.join(", ")}`);
}
lines.push("已清空激活状态。规则库内容保留,随时可重新激活。现在回到出厂状态:无全局指令。");
return { kind: "success", text: lines.join("\n") };
} catch (e) {
return { kind: "error", text: `还原失败: ${e.message}` };
}
}
case "help":
case "h":
default:
return {
kind: "success",
text: "规则集命令:\n" +
" /rules list 列出所有规则\n" +
" /rules use <id> 激活规则(写入 AGENTS.md/CLAUDE.md,并注入 systemPrompt)\n" +
" /rules create <id> [别名] [目标] 新建规则\n" +
" /rules delete <id> 删除规则\n" +
" /rules reset 还原原始状态\n" +
" /rules help 显示帮助",
};
}
}
function renderSkillsStatus(st) {
const out = [];
out.push("官方用户 Skill / $DSH_HOME/skills");
out.push(` ${st.skills_dir}`);
out.push(" 插件只安装目录。调用走官方:任务对上 description/whenToUse 时模型调 skill 工具;聊天输入 /名称 立即加载。");
out.push(" 正文里写「激活」不会触发。官方认 name、description、whenToUse、disable-model-invocation、user-invocable。");
out.push("");
if (!st.skills.length) {
out.push(" (暂无用户 Skill — 用 /skills import <压缩包或文件夹> 或设置页导入)");
return out.join("\n");
}
for (const item of st.skills) {
const mark = item.valid ? "·" : "!";
const desc = item.description || "(无说明)";
const how = item.valid
? [
item.userInvocable ? `/${item.id}` : "不可 /名称",
item.modelInvocable ? "模型可调" : "模型不可调",
].join(" · ")
: "";
out.push(` ${mark} ${item.id} ${desc} (${skills.formatSize(item.size)})`);
if (how) out.push(` ${how}`);
if (!item.valid && item.error) out.push(` ${item.error}`);
}
return out.join("\n");
}
async function handleSkillsCommand(rawInput) {
const args = (rawInput || "").trim().split(/\s+/).filter(Boolean);
const sub = (args[0] || "list").toLowerCase();
const dshHome = core.findDshHome();
switch (sub) {
case "list":
case "ls":
case "s":
return { kind: "success", text: renderSkillsStatus(await skills.skillsStatus(dshHome)) };
case "create":
case "new":
case "c": {
const id = args[1];
if (!id) return { kind: "error", text: "用法: /skills create <id> [说明]" };
if (!skills.validSkillName(id)) {
return { kind: "error", text: "id 必须是 kebab-case,例如 code-review" };
}
const description = args.slice(2).join(" ").trim() || id;
try {
const saved = skills.toPublicSkill(dshHome, await skills.saveSkill(dshHome, id, "", description));
return {
kind: "success",
text: `✓ 已写入官方目录 ${saved.path}\n新会话会进官方 Skill 目录;任务对上或输入 /${id} 时由官方加载。`,
};
} catch (e) {
return { kind: "error", text: `安装失败: ${e.message}` };
}
}
case "import":
case "i": {
const src = args.slice(1).join(" ").trim();
if (!src) return { kind: "error", text: "用法: /skills import <压缩包或文件夹>" };
try {
const packed = await skills.importFromPath(dshHome, src);
const names = packed.imported.map((item) => item.id).join(", ");
return { kind: "success", text: `✓ 已导入到官方 $DSH_HOME/skills:${names}` };
} catch (e) {
return { kind: "error", text: `导入失败: ${e.message}` };
}
}
case "delete":
case "rm":
case "d": {
const id = args[1];
if (!id) return { kind: "error", text: "用法: /skills delete <id>" };
try {
const removed = skills.toPublicSkill(dshHome, await skills.deleteSkill(dshHome, id));
return {
kind: "success",
text: `✓ 已从官方目录删除 ${id}${removed.backup ? `\n备份:${removed.backup}` : ""}`,
};
} catch (e) {
return { kind: "error", text: `删除失败: ${e.message}` };
}
}
case "help":
case "h":
default:
return {
kind: "success",
text: "官方 Skill 安装:\n" +
" /skills list 列出 $DSH_HOME/skills\n" +
" /skills create <id> [说明] 写入 <id>/SKILL.md\n" +
" /skills import <路径> 导入压缩包或文件夹到官方目录\n" +
" /skills delete <id> 删除该目录(官方会从目录拿掉)\n" +
" 也可自己删 $DSH_HOME/skills/<id>\n" +
" 调用:聊天输入 /<id>,或让模型按 description 调 skill 工具\n" +
" 不注入 systemPrompt,不替代提示词",
};
}
}
export function apply(ctx, config) {
const cfg = { ...DEFAULTS, ...(config || {}) };
if (!cfg.enabled) return;
core.setHooksDenyConfigValue(cfg.hooksDenyBypass);
core.refreshHooksDenySettings();
try {
const home = core.pinDshHome() || core.findDshHome();
installHungPromptGlobal();
// Apply→cleanup→restart 成功后,默认把加密提示词 seed 进 prompt-inject.md。
// 用户已经改过的文件不动;只有「没有」或「仍是空文件」时才补写。
try {
const seed = core.seedPromptInjectAtBoot(home);
log(cfg, "seed prompt-inject:", seed?.action || "noop", seed?.reason || "");
} catch (e) {
console.warn("[dsh-purge] seed prompt-inject failed:", String(e && e.message ? e.message : e));
}
refreshHungOperator(home);
} catch { /* 主目录还没有时,组装会再挂内置默认 */ }
try {
const restored = core.restoreMissingOriginalsAllHosts();
if (restored.length) log(cfg, "restored missing overlay from bak:", restored.join(","));
} catch (e) {
console.warn("[dsh-purge] restore bak:", String(e && e.message ? e.message : e));
}
const life = watchContextLife(ctx);
if (!life.alive) {
console.warn("[dsh-purge] skip apply: inactive context");
return;
}
log(cfg, "plugin enabled");
try {
const hide = core.ensureHiddenConsole();
log(cfg, "hide-console:", JSON.stringify(hide));
} catch (e) {
console.warn("[dsh-purge] hide-console error:", String(e));
}
core.applyRuntimeEnv();
// 页面打开前写前端 client.js。settle 在数秒之后,那时再写会让已发出的 rev 404(#63)。
try {
const written = core.patchWatchedClientBundlesSync();
publishClientBundleRevs(ctx, written);
if (written.length) log(cfg, "client bundles patched before page:", written.join(","));
} catch (e) {
console.warn("[dsh-purge] client bundle boot patch:", String(e && e.message ? e.message : e));
}
try {
const home = core.pinDshHome() || core.findDshHome();
refreshHungOperator(home);
} catch { /* 主目录还没有时,组装会再挂内置默认 */ }
try {
const scrubbed = core.sanitizeDesktopCommandRuntimes();
if (scrubbed.length) log(cfg, "desktop runtime scrub on load:", JSON.stringify(scrubbed));
} catch (e) {
console.warn("[dsh-purge] desktop runtime scrub error:", String(e));
}
const settleTimer = setTimeout(() => {
if (!life.alive) {
log(cfg, "settle skipped: inactive context");
return;
}
settleInstalledPatches(cfg, ctx, life)
.then((r) => log(cfg, "settle:", r))
.catch((e) => {
if (isInactiveContextError(e)) {
log(cfg, "settle skipped: inactive context");
return;
}
console.warn("[dsh-purge] settle error:", String(e));
});
rules.ensureInitialState(core.findDshHome())
.then((r) => log(cfg, "rules init:", JSON.stringify(r)))
.catch((e) => console.warn("[dsh-purge] rules init error:", String(e)));
}, settleDelayMs(hostctl.currentSurface()));
try {
ctx.effect(() => () => clearTimeout(settleTimer));
} catch (e) {
if (isInactiveContextError(e)) {
clearTimeout(settleTimer);
console.warn("[dsh-purge] skip apply: inactive context");
return;
}
throw e;
}
try {
applyHostHooks(ctx, cfg);
} catch (e) {
if (isInactiveContextError(e)) {
console.warn("[dsh-purge] skip apply: inactive context");
return;
}
throw e;
}
}
function applyHostHooks(ctx, cfg) {
try {
const home = core.pinDshHome() || core.findDshHome();
refreshHungOperator(home);
} catch { /* 主目录尚未就绪 */ }
installIdentityOverride(ctx, cfg);
installRegionGate(ctx);
const refusalRecoverRuntime = refusalRecover.installRefusalRecover(ctx, {
getConfig: () => ({
enabled: cfg.refusalRecover !== false,
maxPerTurn: cfg.refusalRecoverMax ?? DEFAULTS.refusalRecoverMax,
recoverText: cfg.refusalRecoverText ?? DEFAULTS.refusalRecoverText,
}),
});
installFreshFullAccess(ctx);
installRewindDeriveFilter(ctx);
installMnemonMessageStrip(ctx, cfg);
const injectFile = core.findOverrideFile(core.findDshHome());
registerPromptSections(ctx, cfg, injectFile);
// 本次 apply 作用域内的 continue-retry 状态;旧 ctx dispose 会把这些闭包一并带走。
const continuePluginCfg = cfg;
const savedContinueSettings = continueRetry.loadSettings(core.findDshHome());
const continueRuntime = continueRetry.installContinueRetry(ctx, {
getConfig: () => continueRetry.mergeConfig(continuePluginCfg, savedContinueSettings),
});
try {
ctx.effect(() => () => {
try { refusalRecoverRuntime?.dispose?.(); } catch { /* ignore */ }
try { continueRuntime?.dispose?.(); } catch { /* ignore */ }
});
} catch { /* 无 effect 时靠 cordis scope 自己回收 */ }
const commands = ctx.get?.("commands");
if (commands) {
commands.register({
name: "purge",
description: "dsh 指令权威性清洗(status/apply/revert/uninstall/edit/help)",
input: { hint: "status | apply | revert | uninstall | edit | help" },
handler: async (invocation) => handlePurgeCommand(invocation.rawInput ?? "", cfg, ctx),
});
commands.register({
name: "rules",
description: "规则集切换(写入 AGENTS.md/CLAUDE.md): list | use <id> | create <id> [别名] [目标] | delete <id> | reset | help",
input: { hint: "list | use <id> | create <id> [别名] [目标] | delete <id> | reset | help" },
handler: async (invocation) => handleRulesCommand(invocation.rawInput ?? ""),
});
commands.register({
name: "skills",
description: "官方用户 Skill($DSH_HOME/skills):list | import <路径> | create <id> [说明] | delete <id> | help",
input: { hint: "list | import <路径> | create <id> [说明] | delete <id> | help" },
handler: async (invocation) => handleSkillsCommand(invocation.rawInput ?? ""),
});
commands.register({
name: "rewind",
description: "回退:子代理直接回退一次;主代理 /rewind once 或 /rewind round",
input: { hint: "once | round" },
handler: async (invocation) => {
const sessionId = rewind.invocationSessionId(invocation);
const arg = String(invocation.rawInput || "").trim().split(/\s+/)[0]?.toLowerCase() || "";
const info = await rewind.analyzeRewind(ctx, sessionId);
if (info.ok && info.kind === "main" && arg !== "once" && arg !== "round") {
return {
kind: "success",
text: "主代理请选择:/rewind once 回退一次,/rewind round 回退上一轮(含本轮子代理)。",
};
}
const mode = arg === "round" ? "round" : "once";
const result = await rewind.applyRewind(ctx, sessionId, mode);
if (!result.ok) return { kind: "error", text: result.error };
return {
kind: "success",
text: mode === "round"
? "已回退上一轮。还在这条对话里,上一句已放回输入框。再发送时,之前的回复不会带上。"
: "已回退一次。还在这条对话里,上一句已放回输入框。再发送时,之前的回复不会带上。",
};
},
});
}
if (ctx.tools) {
ctx.tools.register({
name: "purge_status",
description: `查看 dsh 指令权威性清洗状态(${core.ALL_PATCHES.length} patch 清洗进度、shim 注入、override 文件)。`,
parameters: {
type: "object",
additionalProperties: false,
properties: {},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: { text: { type: "string" } },
},
render: (_args, value) => [{ type: "text", text: String(value?.text ?? "") }],
},
async execute() {
const state = await core.gatherState();
return { text: renderStatus(state) };
},
});
ctx.tools.register({
name: "purge_apply",
description: `应用 dsh 指令权威性清洗:${core.ALL_PATCHES.length} patch(提示词层+代码默认+引擎级审批/沙箱绕过)+ shim 注入 + override 文件。需重启 dsh 完全生效。`,
parameters: {
type: "object",
additionalProperties: false,
properties: {},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: { text: { type: "string" } },
},
render: (_args, value) => [{ type: "text", text: String(value?.text ?? "") }],
},
async execute() {
core.applyRuntimeEnv();
const scrubbed = core.sanitizeDesktopCommandRuntimes();
const state = await core.gatherState();
if (!state.ai_base) return { text: "ERROR: " + core.missingAiBaseMessage() };
const blocked = rejectIfNeedPrompt(state.dsh_home);
if (blocked) return { text: "ERROR: " + blocked.error };
const toolMulti = await core.applyPatchesAllHosts(core.ALL_PATCHES, {
preserveClientBundles: false,
forApply: true,
});
await core.patchAllShims();
const flash = core.silenceCmdFlash(toolMulti.hosts[0]?.aiBase || state.ai_base);
const report = toolMulti.report;
const applied = report.filter((r) => r.status === "applied").length;
const already = report.filter((r) => r.status === "already").length;
const scrubNote = scrubbed.length ? ` desktop_scrub=${scrubbed.length}` : "";
return { text: `清洗完成 / applied=${applied}, already=${already}.${scrubNote} phase-1=${flash.phase1} 重启 dsh 生效.` };
},
});
ctx.tools.register({
name: "purge_revert",
description: "回滚 dsh 指令权威性清洗到备份原件(保留 prompt-inject.md 用户文件)。",
parameters: {
type: "object",
additionalProperties: false,
properties: {},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: { text: { type: "string" } },
},
render: (_args, value) => [{ type: "text", text: String(value?.text ?? "") }],
},
async execute() {
const state = await core.gatherState();
const lines = [];
if (state.ai_base) {
const { reverted, errors } = await core.revertAll(state.ai_base);
lines.push(`reverted=${reverted.length}${errors.length ? ` errors=${errors.length}` : ""}`);
}
const shimRevert = await core.revertAllShims();
lines.push(`shim_dirs=${Object.keys(shimRevert).length}`);
return { text: `回滚完成 / ${lines.join(", ")}. 重启 dsh 恢复.` };
},
});
}
installWebServer(ctx, cfg);
}
function headerOf(request, name) {
const headers = request?.headers;
if (!headers) return "";
const value = headers[name] ?? headers[String(name).toLowerCase()];
if (Array.isArray(value)) return String(value[0] || "");
return String(value || "");
}
function denyCrossOrigin(response) {
response.writeHead(403, {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
});
response.end(JSON.stringify({ ok: false, error: "cross-origin blocked" }));
}
function allowPluginRequest(request, response) {
const site = headerOf(request, "sec-fetch-site").toLowerCase();
if (site === "cross-site") {
denyCrossOrigin(response);
return false;
}
const origin = headerOf(request, "origin").trim();
if (origin === "null") {
denyCrossOrigin(response);
return false;
}
if (!origin) return true;
let originHost = "";
try {
originHost = new URL(origin).host;
} catch {
denyCrossOrigin(response);
return false;
}
const host = headerOf(request, "host").trim();
if (!host || originHost.toLowerCase() !== host.toLowerCase()) {
denyCrossOrigin(response);
return false;
}
return true;
}
function installWebServer(ctx, cfg) {
// 桌面端内嵌 @deepseek-ai/dsh-web-app,设置页同样走 webServer(常见 43120),不是只给官方 dsh web。
watchWebTreeReady(ctx);
ctx.inject(["webServer"], (host) => {
host.effect(() => {
// 收集所有子路由的 unregister disposer;host.effect 的 cleanup 一把全撤。
// 旧版本只注册不收,plugin reload 时上一轮的 handler 仍挂着,新的叠在之上。
const routeDisposers = [];
const trackRoute = (d) => {
if (typeof d === "function") routeDisposers.push(d);
return d;
};
const registerPluginRoute = (spec, label) => trackRoute(host.webServer.register({
...spec,
handler: async (request, response) => {
if (!allowPluginRequest(request, response)) return;
return spec.handler(request, response);
},
}, label));
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/status",
handler: async (request, response) => {
if (request.method !== "GET") {
response.writeHead(405, { allow: "GET" });
response.end();
return;
}
try {
if (!webTreeReady) {
sendJson(response, 503, { ok: false, ready: false });
return;
}
const state = await core.gatherState();
const patch_names = {};
for (const p of core.ALL_PATCHES) patch_names[String(p.id)] = p.name;
sendJson(response, 200, {
ok: true,
ready: true,
surface: state.surface || hostctl.currentSurface(),
ai_base: state.ai_base,
desktop_install: state.desktop_install || "",
desktop_exe: state.desktop_exe || "",
asar_still_sealed: state.asar_still_sealed || false,
missing_ai_base: state.ai_base ? "" : core.missingAiBaseMessage(),
patches_applied: state.patches_applied,
patches_pending: state.patches_pending,
patches_skipped: state.patches_skipped || 0,
patches_ready: state.patches_ready || 0,
patches_na: state.patches_na || 0,
patches_total: state.ai_base ? (state.patches_total || 0) : 0,
patches_lost: reapply.patchesLostAfterHostReset(),
patch_status: state.patch_status,
patches: core.ALL_PATCHES.map((p) => ({
id: p.id,
layer: p.layer || "",
status: state.patch_status[p.id] || (state.ai_base ? "missing_file" : "unlocated"),
})).filter((p) => p.status !== "na"),
patch_names,
shim_cmd: state.shim_cmd,
shim_ps1: state.shim_ps1,
shim_bin: state.shim_bin,
has_backup: state.has_backup,
override_status: state.override_status,
override_path: state.override_path,
dsh_home: state.dsh_home,
plugin_version: updater.localVersion(),
boot_full_quit: bootFullQuit,
update: updater.lastUpdateResult(),
...updater.channelPublic(),
hooks_deny_bypass: core.getHooksDenySettings().hooksDenyBypass,
...(await skills.skillsStatus(core.findDshHome()).catch(() => ({ skills: [] }))),
});
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: status");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/hooks-deny",
handler: async (request, response) => {
if (request.method === "GET") {
try {
const settings = core.refreshHooksDenySettings();
sendJson(response, 200, { ok: true, hooksDenyBypass: settings.hooksDenyBypass });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
if (request.method === "POST") {
try {
const body = await readJsonBody(request);
if (typeof body?.hooksDenyBypass !== "boolean") {
sendJson(response, 400, { ok: false, error: "hooksDenyBypass must be boolean" });
return;
}
const saved = core.setHooksDenySettings({ hooksDenyBypass: body.hooksDenyBypass });
sendJson(response, 200, { ok: true, hooksDenyBypass: saved.hooksDenyBypass });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: hooks-deny");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/last-user",
handler: async (request, response) => {
if (request.method !== "GET") {
response.writeHead(405, { allow: "GET" });
response.end();
return;
}
try {
const url = new URL(request.url, "http://127.0.0.1");
const sessionId = String(url.searchParams.get("sessionId") || "").trim();
sendJson(response, 200, await rewind.readLastUserText(ctx, sessionId));
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: last user");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/rewind/options",
handler: async (request, response) => {
if (request.method !== "GET") {
response.writeHead(405, { allow: "GET" });
response.end();
return;
}
try {
const url = new URL(request.url, "http://127.0.0.1");
const sessionId = String(url.searchParams.get("sessionId") || "").trim();
sendJson(response, 200, await rewind.analyzeRewind(ctx, sessionId));
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: rewind options");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/rewind",
handler: async (request, response) => {
if (request.method === "GET") {
try {
const url = new URL(request.url, "http://127.0.0.1");
const sessionId = url.searchParams.get("sessionId");
if (sessionId) {
const draft = rewind.takeDraft(sessionId);
sendJson(response, 200, { ok: true, ...(draft || {}) });
return;
}
const pending = rewind.getPending();
sendJson(response, 200, { ok: true, ...(pending || {}) });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
if (request.method === "POST") {
try {
const body = await readJsonBody(request);
const sessionId = String(body?.sessionId || "").trim();
if (!sessionId) {
sendJson(response, 400, { ok: false, error: "需要 sessionId" });
return;
}
const mode = body?.mode === "round" ? "round" : "once";
const result = await rewind.applyRewind(ctx, sessionId, mode);
sendJson(response, result.ok ? 200 : 409, result);
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: rewind");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/update",
handler: async (request, response) => {
if (request.method === "GET") {
try {
sendJson(response, 200, await updater.checkUpdate());
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
if (request.method === "POST") {
try {
const body = await readJsonBody(request);
const result = await updater.handleUpdateOp(body || {});
if (result && result.needsFullQuit) bootFullQuit = true;
sendJson(response, 200, result);
if (result && result.restartAfter) {
try { hostctl.scheduleRestart(request, ctx); } catch (e) {
console.warn("[dsh-purge] update restart failed:", e);
}
}
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: update");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/apply",
handler: async (request, response) => {
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return;
}
try {
const body = await readJsonBody(request);
const incoming = typeof body?.content === "string" ? body.content : null;
core.applyRuntimeEnv();
core.clearDesktopLocateCache();
const home = core.pinDshHome();
const scrubbed = core.sanitizeDesktopCommandRuntimes();
const state = await core.gatherState();
if (home) state.dsh_home = home;
let located;
try {
located = await core.resolveOfficialAiBase(state.ai_base, { forApply: true });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e && e.message ? e.message : e) });
return;
}
if (!located.aiBase) {
sendJson(response, 500, { ok: false, error: core.missingAiBaseMessage() });
return;
}
state.ai_base = located.aiBase;
if (located.extracted) {
const arch = located.handoff?.archive;
if (arch) core.writeUnpackedAsarStamp(arch);
}
const handoff = located.handoff && located.handoff.swapAsar ? located.handoff : null;
const blocked = rejectIfNeedPrompt(state.dsh_home, incoming === null ? undefined : incoming);
if (blocked) {
const ui = await core.readOverrideForUi(state.dsh_home);
sendJson(response, 400, {
ok: false,
needPrompt: true,
override_content: incoming || "",
defaultContent: ui.defaultContent,
...blocked,
});
return;
}
if (typeof incoming === "string" && core.normalizeOverride(incoming)) {
await core.saveOverrideContent(state.dsh_home, incoming);
}
const httpMulti = await core.applyPatchesAllHosts(core.ALL_PATCHES, {
preserveClientBundles: false,
forApply: true,
});
const report = httpMulti.report;
const shim = await core.patchAllShims();
let cliRepair = null;
try {
cliRepair = core.repairOfficialCliAfterUnpack();
} catch (e) {
cliRepair = { ok: false, error: String(e && e.message ? e.message : e) };
}
const flash = core.silenceCmdFlash(httpMulti.hosts[0]?.aiBase || state.ai_base);
const summary = core.summarizeApplyReport(report);
const hostWritten =
core.allPatchHostsWritten(httpMulti.hosts) || core.hostCleanMarkersPresent(state.ai_base);
const hookDenyRestoreFailed = (httpMulti.hookDenyRestoreFailed || []).length > 0;
const complete = summary.failed.length === 0 && flash.ok === true && hostWritten && !hookDenyRestoreFailed;
const sealedNow = core.desktopAsarStillSealed();
const promptReady = summary.failed.length > 0 && summary.failed.every((item) => item.status === "pattern_not_found");
if (flash.ok === true && (complete || promptReady) && hostWritten) {
reapply.markPatchesApplied();
}
// 全部洗完且标记进宿主才弹重启;密封包重启时换走 asar。
const shouldRestart = applyShouldRestart({
complete: complete || (promptReady && hostWritten),
sealed: sealedNow,
appliedCount: summary.applied,
promptReady: promptReady && hostWritten,
});
if (shouldRestart && handoff && sealedNow) bootFullQuit = true;
else bootFullQuit = false;
if (shouldRestart) reapply.markSettleRestart();
let promptInjectInstall = "skipped";
if (hostWritten && flash.ok === true) {
// Apply → cleanup → restart 成功后,默认把加密提示词 seed 进 prompt-inject.md。
// 用户自改过的内容保留;仅当文件不存在或仍是插件旧默认时才写。
try {
const seed = core.seedPromptInjectAtBoot(state.dsh_home);
promptInjectInstall = seed?.action || "noop";
} catch (e) {
promptInjectInstall = `error:${String(e && e.message ? e.message : e)}`;
}
}
const hung = refreshHungOperator(state.dsh_home);
const override = await core.readOverrideForUi(state.dsh_home);
sendJson(response, 200, {
ok: true,
complete,
restarting: shouldRestart,
restart_after_apply: shouldRestart,
unpacked_asar: shouldRestart,
needs_full_quit: shouldRestart,
asar_still_sealed: sealedNow,
asar_swap_started: Boolean(shouldRestart && handoff && sealedNow),
applied: summary.applied,
already: summary.already,
failed: summary.failed.length,
failed_items: summary.failed.map((r) => ({
id: r.patch_id,
name: r.name,
status: r.status,
})),
report: report.map((r) => ({ id: r.patch_id, name: r.name, status: r.status })),
override_content: hung.text || override.content,
defaultContent: override.defaultContent,
...injectSnapshot(state.dsh_home),
override_path: override.path,
override_installed: promptInjectInstall,
shim,
desktop_scrubbed: scrubbed,
cli_repair: cliRepair,
cmd_flash: flash,
});
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: apply");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/revert",
handler: async (request, response) => {
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return;
}
try {
const state = await core.gatherState();
const result = {};
if (state.ai_base) {
const { reverted, errors } = await core.revertAll(state.ai_base);
result.reverted = reverted.length;
result.errors = errors.length;
}
result.shim = await core.revertAllShims();
sendJson(response, 200, { ok: true, ...result, note: "重启 dsh 恢复; prompt-inject.md 保留" });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: revert");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/uninstall",
handler: async (request, response) => {
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return;
}
try {
const result = await uninstaller.uninstallPurge();
sendJson(response, result.ok ? 200 : 500, {
...result,
surface: hostctl.currentSurface(),
fullApp: hostctl.currentSurface() === "desktop",
note: result.ok ? hostctl.restartNote() : undefined,
});
if (result.ok) {
uninstaller.scheduleCleanupRestart({
request,
cleanup: result.cleanup,
ctx,
});
}
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e && e.message ? e.message : e) });
}
},
}, "dsh-purge: uninstall");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/drill-grant",
handler: async (request, response) => {
const home = core.findDshHome();
if (request.method === "GET") {
sendJson(response, 200, { ok: true, ...readDrillGrant(home) });
return;
}
if (request.method === "POST") {
sendJson(response, 200, { ok: true, ...writeDrillGrant(home) });
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: drill-grant");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/override",
handler: async (request, response) => {
if (request.method === "GET") {
try {
const home = core.findDshHome();
// 面板打开 override 面板时,确保默认加密提示词已 seed;用户自改保留。
try { core.seedPromptInjectAtBoot(home); } catch { /* ignore */ }
refreshHungOperator(home);
const ui = await core.readOverrideForUi(home);
sendJson(response, 200, { ok: true, ...ui, ...injectSnapshot(home) });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
if (request.method === "POST") {
try {
const body = await readJsonBody(request);
const content = typeof body?.content === "string" ? body.content : null;
if (content === null) {
sendJson(response, 400, { ok: false, error: "content must be a string" });
return;
}
const home = core.findDshHome();
const blocked = rejectIfNeedPrompt(home, content);
if (blocked) {
sendJson(response, 400, {
ok: false,
content,
...blocked,
});
return;
}
const saved = await core.saveOverrideContent(home, content);
const hung = refreshHungOperator(home);
const ui = await core.readOverrideForUi(home);
sendJson(response, 200, {
ok: true,
content: hung.text,
defaultContent: saved.defaultContent,
customized: ui.customized,
usingDefault: ui.usingDefault,
path: ui.path,
...injectSnapshot(home),
});
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: override");
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/own-servers",
handler: async (request, response) => {
const home = core.findDshHome();
if (request.method === "GET") {
sendJson(response, 200, { ok: true, content: readAllowText(home) });
return;
}
if (request.method === "POST") {
try {
const body = await readJsonBody(request);
const content = typeof body?.content === "string" ? body.content : null;
if (content === null) {
sendJson(response, 400, { ok: false, error: "content must be a string" });
return;
}
const saved = await writeAllowText(home, content);
sendJson(response, 200, { ok: true, content: saved.text, dropped: saved.dropped });
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e && e.message ? e.message : e) });
}
return;
}
response.writeHead(405, { allow: "GET, POST" });
response.end();
},
}, "dsh-purge: own-servers");
trackRoute(installRulesHttp(host.webServer));
trackRoute(installSkillsHttp(host.webServer));
registerPluginRoute({
kind: "exact",
path: "/dsh-purge/restart",
handler: async (request, response) => {
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return;
}
try {
const ready = await ensureRestartReady();
if (!ready.ok) {
bootFullQuit = false;
sendJson(response, 200, {
ok: false,
error: ready.error || "清洗没有完成,已取消重启",
restarting: false,
pending: ready.pending || [],
});
return;
}
const surface = hostctl.currentSurface();
let planned = null;
if (core.desktopAsarStillSealed()) {
planned = core.restartSealedDesktopIfNeeded({ ctx });
} else {
planned = hostctl.scheduleRestart(request, ctx);
}
if (planned && planned.clean === false) {
bootFullQuit = false;
sendJson(response, 200, {
ok: false,
error: "清洗没有写进当前宿主,已取消重启",
restarting: false,
});
return;
}
// 用户已点重启:清掉粘性标记,避免同进程误刷新/再进面板又弹一次。
bootFullQuit = false;
const restarting = Boolean(planned && (planned.restarting || planned.asarSwap));
const restartError = planned && planned.error ? String(planned.error) : "";
sendJson(response, 200, {
ok: restarting || !restartError,
surface,
fullApp: surface === "desktop",
restarting,
already: Boolean(planned && planned.already),
needsFullQuit: Boolean(planned && planned.asarSwap),
asar_still_sealed: core.desktopAsarStillSealed(),
error: restartError || undefined,
note: restarting ? "正在重启。" : restartError,
});
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e) });
}
},
}, "dsh-purge: restart");
return () => {
for (let i = routeDisposers.length - 1; i >= 0; i -= 1) {
try { routeDisposers[i](); } catch { /* ignore */ }
}
routeDisposers.length = 0;
};
}, "dsh-purge: http routes");
});
}
function methodNotAllowed(request, response, allow) {
if (allow.includes(request.method)) return false;
response.writeHead(405, { allow: allow.join(", ") });
response.end();
return true;
}
async function handleRulesOp(op, body, queryId) {
const dshHome = core.findDshHome();
const id = typeof body?.id === "string" ? body.id : String(queryId || "");
if (op === "status") {
return { ok: true, ...(await rules.rulesStatus(dshHome)) };
}
if (op === "read") {
if (!rules.validRuleId(id)) return { status: 400, ok: false, error: "invalid id" };
const content = await rules.readRule(dshHome, id);
if (content === null) return { status: 404, ok: false, error: `rule not found: ${id}` };
const meta = await rules.readRuleMeta(dshHome, id);
return { ok: true, id, name: meta.name, target: meta.target, content };
}
if (op === "save") {
const content = typeof body?.content === "string" ? body.content : null;
if (!rules.validRuleId(id) || content === null) {
return { status: 400, ok: false, error: "id and content (string) required" };
}
const target = typeof body?.target === "string" ? body.target : undefined;
// 旧版本直接把 body.target 写进 rules.saveRule;非法目标会污染 agents_path 推导。
if (target !== undefined && !rules.validTarget(target)) {
return { status: 400, ok: false, error: `invalid target: ${target}(只能是 AGENTS.md 或 CLAUDE.md)` };
}
await rules.saveRule(dshHome, id, content, {
name: typeof body?.name === "string" ? body.name : undefined,
target,
});
refreshHungOperator(dshHome);
return { ok: true, id, ...injectSnapshot(dshHome) };
}
if (op === "activate") {
if (!rules.validRuleId(id)) return { status: 400, ok: false, error: "invalid id" };
const meta = await rules.activateRule(dshHome, id);
refreshHungOperator(dshHome);
return {
ok: true,
id,
name: meta.name,
target: meta.target,
agents_path: rules.targetPath(dshHome, meta.target),
note: "已写入目标文件并注入 systemPrompt,新会话生效",
...injectSnapshot(dshHome),
};
}
if (op === "delete") {
if (!rules.validRuleId(id)) return { status: 400, ok: false, error: "invalid id" };
await rules.deleteRule(dshHome, id);
refreshHungOperator(dshHome);
return { ok: true, id };
}
if (op === "reset") {
const r = await rules.resetToOriginal(dshHome);
refreshHungOperator(dshHome);
return { ok: true, ...r, note: "已还原出厂状态(无全局指令);规则库保留" };
}
return { status: 400, ok: false, error: `unknown op: ${op}` };
}
function rulesHttpHandler(forcedOp) {
return async (request, response) => {
if (request.method === "OPTIONS") {
sendOptions(response);
return;
}
try {
const url = new URL(request.url ?? "", "http://127.0.0.1");
const tail = url.pathname.replace(/^\/dsh-purge\/rules\/?/, "").replace(/\/$/, "");
let body = {};
if (request.method === "POST") body = await readJsonBody(request);
else if (methodNotAllowed(request, response, forcedOp ? ["GET", "POST"] : ["GET", "POST"])) return;
const op = forcedOp || body.op || tail || "status";
const result = await handleRulesOp(op, body, url.searchParams.get("id"));
sendJson(response, result.status || 200, result);
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e && e.message ? e.message : e) });
}
};
}
async function handleSkillsOp(op, body, queryId) {
const dshHome = core.findDshHome();
const id = typeof body?.id === "string" ? body.id : String(queryId || "");
if (op === "status") {
return { ok: true, ...(await skills.skillsStatus(dshHome)) };
}
if (op === "read") {
if (!skills.validSkillName(id)) return { status: 400, ok: false, error: "invalid skill name" };
const item = await skills.readSkill(dshHome, id);
if (!item) return { status: 404, ok: false, error: `skill not found: ${id}` };
return { ok: true, ...skills.toPublicSkill(dshHome, item) };
}
if (op === "save") {
if (!skills.validSkillName(id)) return { status: 400, ok: false, error: "id 必须是 kebab-case,例如 code-review" };
const content = typeof body?.content === "string" ? body.content : "";
const description = typeof body?.description === "string" ? body.description : "";
const saved = await skills.saveSkill(dshHome, id, content, description);
return { ok: true, ...skills.toPublicSkill(dshHome, saved) };
}
if (op === "delete") {
if (!skills.validSkillName(id)) return { status: 400, ok: false, error: "invalid skill name" };
const removed = await skills.deleteSkill(dshHome, id);
return { ok: true, ...skills.toPublicSkill(dshHome, removed) };
}
if (op === "import") {
if (body?.path || body?.source || body?.folder) {
return { status: 400, ok: false, error: "请在本机选择压缩包或文件夹导入" };
}
if (typeof body?.data === "string" && body.data.trim()) {
const packed = await skills.importArchive(dshHome, Buffer.from(body.data, "base64"), body.name || "skill.zip");
return { ok: true, ...packed };
}
if (Array.isArray(body?.files) && body.files.length) {
const packed = await skills.importFileTree(dshHome, body.files, body.name || "");
return { ok: true, ...packed };
}
return { status: 400, ok: false, error: "请选择压缩包或文件夹" };
}
return { status: 400, ok: false, error: `unknown op: ${op}` };
}
function skillsHttpHandler(forcedOp) {
return async (request, response) => {
if (request.method === "OPTIONS") {
sendOptions(response);
return;
}
if (request.method !== "GET" && request.method !== "POST") {
response.writeHead(405, { allow: "GET, POST, OPTIONS" });
response.end();
return;
}
try {
const url = new URL(request.url ?? "", "http://127.0.0.1");
const tail = skillRouteTail(url.pathname);
const body = request.method === "POST" ? await readJsonBody(request, 14 * 1024 * 1024) : {};
const op = forcedOp
|| body.op
|| tail
|| url.searchParams.get("op")
|| (url.searchParams.get("id") ? "read" : "status");
const result = await handleSkillsOp(op, body, url.searchParams.get("id"));
sendJson(response, result.status || 200, result);
} catch (e) {
sendJson(response, 500, { ok: false, error: String(e && e.message ? e.message : e) });
}
};
}
function skillRouteTail(pathname) {
const match = String(pathname || "").match(/^\/dsh-purge\/skills?(?:\/(.*))?$/i);
if (!match) return "";
return String(match[1] || "").replace(/\/$/, "");
}
function installSkillsHttp(webServer) {
// 旧版把 /dsh-purge/skills[/op] 和 /dsh-purge/skill[/op] 分别 exact 注册 10+ 条,再加 prefix 兜底;
// handler 内部本来就按 tail 分发 op,exact 列表只是把路由表撑大,热重载还更难拆。
// 用 2 条 prefix 覆盖全部,handler 的 forcedOp="" 让 body.op/tail/op 查询串继续生效。
const stop = [];
stop.push(registerRoute(webServer, {
kind: "prefix", path: "/dsh-purge/skills", handler: skillsHttpHandler(""),
}));
stop.push(registerRoute(webServer, {
kind: "prefix", path: "/dsh-purge/skill", handler: skillsHttpHandler(""),
}));
return () => {
for (let i = stop.length - 1; i >= 0; i -= 1) {
try { stop[i]?.(); } catch { /* ignore */ }
}
};
}
function installRulesHttp(webServer) {
// 同 skills:prefix 一条覆盖 /dsh-purge/rules[/op];handler 按 tail 分发 op。
const stop = [];
stop.push(registerRoute(webServer, {
kind: "prefix", path: "/dsh-purge/rules", handler: rulesHttpHandler(""),
}));
return () => {
for (let i = stop.length - 1; i >= 0; i -= 1) {
try { stop[i]?.(); } catch { /* ignore */ }
}
};
}
function sendJson(response, status, payload) {
response.writeHead(status, {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
});
response.end(JSON.stringify(payload));
}
function sendOptions(response) {
response.writeHead(204, { "cache-control": "no-store" });
response.end();
}
function registerRoute(webServer, spec) {
try {
const handler = spec.handler;
return webServer.register({
...spec,
handler: async (request, response) => {
if (!allowPluginRequest(request, response)) return;
return handler(request, response);
},
});
} catch (e) {
console.warn("[dsh-purge] route", spec?.path, String(e && e.message ? e.message : e));
return () => {};
}
}
async function readJsonBody(request, maxBytes = 512 * 1024) {
const chunks = [];
let size = 0;
const limit = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : 512 * 1024;
for await (const chunk of request) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += buffer.length;
if (size > limit) throw new Error("request body too large");
chunks.push(buffer);
}
const raw = Buffer.concat(chunks).toString("utf8").trim();
if (!raw) return {};
return JSON.parse(raw);
}
const plugin = {
name,
inject,
apply,
};
export default plugin;