fix: 命令撞名只丢那一条;主插件 insert 自愈(#162 #159)
This commit is contained in:
1 parent
95102b0bb2
commit
18fe3b51cb
7 files changed
+179
-53
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user