dsh-vscode-mode 0.1.48 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/lib/client.js +557 -32
- package/lib/client.js.map +1 -1
- package/lib/index.js +562 -3
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- package/src/client/index.ts +9 -2
- package/src/client/paths.ts +2 -0
- package/src/client/sidebar/SidebarView.ts +82 -16
- package/src/client/sidebar/panels/RulesPanel.ts +284 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/sidebarMin.ts +48 -0
- package/src/client/styles/editor.css +43 -0
- package/src/client/ui/EditorView.ts +22 -4
- package/src/client/ui/McpSettings.ts +37 -2
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/fileOpenSettings.ts +2 -0
- package/src/index.ts +5 -1
- package/src/rpc.ts +37 -0
- package/src/rules.ts +528 -0
- package/src/shared/rpc.ts +11 -0
- package/src/shared/rules.ts +60 -0
package/lib/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { homedir, tmpdir } from "node:os";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import { inflateRawSync } from "node:zlib";
|
|
10
11
|
//#region src/mcp.ts
|
|
@@ -264,7 +265,8 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
|
|
|
264
265
|
if (!deps) return false;
|
|
265
266
|
const strategy = await runSettingsInstall(ctx, ns, deps.z.object({
|
|
266
267
|
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
267
|
-
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS })
|
|
268
|
+
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
269
|
+
sidebarMinWidth: deps.z.number().default(300)
|
|
268
270
|
}), entry, {
|
|
269
271
|
setSource: (source) => hooks.setSource(source),
|
|
270
272
|
onChange: hooks.onChange
|
|
@@ -3542,6 +3544,497 @@ async function restoreFile(ctx, session, rec) {
|
|
|
3542
3544
|
}
|
|
3543
3545
|
}
|
|
3544
3546
|
//#endregion
|
|
3547
|
+
//#region src/rules.ts
|
|
3548
|
+
/**
|
|
3549
|
+
* dsh-vscode-mode host — 规则管理(Codebuddy/Cursor 式 .mdc 规则)。
|
|
3550
|
+
* - 存储:用户规则 ~/.dsh/rules/*.mdc;项目规则 <工作区>/.dsh/rules/*.mdc(随仓库共享)。
|
|
3551
|
+
* - 格式:frontmatter(description / alwaysApply / globs / enabled)+ markdown 正文;
|
|
3552
|
+
* 类型映射 总是=always / 自动=auto(globs) / 手动=manual(仅索引);enabled 为本插件扩展字段,缺省 true。
|
|
3553
|
+
* - 生效:host 注册 systemPrompt.section(order 400),每次装配同步读取(mtime 缓存),
|
|
3554
|
+
* 项目规则按 AssembleContext.agent.id → 会话 cwd 注入;旧版 DSH 缺服务时优雅降级。
|
|
3555
|
+
* --region 划分:常量 / frontmatter 解析(纯)/ 文件名与开关(纯)/ 注入渲染(纯)/ IO / systemPrompt 装配
|
|
3556
|
+
* 作者 ddj 2026年09月03号
|
|
3557
|
+
*/
|
|
3558
|
+
/** 规则文件名白名单:字母数字开头,仅字母数字点横下划线,.mdc 后缀(天然拒绝路径分隔符)。 */
|
|
3559
|
+
const RULE_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.mdc$/;
|
|
3560
|
+
/** Windows 保留设备名(防意外创建系统设备文件)。 */
|
|
3561
|
+
const RESERVED_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
3562
|
+
/** 单目录最多加载的规则文件数(防失控目录拖垮装配)。 */
|
|
3563
|
+
const RULE_DIR_CAP = 200;
|
|
3564
|
+
/** 注入时单条规则正文字节上限(超出截断)。 */
|
|
3565
|
+
const RULE_SINGLE_CAP = 16384;
|
|
3566
|
+
/** 注入时单个作用域(用户/项目)总字节预算。 */
|
|
3567
|
+
const RULE_SCOPE_CAP = 65536;
|
|
3568
|
+
/** systemPrompt section 名(同层唯一)。 */
|
|
3569
|
+
const SECTION_NAME = "dsh-vscode-mode:rules";
|
|
3570
|
+
/** section 顺序:persona(0) 与 PLAN_POLICY(500) 之间,规则先于计划策略被读到。 */
|
|
3571
|
+
const SECTION_ORDER = 400;
|
|
3572
|
+
/**
|
|
3573
|
+
* 用户规则目录(~/.dsh/rules)。
|
|
3574
|
+
* @author ddj 2026年09月03号
|
|
3575
|
+
* @param home DSH home(缺省自动解析)
|
|
3576
|
+
* @returns 绝对路径
|
|
3577
|
+
*/
|
|
3578
|
+
function userRulesDir(home = dshHome()) {
|
|
3579
|
+
return join(home, "rules");
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* 项目规则目录(<工作区>/.dsh/rules)。
|
|
3583
|
+
* @author ddj 2026年09月03号
|
|
3584
|
+
* @param workspacePath 工作区绝对路径
|
|
3585
|
+
* @returns 绝对路径
|
|
3586
|
+
*/
|
|
3587
|
+
function projectRulesDir(workspacePath) {
|
|
3588
|
+
return join(workspacePath, ".dsh", "rules");
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* 去除值两侧成对引号。
|
|
3592
|
+
* @author ddj 2026年09月03号
|
|
3593
|
+
* @param raw 原始值
|
|
3594
|
+
* @returns 去引号后的值
|
|
3595
|
+
*/
|
|
3596
|
+
function stripQuotes(raw) {
|
|
3597
|
+
const value = raw.trim();
|
|
3598
|
+
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
|
3599
|
+
return value;
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* 解析 globs 值:内联逗号分隔串,或空值后接 YAML 短横列表。
|
|
3603
|
+
* @author ddj 2026年09月03号
|
|
3604
|
+
* @param value 内联值(可为空)
|
|
3605
|
+
* @param listLines 后续短横列表行内容(去掉 `- ` 前缀)
|
|
3606
|
+
* @returns glob 数组
|
|
3607
|
+
*/
|
|
3608
|
+
function parseGlobs(value, listLines) {
|
|
3609
|
+
const inline = stripQuotes(value);
|
|
3610
|
+
return (inline ? inline.split(",") : listLines).map((item) => stripQuotes(item)).filter(Boolean);
|
|
3611
|
+
}
|
|
3612
|
+
/**
|
|
3613
|
+
* 容错解析一条 .mdc 规则:首行 `---` 且 100 行内有闭合 `---` 才视为 frontmatter,
|
|
3614
|
+
* 否则整个文本当正文(类型 manual、enabled=true)。解析永不抛错。
|
|
3615
|
+
* @author ddj 2026年09月03号
|
|
3616
|
+
* @param text 文件全文
|
|
3617
|
+
* @returns 解析结果
|
|
3618
|
+
*/
|
|
3619
|
+
function parseRuleMdc(text) {
|
|
3620
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
3621
|
+
if (lines[0]?.trim() !== "---") return {
|
|
3622
|
+
description: "",
|
|
3623
|
+
alwaysApply: false,
|
|
3624
|
+
globs: [],
|
|
3625
|
+
enabled: true,
|
|
3626
|
+
body: text
|
|
3627
|
+
};
|
|
3628
|
+
let close = -1;
|
|
3629
|
+
for (let i = 1; i < lines.length && i <= 101; i++) if (lines[i].trim() === "---") {
|
|
3630
|
+
close = i;
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
if (close < 0) return {
|
|
3634
|
+
description: "",
|
|
3635
|
+
alwaysApply: false,
|
|
3636
|
+
globs: [],
|
|
3637
|
+
enabled: true,
|
|
3638
|
+
body: text
|
|
3639
|
+
};
|
|
3640
|
+
const result = {
|
|
3641
|
+
description: "",
|
|
3642
|
+
alwaysApply: false,
|
|
3643
|
+
globs: [],
|
|
3644
|
+
enabled: true,
|
|
3645
|
+
body: lines.slice(close + 1).join("\n")
|
|
3646
|
+
};
|
|
3647
|
+
for (let i = 1; i < close; i++) {
|
|
3648
|
+
const match = /^([A-Za-z_-]+)[ \t]*:[ \t]?(.*)$/.exec(lines[i]);
|
|
3649
|
+
if (!match) continue;
|
|
3650
|
+
const [, key, raw] = match;
|
|
3651
|
+
if (key === "description") result.description = stripQuotes(raw);
|
|
3652
|
+
else if (key === "alwaysApply") result.alwaysApply = stripQuotes(raw).toLowerCase() === "true";
|
|
3653
|
+
else if (key === "enabled") result.enabled = stripQuotes(raw).toLowerCase() !== "false";
|
|
3654
|
+
else if (key === "globs") {
|
|
3655
|
+
const listLines = [];
|
|
3656
|
+
for (let j = i + 1; j < close; j++) {
|
|
3657
|
+
const item = /^[ \t]*-[ \t]*(.+)$/.exec(lines[j]);
|
|
3658
|
+
if (!item) break;
|
|
3659
|
+
listLines.push(item[1]);
|
|
3660
|
+
}
|
|
3661
|
+
result.globs = parseGlobs(raw, listLines);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return result;
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* 由 frontmatter 推导规则类型:总是 / 自动(有 globs)/ 手动。
|
|
3668
|
+
* @author ddj 2026年09月03号
|
|
3669
|
+
* @param parsed 解析结果
|
|
3670
|
+
* @returns 规则类型
|
|
3671
|
+
*/
|
|
3672
|
+
function ruleTypeOf(parsed) {
|
|
3673
|
+
if (parsed.alwaysApply) return "always";
|
|
3674
|
+
if (parsed.globs.length) return "auto";
|
|
3675
|
+
return "manual";
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* 校验规则文件名(白名单 + Windows 保留名拒绝)。
|
|
3679
|
+
* @author ddj 2026年09月03号
|
|
3680
|
+
* @param name 文件名(含后缀)
|
|
3681
|
+
* @returns 错误文案;null=合法
|
|
3682
|
+
*/
|
|
3683
|
+
function validateRuleFile(name) {
|
|
3684
|
+
if (!RULE_FILE_RE.test(name)) return "文件名不合法:仅允许字母数字开头,含字母/数字/点/横线/下划线,.mdc 后缀";
|
|
3685
|
+
if (RESERVED_NAME_RE.test(name)) return "文件名是 Windows 保留设备名,不允许";
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* 只改写 frontmatter 的 enabled 行(保留 BOM、CRLF 与其余内容不动);无规则 frontmatter 返回 null。
|
|
3690
|
+
* @author ddj 2026年09月03号
|
|
3691
|
+
* @param text 文件全文
|
|
3692
|
+
* @param enabled 目标开关状态
|
|
3693
|
+
* @returns 改写后的全文;null=无法安全改写
|
|
3694
|
+
*/
|
|
3695
|
+
function toggleEnabledLine(text, enabled) {
|
|
3696
|
+
const hasBom = text.startsWith("");
|
|
3697
|
+
const rest = hasBom ? text.slice(1) : text;
|
|
3698
|
+
const eol = rest.includes("\r\n") ? "\r\n" : "\n";
|
|
3699
|
+
const lines = rest.split(/\r?\n/);
|
|
3700
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
3701
|
+
let close = -1;
|
|
3702
|
+
for (let i = 1; i < lines.length && i <= 101; i++) if (lines[i].trim() === "---") {
|
|
3703
|
+
close = i;
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
if (close < 0) return null;
|
|
3707
|
+
const line = "enabled: " + (enabled ? "true" : "false");
|
|
3708
|
+
let replaced = false;
|
|
3709
|
+
for (let i = 1; i < close; i++) if (/^[ \t]*enabled[ \t]*:/.test(lines[i])) {
|
|
3710
|
+
lines[i] = line;
|
|
3711
|
+
replaced = true;
|
|
3712
|
+
break;
|
|
3713
|
+
}
|
|
3714
|
+
if (!replaced) lines.splice(1, 0, line);
|
|
3715
|
+
return (hasBom ? "" : "") + lines.join(eol);
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* 渲染单条规则块:标题 + 描述 + 截断后的正文;auto 类型追加 glob 条件说明。
|
|
3719
|
+
* @author ddj 2026年09月03号
|
|
3720
|
+
* @param rule 已加载规则
|
|
3721
|
+
* @returns 文本块(含尾空行)
|
|
3722
|
+
*/
|
|
3723
|
+
function ruleBlock(rule) {
|
|
3724
|
+
let body = rule.body;
|
|
3725
|
+
if (body.length > RULE_SINGLE_CAP) body = body.slice(0, RULE_SINGLE_CAP) + "\n…(规则正文超长已截断)";
|
|
3726
|
+
const condition = rule.info.type === "auto" && rule.info.globs.length ? "(当处理匹配 " + rule.info.globs.join(", ") + " 的文件时应用)" : "";
|
|
3727
|
+
const desc = rule.info.description ? "\n" + rule.info.description : "";
|
|
3728
|
+
return "#### " + rule.info.file + condition + desc + "\n" + body + "\n";
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* 渲染一个作用域(用户/项目)的注入段:总是全文 / 自动带条件 / 手动仅索引行。
|
|
3732
|
+
* @author ddj 2026年09月03号
|
|
3733
|
+
* @param title 段标题
|
|
3734
|
+
* @param rules 已加载规则
|
|
3735
|
+
* @returns 段文本;无启用规则时返回空串
|
|
3736
|
+
*/
|
|
3737
|
+
function renderScope(title, rules) {
|
|
3738
|
+
const enabled = rules.filter((rule) => rule.info.enabled && !rule.info.error);
|
|
3739
|
+
const always = enabled.filter((rule) => rule.info.type === "always");
|
|
3740
|
+
const auto = enabled.filter((rule) => rule.info.type === "auto");
|
|
3741
|
+
const manual = enabled.filter((rule) => rule.info.type === "manual");
|
|
3742
|
+
if (!always.length && !auto.length && !manual.length) return "";
|
|
3743
|
+
const out = ["## " + title, ""];
|
|
3744
|
+
let budget = RULE_SCOPE_CAP;
|
|
3745
|
+
let omitted = false;
|
|
3746
|
+
const push = (block) => {
|
|
3747
|
+
if (block.length > budget) {
|
|
3748
|
+
omitted = true;
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
budget -= block.length;
|
|
3752
|
+
out.push(block);
|
|
3753
|
+
};
|
|
3754
|
+
if (always.length) {
|
|
3755
|
+
out.push("### 总是生效", "");
|
|
3756
|
+
for (const rule of always) push(ruleBlock(rule));
|
|
3757
|
+
}
|
|
3758
|
+
if (auto.length) {
|
|
3759
|
+
out.push("### 按文件匹配生效", "");
|
|
3760
|
+
for (const rule of auto) push(ruleBlock(rule));
|
|
3761
|
+
}
|
|
3762
|
+
if (manual.length) {
|
|
3763
|
+
out.push("### 可按需读取(未自动生效;需要时读取对应文件)", "");
|
|
3764
|
+
for (const rule of manual) {
|
|
3765
|
+
const desc = rule.info.description ? " — " + rule.info.description : "";
|
|
3766
|
+
push("- " + rule.info.file + desc + "(路径: " + rule.info.absPath + ")\n");
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
if (omitted) out.push("", "(部分规则因总长度预算被省略,请精简规则文件)");
|
|
3770
|
+
return out.join("\n") + "\n";
|
|
3771
|
+
}
|
|
3772
|
+
/**
|
|
3773
|
+
* 渲染规则注入 section 全文:用户规则段 + 项目规则段;两者皆空返回空串(空 section 无害)。
|
|
3774
|
+
* @author ddj 2026年09月03号
|
|
3775
|
+
* @param user 用户规则
|
|
3776
|
+
* @param project 当前工作区项目规则
|
|
3777
|
+
* @param workspacePath 项目段标注的工作区路径(缺省跳过项目段)
|
|
3778
|
+
* @returns 注入文本
|
|
3779
|
+
*/
|
|
3780
|
+
function renderRulesSection(user, project, workspacePath) {
|
|
3781
|
+
return [renderScope("用户规则(用户配置,必须遵守)", user), workspacePath ? renderScope("项目规则(工作区 " + workspacePath + ",必须遵守)", project) : ""].filter(Boolean).join("\n");
|
|
3782
|
+
}
|
|
3783
|
+
/** 单目录规则元信息列表(仅 *.mdc,最多 RULE_DIR_CAP 个;目录缺失返回空)。 */
|
|
3784
|
+
async function listRulesDir(dir, scope) {
|
|
3785
|
+
const mdcs = (await readdir(dir).catch(() => [])).filter((name) => name.endsWith(".mdc")).sort().slice(0, RULE_DIR_CAP);
|
|
3786
|
+
const out = [];
|
|
3787
|
+
for (const file of mdcs) {
|
|
3788
|
+
const absPath = join(dir, file);
|
|
3789
|
+
const info = await stat(absPath).catch(() => null);
|
|
3790
|
+
if (!info || !info.isFile()) continue;
|
|
3791
|
+
const parsed = parseRuleMdc(await readFile(absPath, "utf8").catch(() => ""));
|
|
3792
|
+
out.push(toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs));
|
|
3793
|
+
}
|
|
3794
|
+
return out;
|
|
3795
|
+
}
|
|
3796
|
+
/** 已注册 workspace 列表(项目 Tab 与写入校验共用)。 */
|
|
3797
|
+
function workspaceList(ctx) {
|
|
3798
|
+
return (ctx.get("workspaceRegistry")?.list?.() ?? []).filter((item) => Boolean(item) && typeof item.path === "string" && item.path !== "");
|
|
3799
|
+
}
|
|
3800
|
+
/** 用户显式 GUI 写操作的策略:danger-full-access(照抄 mcpProject.fullPolicy)。 */
|
|
3801
|
+
function fullPolicy$1(ctx) {
|
|
3802
|
+
const svc = ctx.get("sandboxPolicy");
|
|
3803
|
+
if (!svc || typeof svc.resolve !== "function") return void 0;
|
|
3804
|
+
return svc.resolve({ mode: "danger-full-access" });
|
|
3805
|
+
}
|
|
3806
|
+
/** 校验项目作用域的 workspacePath 已注册为 DSH workspace(防 RPC 写任意目录)。 */
|
|
3807
|
+
function requireWorkspace$1(ctx, workspacePath) {
|
|
3808
|
+
const workspace = workspaceList(ctx).find((item) => item.path === workspacePath);
|
|
3809
|
+
if (!workspace) throw new Error("项目未注册为 DSH workspace,不能管理项目规则");
|
|
3810
|
+
return workspace;
|
|
3811
|
+
}
|
|
3812
|
+
/** 按作用域解析规则目录(project 先过 requireWorkspace)。 */
|
|
3813
|
+
function dirOf(ctx, scope, workspacePath) {
|
|
3814
|
+
if (scope === "project") return projectRulesDir(requireWorkspace$1(ctx, workspacePath).path);
|
|
3815
|
+
return userRulesDir();
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* 规则总列表:用户规则 + 各已注册工作区的项目规则。
|
|
3819
|
+
* @author ddj 2026年09月03号
|
|
3820
|
+
* @param ctx DSH 上下文
|
|
3821
|
+
* @returns rules.list 载荷
|
|
3822
|
+
*/
|
|
3823
|
+
async function rulesList(ctx) {
|
|
3824
|
+
const user = await listRulesDir(userRulesDir(), "user");
|
|
3825
|
+
const projects = [];
|
|
3826
|
+
for (const workspace of workspaceList(ctx)) {
|
|
3827
|
+
const dir = projectRulesDir(workspace.path);
|
|
3828
|
+
const exists = existsSync(dir);
|
|
3829
|
+
const rules = exists ? await listRulesDir(dir, "project") : [];
|
|
3830
|
+
projects.push({
|
|
3831
|
+
workspacePath: workspace.path,
|
|
3832
|
+
title: workspace.title ?? "",
|
|
3833
|
+
rules,
|
|
3834
|
+
missingDir: exists ? void 0 : true
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
return {
|
|
3838
|
+
user,
|
|
3839
|
+
projects
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* 读取一条规则全文(编辑器回填用)。
|
|
3844
|
+
* @author ddj 2026年09月03号
|
|
3845
|
+
* @param ctx DSH 上下文
|
|
3846
|
+
* @param ref 作用域 + 文件名(+ 项目工作区)
|
|
3847
|
+
* @returns 文件全文
|
|
3848
|
+
*/
|
|
3849
|
+
async function rulesRead(ctx, ref) {
|
|
3850
|
+
const dir = dirOf(ctx, ref.scope, ref.workspacePath);
|
|
3851
|
+
return readFile(join(dir, ref.file), "utf8");
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* 组装规则元信息视图(rulesSave / rulesToggle / 注入缓存共用)。
|
|
3855
|
+
* @author ddj 2026年09月03号
|
|
3856
|
+
* @param scope 作用域
|
|
3857
|
+
* @param file 文件名
|
|
3858
|
+
* @param absPath 绝对路径
|
|
3859
|
+
* @param parsed 解析结果
|
|
3860
|
+
* @param size 字节数
|
|
3861
|
+
* @param mtime 修改时间毫秒
|
|
3862
|
+
* @returns 规则元信息
|
|
3863
|
+
*/
|
|
3864
|
+
function toRuleInfo(scope, file, absPath, parsed, size, mtime) {
|
|
3865
|
+
return {
|
|
3866
|
+
scope,
|
|
3867
|
+
file,
|
|
3868
|
+
absPath,
|
|
3869
|
+
relHint: scope === "project" ? ".dsh/rules/" : "rules/",
|
|
3870
|
+
description: parsed.description,
|
|
3871
|
+
type: ruleTypeOf(parsed),
|
|
3872
|
+
globs: parsed.globs,
|
|
3873
|
+
enabled: parsed.enabled,
|
|
3874
|
+
size,
|
|
3875
|
+
mtime,
|
|
3876
|
+
error: parsed.error
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* 保存(新建或覆盖)一条规则:project 作用域写盘走 ctx fs + danger-full-access(镜像 mcpProject)。
|
|
3881
|
+
* @author ddj 2026年09月03号
|
|
3882
|
+
* @param ctx DSH 上下文
|
|
3883
|
+
* @param input 保存入参
|
|
3884
|
+
* @returns 保存后的规则元信息
|
|
3885
|
+
*/
|
|
3886
|
+
async function rulesSave(ctx, input) {
|
|
3887
|
+
const invalid = validateRuleFile(input.file);
|
|
3888
|
+
if (invalid) throw new Error(invalid);
|
|
3889
|
+
const dir = dirOf(ctx, input.scope, input.workspacePath);
|
|
3890
|
+
await mkdir(dir, { recursive: true });
|
|
3891
|
+
const absPath = join(dir, input.file);
|
|
3892
|
+
const content = String(input.content ?? "");
|
|
3893
|
+
if (input.scope === "project") {
|
|
3894
|
+
const fs = ctx.get("fs");
|
|
3895
|
+
if (!fs) throw new Error("缺少 fs 服务");
|
|
3896
|
+
const target = await fs.resolve(".dsh/rules/" + input.file, { cwd: requireWorkspace$1(ctx, input.workspacePath).path });
|
|
3897
|
+
await fs.writeText(target, content, void 0, void 0, fullPolicy$1(ctx));
|
|
3898
|
+
} else await writeFile(absPath, content, "utf8");
|
|
3899
|
+
const info = await stat(absPath).catch(() => null);
|
|
3900
|
+
return toRuleInfo(input.scope, input.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now());
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* 删除一条规则文件(project 作用域同样先过 workspace 注册校验)。
|
|
3904
|
+
* @author ddj 2026年09月03号
|
|
3905
|
+
* @param ref 作用域 + 文件名(+ 项目工作区)
|
|
3906
|
+
*/
|
|
3907
|
+
async function rulesRemove(ctx, ref) {
|
|
3908
|
+
const dir = dirOf(ctx, ref.scope, ref.workspacePath);
|
|
3909
|
+
await rm(join(dir, ref.file), { force: true });
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* 切换规则启用开关:只改写 frontmatter enabled 行,其余内容零改动。
|
|
3913
|
+
* @author ddj 2026年09月03号
|
|
3914
|
+
* @param ref 作用域 + 文件名(+ 项目工作区)
|
|
3915
|
+
* @param enabled 目标状态
|
|
3916
|
+
* @returns 更新后的规则元信息
|
|
3917
|
+
*/
|
|
3918
|
+
async function rulesToggle(ctx, ref, enabled) {
|
|
3919
|
+
const dir = dirOf(ctx, ref.scope, ref.workspacePath);
|
|
3920
|
+
const absPath = join(dir, ref.file);
|
|
3921
|
+
const text = await readRuleText(ctx, ref, absPath);
|
|
3922
|
+
if (text === null) throw new Error("规则文件不存在:" + ref.file);
|
|
3923
|
+
const next = toggleEnabledLine(text, enabled);
|
|
3924
|
+
if (next === null) throw new Error("规则缺少 frontmatter(首行需为 ---),无法记录开关状态");
|
|
3925
|
+
if (next !== text) await rulesSaveContent(ctx, ref, absPath, next);
|
|
3926
|
+
return await ruleInfoOf(ref, absPath, next);
|
|
3927
|
+
}
|
|
3928
|
+
/** 按作用域读规则全文:project 与写盘同通道走 ctx fs,user 走 node fs;缺失返回 null。 */
|
|
3929
|
+
async function readRuleText(ctx, ref, absPath) {
|
|
3930
|
+
if (ref.scope !== "project") return readFile(absPath, "utf8").catch(() => null);
|
|
3931
|
+
const fs = ctx.get("fs");
|
|
3932
|
+
if (!fs) throw new Error("缺少 fs 服务");
|
|
3933
|
+
const target = await fs.resolve(".dsh/rules/" + ref.file, { cwd: requireWorkspace$1(ctx, ref.workspacePath).path });
|
|
3934
|
+
try {
|
|
3935
|
+
return await fs.readText(target);
|
|
3936
|
+
} catch {
|
|
3937
|
+
return null;
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
/** rulesToggle 的写盘通道:与 rulesSave 相同(project 走 ctx fs + fullPolicy,user 走 node fs)。 */
|
|
3941
|
+
async function rulesSaveContent(ctx, ref, absPath, content) {
|
|
3942
|
+
if (ref.scope !== "project") {
|
|
3943
|
+
await writeFile(absPath, content, "utf8");
|
|
3944
|
+
return;
|
|
3945
|
+
}
|
|
3946
|
+
const fs = ctx.get("fs");
|
|
3947
|
+
if (!fs) throw new Error("缺少 fs 服务");
|
|
3948
|
+
const target = await fs.resolve(".dsh/rules/" + ref.file, { cwd: requireWorkspace$1(ctx, ref.workspacePath).path });
|
|
3949
|
+
await fs.writeText(target, content, void 0, void 0, fullPolicy$1(ctx));
|
|
3950
|
+
}
|
|
3951
|
+
/** 重建单条规则元信息(toggle 后回传 UI;stat 失败时用内容长度 + 当前时间兜底)。 */
|
|
3952
|
+
async function ruleInfoOf(ref, absPath, content) {
|
|
3953
|
+
const info = await stat(absPath).catch(() => null);
|
|
3954
|
+
return toRuleInfo(ref.scope, ref.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now());
|
|
3955
|
+
}
|
|
3956
|
+
const injCache = /* @__PURE__ */ new Map();
|
|
3957
|
+
/**
|
|
3958
|
+
* 同步读取一个规则目录(装配 provider 内专用):仅 *.mdc,mtime 缓存,异常静默为空。
|
|
3959
|
+
* @author ddj 2026年09月03号
|
|
3960
|
+
* @param dir 规则目录
|
|
3961
|
+
* @param scope 作用域
|
|
3962
|
+
* @param relHint 相对提示
|
|
3963
|
+
* @returns 已加载规则列表
|
|
3964
|
+
*/
|
|
3965
|
+
function readRulesSync(dir, scope) {
|
|
3966
|
+
if (!existsSync(dir)) return [];
|
|
3967
|
+
let names;
|
|
3968
|
+
try {
|
|
3969
|
+
names = readdirSync(dir);
|
|
3970
|
+
} catch {
|
|
3971
|
+
return [];
|
|
3972
|
+
}
|
|
3973
|
+
const out = [];
|
|
3974
|
+
for (const file of names.filter((name) => name.endsWith(".mdc")).sort().slice(0, RULE_DIR_CAP)) {
|
|
3975
|
+
const absPath = join(dir, file);
|
|
3976
|
+
let info;
|
|
3977
|
+
try {
|
|
3978
|
+
info = statSync(absPath);
|
|
3979
|
+
} catch {
|
|
3980
|
+
continue;
|
|
3981
|
+
}
|
|
3982
|
+
if (!info.isFile()) continue;
|
|
3983
|
+
const cached = injCache.get(absPath);
|
|
3984
|
+
let parsed;
|
|
3985
|
+
if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) parsed = cached.parsed;
|
|
3986
|
+
else {
|
|
3987
|
+
try {
|
|
3988
|
+
parsed = parseRuleMdc(readFileSync(absPath, "utf8"));
|
|
3989
|
+
} catch {
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
injCache.set(absPath, {
|
|
3993
|
+
mtimeMs: info.mtimeMs,
|
|
3994
|
+
size: info.size,
|
|
3995
|
+
parsed
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
out.push({
|
|
3999
|
+
info: toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs),
|
|
4000
|
+
body: parsed.body
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
return out;
|
|
4004
|
+
}
|
|
4005
|
+
/** 从装配上下文解析会话工作区 cwd(agent.id → sessions → header.cwd;缺链返回 null)。 */
|
|
4006
|
+
function cwdFromAssemble(ctx, asm) {
|
|
4007
|
+
const id = asm?.agent?.id;
|
|
4008
|
+
if (typeof id !== "string" || !id) return null;
|
|
4009
|
+
const cwd = (ctx.get("sessions")?.get?.(id))?.header?.cwd;
|
|
4010
|
+
return typeof cwd === "string" && cwd ? cwd : null;
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* 安装规则注入 section(可选探测 systemPrompt 服务;失败/缺失返回 false,不致命)。
|
|
4014
|
+
* @author ddj 2026年09月03号
|
|
4015
|
+
* @param ctx DSH host 上下文
|
|
4016
|
+
* @returns 是否成功注册
|
|
4017
|
+
*/
|
|
4018
|
+
function installRulesSection(ctx) {
|
|
4019
|
+
const sp = ctx.get("systemPrompt");
|
|
4020
|
+
if (!sp || typeof sp.section !== "function") return false;
|
|
4021
|
+
const provider = (asm) => {
|
|
4022
|
+
try {
|
|
4023
|
+
const user = readRulesSync(userRulesDir(), "user");
|
|
4024
|
+
const cwd = cwdFromAssemble(ctx, asm);
|
|
4025
|
+
return renderRulesSection(user, cwd ? readRulesSync(projectRulesDir(cwd), "project") : [], cwd ?? void 0);
|
|
4026
|
+
} catch {
|
|
4027
|
+
return "";
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
ctx.effect(() => sp.section({
|
|
4031
|
+
name: SECTION_NAME,
|
|
4032
|
+
order: SECTION_ORDER,
|
|
4033
|
+
text: provider
|
|
4034
|
+
}), "vscode-mode: rules section");
|
|
4035
|
+
return true;
|
|
4036
|
+
}
|
|
4037
|
+
//#endregion
|
|
3545
4038
|
//#region src/mcpProject.ts
|
|
3546
4039
|
/**
|
|
3547
4040
|
* dsh-vscode-mode host — 项目级 MCP 管理。
|
|
@@ -5331,6 +5824,69 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
5331
5824
|
error: "撤销压缩配置失败:" + String(error)
|
|
5332
5825
|
};
|
|
5333
5826
|
}
|
|
5827
|
+
},
|
|
5828
|
+
"rules.list": async () => {
|
|
5829
|
+
try {
|
|
5830
|
+
return {
|
|
5831
|
+
ok: true,
|
|
5832
|
+
...await rulesList(ctx)
|
|
5833
|
+
};
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
return {
|
|
5836
|
+
ok: false,
|
|
5837
|
+
error: "读取规则失败:" + String(error)
|
|
5838
|
+
};
|
|
5839
|
+
}
|
|
5840
|
+
},
|
|
5841
|
+
"rules.read": async (args) => {
|
|
5842
|
+
try {
|
|
5843
|
+
return {
|
|
5844
|
+
ok: true,
|
|
5845
|
+
content: await rulesRead(ctx, args)
|
|
5846
|
+
};
|
|
5847
|
+
} catch (error) {
|
|
5848
|
+
return {
|
|
5849
|
+
ok: false,
|
|
5850
|
+
error: "读取规则失败:" + String(error)
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
},
|
|
5854
|
+
"rules.save": async (args) => {
|
|
5855
|
+
try {
|
|
5856
|
+
return {
|
|
5857
|
+
ok: true,
|
|
5858
|
+
rule: await rulesSave(ctx, args)
|
|
5859
|
+
};
|
|
5860
|
+
} catch (error) {
|
|
5861
|
+
return {
|
|
5862
|
+
ok: false,
|
|
5863
|
+
error: "保存规则失败:" + String(error)
|
|
5864
|
+
};
|
|
5865
|
+
}
|
|
5866
|
+
},
|
|
5867
|
+
"rules.remove": async (args) => {
|
|
5868
|
+
try {
|
|
5869
|
+
await rulesRemove(ctx, args);
|
|
5870
|
+
return { ok: true };
|
|
5871
|
+
} catch (error) {
|
|
5872
|
+
return {
|
|
5873
|
+
ok: false,
|
|
5874
|
+
error: "删除规则失败:" + String(error)
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
},
|
|
5878
|
+
"rules.toggle": async (args) => {
|
|
5879
|
+
try {
|
|
5880
|
+
return {
|
|
5881
|
+
ok: true,
|
|
5882
|
+
rule: await rulesToggle(ctx, args, args.enabled === true)
|
|
5883
|
+
};
|
|
5884
|
+
} catch (error) {
|
|
5885
|
+
return {
|
|
5886
|
+
ok: false,
|
|
5887
|
+
error: "切换规则失败:" + String(error)
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5334
5890
|
}
|
|
5335
5891
|
};
|
|
5336
5892
|
}
|
|
@@ -8340,6 +8896,9 @@ function apply(ctx, config) {
|
|
|
8340
8896
|
const warnings = [];
|
|
8341
8897
|
setupOpenSettings(ctx, config, () => {});
|
|
8342
8898
|
installLspSettingsSection(ctx, {});
|
|
8899
|
+
/** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
|
|
8900
|
+
const rulesInstalled = installRulesSection(ctx);
|
|
8901
|
+
if (!rulesInstalled) ctx.logger?.warn?.("[" + name + "] 未检测到 systemPrompt 服务,规则仅可管理不注入");
|
|
8343
8902
|
/** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
|
|
8344
8903
|
const lspRpc = createLspRpc({
|
|
8345
8904
|
ctx,
|
|
@@ -8370,7 +8929,7 @@ function apply(ctx, config) {
|
|
|
8370
8929
|
lspManager.disposeAll().catch(() => {});
|
|
8371
8930
|
disposeAllServers();
|
|
8372
8931
|
});
|
|
8373
|
-
ctx.logger?.info?.("[" + name + "] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP
|
|
8932
|
+
ctx.logger?.info?.("[" + name + "] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ")");
|
|
8374
8933
|
}
|
|
8375
8934
|
/**
|
|
8376
8935
|
* 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
|