deskbuddy 0.2.0-beta.0 → 0.2.0-beta.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/README.md +32 -32
- package/dist/cli.js +3354 -424
- package/dist/gate.cjs +38 -0
- package/dist/hook.cjs +279 -6
- package/docs/INSTALL.md +95 -47
- package/hook/gate.cjs +38 -0
- package/hook/hook.cjs +279 -6
- package/package.json +5 -1
package/hook/gate.cjs
CHANGED
|
@@ -106,6 +106,7 @@ function argTool(argv) {
|
|
|
106
106
|
* - Claude Code:见 isInteractiveClaude
|
|
107
107
|
*/
|
|
108
108
|
function isInteractiveAgent(tool) {
|
|
109
|
+
if (process.platform === 'win32') return isInteractiveAgentWin(tool)
|
|
109
110
|
if (tool === 'qoder') return hasAncestor((comm) => comm.includes('/Qoder CN.app/'))
|
|
110
111
|
if (tool === 'claude_code') return isInteractiveClaude()
|
|
111
112
|
// opencode 的窗口确认由插件发起,插件已排除 `opencode run`(hook/opencode.mjs isHeadless)
|
|
@@ -113,6 +114,43 @@ function isInteractiveAgent(tool) {
|
|
|
113
114
|
return false
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** 窗口里 claude 的参数不带 -p / --print / stream-json:与 isInteractiveClaude 同一判据 */
|
|
118
|
+
function claudeArgsInteractive(argsAfterArgv0) {
|
|
119
|
+
return !argsAfterArgv0.some(
|
|
120
|
+
(t) => t === '--print' || /^-[a-zA-Z]*p[a-zA-Z]*$/.test(t) || t === 'stream-json' || t.startsWith('--output-format=stream')
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Windows 版(没有 ps):进程表借 hook.cjs 的实现(同目录,随它一起装),判据同上。
|
|
126
|
+
* - Qoder CN 桌面端:祖先里有 `Qoder CN.exe`
|
|
127
|
+
* - Claude Code:原生版是 `claude.exe`,npm 版是 node 跑 `claude-code/cli.js`
|
|
128
|
+
* 取不到进程表 = 判不出来 = 不表态(窗口模式的兜底方向)
|
|
129
|
+
*/
|
|
130
|
+
function isInteractiveAgentWin(tool) {
|
|
131
|
+
if (tool === 'opencode') return true
|
|
132
|
+
if (tool !== 'qoder' && tool !== 'claude_code') return false
|
|
133
|
+
let winProc
|
|
134
|
+
try {
|
|
135
|
+
winProc = require(path.join(__dirname, 'hook.cjs')).winProc
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
let pid = process.ppid
|
|
140
|
+
const seen = new Set()
|
|
141
|
+
for (let i = 0; i < 8 && pid > 4 && !seen.has(pid); i++) {
|
|
142
|
+
seen.add(pid)
|
|
143
|
+
const p = winProc(pid)
|
|
144
|
+
if (!p) return false
|
|
145
|
+
if (tool === 'qoder' && /qoder cn/i.test(p.name)) return true
|
|
146
|
+
if (tool === 'claude_code' && (/^claude(\.exe)?$/i.test(p.name) || p.name === 'claude-code/cli.js')) {
|
|
147
|
+
return claudeArgsInteractive(p.args.split(' ').slice(1))
|
|
148
|
+
}
|
|
149
|
+
pid = p.ppid
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
function hasAncestor(match) {
|
|
117
155
|
let pid = process.ppid
|
|
118
156
|
for (let i = 0; i < 8 && pid > 1; i++) {
|
package/hook/hook.cjs
CHANGED
|
@@ -22,6 +22,11 @@ const { execFileSync } = require('node:child_process')
|
|
|
22
22
|
const DEFAULT_API_BASE = process.env.DESKBUDDY_API_BASE || 'https://api.shifengintelli.com'
|
|
23
23
|
const KEEPALIVE_MS = 2 * 60 * 1000
|
|
24
24
|
const MAX_SESSIONS = 20
|
|
25
|
+
/**
|
|
26
|
+
* 快照整包带的能力声明(前置 spec 2026-09-29 §4.4,服务端写 deskbuddy:caps:{clientId})。
|
|
27
|
+
* 与 src/core/snapshot.ts 的 CAPS 一致
|
|
28
|
+
*/
|
|
29
|
+
const CAPS = ['cmd_result', 'window_inject']
|
|
25
30
|
/** 状态文件里保留的最近事件条数,与 src/core/state.ts 的 RECENT_MAX 一致 */
|
|
26
31
|
const RECENT_MAX = 8
|
|
27
32
|
const REPORT_TIMEOUT_MS = 2000
|
|
@@ -331,11 +336,141 @@ function resolveTitle(sources, ctx) {
|
|
|
331
336
|
return ''
|
|
332
337
|
}
|
|
333
338
|
|
|
339
|
+
// ---------- Windows 进程表 ----------
|
|
340
|
+
// 🔴 与 src/core/winproc.ts 逐行同构:这里记下的 pidName 要由 daemon 拿同一个算法复核,改一边必须改另一边。
|
|
341
|
+
// Windows 没有 ps,用 PowerShell 一次取全表在内存里走(一次 300ms 起,不能按 pid 一个个查)。
|
|
342
|
+
|
|
343
|
+
const IS_WIN = process.platform === 'win32'
|
|
344
|
+
const SCRIPT_HOSTS = ['node', 'bun', 'deno']
|
|
345
|
+
const WIN_PS_SCRIPT =
|
|
346
|
+
'[Console]::OutputEncoding=[Text.Encoding]::UTF8;' +
|
|
347
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress'
|
|
348
|
+
|
|
349
|
+
function winSplitArgs(cmdline) {
|
|
350
|
+
const out = []
|
|
351
|
+
let cur = ''
|
|
352
|
+
let inQ = false
|
|
353
|
+
let has = false
|
|
354
|
+
const s = cmdline || ''
|
|
355
|
+
for (let i = 0; i < s.length; i++) {
|
|
356
|
+
const c = s[i]
|
|
357
|
+
if (c === '\\') {
|
|
358
|
+
let n = 0
|
|
359
|
+
while (s[i + n] === '\\') n++
|
|
360
|
+
if (s[i + n] === '"') {
|
|
361
|
+
cur += '\\'.repeat(Math.floor(n / 2))
|
|
362
|
+
if (n % 2 === 1) cur += '"'
|
|
363
|
+
else inQ = !inQ
|
|
364
|
+
i += n
|
|
365
|
+
has = true
|
|
366
|
+
} else {
|
|
367
|
+
cur += '\\'.repeat(n)
|
|
368
|
+
i += n - 1
|
|
369
|
+
has = true
|
|
370
|
+
}
|
|
371
|
+
} else if (c === '"') {
|
|
372
|
+
inQ = !inQ
|
|
373
|
+
has = true
|
|
374
|
+
} else if (!inQ && (c === ' ' || c === '\t')) {
|
|
375
|
+
if (has) out.push(cur)
|
|
376
|
+
cur = ''
|
|
377
|
+
has = false
|
|
378
|
+
} else {
|
|
379
|
+
cur += c
|
|
380
|
+
has = true
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (has) out.push(cur)
|
|
384
|
+
return out
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isScriptHost(name) {
|
|
388
|
+
return SCRIPT_HOSTS.indexOf(String(name).replace(/\.exe$/i, '').toLowerCase()) !== -1
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function hostedScript(argv) {
|
|
392
|
+
for (const a of argv.slice(1)) if (!a.startsWith('-')) return a
|
|
393
|
+
return ''
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** 原生程序取映像名;node / bun 跑的取脚本路径最后两段(再往上是用户目录,用户名可能撞智能体名) */
|
|
397
|
+
function winEffectiveName(name, cmdline) {
|
|
398
|
+
if (!isScriptHost(name)) return name
|
|
399
|
+
const script = hostedScript(winSplitArgs(cmdline))
|
|
400
|
+
return script ? script.split(/[\\/]+/).filter(Boolean).slice(-2).join('/') : name
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function winNormalizedArgs(name, cmdline) {
|
|
404
|
+
let argv = winSplitArgs(cmdline)
|
|
405
|
+
if (argv.length === 0) return ''
|
|
406
|
+
if (isScriptHost(name)) {
|
|
407
|
+
const script = hostedScript(argv)
|
|
408
|
+
if (script) argv = argv.slice(argv.indexOf(script))
|
|
409
|
+
}
|
|
410
|
+
return argv.map((a) => a.replace(/\s+/g, '_').replace(/\\/g, '/')).join(' ')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseWinProcJson(json) {
|
|
414
|
+
let d
|
|
415
|
+
try {
|
|
416
|
+
d = JSON.parse(String(json).replace(/^/, ''))
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return []
|
|
419
|
+
}
|
|
420
|
+
const list = Array.isArray(d) ? d : d ? [d] : []
|
|
421
|
+
const out = []
|
|
422
|
+
for (const r of list) {
|
|
423
|
+
const pid = Number(r && r.ProcessId)
|
|
424
|
+
if (!Number.isInteger(pid) || pid <= 0) continue
|
|
425
|
+
const rawName = typeof r.Name === 'string' ? r.Name : ''
|
|
426
|
+
const cmdline = typeof r.CommandLine === 'string' ? r.CommandLine.replace(/[\r\n]+/g, ' ') : ''
|
|
427
|
+
out.push({
|
|
428
|
+
pid: pid,
|
|
429
|
+
ppid: Number(r.ParentProcessId) || 0,
|
|
430
|
+
name: winEffectiveName(rawName, cmdline),
|
|
431
|
+
args: winNormalizedArgs(rawName, cmdline),
|
|
432
|
+
cmdline: cmdline,
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
return out
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let winProcCache
|
|
439
|
+
/** 全表,本进程内只取一次。取不到返回 null —— 🔴 调用方必须当"不知道",不能当"进程不在" */
|
|
440
|
+
function winProcs() {
|
|
441
|
+
if (winProcCache !== undefined) return winProcCache
|
|
442
|
+
try {
|
|
443
|
+
const out = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WIN_PS_SCRIPT], {
|
|
444
|
+
encoding: 'utf8',
|
|
445
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
446
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
447
|
+
windowsHide: true,
|
|
448
|
+
timeout: 15000,
|
|
449
|
+
})
|
|
450
|
+
const procs = parseWinProcJson(out)
|
|
451
|
+
winProcCache = procs.length > 0 ? procs : null
|
|
452
|
+
} catch (e) {
|
|
453
|
+
winProcCache = null
|
|
454
|
+
}
|
|
455
|
+
return winProcCache
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function winProc(pid) {
|
|
459
|
+
const procs = winProcs()
|
|
460
|
+
if (!procs) return null
|
|
461
|
+
for (const p of procs) if (p.pid === pid) return p
|
|
462
|
+
return null
|
|
463
|
+
}
|
|
464
|
+
|
|
334
465
|
// ---------- 进程探测 ----------
|
|
335
466
|
|
|
336
467
|
function processName(pid) {
|
|
468
|
+
if (IS_WIN) {
|
|
469
|
+
const p = winProc(pid)
|
|
470
|
+
return p ? p.name : null
|
|
471
|
+
}
|
|
337
472
|
try {
|
|
338
|
-
// macOS / Linux
|
|
473
|
+
// macOS / Linux 通用
|
|
339
474
|
const out = execFileSync('ps', ['-p', String(pid), '-o', 'comm='], {
|
|
340
475
|
encoding: 'utf8',
|
|
341
476
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -348,6 +483,10 @@ function processName(pid) {
|
|
|
348
483
|
}
|
|
349
484
|
|
|
350
485
|
function parentPid(pid) {
|
|
486
|
+
if (IS_WIN) {
|
|
487
|
+
const p = winProc(pid)
|
|
488
|
+
return p && p.ppid > 4 && p.ppid !== pid ? p.ppid : null
|
|
489
|
+
}
|
|
351
490
|
try {
|
|
352
491
|
const out = execFileSync('ps', ['-p', String(pid), '-o', 'ppid='], {
|
|
353
492
|
encoding: 'utf8',
|
|
@@ -369,6 +508,8 @@ function isAlive(pid, expectName) {
|
|
|
369
508
|
return false
|
|
370
509
|
}
|
|
371
510
|
if (!expectName) return true
|
|
511
|
+
// 🔴 Windows 进程表取不到时只能信 pid:当成"不在"会把所有窗口会话清掉
|
|
512
|
+
if (IS_WIN && !winProcs()) return true
|
|
372
513
|
const name = processName(pid)
|
|
373
514
|
return name != null && name.toLowerCase().includes(String(expectName).toLowerCase())
|
|
374
515
|
}
|
|
@@ -467,7 +608,26 @@ function shouldReport(prev, next, nowMs) {
|
|
|
467
608
|
return nowMs - (prev.lastReportAt || 0) > KEEPALIVE_MS
|
|
468
609
|
}
|
|
469
610
|
|
|
470
|
-
|
|
611
|
+
/**
|
|
612
|
+
* daemon 连着的 Codex app-server 里已加载的会话(切片 B,与 src/core/codexRemote.ts 一致)。
|
|
613
|
+
* daemon 进程不在了就不认;读不到返回 null
|
|
614
|
+
*/
|
|
615
|
+
/** daemon 每 30 秒重写一次;两个保活周期(2 分钟)没更新就当 daemon 卡住了,不认 */
|
|
616
|
+
const CODEX_REMOTE_TTL_MS = 2 * 60 * 1000
|
|
617
|
+
|
|
618
|
+
function readCodexRemote(now) {
|
|
619
|
+
try {
|
|
620
|
+
const r = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'codex-remote.json'), 'utf8'))
|
|
621
|
+
if (!r || typeof r.pid !== 'number' || !Array.isArray(r.threads) || !pidExists(r.pid)) return null
|
|
622
|
+
if (typeof r.at !== 'number' || Math.abs((now || Date.now()) - r.at) > CODEX_REMOTE_TTL_MS) return null
|
|
623
|
+
return new Set(r.threads.filter((x) => typeof x === 'string'))
|
|
624
|
+
} catch (e) {
|
|
625
|
+
return null
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** remote:给了就以它判 remoteWindow(Codex,切片 B),否则看状态文件(与 src/core/snapshot.ts 一致) */
|
|
630
|
+
function buildSnapshot(states, tool, cfg, nowMs, remote) {
|
|
471
631
|
const sessions = states
|
|
472
632
|
.filter((s) => s.tool === tool) // 替换粒度是 (clientId, tool),混进别的工具会冲掉对方
|
|
473
633
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
@@ -481,8 +641,9 @@ function buildSnapshot(states, tool, cfg, nowMs) {
|
|
|
481
641
|
startedAt: s.startedAt,
|
|
482
642
|
updatedAt: s.updatedAt,
|
|
483
643
|
interactive: s.interactive === true,
|
|
644
|
+
remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true,
|
|
484
645
|
}))
|
|
485
|
-
return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions }
|
|
646
|
+
return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions, caps: CAPS.slice() }
|
|
486
647
|
}
|
|
487
648
|
|
|
488
649
|
// ---------- 上报 ----------
|
|
@@ -603,7 +764,8 @@ async function reportIfAuthorized(tool, nowMs) {
|
|
|
603
764
|
// clientId 为空说明还没 setup 过(服务端靠 clientId ↔ userId 绑定关系认人):
|
|
604
765
|
// 绝不能在这里现编一个,那等于凭空造出一台没人认领的机器
|
|
605
766
|
if (!cfg.clientId || cfg.enabledTools.indexOf(tool) === -1) return
|
|
606
|
-
|
|
767
|
+
const remote = tool === 'codex' ? readCodexRemote() || new Set() : null
|
|
768
|
+
await postReport(cfg, buildSnapshot(readAliveStates(tool), tool, cfg, nowMs, remote))
|
|
607
769
|
}
|
|
608
770
|
|
|
609
771
|
/**
|
|
@@ -654,6 +816,10 @@ function pidExists(pid) {
|
|
|
654
816
|
|
|
655
817
|
function detectInteractive(pid) {
|
|
656
818
|
if (!pid) return false
|
|
819
|
+
if (IS_WIN) {
|
|
820
|
+
const p = winProc(pid)
|
|
821
|
+
return !!p && p.args !== '' && isInteractiveArgs(p.args)
|
|
822
|
+
}
|
|
657
823
|
try {
|
|
658
824
|
const args = execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], {
|
|
659
825
|
encoding: 'utf8',
|
|
@@ -772,6 +938,84 @@ function takeNotice(sessionId) {
|
|
|
772
938
|
}
|
|
773
939
|
}
|
|
774
940
|
|
|
941
|
+
// ---------- 窗口接管(spec 2026-09-30 §3.2,与 src/core/wrapWindows.ts 同构) ----------
|
|
942
|
+
|
|
943
|
+
const WINDOW_ID_RE = /^[a-f0-9]{32}$/
|
|
944
|
+
/** 记进 .last 的事件。Notification 不记:闲置 60 秒的「等你输入」通知会把空闲窗口判成忙 */
|
|
945
|
+
const WINDOW_EVENTS = {
|
|
946
|
+
SessionStart: 1,
|
|
947
|
+
UserPromptSubmit: 1,
|
|
948
|
+
PreToolUse: 1,
|
|
949
|
+
PostToolUse: 1,
|
|
950
|
+
PostToolUseFailure: 1,
|
|
951
|
+
PermissionRequest: 1,
|
|
952
|
+
Stop: 1,
|
|
953
|
+
SessionEnd: 1,
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function windowsDir() {
|
|
957
|
+
return path.join(deskbuddyHome(), 'windows')
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function readWindow(windowId) {
|
|
961
|
+
if (!WINDOW_ID_RE.test(windowId)) return null
|
|
962
|
+
try {
|
|
963
|
+
const r = JSON.parse(fs.readFileSync(path.join(windowsDir(), windowId + '.json'), 'utf8'))
|
|
964
|
+
return r && typeof r.pid === 'number' ? r : null
|
|
965
|
+
} catch (e) {
|
|
966
|
+
return null
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* 环境变量里的窗口是不是这个智能体进程的窗口。
|
|
972
|
+
* 🔴 窗口里的 claude 用 Bash 工具再起 claude 时 DESKBUDDY_WINDOW 会被继承:只认包装层直接起的那个进程(childPid);
|
|
973
|
+
* 判不出 pid 时按是
|
|
974
|
+
*/
|
|
975
|
+
function windowOwns(rec, agentPid) {
|
|
976
|
+
if (!rec) return false
|
|
977
|
+
if (rec.childPid && agentPid && rec.childPid !== agentPid) return false
|
|
978
|
+
return true
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function atomicWrite(file, text) {
|
|
982
|
+
const tmp = file + '.' + process.pid + '.tmp'
|
|
983
|
+
fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: 0o600 })
|
|
984
|
+
fs.renameSync(tmp, file)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** 写 by-session 映射(内容没变不重写)+ 本窗口最近一个状态型事件,供包装层判空闲 */
|
|
988
|
+
function noteWindow(windowId, sessionId, event, now) {
|
|
989
|
+
const safe = String(sessionId).replace(/[^A-Za-z0-9_-]/g, '')
|
|
990
|
+
if (!safe || !WINDOW_ID_RE.test(windowId)) return
|
|
991
|
+
try {
|
|
992
|
+
const dir = path.join(windowsDir(), 'by-session')
|
|
993
|
+
let cur = ''
|
|
994
|
+
try {
|
|
995
|
+
cur = fs.readFileSync(path.join(dir, safe), 'utf8').trim()
|
|
996
|
+
} catch (e) {
|
|
997
|
+
cur = ''
|
|
998
|
+
}
|
|
999
|
+
if (cur !== windowId) {
|
|
1000
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
1001
|
+
atomicWrite(path.join(dir, safe), windowId)
|
|
1002
|
+
}
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
/* 映射写不了:设备指令退回后台路径 */
|
|
1005
|
+
}
|
|
1006
|
+
if (!WINDOW_EVENTS[event]) return
|
|
1007
|
+
try {
|
|
1008
|
+
atomicWrite(path.join(windowsDir(), windowId + '.last'), JSON.stringify({ ev: event, sid: sessionId, at: now }))
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
/* 同上:写不了就一直判不空闲,设备指令排队到过期 */
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function codexRemoteHas(sessionId) {
|
|
1015
|
+
const r = readCodexRemote()
|
|
1016
|
+
return !!r && r.has(sessionId)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
775
1019
|
async function main(argv) {
|
|
776
1020
|
// 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
|
|
777
1021
|
const tool = (argv && argv[2]) || 'claude_code'
|
|
@@ -794,6 +1038,19 @@ async function main(argv) {
|
|
|
794
1038
|
|
|
795
1039
|
const registry = loadRegistry()
|
|
796
1040
|
|
|
1041
|
+
// 窗口接管(spec 2026-09-30 §3.2):包装层起的窗口带 DESKBUDDY_WINDOW。只在有这个变量时才多做一点事
|
|
1042
|
+
let windowId = ''
|
|
1043
|
+
const envWin = process.env.DESKBUDDY_WINDOW || ''
|
|
1044
|
+
if (WINDOW_ID_RE.test(envWin)) {
|
|
1045
|
+
const rec = readWindow(envWin)
|
|
1046
|
+
if (rec) {
|
|
1047
|
+
const p0 = readState(sessionId)
|
|
1048
|
+
const agentPid = p0 && p0.pid && pidExists(p0.pid) ? p0.pid : resolveAgentPid(process.pid, agentNames(registry, tool))
|
|
1049
|
+
if (windowOwns(rec, agentPid || undefined)) windowId = envWin
|
|
1050
|
+
}
|
|
1051
|
+
if (windowId) noteWindow(windowId, sessionId, event, Date.now())
|
|
1052
|
+
}
|
|
1053
|
+
|
|
797
1054
|
// /clear 改绑(二期 P2):SessionStart 在映射表里是 ignore,要在那之前处理
|
|
798
1055
|
if (event === 'SessionStart' && payload.source === 'clear') {
|
|
799
1056
|
const pid = resolveAgentPid(process.pid, agentNames(registry, tool))
|
|
@@ -840,7 +1097,10 @@ async function main(argv) {
|
|
|
840
1097
|
pid: pid,
|
|
841
1098
|
pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
|
|
842
1099
|
// 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
|
|
843
|
-
interactive: prevAlive && typeof prev.interactive === 'boolean' ? prev.interactive : detectInteractive(pid),
|
|
1100
|
+
interactive: windowId ? true : prevAlive && typeof prev.interactive === 'boolean' ? prev.interactive : detectInteractive(pid),
|
|
1101
|
+
// 会话开在 DeskBuddy 窗口里(spec 2026-09-30 §3.2):服务端据此放行 8348,指令由包装层写进窗口
|
|
1102
|
+
// Codex(切片 B):会话加载在 daemon 连着的 app-server 里 = 窗口开着且能往里发
|
|
1103
|
+
remoteWindow: windowId || (tool === 'codex' && codexRemoteHas(sessionId)) ? true : undefined,
|
|
844
1104
|
lastReportAt: prev ? prev.lastReportAt : undefined,
|
|
845
1105
|
// daemon 靠它给窗口待批准撤卡(S10),与 src/core/state.ts 的 recent 同构
|
|
846
1106
|
recent: ((prev && Array.isArray(prev.recent) ? prev.recent : []).concat([
|
|
@@ -853,7 +1113,9 @@ async function main(argv) {
|
|
|
853
1113
|
])).slice(-RECENT_MAX),
|
|
854
1114
|
}
|
|
855
1115
|
|
|
856
|
-
|
|
1116
|
+
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
1117
|
+
const windowChanged = !!(prev && prev.remoteWindow) !== !!next.remoteWindow
|
|
1118
|
+
if (!windowChanged && !shouldReport(prev, next, now)) {
|
|
857
1119
|
writeState(next)
|
|
858
1120
|
return
|
|
859
1121
|
}
|
|
@@ -896,6 +1158,7 @@ module.exports = {
|
|
|
896
1158
|
buildGatewayBody,
|
|
897
1159
|
buildHeaders,
|
|
898
1160
|
buildSnapshot,
|
|
1161
|
+
readCodexRemote,
|
|
899
1162
|
isAlive,
|
|
900
1163
|
loadConfig,
|
|
901
1164
|
loadRegistry,
|
|
@@ -919,4 +1182,14 @@ module.exports = {
|
|
|
919
1182
|
sign,
|
|
920
1183
|
titleSources,
|
|
921
1184
|
writeState,
|
|
1185
|
+
winEffectiveName,
|
|
1186
|
+
winNormalizedArgs,
|
|
1187
|
+
winProc,
|
|
1188
|
+
winSplitArgs,
|
|
1189
|
+
parseWinProcJson,
|
|
1190
|
+
CAPS,
|
|
1191
|
+
WINDOW_EVENTS,
|
|
1192
|
+
noteWindow,
|
|
1193
|
+
readWindow,
|
|
1194
|
+
windowOwns,
|
|
922
1195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deskbuddy",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.2",
|
|
4
4
|
"description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"qrcode-terminal": "^0.12.0",
|
|
48
48
|
"ws": "^8.21.3"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"@xterm/headless": "^6.0.0",
|
|
52
|
+
"node-pty": "1.1.0"
|
|
49
53
|
}
|
|
50
54
|
}
|