dsh-vscode-mode 0.1.47 → 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/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
  * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。