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.
Files changed (76) hide show
  1. package/README.md +158 -111
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +13362 -11806
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +4110 -2507
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/activePathStore.ts +28 -0
  14. package/src/client/addToConversation.ts +4 -2
  15. package/src/client/ai/inlineProvider.ts +1 -8
  16. package/src/client/commandBridge.ts +14 -42
  17. package/src/client/externalOpen.ts +40 -19
  18. package/src/client/index.ts +101 -18
  19. package/src/client/keybindings.ts +36 -73
  20. package/src/client/monaco/loader.ts +3 -1
  21. package/src/client/openFlow.ts +36 -28
  22. package/src/client/openReceipt.ts +64 -0
  23. package/src/client/panelCommands.ts +101 -0
  24. package/src/client/pdf/pdfLoader.ts +4 -2
  25. package/src/client/receiptState.ts +120 -0
  26. package/src/client/rpc.ts +30 -4
  27. package/src/client/shortcutRecorder.ts +161 -0
  28. package/src/client/shortcutsOfficial.ts +460 -0
  29. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  30. package/src/client/sidebarBridge.ts +7 -0
  31. package/src/client/ui/DiffBadge.ts +2 -1
  32. package/src/client/ui/EditorView.ts +246 -137
  33. package/src/client/ui/McpSettings.ts +10 -8
  34. package/src/client/ui/OfficialSideTab.ts +5 -2
  35. package/src/client/ui/QuickOpen.ts +16 -16
  36. package/src/client/ui/ShortcutSettings.ts +274 -0
  37. package/src/client/ui/commandCatalog.ts +39 -43
  38. package/src/client/useOpenReceipt.ts +128 -0
  39. package/src/compat.ts +5 -3
  40. package/src/dap/manager.ts +46 -13
  41. package/src/devForm.ts +252 -92
  42. package/src/externalHandoff.ts +93 -52
  43. package/src/fileOpenSettings.ts +4 -24
  44. package/src/hostImport.ts +19 -0
  45. package/src/httpGuard.ts +101 -0
  46. package/src/index.ts +106 -97
  47. package/src/integrate.ts +56 -50
  48. package/src/lsp/transport.ts +84 -127
  49. package/src/mcp.ts +58 -9
  50. package/src/mcpIsolation.ts +211 -102
  51. package/src/mcpProject.ts +220 -133
  52. package/src/mcpRuntime.ts +343 -0
  53. package/src/openBridge.ts +69 -0
  54. package/src/openInbox.ts +207 -0
  55. package/src/paths.ts +8 -5
  56. package/src/processTree.ts +63 -0
  57. package/src/reveal.ts +5 -3
  58. package/src/routes.ts +156 -154
  59. package/src/rpc.ts +14 -13
  60. package/src/rules.ts +100 -44
  61. package/src/shared/appUrl.ts +19 -0
  62. package/src/shared/externalOpen.ts +71 -0
  63. package/src/shared/integration.ts +2 -0
  64. package/src/shared/keybindings.ts +256 -0
  65. package/src/shared/mcp.ts +4 -1
  66. package/src/shared/rpc.ts +7 -4
  67. package/src/shared/rules.ts +9 -1
  68. package/src/shared/svn.ts +6 -8
  69. package/src/svnPlanTool.ts +51 -0
  70. package/src/unityBridge.ts +26 -3
  71. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  72. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  73. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  74. package/unity/com.dsh.editor/README.md +16 -25
  75. package/unity/com.dsh.editor/package.json +2 -2
  76. 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/*.mdc;项目规则 <工作区>/.dsh/rules/*.mdc(随仓库共享)。
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 { join } from 'node:path'
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
- * 用户规则目录(~/.dsh/rules)。
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(() => [] as string[])
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(() => null)
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').catch(() => ''))
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
- /** 按作用域解析规则目录(project 先过 requireWorkspace)。 */
301
- function dirOf(ctx: Ctx, scope: RuleScope, workspacePath?: string): string {
302
- if (scope === 'project') return projectRulesDir(requireWorkspace(ctx, workspacePath).path)
303
- return userRulesDir()
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 listRulesDir(userRulesDir(), 'user')
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.scope, ref.workspacePath)
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 invalid = validateRuleFile(input.file)
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.scope, ref.workspacePath)
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.scope, ref.workspacePath)
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(() => null)
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 relHint 相对提示
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 = readRulesSync(userRulesDir(), '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
  /** 待打开路径(已解码,首个路径可携带行列)。 */
@@ -58,6 +58,8 @@ export interface UnityProjectEntry {
58
58
  upToDate: boolean
59
59
  /** 项目目录不存在。 */
60
60
  missingDir: boolean
61
+ /** 安装时绑定的本机 profile bridge 配置文件。 */
62
+ bridgeConfigPath?: string
61
63
  /** 非法原因(非 Unity 项目根等)。 */
62
64
  error?: string
63
65
  }