fix: 命令撞名只丢那一条;主插件 insert 自愈(#162 #159)

This commit is contained in:
叫我小杨同学 committed 2026-10-26 11:15:56 +08:00
1 parent 95102b0bb2
commit 18fe3b51cb
7 files changed
+179 -53

No files matched your search

+5 -1
View File
@@ -210,7 +210,7 @@ Web controls affect Web only; desktop controls affect the desktop app only. Do n
- `/purge status` in chat prints `DSH_HOME` and the patch list — the path should be the home you actually run.
- Optional packages that are not installed (liangshen) show as skipped and do not block Apply.
If the button is missing, run `dsh --dump-config` (or `dsh --profile desktop --dump-config`) and look for a body row `id: dsh-purge` (not `dsh-purge-redteam`). If it is absent, run `ensure-profile-bundle.mjs` above, then fully quit and reopen the host. `entry "redteam-store" not found` at the top is leftover from an old red-team package and can be ignored.
If the button is missing, run `dsh --dump-config` (or `dsh --profile desktop --dump-config`) and look for a body row `id: dsh-purge` (not `dsh-purge-redteam`). If it is absent, run `ensure-profile-bundle.mjs` above, then fully quit and reopen the host. After this version, a live red-team child entry also writes the missing insert on startup — quit and reopen once more. `entry "redteam-store" not found` at the top is leftover from an old red-team package and can be ignored.
### If the market says `already queued: dsh-purge`
@@ -425,6 +425,10 @@ dsh-purge --edit
/rules list | use <id> | create <id> | delete <id> | reset | help
/rewind
# If another plugin already owns /rewind (or /purge, /rules, /skills), that
# name is skipped or remounted as /purge-rewind. Other commands, tools, and
# /dsh-purge/* routes keep working.
purge_status purge_apply purge_revert
```
+4 -1
View File
@@ -212,7 +212,7 @@ Web 的按钮只动 Web,桌面的只动桌面。不要在一个里面点另一
- 聊天里 `/purge status` 能打出 `DSH_HOME` 和补丁列表,路径是你正在用的那一份。
- 没装的可选包(例如梁神)显示跳过,不挡住完成。
没有按钮时,在对应 profile 跑 `dsh --dump-config`(或 `dsh --profile desktop --dump-config`),看有没有本体行 `id: dsh-purge`(不是 `dsh-purge-redteam`)。没有就再跑上面的 `ensure-profile-bundle.mjs`,然后完全退出重开宿主。开头那几条 `entry "redteam-store" not found` 只是旧红队包不在,可以忽略。
没有按钮时,在对应 profile 跑 `dsh --dump-config`(或 `dsh --profile desktop --dump-config`),看有没有本体行 `id: dsh-purge`(不是 `dsh-purge-redteam`)。没有就再跑上面的 `ensure-profile-bundle.mjs`,然后完全退出重开宿主。升到本版本后,若演练台子行已经在跑,它也会在启动时把缺的 insert 补上,再完全退出重开一次即可。开头那几条 `entry "redteam-store" not found` 只是旧红队包不在,可以忽略。
### 市场报 `already queued: dsh-purge` 时
@@ -435,6 +435,9 @@ dsh-purge --edit
/rules list | use <id> | create <id> | delete <id> | reset | help
/rewind
# 其它插件已占 /rewind(或 /purge、/rules、/skills)时,这条命令会跳过或改挂
# /purge-rewind 等备用名;主插件其余命令、工具、/dsh-purge/* 路由不受影响。
# 模型工具
purge_status purge_apply purge_revert
```
+3 -2
View File
@@ -30,9 +30,10 @@
personaPrefix: ""
personaSuffix: ""
# 本体 insert 写在用户层 profile/cordis.patch.yml(postinstall 补)。
# 这里只改已入队条目的 inject/config。再 insert 一次会 already queued(#152 / #154)。
# 本体 insert 写在用户层 profile/cordis.patch.yml(postinstall / 演练台自愈补)。
# 这里只改已入队条目的 inject/config。再 insert 一次会 already queued(#152 / #154 / #159)。
- id: dsh-purge
name: dsh-purge
inject: [systemPrompt, tools]
config:
enabled: true
+31 -8
View File
@@ -1555,6 +1555,29 @@ export function apply(ctx, config) {
}
}
function isDuplicateCommandError(error) {
return /is already registered|already registered/i.test(String(error && error.message || error));
}
/** 逐命令注册:撞名只丢那一条,可退到备用名(#162)。 */
function tryRegisterCommand(commands, spec, fallbacks = []) {
const names = [spec.name, ...fallbacks];
for (const name of names) {
try {
commands.register({ ...spec, name });
if (name !== spec.name) {
console.warn(`[dsh-purge] /${spec.name} taken, registered /${name}`);
}
return name;
} catch (error) {
if (isDuplicateCommandError(error)) continue;
throw error;
}
}
console.warn(`[dsh-purge] skip /${spec.name}: already registered`);
return null;
}
function applyHostHooks(ctx, cfg) {
try {
const home = core.pinDshHome() || core.findDshHome();
@@ -1582,25 +1605,25 @@ function applyHostHooks(ctx, cfg) {
const commands = ctx.get?.("commands");
if (commands) {
commands.register({
tryRegisterCommand(commands, {
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({
}, ["purge-cmd"]);
tryRegisterCommand(commands, {
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({
}, ["purge-rules"]);
tryRegisterCommand(commands, {
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({
}, ["purge-skills"]);
tryRegisterCommand(commands, {
name: "rewind",
description: "回退:子代理直接回退一次;主代理 /rewind once 或 /rewind round",
input: { hint: "once | round" },
@@ -1624,7 +1647,7 @@ function applyHostHooks(ctx, cfg) {
: "已回退一次。还在这条对话里,上一句已放回输入框。再发送时,之前的回复不会带上。",
};
},
});
}, ["purge-rewind"]);
}
if (ctx.tools) {
+113
View File
@@ -0,0 +1,113 @@
/**
* 用户层 profile/cordis.patch.yml 的本体 insert(#154 / #159)。
*
* 包内 overlay 只改已入队行的 inject/config,不能再 insert 一次(Hub 已入队会
* already queued)。postinstall 和演练台子行都走这里:行不存在就写一条。
*/
import fs from "node:fs";
import path from "node:path";
export const PURGE_NAME = "dsh-purge";
export const INSERT_BLOCK = [
"- insert:",
" - id: dsh-purge",
" name: dsh-purge",
" inject: [systemPrompt, tools]",
" config:",
" enabled: true",
" stripMnemon: true",
" capabilityProtocol: true",
" hooksDenyBypass: true",
"",
].join("\n");
export function profileHasPurgeBodyInsert(text) {
const lines = String(text || "").split(/\r?\n/);
let insertIndent = -1;
for (const line of lines) {
if (/^-\s*insert:\s*$/.test(line) || /^\s+-\s*insert:\s*$/.test(line)) {
insertIndent = (line.match(/^[ \t]*/) || [""])[0].length;
continue;
}
if (insertIndent < 0) continue;
const indent = (line.match(/^[ \t]*/) || [""])[0].length;
const trimmed = line.trim();
if (trimmed && indent <= insertIndent && /^-\s/.test(trimmed)) {
insertIndent = -1;
if (/^-\s*insert:\s*$/.test(trimmed)) {
insertIndent = indent;
}
continue;
}
// 本体行:id: dsh-purge,或用户按 #159 临时绕过写的 dsh-purge-main / name: dsh-purge
if (/^-\s*id:\s*dsh-purge(-main)?\s*$/.test(trimmed)) return true;
if (/^-\s*name:\s*dsh-purge\s*$/.test(trimmed)) return true;
}
return false;
}
export function ensureProfileBodyInsert(profileDir) {
const fp = path.join(profileDir, "cordis.patch.yml");
let text = "";
try { text = fs.readFileSync(fp, "utf8"); } catch { text = ""; }
if (profileHasPurgeBodyInsert(text)) {
return { ok: true, wrote: false, path: fp };
}
const prefix = text && !text.endsWith("\n") ? "\n" : "";
fs.writeFileSync(fp, `${text}${prefix}${text ? "\n" : ""}${INSERT_BLOCK}`, "utf8");
return { ok: true, wrote: true, path: fp };
}
function readJson(fp) {
try {
return JSON.parse(fs.readFileSync(fp, "utf8"));
} catch {
return null;
}
}
function profileOwnsPurge(dir) {
const pkg = readJson(path.join(dir, "package.json"));
const bundles = pkg?.dsh?.profile?.bundles;
if (Array.isArray(bundles) && bundles.includes(PURGE_NAME)) return true;
try {
return fs.existsSync(path.join(dir, "node_modules", PURGE_NAME, "package.json"));
} catch {
return false;
}
}
export function listProfileDirs(dshHome) {
const root = path.join(dshHome || "", "profiles");
const out = [];
let names = [];
try { names = fs.readdirSync(root); } catch { return out; }
for (const name of names) {
const dir = path.join(root, name);
try {
if (fs.statSync(dir).isDirectory()) out.push(dir);
} catch { /* skip */ }
}
return out;
}
/** 已装进 bundles / node_modules 但缺 insert 的 profile,补上本体行。 */
export function ensureAllProfileBodyInserts(dshHome) {
const results = [];
for (const dir of listProfileDirs(dshHome)) {
if (!profileOwnsPurge(dir)) continue;
try {
results.push({ dir, ...ensureProfileBodyInsert(dir) });
} catch (error) {
results.push({
dir,
ok: false,
wrote: false,
path: path.join(dir, "cordis.patch.yml"),
error: error && error.message ? error.message : String(error),
});
}
}
return results;
}
+11
View File
@@ -12,6 +12,7 @@ import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readPresetDefinition } from './preset-composition.js'
import { scheduleClearOwnedSelectedDefault } from '../preset-default.js'
import { ensureAllProfileBodyInserts } from '../profile-insert.js'
/** Cordis 插件名。 */
export const name = 'dsh-purge-redteam'
@@ -460,6 +461,16 @@ export function installPreset(options = {}) {
* @param ctx - 插件上下文。
*/
export function apply(ctx) {
try {
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
const healed = ensureAllProfileBodyInserts(home)
for (const item of healed) {
if (item.wrote) {
ctx.logger?.info?.('redteam-mode: 已补本体 insert %s(下次启动加载主插件)', item.path)
}
}
} catch { /* 自愈失败不挡预设落地 */ }
const paths = packagePaths()
let result
try {
+12 -41
View File
@@ -9,15 +9,13 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
PURGE_NAME as NAME,
ensureProfileBodyInsert,
profileHasPurgeBodyInsert,
} from "../lib/profile-insert.js";
const NAME = "dsh-purge";
const INSERT_BLOCK = [
"- insert:",
" - id: dsh-purge",
" name: dsh-purge",
"",
].join("\n");
export { profileHasPurgeBodyInsert };
function readJson(fp) {
try {
@@ -47,41 +45,14 @@ function findProfileDir() {
return "";
}
export function profileHasPurgeBodyInsert(text) {
const lines = String(text || "").split(/\r?\n/);
let insertIndent = -1;
for (const line of lines) {
if (/^-\s*insert:\s*$/.test(line) || /^\s+-\s*insert:\s*$/.test(line)) {
insertIndent = (line.match(/^[ \t]*/) || [""])[0].length;
continue;
}
if (insertIndent < 0) continue;
const indent = (line.match(/^[ \t]*/) || [""])[0].length;
const trimmed = line.trim();
if (trimmed && indent <= insertIndent && /^-\s/.test(trimmed)) {
insertIndent = -1;
if (/^-\s*insert:\s*$/.test(trimmed)) {
insertIndent = indent;
}
continue;
}
if (/^-\s*id:\s*dsh-purge\s*$/.test(trimmed)) return true;
}
return false;
}
function ensureProfileInsert(profileDir) {
const fp = path.join(profileDir, "cordis.patch.yml");
let text = "";
try { text = fs.readFileSync(fp, "utf8"); } catch { text = ""; }
if (profileHasPurgeBodyInsert(text)) {
process.stdout.write(`dsh-purge: insert already in ${fp}\n`);
return false;
const result = ensureProfileBodyInsert(profileDir);
if (result.wrote) {
process.stdout.write(`dsh-purge: wrote body insert (${result.path})\n`);
} else {
process.stdout.write(`dsh-purge: insert already in ${result.path}\n`);
}
const prefix = text && !text.endsWith("\n") ? "\n" : "";
fs.writeFileSync(fp, `${text}${prefix}${text ? "\n" : ""}${INSERT_BLOCK}`, "utf8");
process.stdout.write(`dsh-purge: wrote body insert (${fp})\n`);
return true;
return result.wrote;
}
function main() {