dsh-lite-pet 0.2.1 → 0.2.2

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/index.js CHANGED
@@ -33,6 +33,9 @@ const ENABLED_ROUTE = '/api/dsh-lite-pet/enabled'
33
33
  const PUSH_INTERVAL_MS = 250
34
34
  const BUBBLE_TEXT_LIMIT = 120
35
35
  const BALANCE_VISIBLE_MS = 9000
36
+ // Backstop for the "completed" bubble. Clicking the pet clears it immediately; this only
37
+ // stops a forgotten message from sitting there for the rest of the session.
38
+ const DONE_VISIBLE_MS = 12 * 60_000
36
39
  const RESTART_DELAY_MS = 4000
37
40
  const MAX_RESTARTS = 3
38
41
 
@@ -313,7 +316,15 @@ function handlePetLine(ctx, runtime, profile, message) {
313
316
  return
314
317
  }
315
318
  if (message?.t === 'open') {
316
- openHarness(ctx, runtime)
319
+ // Clicking the pet is the user acknowledging the result: the completed bubble has
320
+ // done its job once they have gone looking. Clearing it here also means a later
321
+ // "done" from another session is the only thing that can put it back, instead of
322
+ // a twelve-minute-old message reappearing after every click.
323
+ runtime.doneText = null
324
+ runtime.doneAt = 0
325
+ runtime.lastSent = ''
326
+ void openHarness(ctx, runtime)
327
+ pushState(ctx, runtime, profile)
317
328
  return
318
329
  }
319
330
  if (message?.t === 'balance') {
@@ -495,17 +506,24 @@ function pushState(ctx, runtime, profile) {
495
506
  const now = Date.now()
496
507
  let mode = 'idle'
497
508
  let text
498
- let touched = -1
499
509
  let waitingAgent = null
500
510
  let runningAgent = null
511
+ // Each direction compares against its own timestamp. Sharing one accumulator made the
512
+ // running candidate inherit the waiting candidate's timestamp, so with no session
513
+ // waiting the accumulator stayed at its initial value and every running session passed
514
+ // the comparison — the last one inserted into the map won, whatever its actual activity.
515
+ // The bubble then narrated a session the user had already left.
516
+ let waitingTouched = -1
517
+ let runningTouched = -1
501
518
  for (const [agentId, state] of runtime.agents) {
502
519
  if (now - state.touched > 60_000) continue
503
- if (state.waiting > 0 && (waitingAgent === null || state.touched > touched)) {
520
+ if (state.waiting > 0 && state.touched > waitingTouched) {
504
521
  waitingAgent = agentId
505
- touched = state.touched
522
+ waitingTouched = state.touched
506
523
  }
507
- if (state.running && (runningAgent === null || state.touched > touched)) {
524
+ if (state.running && state.touched > runningTouched) {
508
525
  runningAgent = agentId
526
+ runningTouched = state.touched
509
527
  }
510
528
  }
511
529
  if (waitingAgent !== null) {
@@ -514,7 +532,9 @@ function pushState(ctx, runtime, profile) {
514
532
  } else if (runningAgent !== null) {
515
533
  mode = 'working'
516
534
  text = bubbleText(runtime.agents.get(runningAgent).text) || '正在工作…'
517
- } else if (runtime.doneText !== null && now - runtime.doneAt < 12 * 60_000) {
535
+ } else if (runtime.doneText !== null && now - runtime.doneAt < DONE_VISIBLE_MS) {
536
+ // Only when nothing else is going on: a completed session must never take the bubble
537
+ // away from one that is still working or still waiting for an answer.
518
538
  mode = 'done'
519
539
  text = runtime.doneText
520
540
  }
@@ -579,32 +599,105 @@ async function sendBalance(ctx, runtime, profile) {
579
599
  sendPet(runtime, { t: 'state', mode: 'balance', text, level })
580
600
  }
581
601
 
582
- // ——— open the Harness window ——————————————————————————————————————————
583
-
584
- let protocolChecked
585
- function desktopProtocolRegistered() {
586
- if (protocolChecked !== undefined) return protocolChecked
587
- try {
588
- const probe = spawn('reg.exe', ['query', 'HKCU\\Software\\Classes\\dsh', '/ve'], { windowsHide: true })
589
- let out = ''
590
- probe.stdout.setEncoding('utf8')
591
- probe.stdout.on('data', (chunk) => {
592
- out += chunk
593
- })
594
- probe.on('close', () => {
595
- protocolChecked = out.includes('URL:dsh')
602
+ // ——— reopen the desktop window ————————————————————————————————————————
603
+
604
+ // Ask the desktop shell to show its window by clicking its own tray icon.
605
+ //
606
+ // This runs in the plugin process, not in the pet, and that is the whole point. The
607
+ // sandbox starts the pet at low integrity; a low-integrity process is blocked by UIPI
608
+ // from reading the taskbar's XAML automation tree, so it sees five empty children with
609
+ // no tray buttons and every attempt from there fails silently. The plugin host runs at
610
+ // medium integrity, where the same query returns all thirty-one elements and the icon
611
+ // is invocable. Measured on this machine, both ways.
612
+ //
613
+ // PowerShell is used as the automation host because it ships with Windows, inherits
614
+ // this process's integrity, and keeps the plugin free of a compiled helper that would
615
+ // have to be built and shipped alongside it.
616
+ const TRAY_CLICK_SCRIPT = `
617
+ Add-Type -AssemblyName UIAutomationClient
618
+ Add-Type -AssemblyName UIAutomationTypes
619
+ $AE = [System.Windows.Automation.AutomationElement]
620
+ $cond = New-Object System.Windows.Automation.PropertyCondition($AE::ClassNameProperty, 'Shell_TrayWnd')
621
+ $bar = $AE::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Children, $cond)
622
+ if (-not $bar) { exit 2 }
623
+ $all = $bar.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
624
+ foreach ($e in $all) {
625
+ try { $cls = $e.Current.ClassName } catch { continue }
626
+ if ($cls -notlike 'SystemTray.*') { continue }
627
+ try { $nm = $e.Current.Name } catch { continue }
628
+ if ($nm.Trim() -like 'DeepSeek Harness*') {
629
+ $inv = $e.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)
630
+ $inv.Invoke()
631
+ exit 0
632
+ }
633
+ }
634
+ exit 1
635
+ `
636
+
637
+ function clickTrayIcon() {
638
+ return new Promise((resolve) => {
639
+ const powershell = join(
640
+ process.env.SystemRoot ?? 'C:\\Windows',
641
+ 'System32',
642
+ 'WindowsPowerShell',
643
+ 'v1.0',
644
+ 'powershell.exe',
645
+ )
646
+ if (!existsSync(powershell)) {
647
+ resolve(false)
648
+ return
649
+ }
650
+ let child
651
+ try {
652
+ child = spawn(powershell, ['-NoProfile', '-NonInteractive', '-Command', TRAY_CLICK_SCRIPT], {
653
+ windowsHide: true,
654
+ stdio: 'ignore',
655
+ })
656
+ } catch {
657
+ resolve(false)
658
+ return
659
+ }
660
+ let settled = false
661
+ const finish = (value) => {
662
+ if (settled) return
663
+ settled = true
664
+ resolve(value)
665
+ }
666
+ const timer = setTimeout(() => {
667
+ try {
668
+ child.kill()
669
+ } catch {
670
+ /* already gone */
671
+ }
672
+ finish(false)
673
+ }, 6000)
674
+ timer.unref?.()
675
+ child.on('error', () => {
676
+ clearTimeout(timer)
677
+ finish(false)
596
678
  })
597
- probe.on('error', () => {
598
- protocolChecked = false
679
+ child.on('close', (code) => {
680
+ clearTimeout(timer)
681
+ finish(code === 0)
599
682
  })
600
- return true // optimistic for the first click; the probe settles quickly
601
- } catch {
602
- protocolChecked = false
603
- return false
604
- }
683
+ })
605
684
  }
606
-
607
- function openHarness(ctx, runtime) {
685
+ async function openHarness(ctx, runtime) {
686
+ // A hidden window can only be reopened through the shell's own focusPrimaryWindow(), and
687
+ // asking for that means clicking its tray icon. That has to happen here rather than in the
688
+ // pet, for the integrity reason documented on clickTrayIcon.
689
+ const clicked = await clickTrayIcon(ctx)
690
+ if (!clicked && runtime.pet === null) {
691
+ // No pet to fall back through, and the helper failed: leave the click alone rather than
692
+ // guessing at a URL that may not belong to this window.
693
+ return
694
+ }
695
+ if (runtime.pet !== null) {
696
+ // The pet knows how to restore a minimised window and has the dsh:// fallback, and it
697
+ // also brings the window to the foreground once it is back.
698
+ sendPet(runtime, { t: 'raise' })
699
+ return
700
+ }
608
701
  const url = (() => {
609
702
  try {
610
703
  return ctx.get('webApp', false)?.getAdvertisedUrl?.()
@@ -612,16 +705,6 @@ function openHarness(ctx, runtime) {
612
705
  return undefined
613
706
  }
614
707
  })()
615
- const registered = desktopProtocolRegistered()
616
- // The pet does the window work: it is a native process, so it can call
617
- // ShellExecuteEx on dsh://open (measured to be the only invocation that reaches the
618
- // running desktop instance — `cmd start` and `rundll32 url.dll` both failed here),
619
- // and it can restore a minimised window through WM_SYSCOMMAND/SC_RESTORE.
620
- // When the protocol is not registered, fall back to opening the web GUI.
621
- if (registered) {
622
- sendPet(runtime, { t: 'raise' })
623
- return
624
- }
625
708
  if (url === undefined || url === null || url === '') return
626
709
  try {
627
710
  const child = spawn('cmd.exe', ['/c', 'start', '', url], { windowsHide: true, detached: true, stdio: 'ignore' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-pet",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "轻量系统级桌宠:原生进程逐像素透明渲染,工作正文/等待确认/完成提示与余额查询联动,点击穿透",
5
5
  "type": "module",
6
6
  "main": "index.js",
Binary file