dsh-vscode-mode 0.22.0 → 0.23.0
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 +5 -3
- package/lib/client.js +3370 -85
- package/lib/client.js.map +2 -2
- package/lib/index.js +1167 -266
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +166 -0
- package/src/client/quickMenuHost.ts +46 -0
- package/src/client/quickMenuStore.ts +260 -0
- package/src/client/state/diffSummary.ts +9 -4
- package/src/client/styles/quickmenu.css +1049 -0
- package/src/client/ui/QuickMenuDialog.tsx +835 -0
- package/src/client/ui/QuickMenuLauncher.tsx +792 -0
- package/src/client/ui/QuickMenuPickers.ts +577 -0
- package/src/index.ts +2 -2
- package/src/paths.ts +10 -0
- package/src/pending/capture.ts +104 -75
- package/src/pending/persist.ts +6 -1
- package/src/pending/store.ts +10 -5
- package/src/pending/vcs.ts +2 -2
- package/src/pendingCompat.ts +3 -8
- package/src/pendingRpc.ts +140 -58
- package/src/quickMenu.ts +269 -0
- package/src/rpc.ts +37 -0
- package/src/shared/pathKey.ts +54 -0
- package/src/shared/quickMenu.ts +1009 -0
- package/src/shared/quickPick.ts +67 -0
- package/src/shared/rpc.ts +10 -0
- package/src/shared/skillCatalog.ts +60 -0
- package/src/skillCatalog.ts +138 -0
package/lib/index.js
CHANGED
|
@@ -43,7 +43,7 @@ const RESOURCE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
43
43
|
* @param path Workspace or agent cwd.
|
|
44
44
|
* @returns Comparable path with a stable root representation.
|
|
45
45
|
*/
|
|
46
|
-
function normalizePath(path) {
|
|
46
|
+
function normalizePath$1(path) {
|
|
47
47
|
const raw = String(path ?? "").replace(/\\/g, "/");
|
|
48
48
|
const windows = /^[a-z]:/i.test(raw) || raw.startsWith("//");
|
|
49
49
|
const value = (windows ? win32.normalize(raw) : posix.normalize(raw)).replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
@@ -58,12 +58,12 @@ function normalizePath(path) {
|
|
|
58
58
|
*/
|
|
59
59
|
function matchWorkspace(cwd, paths) {
|
|
60
60
|
if (!cwd) return void 0;
|
|
61
|
-
const target = normalizePath(cwd);
|
|
61
|
+
const target = normalizePath$1(cwd);
|
|
62
62
|
const matches = paths.filter((path) => {
|
|
63
|
-
const root = normalizePath(path);
|
|
63
|
+
const root = normalizePath$1(path);
|
|
64
64
|
return target === root || target.startsWith(root === "/" ? root : root + "/");
|
|
65
65
|
});
|
|
66
|
-
matches.sort((a, b) => normalizePath(b).length - normalizePath(a).length);
|
|
66
|
+
matches.sort((a, b) => normalizePath$1(b).length - normalizePath$1(a).length);
|
|
67
67
|
return matches[0];
|
|
68
68
|
}
|
|
69
69
|
const isProjectEntry = isProjectEntryId;
|
|
@@ -684,6 +684,15 @@ function debugLogFile(cwd, home = dshHome()) {
|
|
|
684
684
|
function archiveDbFile(cwd, home = dshHome()) {
|
|
685
685
|
return join(home, PLUGIN_ID, "data", "archive", hashOf(cwd) + ".sqlite");
|
|
686
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* 快捷菜单配置文本(用户数据,不受缓存清理预算约束,跨工作区共享一份)。
|
|
689
|
+
* @author ddj 2026年10月30号
|
|
690
|
+
* @param home DSH home(测试可注入)
|
|
691
|
+
* @returns 快捷菜单配置绝对路径
|
|
692
|
+
*/
|
|
693
|
+
function quickMenuFile(home = dshHome()) {
|
|
694
|
+
return join(home, PLUGIN_ID, "data", "quick-menu.json");
|
|
695
|
+
}
|
|
687
696
|
/** 插件包 assets 目录(import.meta.url 派生;config.imageDir 可覆盖,见 imageDirOf)。 */
|
|
688
697
|
function assetsDirOf(moduleUrl) {
|
|
689
698
|
return join(dirname(fileURLToPath(moduleUrl)), "..", "assets");
|
|
@@ -5418,7 +5427,7 @@ const SKILL_PREFIXES = ["dsh-vscodemode-"];
|
|
|
5418
5427
|
/** 技能发现来源标签(skill-explorer 归入 "System bundled" 组)。 */
|
|
5419
5428
|
const SKILL_SOURCE = "bundled";
|
|
5420
5429
|
/** 目录式技能的文件名。 */
|
|
5421
|
-
const SKILL_FILE = "SKILL.md";
|
|
5430
|
+
const SKILL_FILE$1 = "SKILL.md";
|
|
5422
5431
|
/** DSH 技能名语法(与 @deepseek-ai/dsh-skill 的 SKILL_NAME 一致:下划线非法)。 */
|
|
5423
5432
|
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5424
5433
|
/** 不受支持的遗留字段 → 规范字段(官方同样拒绝,避免写错键位却静默无效果)。 */
|
|
@@ -5596,7 +5605,7 @@ function hasGroupPrefix(name) {
|
|
|
5596
5605
|
* @returns 候选文件绝对路径;不构成技能时返回 undefined
|
|
5597
5606
|
*/
|
|
5598
5607
|
function skillFileOf(dir, entry) {
|
|
5599
|
-
if (entry.isDirectory()) return join(dir, entry.name, SKILL_FILE);
|
|
5608
|
+
if (entry.isDirectory()) return join(dir, entry.name, SKILL_FILE$1);
|
|
5600
5609
|
if (entry.isFile() && entry.name.endsWith(".md")) return join(dir, entry.name);
|
|
5601
5610
|
}
|
|
5602
5611
|
/**
|
|
@@ -8139,97 +8148,107 @@ const WRITE_TOOLS = [
|
|
|
8139
8148
|
/** 单文件文本上限:超过即标注 oversized,不静默截断。 */
|
|
8140
8149
|
const MAX_TEXT = 1048576;
|
|
8141
8150
|
/**
|
|
8142
|
-
* @author ddj 2026年10月
|
|
8151
|
+
* @author ddj 2026年10月30号
|
|
8143
8152
|
* @description: 安装写文件采集;读写失败只影响待审来源,绝不影响用户工具本身。
|
|
8144
8153
|
* @param ctx Host 生命周期
|
|
8145
8154
|
* @param sink 已确认写入的接收方
|
|
8146
8155
|
* @returns 可释放的采集器
|
|
8147
8156
|
*/
|
|
8148
|
-
function
|
|
8149
|
-
const
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8157
|
+
function installPending(ctx, sink) {
|
|
8158
|
+
const state = {
|
|
8159
|
+
busy: /* @__PURE__ */ new Set(),
|
|
8160
|
+
stopped: false
|
|
8161
|
+
};
|
|
8162
|
+
ctx.on("tools/execute", (exec, next) => aroundTool(ctx, state, sink, exec, next));
|
|
8163
|
+
return { dispose() {
|
|
8164
|
+
state.stopped = true;
|
|
8165
|
+
state.busy.clear();
|
|
8166
|
+
} };
|
|
8167
|
+
}
|
|
8168
|
+
/**
|
|
8169
|
+
* @author ddj 2026年10月30号
|
|
8170
|
+
* @description: 判定本次调用是否值得采集;不值得时返回 null,调用方直接透传原始分发。
|
|
8171
|
+
* @param ctx Host 生命周期
|
|
8172
|
+
* @param state 采集状态
|
|
8173
|
+
* @param exec 工具分发上下文
|
|
8174
|
+
* @returns 采集计划或 null
|
|
8175
|
+
*/
|
|
8176
|
+
async function planOf(ctx, state, exec) {
|
|
8177
|
+
const session = exec.agent?.session;
|
|
8178
|
+
const requested = exec.arguments?.file_path ?? exec.arguments?.path;
|
|
8179
|
+
if (state.stopped || !session?.id || typeof requested !== "string") return null;
|
|
8180
|
+
if (!WRITE_TOOLS.includes(String(exec.name))) return null;
|
|
8181
|
+
const cwd = cwdOf(session);
|
|
8182
|
+
if (!cwd) return null;
|
|
8183
|
+
const fs = ctx.get("fs");
|
|
8184
|
+
const target = await resolveTarget(ctx, session, requested);
|
|
8185
|
+
const identity = fs.processPath(target);
|
|
8186
|
+
if (typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
|
|
8187
|
+
const before = await readText(fs, target);
|
|
8188
|
+
if (before !== null && before.length > MAX_TEXT) return null;
|
|
8189
|
+
return {
|
|
8190
|
+
cwd: String(cwd),
|
|
8191
|
+
sessionId: String(session.id),
|
|
8192
|
+
path: requested,
|
|
8193
|
+
identity,
|
|
8194
|
+
before,
|
|
8195
|
+
session
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
/**
|
|
8199
|
+
* @author ddj 2026年10月30号
|
|
8200
|
+
* @description: 写工具执行前后各读一次,内容变化才上报;任何采集异常都不改变工具结果。
|
|
8201
|
+
* @param ctx Host 生命周期
|
|
8202
|
+
* @param state 采集状态
|
|
8203
|
+
* @param sink 已确认写入的接收方
|
|
8204
|
+
* @param exec 工具分发上下文
|
|
8205
|
+
* @param next 原始分发
|
|
8206
|
+
* @returns 原始工具结果
|
|
8207
|
+
*/
|
|
8208
|
+
async function aroundTool(ctx, state, sink, exec, next) {
|
|
8209
|
+
let plan = null;
|
|
8210
|
+
try {
|
|
8211
|
+
plan = await planOf(ctx, state, exec);
|
|
8212
|
+
} catch (error) {
|
|
8213
|
+
log.debug("待审采集跳过:" + String(error));
|
|
8214
|
+
}
|
|
8215
|
+
if (!plan) return next();
|
|
8216
|
+
state.busy.add(plan.identity);
|
|
8217
|
+
try {
|
|
8218
|
+
return await next();
|
|
8219
|
+
} finally {
|
|
8220
|
+
state.busy.delete(plan.identity);
|
|
8168
8221
|
try {
|
|
8169
|
-
|
|
8170
|
-
const target = await resolveTarget(ctx, session, path);
|
|
8171
|
-
const key = fs.processPath(target);
|
|
8172
|
-
if (typeof key !== "string" || !key) return next();
|
|
8173
|
-
if (busy.has(key)) return next();
|
|
8174
|
-
before = await readText(fs, target);
|
|
8175
|
-
if (before !== null && before.length > MAX_TEXT) return next();
|
|
8176
|
-
identity = key;
|
|
8177
|
-
busy.add(key);
|
|
8222
|
+
reportWrite(ctx, sink, plan);
|
|
8178
8223
|
} catch (error) {
|
|
8179
|
-
log.debug("
|
|
8180
|
-
return next();
|
|
8181
|
-
}
|
|
8182
|
-
try {
|
|
8183
|
-
return await next();
|
|
8184
|
-
} finally {
|
|
8185
|
-
if (identity !== null) {
|
|
8186
|
-
const key = identity;
|
|
8187
|
-
try {
|
|
8188
|
-
report(String(cwd), String(session.id), String(path), key, before, exec);
|
|
8189
|
-
} catch (error) {
|
|
8190
|
-
log.debug("待审上报失败:" + String(error));
|
|
8191
|
-
}
|
|
8192
|
-
busy.delete(key);
|
|
8193
|
-
}
|
|
8224
|
+
log.debug("待审上报失败:" + String(error));
|
|
8194
8225
|
}
|
|
8195
8226
|
}
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
const
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
});
|
|
8222
|
-
}).catch((error) => log.debug("待审读回失败:" + String(error)));
|
|
8223
|
-
}
|
|
8224
|
-
/**
|
|
8225
|
-
* @author ddj 2026年10月29号
|
|
8226
|
-
* @description: 释放监听。
|
|
8227
|
-
* @returns 无
|
|
8228
|
-
*/
|
|
8229
|
-
function dispose() {
|
|
8230
|
-
stopped = true;
|
|
8231
|
-
busy.clear();
|
|
8232
|
-
}
|
|
8227
|
+
}
|
|
8228
|
+
/**
|
|
8229
|
+
* @author ddj 2026年10月29号
|
|
8230
|
+
* @description: 写后读回并比较,内容一致视为无净变化不产生条目。
|
|
8231
|
+
* @param ctx Host 生命周期
|
|
8232
|
+
* @param sink 已确认写入的接收方
|
|
8233
|
+
* @param plan 采集计划
|
|
8234
|
+
* @returns 无;读回失败只记日志
|
|
8235
|
+
*/
|
|
8236
|
+
function reportWrite(ctx, sink, plan) {
|
|
8237
|
+
const fs = ctx.get("fs");
|
|
8238
|
+
resolveTarget(ctx, plan.session, plan.path).then(async (target) => {
|
|
8239
|
+
const after = await readText(fs, target);
|
|
8240
|
+
if (after !== null && after.length > MAX_TEXT) return;
|
|
8241
|
+
if (plan.before === after) return;
|
|
8242
|
+
sink({
|
|
8243
|
+
cwd: plan.cwd,
|
|
8244
|
+
sessionId: plan.sessionId,
|
|
8245
|
+
path: plan.path,
|
|
8246
|
+
identity: plan.identity,
|
|
8247
|
+
before: plan.before,
|
|
8248
|
+
after,
|
|
8249
|
+
now: Date.now()
|
|
8250
|
+
});
|
|
8251
|
+
}).catch((error) => log.debug("待审读回失败:" + String(error)));
|
|
8233
8252
|
}
|
|
8234
8253
|
/**
|
|
8235
8254
|
* @author ddj 2026年10月29号
|
|
@@ -9300,7 +9319,12 @@ CREATE TABLE IF NOT EXISTS pending_history (
|
|
|
9300
9319
|
snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
9301
9320
|
state TEXT NOT NULL DEFAULT 'applied');
|
|
9302
9321
|
`;
|
|
9303
|
-
/**
|
|
9322
|
+
/**
|
|
9323
|
+
* @author ddj 2026年10月29号
|
|
9324
|
+
* @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
|
|
9325
|
+
* @param db 已打开的工作区库
|
|
9326
|
+
* @returns 无
|
|
9327
|
+
*/
|
|
9304
9328
|
function migrateHistory(db) {
|
|
9305
9329
|
if (db.prepare("PRAGMA table_info(pending_history)").all().some((column) => column.name === "state")) return;
|
|
9306
9330
|
db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'");
|
|
@@ -9502,7 +9526,12 @@ function flagsOfRow(row) {
|
|
|
9502
9526
|
}
|
|
9503
9527
|
//#endregion
|
|
9504
9528
|
//#region src/pending/store.ts
|
|
9505
|
-
/**
|
|
9529
|
+
/**
|
|
9530
|
+
* @author ddj 2026年10月29号
|
|
9531
|
+
* @description: 统计一次差异的增删行数;非文本差异统一记 0/0。
|
|
9532
|
+
* @param entry 待审条目
|
|
9533
|
+
* @returns 增删行数
|
|
9534
|
+
*/
|
|
9506
9535
|
function countsOf(entry) {
|
|
9507
9536
|
const diff = diffOf(entry);
|
|
9508
9537
|
if (diff.kind !== "text") return {
|
|
@@ -9739,11 +9768,11 @@ function writerFor(fs, cwd, identity, expected, signal, policy, allowCreate = fa
|
|
|
9739
9768
|
ok: false,
|
|
9740
9769
|
reason: "diverged"
|
|
9741
9770
|
};
|
|
9742
|
-
const
|
|
9771
|
+
const currentStat = await fs.stat(target, signal).catch(() => void 0);
|
|
9743
9772
|
try {
|
|
9744
|
-
await fs.writeText(target, content ?? "",
|
|
9773
|
+
await fs.writeText(target, content ?? "", currentStat ? {
|
|
9745
9774
|
kind: "replaceIfVersion",
|
|
9746
|
-
version:
|
|
9775
|
+
version: currentStat.version
|
|
9747
9776
|
} : void 0, signal, policy);
|
|
9748
9777
|
const readBack = await readText(fs, target);
|
|
9749
9778
|
if ((content ?? "") !== (readBack ?? "")) return {
|
|
@@ -10014,6 +10043,39 @@ function reasonOf(error) {
|
|
|
10014
10043
|
return typeof code === "string" ? code : String(error);
|
|
10015
10044
|
}
|
|
10016
10045
|
//#endregion
|
|
10046
|
+
//#region src/shared/pathKey.ts
|
|
10047
|
+
/**
|
|
10048
|
+
* 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
|
|
10049
|
+
* @author ddj 2026年10月30号
|
|
10050
|
+
*/
|
|
10051
|
+
/** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
|
|
10052
|
+
const REMOTE = /^[a-z]+:\/\//i;
|
|
10053
|
+
/** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
|
|
10054
|
+
const DRIVE = /^[a-z]:\//i;
|
|
10055
|
+
/**
|
|
10056
|
+
* @author ddj 2026年10月30号
|
|
10057
|
+
* @description: 统一分隔符为 `/`,其余字符原样保留。
|
|
10058
|
+
* @param path 原始路径
|
|
10059
|
+
* @returns 反斜杠已归一化的路径
|
|
10060
|
+
*/
|
|
10061
|
+
function unifiedPath(path) {
|
|
10062
|
+
return path.replace(/\\/g, "/");
|
|
10063
|
+
}
|
|
10064
|
+
/**
|
|
10065
|
+
* @author ddj 2026年10月30号
|
|
10066
|
+
* @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
|
|
10067
|
+
* (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
|
|
10068
|
+
* @param path 原始路径
|
|
10069
|
+
* @param foldCase 非盘符路径是否折叠大小写
|
|
10070
|
+
* @returns 比较键
|
|
10071
|
+
*/
|
|
10072
|
+
function pathKeyFor(path, foldCase) {
|
|
10073
|
+
const unified = unifiedPath(path);
|
|
10074
|
+
if (REMOTE.test(unified)) return unified;
|
|
10075
|
+
if (DRIVE.test(unified)) return unified.toLowerCase();
|
|
10076
|
+
return foldCase ? unified.toLowerCase() : unified;
|
|
10077
|
+
}
|
|
10078
|
+
//#endregion
|
|
10017
10079
|
//#region src/shared/pendingReview.ts
|
|
10018
10080
|
/** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
|
|
10019
10081
|
const PENDING_PREFIX = "pending:";
|
|
@@ -10037,15 +10099,12 @@ function stableSeq(id) {
|
|
|
10037
10099
|
}
|
|
10038
10100
|
/**
|
|
10039
10101
|
* @author ddj 2026年10月29号
|
|
10040
|
-
* @description:
|
|
10102
|
+
* @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
|
|
10041
10103
|
* @param path 路径
|
|
10042
10104
|
* @returns 比较键
|
|
10043
10105
|
*/
|
|
10044
10106
|
function pathKey$1(path) {
|
|
10045
|
-
|
|
10046
|
-
if (/^[a-z]+:\/\//i.test(unified)) return unified;
|
|
10047
|
-
if (/^[a-z]:\//i.test(unified)) return unified.toLowerCase();
|
|
10048
|
-
return process.platform === "win32" ? unified.toLowerCase() : unified;
|
|
10107
|
+
return pathKeyFor(path, process.platform === "win32");
|
|
10049
10108
|
}
|
|
10050
10109
|
/**
|
|
10051
10110
|
* @author ddj 2026年10月30号
|
|
@@ -12208,6 +12267,131 @@ function installRulesSection(ctx, dirs = userRuleDirs()) {
|
|
|
12208
12267
|
return true;
|
|
12209
12268
|
}
|
|
12210
12269
|
//#endregion
|
|
12270
|
+
//#region src/skillCatalog.ts
|
|
12271
|
+
/**
|
|
12272
|
+
* dsh-vscode-mode host — 技能候选枚举(全局技能 + 当前工作区技能)。
|
|
12273
|
+
*
|
|
12274
|
+
* 为什么要另起一条通道:本插件原有的 `skills.ts` 只管「随包分发的技能组」
|
|
12275
|
+
* (<包根>/skills,按 SKILL_PREFIXES 前缀白名单过滤);而快捷菜单「绑定技能」需要的是
|
|
12276
|
+
* 用户能自由增删的两类技能目录,与 DSH 文件系统 skill provider 的两个根一致:
|
|
12277
|
+
* - 全局技能:`$DSH_HOME/skills`(provider rank 400 / user-dsh)
|
|
12278
|
+
* - 工作区技能:`<projectRoot>/.dsh/skills`(provider rank 100 / project-dsh)
|
|
12279
|
+
* 只读、不写盘、不缓存:每次打开配置弹窗重扫一遍(目录数量级很小)。
|
|
12280
|
+
*
|
|
12281
|
+
* 作者 ddj 2026年10月30号
|
|
12282
|
+
*/
|
|
12283
|
+
/** 单个根目录最多枚举的技能数(防失控目录拖垮弹窗)。 */
|
|
12284
|
+
const SKILL_CATALOG_CAP = 200;
|
|
12285
|
+
/** 向上找项目根的层数上限(防异常路径下的长链遍历)。 */
|
|
12286
|
+
const ROOT_DEPTH_CAP = 8;
|
|
12287
|
+
/** 目录式技能的清单文件名。 */
|
|
12288
|
+
const SKILL_FILE = "SKILL.md";
|
|
12289
|
+
/**
|
|
12290
|
+
* 从 cwd 向上找项目根(含 .git 的最近祖先;找不到回落 cwd)。
|
|
12291
|
+
* @author ddj 2026年10月30号
|
|
12292
|
+
* @param cwd 起始目录
|
|
12293
|
+
* @returns 项目根绝对路径;cwd 缺失时 undefined
|
|
12294
|
+
*/
|
|
12295
|
+
function projectRootOf(cwd) {
|
|
12296
|
+
if (cwd === void 0 || cwd === "") return void 0;
|
|
12297
|
+
let current = cwd;
|
|
12298
|
+
for (let depth = 0; depth < ROOT_DEPTH_CAP; depth += 1) {
|
|
12299
|
+
const parent = join(current, "..");
|
|
12300
|
+
if (entryExists(join(current, ".git"))) return current;
|
|
12301
|
+
if (parent === current) break;
|
|
12302
|
+
current = parent;
|
|
12303
|
+
}
|
|
12304
|
+
return cwd;
|
|
12305
|
+
}
|
|
12306
|
+
/**
|
|
12307
|
+
* 探测路径是否存在(同步存在性判断,避免为一次 up-walk 引入异步瀑布)。
|
|
12308
|
+
* @author ddj 2026年10月30号
|
|
12309
|
+
* @param path 绝对路径
|
|
12310
|
+
* @returns 存在为 true
|
|
12311
|
+
*/
|
|
12312
|
+
function entryExists(path) {
|
|
12313
|
+
try {
|
|
12314
|
+
statSync(path);
|
|
12315
|
+
return true;
|
|
12316
|
+
} catch {
|
|
12317
|
+
return false;
|
|
12318
|
+
}
|
|
12319
|
+
}
|
|
12320
|
+
/**
|
|
12321
|
+
* 技能根目录(全局 + 当前工作区)。
|
|
12322
|
+
* @author ddj 2026年10月30号
|
|
12323
|
+
* @param cwd 当前工作区
|
|
12324
|
+
* @returns 两个根的绝对路径
|
|
12325
|
+
*/
|
|
12326
|
+
function skillRootsOf(cwd) {
|
|
12327
|
+
const projectRoot = projectRootOf(cwd);
|
|
12328
|
+
const project = projectRoot === void 0 ? "" : join(projectRoot, ".dsh", "skills");
|
|
12329
|
+
return {
|
|
12330
|
+
user: join(dshHome(), "skills"),
|
|
12331
|
+
project,
|
|
12332
|
+
projectRoot
|
|
12333
|
+
};
|
|
12334
|
+
}
|
|
12335
|
+
/**
|
|
12336
|
+
* 扫描单个技能根(深度 1:<root>/<名>/SKILL.md 或 <root>/<名>.md)。
|
|
12337
|
+
* 目录缺失/读失败/frontmatter 非法 → 跳过该条(不抛、不返回半成品)。
|
|
12338
|
+
* @author ddj 2026年10月30号
|
|
12339
|
+
* @param dir 技能根目录
|
|
12340
|
+
* @param source 来源标记
|
|
12341
|
+
* @returns 通过校验的技能(按名字排序,受 SKILL_CATALOG_CAP 约束)
|
|
12342
|
+
*/
|
|
12343
|
+
async function listRootSkills(dir, source) {
|
|
12344
|
+
if (dir === "") return [];
|
|
12345
|
+
const ordered = [...await readdir(dir, { withFileTypes: true }).catch(() => [])].sort((left, right) => left.name.localeCompare(right.name)).slice(0, SKILL_CATALOG_CAP);
|
|
12346
|
+
const found = [];
|
|
12347
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12348
|
+
for (const entry of ordered) {
|
|
12349
|
+
const info = await readSkillInfo(entry.name.endsWith(".md") ? join(dir, entry.name) : join(dir, entry.name, SKILL_FILE), source).catch(() => void 0);
|
|
12350
|
+
if (info === void 0 || seen.has(info.name)) continue;
|
|
12351
|
+
seen.add(info.name);
|
|
12352
|
+
found.push(info);
|
|
12353
|
+
}
|
|
12354
|
+
return found.sort((left, right) => left.name.localeCompare(right.name));
|
|
12355
|
+
}
|
|
12356
|
+
/**
|
|
12357
|
+
* 读取并解析单个技能文件。
|
|
12358
|
+
* @author ddj 2026年10月30号
|
|
12359
|
+
* @param file 技能文件绝对路径
|
|
12360
|
+
* @param source 来源标记
|
|
12361
|
+
* @returns 技能元信息;文件缺失/不可读/frontmatter 非法时 undefined
|
|
12362
|
+
*/
|
|
12363
|
+
async function readSkillInfo(file, source) {
|
|
12364
|
+
const text = await readFile(file, "utf8").catch(() => void 0);
|
|
12365
|
+
if (text === void 0) return void 0;
|
|
12366
|
+
const parsed = parseSkillMd(text);
|
|
12367
|
+
if (!isParsedSkill(parsed)) return void 0;
|
|
12368
|
+
return {
|
|
12369
|
+
name: parsed.name,
|
|
12370
|
+
description: parsed.description,
|
|
12371
|
+
path: file,
|
|
12372
|
+
source
|
|
12373
|
+
};
|
|
12374
|
+
}
|
|
12375
|
+
/**
|
|
12376
|
+
* 枚举技能候选(全局 + 当前工作区;只读,永不抛)。
|
|
12377
|
+
* @author ddj 2026年10月30号
|
|
12378
|
+
* @param cwd 当前工作区(缺失时只枚举全局根)
|
|
12379
|
+
* @returns 技能候选快照
|
|
12380
|
+
*/
|
|
12381
|
+
async function listSkillCatalog(cwd) {
|
|
12382
|
+
const roots = skillRootsOf(cwd);
|
|
12383
|
+
const user = await listRootSkills(roots.user, "user");
|
|
12384
|
+
const project = await listRootSkills(roots.project, "project");
|
|
12385
|
+
return roots.projectRoot === void 0 ? {
|
|
12386
|
+
user,
|
|
12387
|
+
project
|
|
12388
|
+
} : {
|
|
12389
|
+
user,
|
|
12390
|
+
project,
|
|
12391
|
+
projectRoot: roots.projectRoot
|
|
12392
|
+
};
|
|
12393
|
+
}
|
|
12394
|
+
//#endregion
|
|
12211
12395
|
//#region src/snippets.ts
|
|
12212
12396
|
/**
|
|
12213
12397
|
* dsh-vscode-mode host — 代码片段管理(VS Code 兼容 .code-snippets 文件)。
|
|
@@ -12564,106 +12748,687 @@ async function snippetsEntries(ctx, workspacePath) {
|
|
|
12564
12748
|
if (workspacePath && registered) await load(projectSnippetsDir(workspacePath), "project");
|
|
12565
12749
|
return { entries: flattenSnippetEntries(loaded) };
|
|
12566
12750
|
}
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
const
|
|
12751
|
+
/** 提示词长度上限。 */
|
|
12752
|
+
const QUICK_MENU_PROMPT_CAP = 8192;
|
|
12753
|
+
/** 技能名合法形态。 */
|
|
12754
|
+
const QUICK_MENU_SKILL_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
12755
|
+
/** 未命名项兜底名。 */
|
|
12756
|
+
const FALLBACK_NAME = "未命名";
|
|
12571
12757
|
/**
|
|
12572
|
-
*
|
|
12573
|
-
* @author ddj 2026年
|
|
12574
|
-
* @
|
|
12758
|
+
* 数值夹取(NaN/非有限回落到 fallback)。
|
|
12759
|
+
* @author ddj 2026年10月30号
|
|
12760
|
+
* @param value 原始值
|
|
12761
|
+
* @param min 下限
|
|
12762
|
+
* @param max 上限
|
|
12763
|
+
* @param fallback 非法值兜底
|
|
12764
|
+
* @returns 夹取后的数值
|
|
12575
12765
|
*/
|
|
12576
|
-
function
|
|
12577
|
-
|
|
12766
|
+
function clampNumber(value, min, max, fallback) {
|
|
12767
|
+
const number = typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
12768
|
+
if (number < min) return min;
|
|
12769
|
+
if (number > max) return max;
|
|
12770
|
+
return number;
|
|
12578
12771
|
}
|
|
12579
12772
|
/**
|
|
12580
|
-
*
|
|
12581
|
-
*
|
|
12582
|
-
* @
|
|
12583
|
-
* @
|
|
12584
|
-
* @param right Expected profile directory.
|
|
12585
|
-
* @param platform Platform name for separator and case rules.
|
|
12586
|
-
* @returns Whether both name the same profile.
|
|
12773
|
+
* 取对象形态(非对象/数组返回 null)。
|
|
12774
|
+
* @author ddj 2026年10月30号
|
|
12775
|
+
* @param value 原始值
|
|
12776
|
+
* @returns 普通对象或 null
|
|
12587
12777
|
*/
|
|
12588
|
-
function
|
|
12589
|
-
if (
|
|
12590
|
-
|
|
12591
|
-
return fold(left) === fold(right);
|
|
12592
|
-
}
|
|
12593
|
-
return left.replace(/\/+$/, "") === right.replace(/\/+$/, "");
|
|
12778
|
+
function asRecord$1(value) {
|
|
12779
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
12780
|
+
return value;
|
|
12594
12781
|
}
|
|
12595
|
-
/**
|
|
12596
|
-
*
|
|
12597
|
-
* @
|
|
12598
|
-
* @param
|
|
12599
|
-
* @
|
|
12600
|
-
* @param platform Platform name used for profile identity.
|
|
12601
|
-
* @returns A typed request or null.
|
|
12782
|
+
/**
|
|
12783
|
+
* 取字符串(非字符串回落空串)。
|
|
12784
|
+
* @author ddj 2026年10月30号
|
|
12785
|
+
* @param value 原始值
|
|
12786
|
+
* @returns 字符串
|
|
12602
12787
|
*/
|
|
12603
|
-
function
|
|
12604
|
-
|
|
12605
|
-
const row = value;
|
|
12606
|
-
const keys = [
|
|
12607
|
-
"version",
|
|
12608
|
-
"requestId",
|
|
12609
|
-
"profile",
|
|
12610
|
-
"paths",
|
|
12611
|
-
"line",
|
|
12612
|
-
"column",
|
|
12613
|
-
"createdAt"
|
|
12614
|
-
];
|
|
12615
|
-
if (Object.keys(row).some((key) => !keys.includes(key))) return null;
|
|
12616
|
-
if (row.version !== 1 || typeof row.profile !== "string" || !sameProfile(row.profile, profile, platform) || typeof row.requestId !== "string" || !OPEN_ID.test(row.requestId)) return null;
|
|
12617
|
-
if (typeof row.createdAt !== "number" || !Number.isSafeInteger(row.createdAt)) return null;
|
|
12618
|
-
if (now - row.createdAt >= 6e4 || row.createdAt > now + 5e3) return null;
|
|
12619
|
-
if (!Array.isArray(row.paths) || !row.paths.length || row.paths.length > 20) return null;
|
|
12620
|
-
if (!row.paths.every((path) => typeof path === "string" && path.length <= 8192 && !/[\u0000-\u001f]/.test(path) && /^(?:\/|[a-z]:[\\/]|\\\\)/i.test(path))) return null;
|
|
12621
|
-
for (const key of ["line", "column"]) if (row[key] !== void 0 && (!Number.isSafeInteger(row[key]) || Number(row[key]) < 1)) return null;
|
|
12622
|
-
return row;
|
|
12788
|
+
function asString$1(value) {
|
|
12789
|
+
return typeof value === "string" ? value : "";
|
|
12623
12790
|
}
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
* Queue a bounded request without replacing earlier opens.
|
|
12631
|
-
* @param req Paths and navigation coordinates.
|
|
12632
|
-
* @param token Optional inbox request ID.
|
|
12633
|
-
* @param createdAt Original producer timestamp; queue ingestion never extends it.
|
|
12634
|
-
* @returns Presence and stable request token.
|
|
12791
|
+
/**
|
|
12792
|
+
* 文本截断(超长补省略号)。
|
|
12793
|
+
* @author ddj 2026年10月30号
|
|
12794
|
+
* @param text 原文
|
|
12795
|
+
* @param cap 上限字符数
|
|
12796
|
+
* @returns 截断后文本
|
|
12635
12797
|
*/
|
|
12636
|
-
function
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12798
|
+
function cutText(text, cap) {
|
|
12799
|
+
if (text.length <= cap) return text;
|
|
12800
|
+
return text.slice(0, Math.max(0, cap - 1)) + "…";
|
|
12801
|
+
}
|
|
12802
|
+
/**
|
|
12803
|
+
* 归一化工作区路径(统一分隔符、去尾分隔符、win32 转小写),用于比较而非展示。
|
|
12804
|
+
* @author ddj 2026年10月30号
|
|
12805
|
+
* @param path 原始路径
|
|
12806
|
+
* @returns 归一化路径
|
|
12807
|
+
*/
|
|
12808
|
+
function normalizePath(path) {
|
|
12809
|
+
const unified = path.trim().replace(/\//g, "\\").replace(/\\+$/, "");
|
|
12810
|
+
if (unified === "") return "";
|
|
12811
|
+
return unified.toLowerCase();
|
|
12812
|
+
}
|
|
12813
|
+
/**
|
|
12814
|
+
* 默认文档(首次打开菜单时的初始状态)。
|
|
12815
|
+
* @author ddj 2026年10月30号
|
|
12816
|
+
* @returns 默认文档
|
|
12817
|
+
*/
|
|
12818
|
+
function defaultDoc() {
|
|
12652
12819
|
return {
|
|
12653
|
-
|
|
12654
|
-
|
|
12820
|
+
version: 1,
|
|
12821
|
+
revision: 0,
|
|
12822
|
+
updatedAt: "",
|
|
12823
|
+
enabled: true,
|
|
12824
|
+
clickAction: "send",
|
|
12825
|
+
maxDepth: 3,
|
|
12826
|
+
launcher: {
|
|
12827
|
+
x: null,
|
|
12828
|
+
y: null
|
|
12829
|
+
},
|
|
12830
|
+
items: []
|
|
12655
12831
|
};
|
|
12656
12832
|
}
|
|
12657
|
-
/**
|
|
12658
|
-
*
|
|
12659
|
-
* @
|
|
12660
|
-
* @
|
|
12833
|
+
/**
|
|
12834
|
+
* 归一化限定工作区列表(去空、去重、归一化;空结果回落到 null=全部)。
|
|
12835
|
+
* @author ddj 2026年10月30号
|
|
12836
|
+
* @param raw 原始值
|
|
12837
|
+
* @returns 限定列表或 null
|
|
12661
12838
|
*/
|
|
12662
|
-
function
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12839
|
+
function normalizeWorkspaces(raw) {
|
|
12840
|
+
if (!Array.isArray(raw)) return null;
|
|
12841
|
+
const out = [];
|
|
12842
|
+
for (const entry of raw) {
|
|
12843
|
+
if (typeof entry !== "string") continue;
|
|
12844
|
+
const normalized = normalizePath(entry);
|
|
12845
|
+
if (normalized === "") continue;
|
|
12846
|
+
if (out.includes(normalized)) continue;
|
|
12847
|
+
out.push(normalized);
|
|
12848
|
+
}
|
|
12849
|
+
return out.length > 0 ? out : null;
|
|
12850
|
+
}
|
|
12851
|
+
/**
|
|
12852
|
+
* 绑定规则的稳定键(校验、去重、内联正文映射共用)。
|
|
12853
|
+
* @author ddj 2026年10月30号
|
|
12854
|
+
* @param binding 规则绑定
|
|
12855
|
+
* @returns 键
|
|
12856
|
+
*/
|
|
12857
|
+
function bindingKey(binding) {
|
|
12858
|
+
const workspace = normalizePath(binding.workspacePath ?? "");
|
|
12859
|
+
return binding.scope + "|" + workspace + "|" + binding.file;
|
|
12860
|
+
}
|
|
12861
|
+
/**
|
|
12862
|
+
* 归一化单条规则绑定(scope/file 必填,项目规则必须带工作区)。
|
|
12863
|
+
* @author ddj 2026年10月30号
|
|
12864
|
+
* @param raw 原始值
|
|
12865
|
+
* @returns 绑定或 null
|
|
12866
|
+
*/
|
|
12867
|
+
function normalizeRule(raw) {
|
|
12868
|
+
const record = asRecord$1(raw);
|
|
12869
|
+
if (!record) return null;
|
|
12870
|
+
const file = asString$1(record.file).trim();
|
|
12871
|
+
if (file === "") return null;
|
|
12872
|
+
const scope = record.scope === "project" ? "project" : "user";
|
|
12873
|
+
const binding = {
|
|
12874
|
+
scope,
|
|
12875
|
+
file
|
|
12876
|
+
};
|
|
12877
|
+
if (scope === "project") {
|
|
12878
|
+
const workspacePath = asString$1(record.workspacePath).trim();
|
|
12879
|
+
if (workspacePath === "") return null;
|
|
12880
|
+
binding.workspacePath = workspacePath;
|
|
12881
|
+
}
|
|
12882
|
+
if (record.origin === "current" || record.origin === "legacy") binding.origin = record.origin;
|
|
12883
|
+
return binding;
|
|
12884
|
+
}
|
|
12885
|
+
/**
|
|
12886
|
+
* 归一化规则绑定列表(去重 + 上限)。
|
|
12887
|
+
* @author ddj 2026年10月30号
|
|
12888
|
+
* @param raw 原始值
|
|
12889
|
+
* @returns 绑定列表
|
|
12890
|
+
*/
|
|
12891
|
+
function normalizeRules(raw) {
|
|
12892
|
+
if (!Array.isArray(raw)) return [];
|
|
12893
|
+
const out = [];
|
|
12894
|
+
for (const entry of raw) {
|
|
12895
|
+
const binding = normalizeRule(entry);
|
|
12896
|
+
if (!binding) continue;
|
|
12897
|
+
if (out.some((existing) => bindingKey(existing) === bindingKey(binding))) continue;
|
|
12898
|
+
out.push(binding);
|
|
12899
|
+
}
|
|
12900
|
+
return out.slice(0, 24);
|
|
12901
|
+
}
|
|
12902
|
+
/**
|
|
12903
|
+
* 归一化技能名列表(只保留 kebab-case,去重 + 上限)。
|
|
12904
|
+
* @author ddj 2026年10月30号
|
|
12905
|
+
* @param raw 原始值
|
|
12906
|
+
* @returns 技能名列表
|
|
12907
|
+
*/
|
|
12908
|
+
function normalizeSkills(raw) {
|
|
12909
|
+
if (!Array.isArray(raw)) return [];
|
|
12910
|
+
const out = [];
|
|
12911
|
+
for (const entry of raw) {
|
|
12912
|
+
if (typeof entry !== "string") continue;
|
|
12913
|
+
const name = entry.trim();
|
|
12914
|
+
if (name === "" || !QUICK_MENU_SKILL_RE.test(name)) continue;
|
|
12915
|
+
if (out.includes(name)) continue;
|
|
12916
|
+
out.push(name);
|
|
12917
|
+
}
|
|
12918
|
+
return out.slice(0, 24);
|
|
12919
|
+
}
|
|
12920
|
+
/**
|
|
12921
|
+
* 归一化单个菜单项(非法类型/缺 id 返回 null)。
|
|
12922
|
+
* @author ddj 2026年10月30号
|
|
12923
|
+
* @param raw 原始值
|
|
12924
|
+
* @returns 菜单项或 null
|
|
12925
|
+
*/
|
|
12926
|
+
function normalizeItem(raw) {
|
|
12927
|
+
const record = asRecord$1(raw);
|
|
12928
|
+
if (!record) return null;
|
|
12929
|
+
const type = record.type === "group" ? "group" : "button";
|
|
12930
|
+
const id = asString$1(record.id).trim();
|
|
12931
|
+
if (id === "") return null;
|
|
12932
|
+
const name = cutText(asString$1(record.name).trim() || FALLBACK_NAME, 40);
|
|
12933
|
+
const order = clampNumber(record.order, 0, 999, 100);
|
|
12934
|
+
const rawParent = record.parentId;
|
|
12935
|
+
const base = {
|
|
12936
|
+
id,
|
|
12937
|
+
type,
|
|
12938
|
+
name,
|
|
12939
|
+
order,
|
|
12940
|
+
parentId: typeof rawParent === "string" && rawParent.trim() !== "" ? rawParent.trim() : null,
|
|
12941
|
+
workspaces: normalizeWorkspaces(record.workspaces)
|
|
12942
|
+
};
|
|
12943
|
+
if (type === "group") return {
|
|
12944
|
+
...base,
|
|
12945
|
+
type: "group"
|
|
12946
|
+
};
|
|
12947
|
+
const button = {
|
|
12948
|
+
...base,
|
|
12949
|
+
type: "button",
|
|
12950
|
+
prompt: cutText(asString$1(record.prompt), QUICK_MENU_PROMPT_CAP),
|
|
12951
|
+
skills: normalizeSkills(record.skills),
|
|
12952
|
+
rules: normalizeRules(record.rules),
|
|
12953
|
+
ruleMode: record.ruleMode === "inline" ? "inline" : "reference"
|
|
12954
|
+
};
|
|
12955
|
+
if (record.clickAction === "send" || record.clickAction === "fill") button.clickAction = record.clickAction;
|
|
12956
|
+
return button;
|
|
12957
|
+
}
|
|
12958
|
+
/**
|
|
12959
|
+
* 判断从起点上溯的父链是否会回到目标项(环检测;超深链也按非法处理)。
|
|
12960
|
+
* @author ddj 2026年10月30号
|
|
12961
|
+
* @param byId id → 项
|
|
12962
|
+
* @param startId 起点父 id
|
|
12963
|
+
* @param targetId 目标项 id
|
|
12964
|
+
* @returns 是否成环
|
|
12965
|
+
*/
|
|
12966
|
+
function chainHas(byId, startId, targetId) {
|
|
12967
|
+
let current = startId;
|
|
12968
|
+
let guard = 0;
|
|
12969
|
+
while (current !== null) {
|
|
12970
|
+
if (current === targetId) return true;
|
|
12971
|
+
if (guard > 7) return true;
|
|
12972
|
+
const parent = byId.get(current);
|
|
12973
|
+
if (!parent) return false;
|
|
12974
|
+
current = parent.parentId;
|
|
12975
|
+
guard += 1;
|
|
12976
|
+
}
|
|
12977
|
+
return false;
|
|
12978
|
+
}
|
|
12979
|
+
/**
|
|
12980
|
+
* 修补父链问题:父不存在/父是按钮/自指/形成环时把子项上抬到最近合法祖组或顶层。
|
|
12981
|
+
* @author ddj 2026年10月30号
|
|
12982
|
+
* @param items 归一化后的项
|
|
12983
|
+
* @returns 修补后的项
|
|
12984
|
+
*/
|
|
12985
|
+
function repairParents(items) {
|
|
12986
|
+
const byId = /* @__PURE__ */ new Map();
|
|
12987
|
+
for (const item of items) byId.set(item.id, item);
|
|
12988
|
+
for (const item of items) {
|
|
12989
|
+
let resolved = item.parentId;
|
|
12990
|
+
let guard = 0;
|
|
12991
|
+
while (resolved !== null && guard <= 7) {
|
|
12992
|
+
const parent = byId.get(resolved);
|
|
12993
|
+
if (!parent) {
|
|
12994
|
+
resolved = null;
|
|
12995
|
+
break;
|
|
12996
|
+
}
|
|
12997
|
+
if (parent.type === "group") break;
|
|
12998
|
+
resolved = parent.parentId;
|
|
12999
|
+
guard += 1;
|
|
13000
|
+
}
|
|
13001
|
+
if (resolved !== null && chainHas(byId, resolved, item.id)) resolved = null;
|
|
13002
|
+
item.parentId = resolved;
|
|
13003
|
+
}
|
|
13004
|
+
return items;
|
|
13005
|
+
}
|
|
13006
|
+
/**
|
|
13007
|
+
* 按父级分组重排优先级(order 归一为 10/20/30…,保证展示稳定且无并列)。
|
|
13008
|
+
* @author ddj 2026年10月30号
|
|
13009
|
+
* @param items 菜单项
|
|
13010
|
+
* @returns 同一数组(就地写入 order)
|
|
13011
|
+
*/
|
|
13012
|
+
function normalizeOrder(items) {
|
|
13013
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13014
|
+
for (const item of items) {
|
|
13015
|
+
const key = item.parentId ?? "";
|
|
13016
|
+
const bucket = groups.get(key);
|
|
13017
|
+
if (bucket) bucket.push(item);
|
|
13018
|
+
else groups.set(key, [item]);
|
|
13019
|
+
}
|
|
13020
|
+
for (const bucket of groups.values()) {
|
|
13021
|
+
bucket.sort((left, right) => {
|
|
13022
|
+
if (left.order !== right.order) return left.order - right.order;
|
|
13023
|
+
if (left.name !== right.name) return left.name < right.name ? -1 : 1;
|
|
13024
|
+
return left.id < right.id ? -1 : 1;
|
|
13025
|
+
});
|
|
13026
|
+
bucket.forEach((item, index) => {
|
|
13027
|
+
item.order = (index + 1) * 10;
|
|
13028
|
+
});
|
|
13029
|
+
}
|
|
13030
|
+
return items;
|
|
13031
|
+
}
|
|
13032
|
+
/**
|
|
13033
|
+
* 归一化整份文档(任何脏数据都收敛到可用形态,绝不抛错)。
|
|
13034
|
+
* @author ddj 2026年10月30号
|
|
13035
|
+
* @param raw 原始值(JSON.parse 结果或任意 unknown)
|
|
13036
|
+
* @returns 合法文档
|
|
13037
|
+
*/
|
|
13038
|
+
function normalizeDoc(raw) {
|
|
13039
|
+
const fallback = defaultDoc();
|
|
13040
|
+
const record = asRecord$1(raw);
|
|
13041
|
+
if (!record) return fallback;
|
|
13042
|
+
const rawItems = Array.isArray(record.items) ? record.items : [];
|
|
13043
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13044
|
+
const items = [];
|
|
13045
|
+
for (const entry of rawItems) {
|
|
13046
|
+
const item = normalizeItem(entry);
|
|
13047
|
+
if (!item || seen.has(item.id)) continue;
|
|
13048
|
+
seen.add(item.id);
|
|
13049
|
+
items.push(item);
|
|
13050
|
+
if (items.length >= 200) break;
|
|
13051
|
+
}
|
|
13052
|
+
const x = typeof record.launcher === "object" && record.launcher !== null ? asRecord$1(record.launcher) : null;
|
|
13053
|
+
return {
|
|
13054
|
+
version: 1,
|
|
13055
|
+
revision: Math.floor(clampNumber(record.revision, 0, Number.MAX_SAFE_INTEGER, 0)),
|
|
13056
|
+
updatedAt: cutText(asString$1(record.updatedAt), 40),
|
|
13057
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : true,
|
|
13058
|
+
clickAction: record.clickAction === "fill" ? "fill" : "send",
|
|
13059
|
+
maxDepth: Math.round(clampNumber(record.maxDepth, 1, 5, 3)),
|
|
13060
|
+
launcher: {
|
|
13061
|
+
x: x && typeof x.x === "number" && Number.isFinite(x.x) ? x.x : null,
|
|
13062
|
+
y: x && typeof x.y === "number" && Number.isFinite(x.y) ? x.y : null
|
|
13063
|
+
},
|
|
13064
|
+
items: normalizeOrder(repairParents(items))
|
|
13065
|
+
};
|
|
13066
|
+
}
|
|
13067
|
+
/**
|
|
13068
|
+
* 结构迁移入口(当前只有 v1;后续版本在此收敛旧结构)。
|
|
13069
|
+
* @author ddj 2026年10月30号
|
|
13070
|
+
* @param raw 原始值
|
|
13071
|
+
* @returns 合法文档
|
|
13072
|
+
*/
|
|
13073
|
+
function migrateDoc(raw) {
|
|
13074
|
+
return normalizeDoc(raw);
|
|
13075
|
+
}
|
|
13076
|
+
/**
|
|
13077
|
+
* 序列化文档(host 落盘与导入导出共用)。
|
|
13078
|
+
* @author ddj 2026年10月30号
|
|
13079
|
+
* @param doc 文档
|
|
13080
|
+
* @returns JSON 文本
|
|
13081
|
+
*/
|
|
13082
|
+
function serializeDoc(doc) {
|
|
13083
|
+
return JSON.stringify(normalizeDoc(doc), null, 2);
|
|
13084
|
+
}
|
|
13085
|
+
/**
|
|
13086
|
+
* 解析文档文本(解析失败返回默认文档并给出告警文案)。
|
|
13087
|
+
* @author ddj 2026年10月30号
|
|
13088
|
+
* @param text 文件内容
|
|
13089
|
+
* @returns 文档 + 可选告警
|
|
13090
|
+
*/
|
|
13091
|
+
function parseDoc(text) {
|
|
13092
|
+
try {
|
|
13093
|
+
return { doc: migrateDoc(JSON.parse(text)) };
|
|
13094
|
+
} catch (error) {
|
|
13095
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
13096
|
+
return {
|
|
13097
|
+
doc: defaultDoc(),
|
|
13098
|
+
warning: "快捷菜单配置解析失败(" + reason + "),已回落默认配置"
|
|
13099
|
+
};
|
|
13100
|
+
}
|
|
13101
|
+
}
|
|
13102
|
+
//#endregion
|
|
13103
|
+
//#region src/quickMenu.ts
|
|
13104
|
+
/**
|
|
13105
|
+
* dsh-vscode-mode host — 快捷菜单配置的读写(~/.dsh/dsh-vscode-mode/data/quick-menu.json)。
|
|
13106
|
+
*
|
|
13107
|
+
* 为什么单独成模块:菜单项是用户数据而不是界面镜像——不能落在 cache/(会被 sweep、
|
|
13108
|
+
* 且与 uiState 共享 512KB 预算),也不适合塞进 Config schema(嵌套 JSON 不参与校验)。
|
|
13109
|
+
* 写入约定:revision 乐观并发 + 同文件串行队列 + tmp 原子替换 + 单代 .bak 备份,
|
|
13110
|
+
* 解析失败时把原文件另存 .bad-<ts>.json 再回落默认文档,绝不让坏文件阻塞功能。
|
|
13111
|
+
* 作者 ddj 2026年10月30号
|
|
13112
|
+
*/
|
|
13113
|
+
/** mtime 缓存(文件未变则复用解析结果,避免每次打开菜单都读盘)。 */
|
|
13114
|
+
const stateCache = /* @__PURE__ */ new Map();
|
|
13115
|
+
/** 同文件写队列尾(串行化并发写入;队列清空即移除条目,与 uiState 同套路)。 */
|
|
13116
|
+
const writeChains$1 = /* @__PURE__ */ new Map();
|
|
13117
|
+
/**
|
|
13118
|
+
* 把写任务挂到该文件的队尾串行执行(前序失败不影响后续)。
|
|
13119
|
+
* @author ddj 2026年10月30号
|
|
13120
|
+
* @param file 目标文件绝对路径
|
|
13121
|
+
* @param run 实际写操作
|
|
13122
|
+
* @returns 本次写操作结果
|
|
13123
|
+
*/
|
|
13124
|
+
function chainWrite$1(file, run) {
|
|
13125
|
+
const next = (writeChains$1.get(file) ?? Promise.resolve()).then(run, run);
|
|
13126
|
+
const tail = next.then(() => void 0, () => void 0);
|
|
13127
|
+
writeChains$1.set(file, tail);
|
|
13128
|
+
tail.then(() => {
|
|
13129
|
+
if (writeChains$1.get(file) === tail) writeChains$1.delete(file);
|
|
13130
|
+
});
|
|
13131
|
+
return next;
|
|
13132
|
+
}
|
|
13133
|
+
/**
|
|
13134
|
+
* 读取文件修改时间(缺失/不可读返回 0)。
|
|
13135
|
+
* @author ddj 2026年10月30号
|
|
13136
|
+
* @param file 文件路径
|
|
13137
|
+
* @returns mtime 毫秒
|
|
13138
|
+
*/
|
|
13139
|
+
async function mtimeOf(file) {
|
|
13140
|
+
try {
|
|
13141
|
+
return (await stat(file)).mtimeMs;
|
|
13142
|
+
} catch {
|
|
13143
|
+
return 0;
|
|
13144
|
+
}
|
|
13145
|
+
}
|
|
13146
|
+
/**
|
|
13147
|
+
* 原子写文本(先写 .tmp 再改名,缺目录自动建;失败时尽量清掉临时文件)。
|
|
13148
|
+
* @author ddj 2026年10月30号
|
|
13149
|
+
* @param file 目标文件
|
|
13150
|
+
* @param text 文本
|
|
13151
|
+
*/
|
|
13152
|
+
async function writeAtomic(file, text) {
|
|
13153
|
+
await mkdir(dirname(file), { recursive: true });
|
|
13154
|
+
const temporary = file + ".tmp";
|
|
13155
|
+
await writeFile(temporary, text, "utf8");
|
|
13156
|
+
await rename(temporary, file);
|
|
13157
|
+
}
|
|
13158
|
+
/**
|
|
13159
|
+
* 把坏文件另存为 .bad-<ts>.json(尽力而为,失败不影响主流程)。
|
|
13160
|
+
* @author ddj 2026年10月30号
|
|
13161
|
+
* @param file 源文件
|
|
13162
|
+
* @returns 备份路径(失败为 null)
|
|
13163
|
+
*/
|
|
13164
|
+
async function backupCorrupt(file) {
|
|
13165
|
+
const target = file + ".bad-" + Date.now() + ".json";
|
|
13166
|
+
try {
|
|
13167
|
+
await copyFile(file, target);
|
|
13168
|
+
return target;
|
|
13169
|
+
} catch {
|
|
13170
|
+
return null;
|
|
13171
|
+
}
|
|
13172
|
+
}
|
|
13173
|
+
/**
|
|
13174
|
+
* 把上一次内容写成单代备份(quick-menu.bak.json)。
|
|
13175
|
+
* @author ddj 2026年10月30号
|
|
13176
|
+
* @param file 源文件
|
|
13177
|
+
*/
|
|
13178
|
+
async function backupPrevious(file) {
|
|
13179
|
+
try {
|
|
13180
|
+
await copyFile(file, file.replace(/\.json$/, "") + ".bak.json");
|
|
13181
|
+
} catch {}
|
|
13182
|
+
}
|
|
13183
|
+
/**
|
|
13184
|
+
* 读盘并归一(绕过缓存;文件缺失按默认文档处理)。
|
|
13185
|
+
* @author ddj 2026年10月30号
|
|
13186
|
+
* @param file 文件路径
|
|
13187
|
+
* @returns 文档 + 告警 + mtime
|
|
13188
|
+
*/
|
|
13189
|
+
async function loadFile(file) {
|
|
13190
|
+
const mtime = await mtimeOf(file);
|
|
13191
|
+
if (mtime === 0) return {
|
|
13192
|
+
doc: defaultDoc(),
|
|
13193
|
+
mtime: 0
|
|
13194
|
+
};
|
|
13195
|
+
let text = "";
|
|
13196
|
+
try {
|
|
13197
|
+
text = await readFile(file, "utf8");
|
|
13198
|
+
} catch (error) {
|
|
13199
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
13200
|
+
return {
|
|
13201
|
+
doc: defaultDoc(),
|
|
13202
|
+
warning: "快捷菜单配置读取失败(" + reason + "),已回落默认配置",
|
|
13203
|
+
mtime
|
|
13204
|
+
};
|
|
13205
|
+
}
|
|
13206
|
+
const parsed = parseDoc(text);
|
|
13207
|
+
if (!parsed.warning) return {
|
|
13208
|
+
doc: parsed.doc,
|
|
13209
|
+
mtime
|
|
13210
|
+
};
|
|
13211
|
+
const backup = await backupCorrupt(file);
|
|
13212
|
+
const doc = parsed.doc;
|
|
13213
|
+
try {
|
|
13214
|
+
await writeAtomic(file, serializeDoc(doc));
|
|
13215
|
+
} catch {}
|
|
13216
|
+
const hint = backup ? "已备份为 " + backup : "备份失败";
|
|
13217
|
+
return {
|
|
13218
|
+
doc,
|
|
13219
|
+
warning: parsed.warning + "(" + hint + ")",
|
|
13220
|
+
mtime
|
|
13221
|
+
};
|
|
13222
|
+
}
|
|
13223
|
+
/**
|
|
13224
|
+
* 读取快捷菜单文档(mtime 未变时复用缓存)。
|
|
13225
|
+
* @author ddj 2026年10月30号
|
|
13226
|
+
* @param home DSH home(测试可注入)
|
|
13227
|
+
* @returns 文档 + 告警
|
|
13228
|
+
*/
|
|
13229
|
+
async function readQuickMenu(home) {
|
|
13230
|
+
const file = quickMenuFile(home);
|
|
13231
|
+
const mtime = await mtimeOf(file);
|
|
13232
|
+
const cached = stateCache.get(file);
|
|
13233
|
+
if (cached && cached.mtime === mtime) return cached;
|
|
13234
|
+
const state = await loadFile(file);
|
|
13235
|
+
stateCache.set(file, state);
|
|
13236
|
+
return state;
|
|
13237
|
+
}
|
|
13238
|
+
/**
|
|
13239
|
+
* 校验待保存文档的硬性上限(超出即拒绝,避免静默截断用户数据)。
|
|
13240
|
+
* @author ddj 2026年10月30号
|
|
13241
|
+
* @param doc 待保存文档
|
|
13242
|
+
* @returns 错误文案(合法为 null)
|
|
13243
|
+
*/
|
|
13244
|
+
function checkLimits(doc) {
|
|
13245
|
+
if (doc.items.length > 200) return "菜单项超过上限(200 项)";
|
|
13246
|
+
if (Buffer.byteLength(serializeDoc(doc), "utf8") > 262144) return "配置超出容量上限(262144 字节)";
|
|
13247
|
+
return null;
|
|
13248
|
+
}
|
|
13249
|
+
/**
|
|
13250
|
+
* 保存文档(revision 不匹配即判定为并发冲突,返回磁盘最新文档)。
|
|
13251
|
+
* @author ddj 2026年10月30号
|
|
13252
|
+
* @param doc 客户端提交的完整文档
|
|
13253
|
+
* @param baseRevision 客户端读到的 revision
|
|
13254
|
+
* @param home DSH home(测试可注入)
|
|
13255
|
+
* @returns 保存结果
|
|
13256
|
+
*/
|
|
13257
|
+
async function saveQuickMenu(doc, baseRevision, home) {
|
|
13258
|
+
const file = quickMenuFile(home);
|
|
13259
|
+
return chainWrite$1(file, async () => {
|
|
13260
|
+
const limit = checkLimits(doc);
|
|
13261
|
+
if (limit) return {
|
|
13262
|
+
ok: false,
|
|
13263
|
+
error: limit
|
|
13264
|
+
};
|
|
13265
|
+
const current = await loadFile(file);
|
|
13266
|
+
if (current.doc.revision !== baseRevision) return {
|
|
13267
|
+
ok: false,
|
|
13268
|
+
error: "配置已被其他窗口修改,请重新加载后再保存",
|
|
13269
|
+
doc: current.doc
|
|
13270
|
+
};
|
|
13271
|
+
const next = normalizeDoc(doc);
|
|
13272
|
+
next.launcher = current.doc.launcher;
|
|
13273
|
+
next.revision = current.doc.revision + 1;
|
|
13274
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13275
|
+
try {
|
|
13276
|
+
await backupPrevious(file);
|
|
13277
|
+
await writeAtomic(file, serializeDoc(next));
|
|
13278
|
+
} catch (error) {
|
|
13279
|
+
return {
|
|
13280
|
+
ok: false,
|
|
13281
|
+
error: "快捷菜单配置写入失败:" + (error instanceof Error ? error.message : String(error))
|
|
13282
|
+
};
|
|
13283
|
+
}
|
|
13284
|
+
const state = {
|
|
13285
|
+
doc: next,
|
|
13286
|
+
mtime: await mtimeOf(file)
|
|
13287
|
+
};
|
|
13288
|
+
stateCache.set(file, state);
|
|
13289
|
+
return {
|
|
13290
|
+
ok: true,
|
|
13291
|
+
doc: next
|
|
13292
|
+
};
|
|
13293
|
+
});
|
|
13294
|
+
}
|
|
13295
|
+
/**
|
|
13296
|
+
* 只更新悬浮按钮坐标(不推进 revision,避免拖拽把正在编辑的表单顶掉)。
|
|
13297
|
+
* @author ddj 2026年10月30号
|
|
13298
|
+
* @param launcher 坐标(null 表示回到输入框锚点)
|
|
13299
|
+
* @param home DSH home(测试可注入)
|
|
13300
|
+
* @returns 保存结果
|
|
13301
|
+
*/
|
|
13302
|
+
async function patchLauncher(launcher, home) {
|
|
13303
|
+
const file = quickMenuFile(home);
|
|
13304
|
+
return chainWrite$1(file, async () => {
|
|
13305
|
+
const next = {
|
|
13306
|
+
...(await loadFile(file)).doc,
|
|
13307
|
+
launcher: {
|
|
13308
|
+
x: launcher.x,
|
|
13309
|
+
y: launcher.y
|
|
13310
|
+
},
|
|
13311
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13312
|
+
};
|
|
13313
|
+
try {
|
|
13314
|
+
await writeAtomic(file, serializeDoc(next));
|
|
13315
|
+
} catch (error) {
|
|
13316
|
+
return {
|
|
13317
|
+
ok: false,
|
|
13318
|
+
error: "保存悬浮按钮位置失败:" + (error instanceof Error ? error.message : String(error))
|
|
13319
|
+
};
|
|
13320
|
+
}
|
|
13321
|
+
const state = {
|
|
13322
|
+
doc: next,
|
|
13323
|
+
mtime: await mtimeOf(file)
|
|
13324
|
+
};
|
|
13325
|
+
stateCache.set(file, state);
|
|
13326
|
+
return {
|
|
13327
|
+
ok: true,
|
|
13328
|
+
doc: next
|
|
13329
|
+
};
|
|
13330
|
+
});
|
|
13331
|
+
}
|
|
13332
|
+
//#endregion
|
|
13333
|
+
//#region src/shared/externalOpen.ts
|
|
13334
|
+
const OPEN_TTL_MS = 6e4;
|
|
13335
|
+
const OPEN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13336
|
+
/**
|
|
13337
|
+
* Detect the host platform without importing node builtins.
|
|
13338
|
+
* @author ddj 2026年09月28号
|
|
13339
|
+
* @returns Platform name, or an empty string outside Node.
|
|
13340
|
+
*/
|
|
13341
|
+
function platformName() {
|
|
13342
|
+
return typeof process === "object" && process !== null ? String(process.platform) : "";
|
|
13343
|
+
}
|
|
13344
|
+
/**
|
|
13345
|
+
* Compare profile directories by physical identity, not by spelling.
|
|
13346
|
+
* Windows accepts either separator and folds case; other platforms stay exact.
|
|
13347
|
+
* @author ddj 2026年09月28号
|
|
13348
|
+
* @param left Declared profile directory.
|
|
13349
|
+
* @param right Expected profile directory.
|
|
13350
|
+
* @param platform Platform name for separator and case rules.
|
|
13351
|
+
* @returns Whether both name the same profile.
|
|
13352
|
+
*/
|
|
13353
|
+
function sameProfile(left, right, platform = platformName()) {
|
|
13354
|
+
if (platform === "win32") {
|
|
13355
|
+
const fold = (value) => value.replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
|
|
13356
|
+
return fold(left) === fold(right);
|
|
13357
|
+
}
|
|
13358
|
+
return left.replace(/\/+$/, "") === right.replace(/\/+$/, "");
|
|
13359
|
+
}
|
|
13360
|
+
/** @public @author ddj 2026年09月28号
|
|
13361
|
+
* Validate the open-only envelope before accepting filesystem input.
|
|
13362
|
+
* @param value Parsed untrusted JSON.
|
|
13363
|
+
* @param profile Exact expected profile directory.
|
|
13364
|
+
* @param now Current epoch milliseconds.
|
|
13365
|
+
* @param platform Platform name used for profile identity.
|
|
13366
|
+
* @returns A typed request or null.
|
|
13367
|
+
*/
|
|
13368
|
+
function parseInboxOpen(value, profile, now = Date.now(), platform = platformName()) {
|
|
13369
|
+
if (!value || typeof value !== "object") return null;
|
|
13370
|
+
const row = value;
|
|
13371
|
+
const keys = [
|
|
13372
|
+
"version",
|
|
13373
|
+
"requestId",
|
|
13374
|
+
"profile",
|
|
13375
|
+
"paths",
|
|
13376
|
+
"line",
|
|
13377
|
+
"column",
|
|
13378
|
+
"createdAt"
|
|
13379
|
+
];
|
|
13380
|
+
if (Object.keys(row).some((key) => !keys.includes(key))) return null;
|
|
13381
|
+
if (row.version !== 1 || typeof row.profile !== "string" || !sameProfile(row.profile, profile, platform) || typeof row.requestId !== "string" || !OPEN_ID.test(row.requestId)) return null;
|
|
13382
|
+
if (typeof row.createdAt !== "number" || !Number.isSafeInteger(row.createdAt)) return null;
|
|
13383
|
+
if (now - row.createdAt >= 6e4 || row.createdAt > now + 5e3) return null;
|
|
13384
|
+
if (!Array.isArray(row.paths) || !row.paths.length || row.paths.length > 20) return null;
|
|
13385
|
+
if (!row.paths.every((path) => typeof path === "string" && path.length <= 8192 && !/[\u0000-\u001f]/.test(path) && /^(?:\/|[a-z]:[\\/]|\\\\)/i.test(path))) return null;
|
|
13386
|
+
for (const key of ["line", "column"]) if (row[key] !== void 0 && (!Number.isSafeInteger(row[key]) || Number(row[key]) < 1)) return null;
|
|
13387
|
+
return row;
|
|
13388
|
+
}
|
|
13389
|
+
//#endregion
|
|
13390
|
+
//#region src/externalHandoff.ts
|
|
13391
|
+
const pending = /* @__PURE__ */ new Map();
|
|
13392
|
+
const LEASE_MS = 3e4;
|
|
13393
|
+
let lastPollAt = 0;
|
|
13394
|
+
/** @public @author ddj 2026年09月28号
|
|
13395
|
+
* Queue a bounded request without replacing earlier opens.
|
|
13396
|
+
* @param req Paths and navigation coordinates.
|
|
13397
|
+
* @param token Optional inbox request ID.
|
|
13398
|
+
* @param createdAt Original producer timestamp; queue ingestion never extends it.
|
|
13399
|
+
* @returns Presence and stable request token.
|
|
13400
|
+
*/
|
|
13401
|
+
function handoffOpen(req, token = randomUUID(), createdAt = Date.now()) {
|
|
13402
|
+
prunePending();
|
|
13403
|
+
const clients = Date.now() - lastPollAt <= 15e3 ? 1 : 0;
|
|
13404
|
+
if (!Number.isSafeInteger(createdAt) || createdAt + 6e4 <= Date.now()) return {
|
|
13405
|
+
token,
|
|
13406
|
+
clients
|
|
13407
|
+
};
|
|
13408
|
+
if (!pending.has(token)) {
|
|
13409
|
+
if (pending.size >= 128) throw new Error("外部打开队列已满");
|
|
13410
|
+
pending.set(token, {
|
|
13411
|
+
...req,
|
|
13412
|
+
paths: [...req.paths],
|
|
13413
|
+
token,
|
|
13414
|
+
at: createdAt
|
|
13415
|
+
});
|
|
13416
|
+
}
|
|
13417
|
+
return {
|
|
13418
|
+
token,
|
|
13419
|
+
clients
|
|
13420
|
+
};
|
|
13421
|
+
}
|
|
13422
|
+
/** @public @author ddj 2026年09月28号
|
|
13423
|
+
* Claim one request; repeated heartbeats renew this client's active lease.
|
|
13424
|
+
* @param args Authenticated page identity.
|
|
13425
|
+
* @returns A leased request or null.
|
|
13426
|
+
*/
|
|
13427
|
+
function pollPending(args) {
|
|
13428
|
+
lastPollAt = Date.now();
|
|
13429
|
+
prunePending();
|
|
13430
|
+
if (!args?.clientId || args.clientId.length > 128) return null;
|
|
13431
|
+
const active = [...pending.values()].find((row) => row.clientId === args.clientId && row.success === void 0);
|
|
12667
13432
|
if (active && Date.now() - (active.leaseAt ?? 0) < LEASE_MS) {
|
|
12668
13433
|
active.leaseAt = Date.now();
|
|
12669
13434
|
return null;
|
|
@@ -15335,38 +16100,67 @@ function reviewHandlers(ctx, registry) {
|
|
|
15335
16100
|
//#endregion
|
|
15336
16101
|
//#region src/pendingRpc.ts
|
|
15337
16102
|
/**
|
|
15338
|
-
* @author ddj 2026年10月
|
|
15339
|
-
* @description:
|
|
16103
|
+
* @author ddj 2026年10月30号
|
|
16104
|
+
* @description: 解析会话工作区并预热记录桶;读路径也走它,避免冷读时桶未就绪。
|
|
15340
16105
|
* @param ctx Host 上下文
|
|
15341
16106
|
* @param registry 工作区记录桶缓存
|
|
15342
|
-
* @
|
|
16107
|
+
* @param sessionId 请求作用域
|
|
16108
|
+
* @returns 工作区路径
|
|
15343
16109
|
*/
|
|
15344
|
-
function
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
16110
|
+
async function workspaceOf(ctx, registry, sessionId) {
|
|
16111
|
+
const scope = await resolveScope(ctx, sessionId);
|
|
16112
|
+
if ("err" in scope) throw new Error("会话不存在或没有工作区");
|
|
16113
|
+
await bucketOf(registry, ctx, scope.cwd);
|
|
16114
|
+
return scope.cwd;
|
|
16115
|
+
}
|
|
16116
|
+
/**
|
|
16117
|
+
* @author ddj 2026年10月30号
|
|
16118
|
+
* @description: 写路径专用的工作区解析:本端没有该会话就直接拒绝,不做跨实例写。
|
|
16119
|
+
* @param ctx Host 上下文
|
|
16120
|
+
* @param registry 工作区记录桶缓存
|
|
16121
|
+
* @param sessionId 请求作用域
|
|
16122
|
+
* @returns 工作区路径
|
|
16123
|
+
*/
|
|
16124
|
+
async function writeWorkspaceOf(ctx, registry, sessionId) {
|
|
16125
|
+
const cwd = await workspaceOf(ctx, registry, sessionId);
|
|
16126
|
+
if (!sessionOf(ctx, sessionId)) throw new Error("写入要求本端活跃会话");
|
|
16127
|
+
return cwd;
|
|
16128
|
+
}
|
|
16129
|
+
/**
|
|
16130
|
+
* @author ddj 2026年10月30号
|
|
16131
|
+
* @description: 把异常归一成可播报文案,避免处理器各自重复三元表达式。
|
|
16132
|
+
* @param error 捕获到的异常
|
|
16133
|
+
* @returns 错误文案
|
|
16134
|
+
*/
|
|
16135
|
+
function failureOf(error) {
|
|
16136
|
+
return error instanceof Error ? error.message : String(error);
|
|
16137
|
+
}
|
|
16138
|
+
/**
|
|
16139
|
+
* @author ddj 2026年10月30号
|
|
16140
|
+
* @description: 按会话策略包装 shell:执行器默认按 workspace-write 跑,本机无可用沙箱后端会直接拒绝。
|
|
16141
|
+
* @param raw 原始 shell 能力
|
|
16142
|
+
* @param policy 会话沙箱策略
|
|
16143
|
+
* @returns 带策略的 shell 视图
|
|
16144
|
+
*/
|
|
16145
|
+
function shellOf(raw, policy) {
|
|
16146
|
+
return {
|
|
16147
|
+
resolve: (request) => raw.resolve(policy ? {
|
|
16148
|
+
...request,
|
|
16149
|
+
sandboxPolicy: policy
|
|
16150
|
+
} : request),
|
|
16151
|
+
execute: (spec) => raw.execute(spec)
|
|
16152
|
+
};
|
|
16153
|
+
}
|
|
16154
|
+
/**
|
|
16155
|
+
* @author ddj 2026年10月30号
|
|
16156
|
+
* @description: 读路径处理器:列表与详情,允许跨实例冷读。
|
|
16157
|
+
* @param kit 作用域能力
|
|
16158
|
+
* @returns 处理器表
|
|
16159
|
+
*/
|
|
16160
|
+
function readHandlers(kit) {
|
|
15367
16161
|
return {
|
|
15368
16162
|
"edrv.pending.list": async (args) => {
|
|
15369
|
-
const cwd = await
|
|
16163
|
+
const cwd = await kit.read(args.sessionId);
|
|
15370
16164
|
const page = loadPage(cwd, args.sessionId);
|
|
15371
16165
|
for (const item of page.items) trackPendingCover(cwd, item.path, true);
|
|
15372
16166
|
return {
|
|
@@ -15375,7 +16169,7 @@ function pendingHandlers(ctx, registry) {
|
|
|
15375
16169
|
};
|
|
15376
16170
|
},
|
|
15377
16171
|
"edrv.pending.detail": async (args) => {
|
|
15378
|
-
const detail = loadDetail(await
|
|
16172
|
+
const detail = loadDetail(await kit.read(args.sessionId), pendingRawId(args.id));
|
|
15379
16173
|
return detail ? {
|
|
15380
16174
|
ok: true,
|
|
15381
16175
|
detail
|
|
@@ -15383,39 +16177,49 @@ function pendingHandlers(ctx, registry) {
|
|
|
15383
16177
|
ok: false,
|
|
15384
16178
|
error: "待审条目不存在"
|
|
15385
16179
|
};
|
|
15386
|
-
}
|
|
16180
|
+
}
|
|
16181
|
+
};
|
|
16182
|
+
}
|
|
16183
|
+
/**
|
|
16184
|
+
* @author ddj 2026年10月30号
|
|
16185
|
+
* @description: 判定处理器:Keep/Revert 与已解决条目移除,都会改变待审状态。
|
|
16186
|
+
* @param kit 作用域能力
|
|
16187
|
+
* @returns 处理器表
|
|
16188
|
+
*/
|
|
16189
|
+
function decideHandlers(kit) {
|
|
16190
|
+
return {
|
|
15387
16191
|
"edrv.pending.decide": async (args) => {
|
|
15388
|
-
const cwd = await
|
|
16192
|
+
const cwd = await kit.write(args.sessionId);
|
|
16193
|
+
const id = pendingRawId(args.id);
|
|
15389
16194
|
try {
|
|
15390
|
-
const
|
|
15391
|
-
const result = await applyDecide(ctx.get("fs"), cwd, {
|
|
16195
|
+
const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId));
|
|
16196
|
+
const result = await applyDecide(kit.ctx.get("fs"), cwd, {
|
|
15392
16197
|
...args,
|
|
15393
16198
|
id
|
|
15394
|
-
}, void 0,
|
|
16199
|
+
}, void 0, policy);
|
|
15395
16200
|
if (!result.ok) return {
|
|
15396
16201
|
ok: false,
|
|
15397
16202
|
error: result.reason
|
|
15398
16203
|
};
|
|
15399
16204
|
const detail = loadDetail(cwd, id);
|
|
15400
|
-
|
|
15401
|
-
ok: false,
|
|
15402
|
-
error: "待审条目不存在"
|
|
15403
|
-
};
|
|
15404
|
-
return {
|
|
16205
|
+
return detail ? {
|
|
15405
16206
|
ok: true,
|
|
15406
16207
|
item: detail.item,
|
|
15407
16208
|
wrote: result.wrote
|
|
16209
|
+
} : {
|
|
16210
|
+
ok: false,
|
|
16211
|
+
error: "待审条目不存在"
|
|
15408
16212
|
};
|
|
15409
16213
|
} catch (error) {
|
|
15410
16214
|
return {
|
|
15411
16215
|
ok: false,
|
|
15412
|
-
error:
|
|
16216
|
+
error: failureOf(error)
|
|
15413
16217
|
};
|
|
15414
16218
|
}
|
|
15415
16219
|
},
|
|
15416
16220
|
"edrv.pending.remove": async (args) => {
|
|
15417
|
-
const cwd = await
|
|
15418
|
-
const id =
|
|
16221
|
+
const cwd = await kit.write(args.sessionId);
|
|
16222
|
+
const id = pendingRawId(args.id);
|
|
15419
16223
|
const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path;
|
|
15420
16224
|
const removed = removeEntry(cwd, id, args.revision);
|
|
15421
16225
|
if (removed && path) trackPendingCover(cwd, path, false);
|
|
@@ -15423,49 +16227,59 @@ function pendingHandlers(ctx, registry) {
|
|
|
15423
16227
|
ok: true,
|
|
15424
16228
|
removed
|
|
15425
16229
|
};
|
|
15426
|
-
}
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
}
|
|
15446
|
-
|
|
16230
|
+
}
|
|
16231
|
+
};
|
|
16232
|
+
}
|
|
16233
|
+
/**
|
|
16234
|
+
* @author ddj 2026年10月30号
|
|
16235
|
+
* @description: 历史处理器:撤销/重做一次决定。
|
|
16236
|
+
* @param kit 作用域能力
|
|
16237
|
+
* @returns 处理器表
|
|
16238
|
+
*/
|
|
16239
|
+
function historyHandlers(kit) {
|
|
16240
|
+
return { "edrv.pending.history": async (args) => {
|
|
16241
|
+
const cwd = await kit.write(args.sessionId);
|
|
16242
|
+
const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId));
|
|
16243
|
+
try {
|
|
16244
|
+
const result = await undoDecide(kit.ctx.get("fs"), cwd, args, void 0, policy);
|
|
16245
|
+
return result.ok ? {
|
|
16246
|
+
ok: true,
|
|
16247
|
+
item: result.item,
|
|
16248
|
+
wrote: result.wrote
|
|
16249
|
+
} : {
|
|
16250
|
+
ok: false,
|
|
16251
|
+
error: result.reason
|
|
16252
|
+
};
|
|
16253
|
+
} catch (error) {
|
|
16254
|
+
return {
|
|
16255
|
+
ok: false,
|
|
16256
|
+
error: failureOf(error)
|
|
16257
|
+
};
|
|
16258
|
+
}
|
|
16259
|
+
} };
|
|
16260
|
+
}
|
|
16261
|
+
/**
|
|
16262
|
+
* @author ddj 2026年10月30号
|
|
16263
|
+
* @description: 投影维护处理器:从版本库导入、按磁盘现状刷新单条。
|
|
16264
|
+
* @param kit 作用域能力
|
|
16265
|
+
* @returns 处理器表
|
|
16266
|
+
*/
|
|
16267
|
+
function refreshHandlers(kit) {
|
|
16268
|
+
return {
|
|
15447
16269
|
"edrv.pending.import": async (args) => {
|
|
15448
|
-
const cwd = await
|
|
15449
|
-
const raw = ctx.get("shell");
|
|
16270
|
+
const cwd = await kit.write(args.sessionId);
|
|
16271
|
+
const raw = kit.ctx.get("shell");
|
|
15450
16272
|
if (!raw) return {
|
|
15451
16273
|
ok: false,
|
|
15452
16274
|
error: "当前环境没有 shell 能力,无法读取版本库基线"
|
|
15453
16275
|
};
|
|
15454
|
-
const policy = policyOf(ctx, sessionOf(ctx, args.sessionId));
|
|
15455
|
-
const shell = {
|
|
15456
|
-
resolve: (request) => raw.resolve(policy ? {
|
|
15457
|
-
...request,
|
|
15458
|
-
sandboxPolicy: policy
|
|
15459
|
-
} : request),
|
|
15460
|
-
execute: (spec) => raw.execute(spec)
|
|
15461
|
-
};
|
|
16276
|
+
const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId));
|
|
15462
16277
|
try {
|
|
15463
|
-
const result = await importFromVcs(
|
|
16278
|
+
const result = await importFromVcs(shellOf(raw, policy), cwd, args.sessionId, {
|
|
15464
16279
|
includeUntracked: args.includeUntracked,
|
|
15465
16280
|
path: args.path
|
|
15466
16281
|
});
|
|
15467
|
-
const
|
|
15468
|
-
for (const item of page.items) trackPendingCover(cwd, item.path, true);
|
|
16282
|
+
for (const item of loadPage(cwd, args.sessionId).items) trackPendingCover(cwd, item.path, true);
|
|
15469
16283
|
return {
|
|
15470
16284
|
ok: true,
|
|
15471
16285
|
imported: result.imported,
|
|
@@ -15474,20 +16288,20 @@ function pendingHandlers(ctx, registry) {
|
|
|
15474
16288
|
} catch (error) {
|
|
15475
16289
|
return {
|
|
15476
16290
|
ok: false,
|
|
15477
|
-
error:
|
|
16291
|
+
error: failureOf(error)
|
|
15478
16292
|
};
|
|
15479
16293
|
}
|
|
15480
16294
|
},
|
|
15481
16295
|
"edrv.pending.refresh": async (args) => {
|
|
15482
|
-
const cwd = await
|
|
15483
|
-
const id =
|
|
16296
|
+
const cwd = await kit.write(args.sessionId);
|
|
16297
|
+
const id = pendingRawId(args.id);
|
|
15484
16298
|
const stored = loadEntries(cwd).find((entry) => entry.entry.id === id);
|
|
15485
16299
|
if (!stored) return {
|
|
15486
16300
|
ok: false,
|
|
15487
16301
|
error: "待审条目不存在"
|
|
15488
16302
|
};
|
|
15489
|
-
const
|
|
15490
|
-
const
|
|
16303
|
+
const fs = kit.ctx.get("fs");
|
|
16304
|
+
const session = sessionOf(kit.ctx, args.sessionId);
|
|
15491
16305
|
const content = await readText(fs, await fs.resolve(id, {
|
|
15492
16306
|
cwd,
|
|
15493
16307
|
agent: session
|
|
@@ -15505,6 +16319,26 @@ function pendingHandlers(ctx, registry) {
|
|
|
15505
16319
|
}
|
|
15506
16320
|
};
|
|
15507
16321
|
}
|
|
16322
|
+
/**
|
|
16323
|
+
* @author ddj 2026年10月29号
|
|
16324
|
+
* @description: 待审投影处理器表:按用途拆成读/判定/历史/维护四组,便于分别收敛权限。
|
|
16325
|
+
* @param ctx Host 上下文
|
|
16326
|
+
* @param registry 工作区记录桶缓存
|
|
16327
|
+
* @returns 待审相关处理器表
|
|
16328
|
+
*/
|
|
16329
|
+
function pendingHandlers(ctx, registry) {
|
|
16330
|
+
const kit = {
|
|
16331
|
+
ctx,
|
|
16332
|
+
read: (sessionId) => workspaceOf(ctx, registry, sessionId),
|
|
16333
|
+
write: (sessionId) => writeWorkspaceOf(ctx, registry, sessionId)
|
|
16334
|
+
};
|
|
16335
|
+
return {
|
|
16336
|
+
...readHandlers(kit),
|
|
16337
|
+
...decideHandlers(kit),
|
|
16338
|
+
...historyHandlers(kit),
|
|
16339
|
+
...refreshHandlers(kit)
|
|
16340
|
+
};
|
|
16341
|
+
}
|
|
15508
16342
|
//#endregion
|
|
15509
16343
|
//#region src/missingReview.ts
|
|
15510
16344
|
/** Missing historical files: frozen, whole-file deletion reviews. */
|
|
@@ -17914,6 +18748,19 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17914
18748
|
};
|
|
17915
18749
|
}
|
|
17916
18750
|
},
|
|
18751
|
+
"skills.list": async (args) => {
|
|
18752
|
+
try {
|
|
18753
|
+
return {
|
|
18754
|
+
ok: true,
|
|
18755
|
+
...await listSkillCatalog(args?.cwd)
|
|
18756
|
+
};
|
|
18757
|
+
} catch (error) {
|
|
18758
|
+
return {
|
|
18759
|
+
ok: false,
|
|
18760
|
+
error: "读取技能候选失败:" + String(error)
|
|
18761
|
+
};
|
|
18762
|
+
}
|
|
18763
|
+
},
|
|
17917
18764
|
"snippets.list": async () => {
|
|
17918
18765
|
try {
|
|
17919
18766
|
return {
|
|
@@ -17977,6 +18824,60 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17977
18824
|
error: "读取代码片段条目失败:" + String(error)
|
|
17978
18825
|
};
|
|
17979
18826
|
}
|
|
18827
|
+
},
|
|
18828
|
+
"quickMenu.get": async () => {
|
|
18829
|
+
try {
|
|
18830
|
+
const state = await readQuickMenu();
|
|
18831
|
+
return {
|
|
18832
|
+
ok: true,
|
|
18833
|
+
doc: state.doc,
|
|
18834
|
+
warning: state.warning
|
|
18835
|
+
};
|
|
18836
|
+
} catch (error) {
|
|
18837
|
+
return {
|
|
18838
|
+
ok: false,
|
|
18839
|
+
error: "读取快捷菜单失败:" + String(error)
|
|
18840
|
+
};
|
|
18841
|
+
}
|
|
18842
|
+
},
|
|
18843
|
+
"quickMenu.save": async (args) => {
|
|
18844
|
+
try {
|
|
18845
|
+
const result = await saveQuickMenu(args.doc, args.baseRevision);
|
|
18846
|
+
if (!result.ok) return {
|
|
18847
|
+
ok: false,
|
|
18848
|
+
error: result.error
|
|
18849
|
+
};
|
|
18850
|
+
return {
|
|
18851
|
+
ok: true,
|
|
18852
|
+
doc: result.doc
|
|
18853
|
+
};
|
|
18854
|
+
} catch (error) {
|
|
18855
|
+
return {
|
|
18856
|
+
ok: false,
|
|
18857
|
+
error: "保存快捷菜单失败:" + String(error)
|
|
18858
|
+
};
|
|
18859
|
+
}
|
|
18860
|
+
},
|
|
18861
|
+
"quickMenu.launcher": async (args) => {
|
|
18862
|
+
try {
|
|
18863
|
+
const result = await patchLauncher({
|
|
18864
|
+
x: args.x,
|
|
18865
|
+
y: args.y
|
|
18866
|
+
});
|
|
18867
|
+
if (!result.ok) return {
|
|
18868
|
+
ok: false,
|
|
18869
|
+
error: result.error
|
|
18870
|
+
};
|
|
18871
|
+
return {
|
|
18872
|
+
ok: true,
|
|
18873
|
+
doc: result.doc
|
|
18874
|
+
};
|
|
18875
|
+
} catch (error) {
|
|
18876
|
+
return {
|
|
18877
|
+
ok: false,
|
|
18878
|
+
error: "保存悬浮按钮位置失败:" + String(error)
|
|
18879
|
+
};
|
|
18880
|
+
}
|
|
17980
18881
|
}
|
|
17981
18882
|
};
|
|
17982
18883
|
}
|
|
@@ -28415,7 +29316,7 @@ function attachRuntime(ctx, runtime) {
|
|
|
28415
29316
|
evidence.persist(cwd, snapshot);
|
|
28416
29317
|
return true;
|
|
28417
29318
|
});
|
|
28418
|
-
const pending =
|
|
29319
|
+
const pending = installPending(ctx, (write) => {
|
|
28419
29320
|
try {
|
|
28420
29321
|
trackPendingCover(write.cwd, write.path, true);
|
|
28421
29322
|
} catch {}
|