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 +122 -39
- package/package.json +1 -1
- package/runtime/pet/DshLitePet.exe +0 -0
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
|
-
|
|
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 &&
|
|
520
|
+
if (state.waiting > 0 && state.touched > waitingTouched) {
|
|
504
521
|
waitingAgent = agentId
|
|
505
|
-
|
|
522
|
+
waitingTouched = state.touched
|
|
506
523
|
}
|
|
507
|
-
if (state.running &&
|
|
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 <
|
|
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
|
-
// ———
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
598
|
-
|
|
679
|
+
child.on('close', (code) => {
|
|
680
|
+
clearTimeout(timer)
|
|
681
|
+
finish(code === 0)
|
|
599
682
|
})
|
|
600
|
-
|
|
601
|
-
} catch {
|
|
602
|
-
protocolChecked = false
|
|
603
|
-
return false
|
|
604
|
-
}
|
|
683
|
+
})
|
|
605
684
|
}
|
|
606
|
-
|
|
607
|
-
|
|
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
|
Binary file
|