dsh-vscode-mode 0.11.0 → 0.13.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 +158 -111
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +13362 -11806
- package/lib/client.js.map +1 -1
- package/lib/index.js +4110 -2507
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/activePathStore.ts +28 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +14 -42
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +101 -18
- package/src/client/keybindings.ts +36 -73
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +460 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +246 -137
- package/src/client/ui/McpSettings.ts +10 -8
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +16 -16
- package/src/client/ui/ShortcutSettings.ts +274 -0
- package/src/client/ui/commandCatalog.ts +39 -43
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +4 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/rules.ts +100 -44
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +256 -0
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
- package/src/client/ui/KeybindingsPanel.ts +0 -190
package/src/rules.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — 规则管理(Codebuddy/Cursor 式 .mdc 规则)。
|
|
3
|
-
* - 存储:用户规则 ~/.dsh/rules
|
|
3
|
+
* - 存储:用户规则 DSH_HOME/rules + ~/.dsh/rules(当前目录优先);项目规则 <工作区>/.dsh/rules。
|
|
4
4
|
* - 格式:frontmatter(description / alwaysApply / globs / enabled)+ markdown 正文;
|
|
5
5
|
* 类型映射 总是=always / 自动=auto(globs) / 手动=manual(仅索引);enabled 为本插件扩展字段,缺省 true。
|
|
6
6
|
* - 生效:host 注册 systemPrompt.section(order 400),每次装配同步读取(mtime 缓存),
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
12
12
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
|
|
13
13
|
import { Buffer } from 'node:buffer'
|
|
14
|
-
import {
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { join, resolve } from 'node:path'
|
|
15
16
|
import { dshHome } from './paths.js'
|
|
16
|
-
import type { RuleInfo, RuleProject, RuleRefInput, RuleScope, RuleSaveInput } from './shared/rules.js'
|
|
17
|
+
import type { RuleInfo, RuleProject, RuleRefInput, RuleScope, RuleSaveInput, UserRuleOrigin } from './shared/rules.js'
|
|
17
18
|
import type { Ctx } from './store.js'
|
|
18
19
|
|
|
19
20
|
// --region 常量与目录定位
|
|
@@ -33,7 +34,7 @@ const SECTION_NAME = 'dsh-vscode-mode:rules'
|
|
|
33
34
|
const SECTION_ORDER = 400
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* 当前用户规则目录(DSH_HOME/rules)。
|
|
37
38
|
* @author ddj 2026年09月03号
|
|
38
39
|
* @param home DSH home(缺省自动解析)
|
|
39
40
|
* @returns 绝对路径
|
|
@@ -42,6 +43,32 @@ export function userRulesDir(home = dshHome()): string {
|
|
|
42
43
|
return join(home, 'rules')
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface UserRulesDirs {
|
|
47
|
+
current: string
|
|
48
|
+
legacy?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** DSH_HOME 改变时仍读取系统用户目录的旧规则;相同路径不重复加载。 */
|
|
52
|
+
export function userRuleDirs(activeHome = dshHome(), systemHome = homedir()): UserRulesDirs {
|
|
53
|
+
const current = resolve(userRulesDir(activeHome))
|
|
54
|
+
const legacy = resolve(userRulesDir(join(systemHome, '.dsh')))
|
|
55
|
+
const key = (path: string) => process.platform === 'win32' ? path.toLowerCase() : path
|
|
56
|
+
return key(current) === key(legacy) ? { current } : { current, legacy }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function userDirOf(origin: UserRuleOrigin | undefined, dirs: UserRulesDirs): string {
|
|
60
|
+
if (origin === undefined || origin === 'current') return dirs.current
|
|
61
|
+
if (origin === 'legacy' && dirs.legacy) return dirs.legacy
|
|
62
|
+
throw new Error('无效的用户规则来源')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 同名时当前目录优先;禁用的当前规则也应遮蔽旧目录规则。 */
|
|
66
|
+
function mergeUserRules<T>(current: T[], legacy: T[], fileOf: (rule: T) => string): T[] {
|
|
67
|
+
const key = (rule: T) => process.platform === 'win32' ? fileOf(rule).toLowerCase() : fileOf(rule)
|
|
68
|
+
const seen = new Set(current.map(key))
|
|
69
|
+
return [...current, ...legacy.filter((rule) => !seen.has(key(rule)))].sort((a, b) => fileOf(a).localeCompare(fileOf(b)))
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
/**
|
|
46
73
|
* 项目规则目录(<工作区>/.dsh/rules)。
|
|
47
74
|
* @author ddj 2026年09月03号
|
|
@@ -261,20 +288,33 @@ export function renderRulesSection(user: LoadedRule[], project: LoadedRule[], wo
|
|
|
261
288
|
|
|
262
289
|
// --region IO(列表 / 读 / 存 / 删 / 开关)
|
|
263
290
|
/** 单目录规则元信息列表(仅 *.mdc,最多 RULE_DIR_CAP 个;目录缺失返回空)。 */
|
|
264
|
-
async function listRulesDir(dir: string, scope: RuleScope): Promise<RuleInfo[]> {
|
|
265
|
-
const names = await readdir(dir).catch(() =>
|
|
291
|
+
async function listRulesDir(dir: string, scope: RuleScope, origin?: UserRuleOrigin): Promise<RuleInfo[]> {
|
|
292
|
+
const names = await readdir(dir).catch((error: NodeJS.ErrnoException) => {
|
|
293
|
+
if (error.code === 'ENOENT') return [] as string[]
|
|
294
|
+
throw error
|
|
295
|
+
})
|
|
266
296
|
const mdcs = names.filter((name) => name.endsWith('.mdc')).sort().slice(0, RULE_DIR_CAP)
|
|
267
297
|
const out: RuleInfo[] = []
|
|
268
298
|
for (const file of mdcs) {
|
|
269
299
|
const absPath = join(dir, file)
|
|
270
|
-
const info = await stat(absPath).catch(() =>
|
|
300
|
+
const info = await stat(absPath).catch((error: NodeJS.ErrnoException) => {
|
|
301
|
+
if (error.code === 'ENOENT') return null
|
|
302
|
+
throw error
|
|
303
|
+
})
|
|
271
304
|
if (!info || !info.isFile()) continue
|
|
272
|
-
const parsed = parseRuleMdc(await readFile(absPath, 'utf8')
|
|
273
|
-
out.push(toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs))
|
|
305
|
+
const parsed = parseRuleMdc(await readFile(absPath, 'utf8'))
|
|
306
|
+
out.push(toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs, origin))
|
|
274
307
|
}
|
|
275
308
|
return out
|
|
276
309
|
}
|
|
277
310
|
|
|
311
|
+
/** 列表与注入共用当前目录优先的合并约定。 */
|
|
312
|
+
export async function listUserRules(dirs = userRuleDirs()): Promise<RuleInfo[]> {
|
|
313
|
+
const current = await listRulesDir(dirs.current, 'user', 'current')
|
|
314
|
+
const legacy = dirs.legacy ? await listRulesDir(dirs.legacy, 'user', 'legacy') : []
|
|
315
|
+
return mergeUserRules(current, legacy, (rule) => rule.file)
|
|
316
|
+
}
|
|
317
|
+
|
|
278
318
|
/** 已注册 workspace 列表(项目 Tab 与写入校验共用)。 */
|
|
279
319
|
function workspaceList(ctx: Ctx): Array<{ path: string; title?: string }> {
|
|
280
320
|
const list = ctx.get('workspaceRegistry')?.list?.() ?? []
|
|
@@ -297,10 +337,16 @@ function requireWorkspace(ctx: Ctx, workspacePath: string | undefined): { path:
|
|
|
297
337
|
return workspace as { path: string; title?: string }
|
|
298
338
|
}
|
|
299
339
|
|
|
300
|
-
/**
|
|
301
|
-
function dirOf(ctx: Ctx,
|
|
302
|
-
|
|
303
|
-
|
|
340
|
+
/** 按作用域解析规则目录;客户端路径和未识别的来源均不作为目标使用。 */
|
|
341
|
+
function dirOf(ctx: Ctx, ref: RuleRefInput, dirs: UserRulesDirs): string {
|
|
342
|
+
const invalid = validateRuleFile(ref.file)
|
|
343
|
+
if (invalid) throw new Error(invalid)
|
|
344
|
+
if (ref.scope === 'project') {
|
|
345
|
+
if (ref.origin !== undefined) throw new Error('项目规则不能指定用户规则来源')
|
|
346
|
+
return projectRulesDir(requireWorkspace(ctx, ref.workspacePath).path)
|
|
347
|
+
}
|
|
348
|
+
if (ref.scope !== 'user') throw new Error('无效的规则作用域')
|
|
349
|
+
return userDirOf(ref.origin, dirs)
|
|
304
350
|
}
|
|
305
351
|
|
|
306
352
|
/**
|
|
@@ -309,8 +355,8 @@ function dirOf(ctx: Ctx, scope: RuleScope, workspacePath?: string): string {
|
|
|
309
355
|
* @param ctx DSH 上下文
|
|
310
356
|
* @returns rules.list 载荷
|
|
311
357
|
*/
|
|
312
|
-
export async function rulesList(ctx: Ctx): Promise<{ user: RuleInfo[]; projects: RuleProject[] }> {
|
|
313
|
-
const user = await
|
|
358
|
+
export async function rulesList(ctx: Ctx, dirs = userRuleDirs()): Promise<{ user: RuleInfo[]; projects: RuleProject[] }> {
|
|
359
|
+
const user = await listUserRules(dirs)
|
|
314
360
|
const projects: RuleProject[] = []
|
|
315
361
|
for (const workspace of workspaceList(ctx)) {
|
|
316
362
|
const dir = projectRulesDir(workspace.path)
|
|
@@ -328,8 +374,8 @@ export async function rulesList(ctx: Ctx): Promise<{ user: RuleInfo[]; projects:
|
|
|
328
374
|
* @param ref 作用域 + 文件名(+ 项目工作区)
|
|
329
375
|
* @returns 文件全文
|
|
330
376
|
*/
|
|
331
|
-
export async function rulesRead(ctx: Ctx, ref: RuleRefInput): Promise<string> {
|
|
332
|
-
const dir = dirOf(ctx, ref
|
|
377
|
+
export async function rulesRead(ctx: Ctx, ref: RuleRefInput, dirs = userRuleDirs()): Promise<string> {
|
|
378
|
+
const dir = dirOf(ctx, ref, dirs)
|
|
333
379
|
return readFile(join(dir, ref.file), 'utf8')
|
|
334
380
|
}
|
|
335
381
|
|
|
@@ -344,10 +390,10 @@ export async function rulesRead(ctx: Ctx, ref: RuleRefInput): Promise<string> {
|
|
|
344
390
|
* @param mtime 修改时间毫秒
|
|
345
391
|
* @returns 规则元信息
|
|
346
392
|
*/
|
|
347
|
-
function toRuleInfo(scope: RuleScope, file: string, absPath: string, parsed: ParsedRule, size: number, mtime: number): RuleInfo {
|
|
393
|
+
function toRuleInfo(scope: RuleScope, file: string, absPath: string, parsed: ParsedRule, size: number, mtime: number, origin?: UserRuleOrigin): RuleInfo {
|
|
348
394
|
return {
|
|
349
|
-
scope, file, absPath,
|
|
350
|
-
relHint: scope === 'project' ? '.dsh/rules/' : 'rules/',
|
|
395
|
+
scope, file, absPath, ...(scope === 'user' ? { origin: origin ?? 'current' } : {}),
|
|
396
|
+
relHint: scope === 'project' ? '.dsh/rules/' : origin === 'legacy' ? '~/.dsh/rules/' : 'rules/',
|
|
351
397
|
description: parsed.description, type: ruleTypeOf(parsed), globs: parsed.globs, enabled: parsed.enabled,
|
|
352
398
|
size, mtime, error: parsed.error,
|
|
353
399
|
}
|
|
@@ -360,10 +406,8 @@ function toRuleInfo(scope: RuleScope, file: string, absPath: string, parsed: Par
|
|
|
360
406
|
* @param input 保存入参
|
|
361
407
|
* @returns 保存后的规则元信息
|
|
362
408
|
*/
|
|
363
|
-
export async function rulesSave(ctx: Ctx, input: RuleSaveInput): Promise<RuleInfo> {
|
|
364
|
-
const
|
|
365
|
-
if (invalid) throw new Error(invalid)
|
|
366
|
-
const dir = dirOf(ctx, input.scope, input.workspacePath)
|
|
409
|
+
export async function rulesSave(ctx: Ctx, input: RuleSaveInput, dirs = userRuleDirs()): Promise<RuleInfo> {
|
|
410
|
+
const dir = dirOf(ctx, input, dirs)
|
|
367
411
|
await mkdir(dir, { recursive: true })
|
|
368
412
|
const absPath = join(dir, input.file)
|
|
369
413
|
const content = String(input.content ?? '')
|
|
@@ -376,7 +420,7 @@ export async function rulesSave(ctx: Ctx, input: RuleSaveInput): Promise<RuleInf
|
|
|
376
420
|
await writeFile(absPath, content, 'utf8')
|
|
377
421
|
}
|
|
378
422
|
const info = await stat(absPath).catch(() => null)
|
|
379
|
-
return toRuleInfo(input.scope, input.file, absPath, parseRuleMdc(content), info?.size ?? Buffer.byteLength(content), info?.mtimeMs ?? Date.now())
|
|
423
|
+
return toRuleInfo(input.scope, input.file, absPath, parseRuleMdc(content), info?.size ?? Buffer.byteLength(content), info?.mtimeMs ?? Date.now(), input.origin)
|
|
380
424
|
}
|
|
381
425
|
|
|
382
426
|
/**
|
|
@@ -384,8 +428,8 @@ export async function rulesSave(ctx: Ctx, input: RuleSaveInput): Promise<RuleInf
|
|
|
384
428
|
* @author ddj 2026年09月03号
|
|
385
429
|
* @param ref 作用域 + 文件名(+ 项目工作区)
|
|
386
430
|
*/
|
|
387
|
-
export async function rulesRemove(ctx: Ctx, ref: RuleRefInput): Promise<void> {
|
|
388
|
-
const dir = dirOf(ctx, ref
|
|
431
|
+
export async function rulesRemove(ctx: Ctx, ref: RuleRefInput, dirs = userRuleDirs()): Promise<void> {
|
|
432
|
+
const dir = dirOf(ctx, ref, dirs)
|
|
389
433
|
await rm(join(dir, ref.file), { force: true })
|
|
390
434
|
}
|
|
391
435
|
|
|
@@ -396,8 +440,8 @@ export async function rulesRemove(ctx: Ctx, ref: RuleRefInput): Promise<void> {
|
|
|
396
440
|
* @param enabled 目标状态
|
|
397
441
|
* @returns 更新后的规则元信息
|
|
398
442
|
*/
|
|
399
|
-
export async function rulesToggle(ctx: Ctx, ref: RuleRefInput, enabled: boolean): Promise<RuleInfo> {
|
|
400
|
-
const dir = dirOf(ctx, ref
|
|
443
|
+
export async function rulesToggle(ctx: Ctx, ref: RuleRefInput, enabled: boolean, dirs = userRuleDirs()): Promise<RuleInfo> {
|
|
444
|
+
const dir = dirOf(ctx, ref, dirs)
|
|
401
445
|
const absPath = join(dir, ref.file)
|
|
402
446
|
const text = await readRuleText(ctx, ref, absPath)
|
|
403
447
|
if (text === null) throw new Error('规则文件不存在:' + ref.file)
|
|
@@ -409,7 +453,10 @@ export async function rulesToggle(ctx: Ctx, ref: RuleRefInput, enabled: boolean)
|
|
|
409
453
|
|
|
410
454
|
/** 按作用域读规则全文:project 与写盘同通道走 ctx fs,user 走 node fs;缺失返回 null。 */
|
|
411
455
|
async function readRuleText(ctx: Ctx, ref: RuleRefInput, absPath: string): Promise<string | null> {
|
|
412
|
-
if (ref.scope !== 'project') return readFile(absPath, 'utf8').catch(() =>
|
|
456
|
+
if (ref.scope !== 'project') return readFile(absPath, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
457
|
+
if (error.code === 'ENOENT') return null
|
|
458
|
+
throw error
|
|
459
|
+
})
|
|
413
460
|
const fs = ctx.get('fs')
|
|
414
461
|
if (!fs) throw new Error('缺少 fs 服务')
|
|
415
462
|
const target = await fs.resolve('.dsh/rules/' + ref.file, { cwd: requireWorkspace(ctx, ref.workspacePath).path })
|
|
@@ -435,7 +482,7 @@ async function rulesSaveContent(ctx: Ctx, ref: RuleRefInput, absPath: string, co
|
|
|
435
482
|
/** 重建单条规则元信息(toggle 后回传 UI;stat 失败时用内容长度 + 当前时间兜底)。 */
|
|
436
483
|
async function ruleInfoOf(ref: RuleRefInput, absPath: string, content: string): Promise<RuleInfo> {
|
|
437
484
|
const info = await stat(absPath).catch(() => null)
|
|
438
|
-
return toRuleInfo(ref.scope, ref.file, absPath, parseRuleMdc(content), info?.size ?? Buffer.byteLength(content), info?.mtimeMs ?? Date.now())
|
|
485
|
+
return toRuleInfo(ref.scope, ref.file, absPath, parseRuleMdc(content), info?.size ?? Buffer.byteLength(content), info?.mtimeMs ?? Date.now(), ref.origin)
|
|
439
486
|
}
|
|
440
487
|
// --endregion
|
|
441
488
|
|
|
@@ -449,20 +496,20 @@ interface InjCacheEntry {
|
|
|
449
496
|
const injCache = new Map<string, InjCacheEntry>()
|
|
450
497
|
|
|
451
498
|
/**
|
|
452
|
-
* 同步读取一个规则目录(装配 provider 内专用):仅 *.mdc,mtime
|
|
499
|
+
* 同步读取一个规则目录(装配 provider 内专用):仅 *.mdc,mtime 缓存;缺失视为空。
|
|
453
500
|
* @author ddj 2026年09月03号
|
|
454
501
|
* @param dir 规则目录
|
|
455
502
|
* @param scope 作用域
|
|
456
|
-
* @param
|
|
503
|
+
* @param origin 用户规则来源
|
|
457
504
|
* @returns 已加载规则列表
|
|
458
505
|
*/
|
|
459
|
-
function readRulesSync(dir: string, scope: RuleScope): LoadedRule[] {
|
|
460
|
-
if (!existsSync(dir)) return []
|
|
506
|
+
function readRulesSync(dir: string, scope: RuleScope, origin?: UserRuleOrigin): LoadedRule[] {
|
|
461
507
|
let names: string[]
|
|
462
508
|
try {
|
|
463
509
|
names = readdirSync(dir)
|
|
464
|
-
} catch {
|
|
465
|
-
return []
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
512
|
+
throw error
|
|
466
513
|
}
|
|
467
514
|
const out: LoadedRule[] = []
|
|
468
515
|
for (const file of names.filter((name) => name.endsWith('.mdc')).sort().slice(0, RULE_DIR_CAP)) {
|
|
@@ -470,8 +517,9 @@ function readRulesSync(dir: string, scope: RuleScope): LoadedRule[] {
|
|
|
470
517
|
let info: { mtimeMs: number; size: number; isFile(): boolean }
|
|
471
518
|
try {
|
|
472
519
|
info = statSync(absPath)
|
|
473
|
-
} catch {
|
|
474
|
-
continue
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
522
|
+
throw error
|
|
475
523
|
}
|
|
476
524
|
if (!info.isFile()) continue
|
|
477
525
|
const cached = injCache.get(absPath)
|
|
@@ -481,19 +529,27 @@ function readRulesSync(dir: string, scope: RuleScope): LoadedRule[] {
|
|
|
481
529
|
} else {
|
|
482
530
|
try {
|
|
483
531
|
parsed = parseRuleMdc(readFileSync(absPath, 'utf8'))
|
|
484
|
-
} catch {
|
|
485
|
-
continue
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
534
|
+
throw error
|
|
486
535
|
}
|
|
487
536
|
injCache.set(absPath, { mtimeMs: info.mtimeMs, size: info.size, parsed })
|
|
488
537
|
}
|
|
489
538
|
out.push({
|
|
490
|
-
info: toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs),
|
|
539
|
+
info: toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs, origin),
|
|
491
540
|
body: parsed.body,
|
|
492
541
|
})
|
|
493
542
|
}
|
|
494
543
|
return out
|
|
495
544
|
}
|
|
496
545
|
|
|
546
|
+
/** 同步装配与列表同名遮蔽策略一致。 */
|
|
547
|
+
export function readUserRulesSync(dirs = userRuleDirs()): LoadedRule[] {
|
|
548
|
+
const current = readRulesSync(dirs.current, 'user', 'current')
|
|
549
|
+
const legacy = dirs.legacy ? readRulesSync(dirs.legacy, 'user', 'legacy') : []
|
|
550
|
+
return mergeUserRules(current, legacy, (rule) => rule.info.file)
|
|
551
|
+
}
|
|
552
|
+
|
|
497
553
|
/** 从装配上下文解析会话工作区 cwd(agent.id → sessions → header.cwd;缺链返回 null)。 */
|
|
498
554
|
function cwdFromAssemble(ctx: Ctx, asm: { agent?: { id?: unknown } } | undefined): string | null {
|
|
499
555
|
const id = asm?.agent?.id
|
|
@@ -509,12 +565,12 @@ function cwdFromAssemble(ctx: Ctx, asm: { agent?: { id?: unknown } } | undefined
|
|
|
509
565
|
* @param ctx DSH host 上下文
|
|
510
566
|
* @returns 是否成功注册
|
|
511
567
|
*/
|
|
512
|
-
export function installRulesSection(ctx: Ctx): boolean {
|
|
568
|
+
export function installRulesSection(ctx: Ctx, dirs = userRuleDirs()): boolean {
|
|
513
569
|
const sp = ctx.get('systemPrompt')
|
|
514
570
|
if (!sp || typeof sp.section !== 'function') return false
|
|
515
571
|
const provider = (asm: { agent?: { id?: unknown } }): string => {
|
|
516
572
|
try {
|
|
517
|
-
const user =
|
|
573
|
+
const user = readUserRulesSync(dirs)
|
|
518
574
|
const cwd = cwdFromAssemble(ctx, asm)
|
|
519
575
|
const project = cwd ? readRulesSync(projectRulesDir(cwd), 'project') : []
|
|
520
576
|
return renderRulesSection(user, project, cwd ?? undefined)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Browser application URL resolution shared by transport and asset loaders. @author ddj 2026年09月28号 */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a product-local path under the current application's document base.
|
|
5
|
+
* Supports HTTP reverse-proxy prefixes and the official Desktop dsh-app origin.
|
|
6
|
+
* @author ddj 2026年09月28号
|
|
7
|
+
* @param path Root-shaped internal endpoint or asset path; never an external URL.
|
|
8
|
+
* @param base Explicit document base for tests; otherwise document.baseURI or location.href.
|
|
9
|
+
* @returns Absolute application URL, or the original root path outside a browser.
|
|
10
|
+
*/
|
|
11
|
+
export function appUrl(path: string, base?: string): string {
|
|
12
|
+
if (!path.startsWith('/') || path.startsWith('//')) throw new Error('Expected an application-local path')
|
|
13
|
+
const source = base ?? (typeof document !== 'undefined' ? document.baseURI : undefined)
|
|
14
|
+
?? (typeof location !== 'undefined' ? location.href : undefined)
|
|
15
|
+
if (!source) return path
|
|
16
|
+
const root = new URL('.', source)
|
|
17
|
+
if (!['http:', 'https:', 'dsh-app:'].includes(root.protocol)) throw new Error('Unsupported application origin')
|
|
18
|
+
return new URL(path.slice(1), root).href
|
|
19
|
+
}
|
|
@@ -21,6 +21,77 @@ export const EDRV_PARAM_KEYS = [EDRV_OPEN_FLAG, EDRV_PATHS_KEY, EDRV_LINE_KEY, E
|
|
|
21
21
|
/** 路径分隔符(编码后路径不含裸逗号,分隔安全)。 */
|
|
22
22
|
export const PATHS_SEPARATOR = ','
|
|
23
23
|
|
|
24
|
+
export const OPEN_TTL_MS = 60_000
|
|
25
|
+
export const OPEN_MAX_BYTES = 65_536
|
|
26
|
+
export const OPEN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
27
|
+
|
|
28
|
+
export interface InboxOpen extends OpenParams {
|
|
29
|
+
version: 1
|
|
30
|
+
requestId: string
|
|
31
|
+
profile: string
|
|
32
|
+
createdAt: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InboxAck {
|
|
36
|
+
version: 1
|
|
37
|
+
requestId: string
|
|
38
|
+
profile: string
|
|
39
|
+
createdAt: number
|
|
40
|
+
success: boolean
|
|
41
|
+
error?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect the host platform without importing node builtins.
|
|
46
|
+
* @author ddj 2026年09月28号
|
|
47
|
+
* @returns Platform name, or an empty string outside Node.
|
|
48
|
+
*/
|
|
49
|
+
function platformName(): string {
|
|
50
|
+
return typeof process === 'object' && process !== null ? String(process.platform) : ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compare profile directories by physical identity, not by spelling.
|
|
55
|
+
* Windows accepts either separator and folds case; other platforms stay exact.
|
|
56
|
+
* @author ddj 2026年09月28号
|
|
57
|
+
* @param left Declared profile directory.
|
|
58
|
+
* @param right Expected profile directory.
|
|
59
|
+
* @param platform Platform name for separator and case rules.
|
|
60
|
+
* @returns Whether both name the same profile.
|
|
61
|
+
*/
|
|
62
|
+
export function sameProfile(left: string, right: string, platform = platformName()): boolean {
|
|
63
|
+
if (platform === 'win32') {
|
|
64
|
+
const fold = (value: string) => value.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase()
|
|
65
|
+
return fold(left) === fold(right)
|
|
66
|
+
}
|
|
67
|
+
return left.replace(/\/+$/, '') === right.replace(/\/+$/, '')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @public @author ddj 2026年09月28号
|
|
71
|
+
* Validate the open-only envelope before accepting filesystem input.
|
|
72
|
+
* @param value Parsed untrusted JSON.
|
|
73
|
+
* @param profile Exact expected profile directory.
|
|
74
|
+
* @param now Current epoch milliseconds.
|
|
75
|
+
* @param platform Platform name used for profile identity.
|
|
76
|
+
* @returns A typed request or null.
|
|
77
|
+
*/
|
|
78
|
+
export function parseInboxOpen(value: unknown, profile: string, now = Date.now(), platform = platformName()): InboxOpen | null {
|
|
79
|
+
if (!value || typeof value !== 'object') return null
|
|
80
|
+
const row = value as Record<string, unknown>
|
|
81
|
+
const keys = ['version', 'requestId', 'profile', 'paths', 'line', 'column', 'createdAt']
|
|
82
|
+
if (Object.keys(row).some((key) => !keys.includes(key))) return null
|
|
83
|
+
if (row.version !== 1 || typeof row.profile !== 'string' || !sameProfile(row.profile, profile, platform)
|
|
84
|
+
|| typeof row.requestId !== 'string' || !OPEN_ID.test(row.requestId)) return null
|
|
85
|
+
if (typeof row.createdAt !== 'number' || !Number.isSafeInteger(row.createdAt)) return null
|
|
86
|
+
if (now - row.createdAt >= OPEN_TTL_MS || row.createdAt > now + 5000) return null
|
|
87
|
+
if (!Array.isArray(row.paths) || !row.paths.length || row.paths.length > 20) return null
|
|
88
|
+
if (!row.paths.every((path) => typeof path === 'string' && path.length <= 8192 && !/[\u0000-\u001f]/.test(path) && /^(?:\/|[a-z]:[\\/]|\\\\)/i.test(path))) return null
|
|
89
|
+
for (const key of ['line', 'column']) {
|
|
90
|
+
if (row[key] !== undefined && (!Number.isSafeInteger(row[key]) || Number(row[key]) < 1)) return null
|
|
91
|
+
}
|
|
92
|
+
return row as unknown as InboxOpen
|
|
93
|
+
}
|
|
94
|
+
|
|
24
95
|
/** 一次外部深链请求(解析结果)。 */
|
|
25
96
|
export interface OpenParams {
|
|
26
97
|
/** 待打开路径(已解码,首个路径可携带行列)。 */
|