dsh-artifact-linker 0.3.7 → 0.3.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8 — 2026-10-29
4
+
5
+ ### 修:「在文件夹中显示」有时不给定位
6
+
7
+ `SelectItem` 一直是能选中的,问题是它**不会把窗口带到最前**。于是结果取决于调用瞬间的窗口状态:该目录没窗口时 Explore 会新建一个、天然在前,看着正常;但如果该目录早已开着窗口(尤其同时开着好几个),选中会落在后台那个窗口上——用户眼前没有反应,观感就是「打开了但没定位」。
8
+
9
+ 实测证据:日志一直写 `reveal selected via shell COM` 成功,而同一目录当时开着 7 个资源管理器窗口,选中确实落在 `hwnd=34688214`,但前台是别的程序;直接 `SetForegroundWindow` 返回 `False`(Windows 前台锁),焦点纹丝不动。
10
+
11
+ 三处改动:
12
+
13
+ 1. **抢焦点**:先 `AttachThreadInput` 到当前前台线程,再 `BringWindowToTop` + `SetForegroundWindow`,最后 detach;窗口最小化时先 `ShowWindow(SW_RESTORE)`。
14
+ 2. **校验判据收紧**:原来只看 `SelectedItems().Count >= 1`,窗口里本来就选着别的东西就会假阳性;改为逐项比对**选中的文件名是否等于目标文件**。
15
+ 3. **选窗优先前台**:同目录有多个窗口时,优先选「Explore 之前就在前台的那个」,避免把选中扔到用户没在看的窗口。
16
+
17
+ 退出码也随之细化:`0` = 选中且已在前台(用户真看得见),`6` = 选中成功但没抢到前台(可 alt-tab 找到)。
3
18
  ## 0.3.7 — 2026-10-29
4
19
 
5
20
  ### 改:README 主文件改为中文
package/lib/client.js CHANGED
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  var exports = module.exports;
30
30
 
31
31
  var GUARD = "__dshArtifactLinkerV1";
32
- var VERSION = "0.3.7";
32
+ var VERSION = "0.3.8";
33
33
  // 路由前缀必须与宿主的 ROUTE_PREFIX 一致;永远根相对,绝不拼宿主 origin
34
34
  var ROUTE = "/dsh-artifact-linker";
35
35
 
package/lib/index.js CHANGED
@@ -390,20 +390,55 @@ function psLiteral(s) {
390
390
  }
391
391
 
392
392
  function revealViaShellCom(abs, done) {
393
+ // 「在文件夹中显示」为什么**有时**失效 —— 完整根因链(2026-10-29 实测):
394
+ //
395
+ // SelectItem 能把文件选中,但它**不会**把窗口带到最前。于是结果取决于当时的窗口状态:
396
+ // · 该目录本来没窗口 → Explore 新建窗口,新窗口天然在前,选中可见 ✅
397
+ // · 该目录窗口就在前台 → 选中立即可见 ✅
398
+ // · 该目录窗口在后台 → 选中生效但被别的窗口盖住 ❌ 看着像没定位
399
+ // · 该目录开过多个窗口 → Explore 复用其中一个(未必是用户在看那个)❌ 选在别处
400
+ //
401
+ // 实测证据:日志写「reveal selected via shell COM」成功,但同一目录当时开着 7 个窗口,
402
+ // 选中落在 hwnd=34688214,而前台是微信;直接 SetForegroundWindow 返回 False
403
+ // (Windows 前台锁),必须先 AttachThreadInput 到当前前台线程才抢得到焦点。
404
+ //
405
+ // 所以这里做三件事:优先选「前台那个窗口」、按「选中的就是目标文件」校验、
406
+ // 抢焦点并回读确认前台。
393
407
  var lines = [
394
408
  "$ErrorActionPreference = 'Stop'",
395
409
  "$dir = " + psLiteral(dirname(abs)),
396
410
  "$nm = " + psLiteral(basename(abs)),
411
+ "$sig = '[DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr h);' + " +
412
+ "'[DllImport(\"user32.dll\")] public static extern IntPtr GetForegroundWindow();' + " +
413
+ "'[DllImport(\"user32.dll\")] public static extern bool IsIconic(IntPtr h);' + " +
414
+ "'[DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr h, int n);' + " +
415
+ "'[DllImport(\"user32.dll\")] public static extern bool BringWindowToTop(IntPtr h);' + " +
416
+ "'[DllImport(\"user32.dll\")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);' + " +
417
+ "'[DllImport(\"user32.dll\")] public static extern bool AttachThreadInput(uint a, uint b, bool f);' + " +
418
+ "'[DllImport(\"kernel32.dll\")] public static extern uint GetCurrentThreadId();'",
419
+ "try { Add-Type -MemberDefinition $sig -Name U -Namespace RevealWin -ErrorAction Stop } catch { }",
420
+ // 记住 Explore 之前谁在前台 —— 若它本来就是目标目录,就别把用户从别的窗口里拽走。
421
+ "$fgBefore = [IntPtr]::Zero",
422
+ "try { $fgBefore = [RevealWin.U]::GetForegroundWindow() } catch { }",
397
423
  "$sh = New-Object -ComObject Shell.Application",
398
424
  // Explore 是唯一能可靠「把窗口摆到用户眼前」的入口。
399
- // SHOpenFolderAndSelectItems 能选中却不弹窗(它复用已有窗口,没有就什么都不显示),
400
- // 所以开窗这一步必须交给 Explore,选中的事再交给 SelectItem。
425
+ // SHOpenFolderAndSelectItems 能选中却不弹窗(复用已有窗口,没有就什么都不显示),
426
+ // 所以开窗这一步交给 Explore,选中的事交给 SelectItem。
401
427
  "$sh.Explore($dir)",
428
+ // 选窗策略:先挑「前台那个且目录匹配」的;没有就挑任一目录匹配的。
402
429
  "$w = $null",
430
+ "$wFg = $null",
403
431
  "for ($i = 0; $i -lt 60; $i++) {",
432
+ " $w = $null; $wFg = $null",
404
433
  " foreach ($x in @($sh.Windows())) {",
405
- " try { if ($x.Document.Folder.Self.Path -eq $dir) { $w = $x; break } } catch { }",
434
+ " try {",
435
+ " if ($x.Document.Folder.Self.Path -eq $dir) {",
436
+ " if (-not $w) { $w = $x }",
437
+ " if ([IntPtr]$x.HWND -eq $fgBefore) { $wFg = $x; break }",
438
+ " }",
439
+ " } catch { }",
406
440
  " }",
441
+ " if ($wFg) { $w = $wFg; break }",
407
442
  " if ($w) { break }",
408
443
  " Start-Sleep -Milliseconds 100",
409
444
  "}",
@@ -418,13 +453,40 @@ function revealViaShellCom(abs, done) {
418
453
  "if (-not $it) { exit 3 }",
419
454
  // 29 = SVSI_SELECT(1) | SVSI_DESELECTOTHERS(4) | SVSI_ENSUREVISIBLE(8) | SVSI_FOCUSED(16)。
420
455
  // 旧代码只传 1,在某些窗口状态下静默无效;带上 ENS|FOCUSED 才保证滚到可视区并高亮。
421
- // 循环校验 SelectedItems(),选中了才 exit 0 —— 不靠「脚本没报错」自欺。
456
+ "$okName = $false",
422
457
  "for ($i = 0; $i -lt 25; $i++) {",
423
458
  " $w.Document.SelectItem($it, 29)",
424
459
  " Start-Sleep -Milliseconds 120",
425
- " if (@($w.Document.SelectedItems()).Count -ge 1) { exit 0 }",
460
+ // 判据必须是「选中的就是目标文件」,不能只看 SelectedItems() 有没有值 ——
461
+ // 窗口里本来就选着别的东西时,旧的判据会假阳性(真实事故)。
462
+ " $okName = $false",
463
+ " foreach ($s in @($w.Document.SelectedItems())) { if ($s.Name -eq $nm) { $okName = $true; break } }",
464
+ " if ($okName) { break }",
426
465
  "}",
427
- "exit 4",
466
+ "if (-not $okName) { exit 4 }",
467
+ // ── 抢焦点 ────────────────────────────────────────────────────────────
468
+ // 直接 SetForegroundWindow 会被前台锁拒绝(返回 False,焦点纹丝不动),
469
+ // 必须先 AttachThreadInput 到当前前台线程,共享输入队列后才能改前台。
470
+ "$h = [IntPtr]$w.HWND",
471
+ "$fgOk = $false",
472
+ "try {",
473
+ " if ([RevealWin.U]::IsIconic($h)) { [void][RevealWin.U]::ShowWindow($h, 9) }",
474
+ " $fg = [RevealWin.U]::GetForegroundWindow()",
475
+ " if ($fg -ne $h) {",
476
+ " $fgPid = 0",
477
+ " $fgT = [RevealWin.U]::GetWindowThreadProcessId($fg, [ref]$fgPid)",
478
+ " $me = [RevealWin.U]::GetCurrentThreadId()",
479
+ " if ($fgT -ne 0 -and $fgT -ne $me) { [void][RevealWin.U]::AttachThreadInput($me, $fgT, $true) }",
480
+ " [void][RevealWin.U]::BringWindowToTop($h)",
481
+ " [void][RevealWin.U]::SetForegroundWindow($h)",
482
+ " if ($fgT -ne 0 -and $fgT -ne $me) { [void][RevealWin.U]::AttachThreadInput($me, $fgT, $false) }",
483
+ " }",
484
+ " Start-Sleep -Milliseconds 150",
485
+ " $fgOk = ([RevealWin.U]::GetForegroundWindow() -eq $h)",
486
+ "} catch { }",
487
+ // exit 0 = 选中且已在前台(用户确实看得见);exit 6 = 选中了但没抢到前台(仍可用 alt-tab 找到)。
488
+ "if ($fgOk) { exit 0 }",
489
+ "exit 6",
428
490
  ];
429
491
  runPwshEncoded(lines, { timeout: 30000 }, done);
430
492
  }
@@ -542,12 +604,19 @@ function launchPath(abs, action, res) {
542
604
  // Windows 的 reveal 走 Shell COM,不经过 explorer 命令行(原因见 revealViaShellCom 上方注释)
543
605
  if (process.platform === "win32" && action === "reveal") {
544
606
  revealViaShellCom(abs, function (err) {
607
+ var code = err && err.code != null ? Number(err.code) : (err ? null : 0);
545
608
  if (!err) {
546
- try { console.log("[dsh-artifact-linker] reveal selected via shell COM -> " + abs); } catch (e) { }
609
+ try { console.log("[dsh-artifact-linker] reveal selected & focused (exit 0) -> " + abs); } catch (e) { }
610
+ return;
611
+ }
612
+ // exit 6 = 选中成功但没抢到前台(窗口仍被别的程序盖着)。此时**不要**再降级开一次文件夹,
613
+ // 否则会多开一个窗口,反而更乱;只要如实记下来,让用户能 alt-tab 找到。
614
+ if (code === 6) {
615
+ try { console.log("[dsh-artifact-linker] reveal selected but window stayed in background (exit 6, foreground lock) -> " + abs); } catch (e) { }
547
616
  return;
548
617
  }
549
- // 降级:COM 没成时至少把父目录打开,避免「完全没反应」
550
- try { console.log("[dsh-artifact-linker] reveal COM failed (" + (err.code == null ? "exit-or-timeout" : err.code) + "); falling back to opening the folder"); } catch (e) { }
618
+ // 其余失败:至少把父目录打开,避免「完全没反应」
619
+ try { console.log("[dsh-artifact-linker] reveal COM failed (exit " + (code == null ? "unknown" : code) + "); falling back to opening the folder -> " + abs); } catch (e) { }
551
620
  var fb = execFile("explorer.exe", [isDir ? abs : dirname(abs)], {}, function () { });
552
621
  if (fb && typeof fb.unref === "function") fb.unref();
553
622
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-artifact-linker",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "DSH 产物路径联动器:常驻注入「产物路径铁律」让模型输出完整绝对路径,同时把聊天正文里的文件名/路径渲染成一键可点的产物芯片——左键预览、右键菜单可打开/定位/另存为/复制文件/复制路径/添加到对话框。宿主端提供回环只读路由与 /saveas /copyfile 两个文件搬运动作(Shell COM 揭示、WinForms 保存对话框、CF_HDROP 剪贴板),客户端零侵入、不改 React 节点、不接管 markdown 图片语法。解决「产物要打开得先复制路径再粘到文件管理器」的反复割裂。/ Turn declared artifact paths into one-click chips: resident system-prompt rules force absolute paths, the client half renders file names as clickable chips, and the right-click menu offers open / reveal in Explorer / save-as / copy-file / copy-path / insert-into-composer. Host routes are loopback-only; zero telemetry.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",