deskbuddy 0.2.0-beta.13 → 0.2.0-beta.15
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/dist/cli.js +5342 -1566
- package/dist/deskbuddy-dsh.mjs +706 -0
- package/dist/gate.cjs +24 -14
- package/dist/hook.cjs +404 -11
- package/dist/ocTranslate.mjs +106 -0
- package/dist/opencode.mjs +100 -69
- package/hook/deskbuddy-dsh.d.mts +24 -0
- package/hook/deskbuddy-dsh.mjs +706 -0
- package/hook/gate.cjs +24 -14
- package/hook/hook-debug.log +11 -0
- package/hook/hook.cjs +404 -11
- package/hook/ocTranslate.d.mts +7 -0
- package/hook/ocTranslate.mjs +106 -0
- package/hook/opencode.d.mts +4 -1
- package/hook/opencode.mjs +100 -69
- package/package.json +3 -2
- package/vendor/node-pty/LICENSE +69 -0
- package/vendor/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/vendor/node-pty/lib/eventEmitter2.js +47 -0
- package/vendor/node-pty/lib/index.js +52 -0
- package/vendor/node-pty/lib/interfaces.js +7 -0
- package/vendor/node-pty/lib/shared/conout.js +11 -0
- package/vendor/node-pty/lib/terminal.js +190 -0
- package/vendor/node-pty/lib/types.js +7 -0
- package/vendor/node-pty/lib/unixTerminal.js +346 -0
- package/vendor/node-pty/lib/utils.js +39 -0
- package/vendor/node-pty/lib/windowsConoutConnection.js +125 -0
- package/vendor/node-pty/lib/windowsPtyAgent.js +320 -0
- package/vendor/node-pty/lib/windowsTerminal.js +199 -0
- package/vendor/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/vendor/node-pty/package.json +7 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
package/dist/gate.cjs
CHANGED
|
@@ -113,6 +113,8 @@ function isInteractiveAgent(tool) {
|
|
|
113
113
|
if (tool === 'claude_code') return isInteractiveClaude()
|
|
114
114
|
// opencode 的窗口确认由插件发起,插件已排除 `opencode run`(hook/opencode.mjs isHeadless)
|
|
115
115
|
if (tool === 'opencode') return true
|
|
116
|
+
// DeepSeek Harness 同理:插件只在 web / Desktop 这类有人在电脑前的 profile 里发起(hook/deskbuddy-dsh.mjs isWindowProfile)
|
|
117
|
+
if (tool === 'deepseek_harness') return true
|
|
116
118
|
return false
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -130,7 +132,7 @@ function claudeArgsInteractive(argsAfterArgv0) {
|
|
|
130
132
|
* 取不到进程表 = 判不出来 = 不表态(窗口模式的兜底方向)
|
|
131
133
|
*/
|
|
132
134
|
function isInteractiveAgentWin(tool) {
|
|
133
|
-
if (tool === 'opencode') return true
|
|
135
|
+
if (tool === 'opencode' || tool === 'deepseek_harness') return true
|
|
134
136
|
if (tool !== 'qoder_cn' && tool !== 'qoder' && tool !== 'claude_code') return false
|
|
135
137
|
let winProc
|
|
136
138
|
try {
|
|
@@ -213,13 +215,16 @@ async function windowMain() {
|
|
|
213
215
|
if (res.status !== 200) return fail()
|
|
214
216
|
const out = JSON.parse(res.data)
|
|
215
217
|
if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
let decision
|
|
219
|
+
if (out.decision === 'allow') {
|
|
220
|
+
decision = { behavior: 'allow' }
|
|
221
|
+
// 选择题作答(设备答选择题 spec §3.4):daemon 用本地原始 tool_input 按下标拼好的,原样交回
|
|
222
|
+
if (out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) decision.updatedInput = out.updatedInput
|
|
223
|
+
if (Array.isArray(out.updatedPermissions) && out.updatedPermissions.length > 0) decision.updatedPermissions = out.updatedPermissions
|
|
224
|
+
} else decision = { behavior: 'deny', message: (typeof out.reason === 'string' && out.reason) || '用户在设备上拒绝了这一步' }
|
|
222
225
|
const body = { hookSpecificOutput: { hookEventName: 'PermissionRequest', decision } }
|
|
226
|
+
// opencode 插件回填选择题要的是每题的 label 数组(它不认 updatedInput);Claude 系不带,免得 hook 输出校验不认这个字段
|
|
227
|
+
if ((tool === 'opencode' || tool === 'deepseek_harness') && out.decision === 'allow' && Array.isArray(out.answerLists)) body.deskbuddyAnswers = out.answerLists
|
|
223
228
|
// 窗口里告诉人是在哪台设备上处理的
|
|
224
229
|
if (typeof out.notice === 'string' && out.notice) body.systemMessage = out.notice.slice(0, 300)
|
|
225
230
|
process.stdout.write(JSON.stringify(body), () => process.exit(0))
|
|
@@ -273,13 +278,18 @@ async function main() {
|
|
|
273
278
|
return
|
|
274
279
|
}
|
|
275
280
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
const spec = {
|
|
282
|
+
hookEventName: 'PreToolUse',
|
|
283
|
+
permissionDecision: out.decision,
|
|
284
|
+
permissionDecisionReason: reason,
|
|
285
|
+
}
|
|
286
|
+
// 远程选择题作答(WorkBuddy 的 codebuddy -p):带上答案。codebuddy 的命令 hook 读 hookSpecificOutput.modifiedInput,
|
|
287
|
+
// 合并多个 hook 结果时读 updatedInput(Claude 兼容字段)—— 两个都带(codebuddy WorkBuddy 内置版源码核对)
|
|
288
|
+
if (out.decision === 'allow' && out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) {
|
|
289
|
+
spec.updatedInput = out.updatedInput
|
|
290
|
+
spec.modifiedInput = out.updatedInput
|
|
291
|
+
}
|
|
292
|
+
const payload = JSON.stringify({ hookSpecificOutput: spec })
|
|
283
293
|
// 等写完再退:管道写入是异步的,写一半就 exit 的话 allow / defer 会丢
|
|
284
294
|
process.stdout.write(payload, (err) => (err ? fail('写 hook 输出失败') : process.exit(0)))
|
|
285
295
|
}
|
package/dist/hook.cjs
CHANGED
|
@@ -147,6 +147,60 @@ const FALLBACK_REGISTRY = {
|
|
|
147
147
|
},
|
|
148
148
|
lifecycle: { SessionStart: 'ignore', SessionEnd: 'remove' },
|
|
149
149
|
},
|
|
150
|
+
// 千问办公(QwenWorkCN.app,内核是 App 内置的 Qoder CN CLI,hooks 读 ~/.QwenWorkCN/settings.json)。
|
|
151
|
+
// PermissionRequest 不触发:授权框只来一条 Notification(permission_prompt)(spec 2026-10-03 §2.1)
|
|
152
|
+
qwenwork: {
|
|
153
|
+
format: 'cc-hook',
|
|
154
|
+
processNames: ['QwenWorkCN'],
|
|
155
|
+
// 工作目录是 ~/.qwenworkcn/workspace/<chatId>,目录名不能当标题
|
|
156
|
+
titleSources: ['first-prompt', 'project'],
|
|
157
|
+
turnEvents: ['UserPromptSubmit'],
|
|
158
|
+
events: {
|
|
159
|
+
UserPromptSubmit: 'watch',
|
|
160
|
+
PreToolUse: 'watch',
|
|
161
|
+
PostToolUse: 'watch',
|
|
162
|
+
Stop: 'done',
|
|
163
|
+
},
|
|
164
|
+
conditionalEvents: {
|
|
165
|
+
Notification: [
|
|
166
|
+
// 实测 message 是 "Tool Bash requires confirmation"(选择题是 "Tool AskUserQuestion requires confirmation"),与服务端注册表一致
|
|
167
|
+
{ match: 'requires confirmation|permission|approve|需要确认|授权', state: 'ask_approval' },
|
|
168
|
+
{ match: 'waiting for your input|idle|闲置', state: 'done' },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
lifecycle: { SessionStart: 'ignore', SessionEnd: 'remove' },
|
|
172
|
+
},
|
|
173
|
+
// 百度搭子(DuMate.app,内核是改名的 opencode:dumate-opencode serve)。不往 DuMate 里装任何东西:
|
|
174
|
+
// daemon 的 DumateWatch 直连它的 HTTP 接口,把事件翻成 cc 同构输入喂 hook.cjs(spec 2026-10-03 §3.2)
|
|
175
|
+
dumate: {
|
|
176
|
+
format: 'dumate-http',
|
|
177
|
+
processNames: ['dumate-opencode'],
|
|
178
|
+
titleSources: ['oc-session-title', 'first-prompt'],
|
|
179
|
+
turnEvents: ['UserPromptSubmit'],
|
|
180
|
+
events: {
|
|
181
|
+
UserPromptSubmit: 'watch',
|
|
182
|
+
PreToolUse: 'watch',
|
|
183
|
+
PostToolUse: 'watch',
|
|
184
|
+
Stop: 'done',
|
|
185
|
+
PermissionRequest: 'ask_approval',
|
|
186
|
+
},
|
|
187
|
+
lifecycle: { SessionEnd: 'remove' },
|
|
188
|
+
},
|
|
189
|
+
// DeepSeek Harness(dsh web / Desktop / headless):原生插件 hook/deskbuddy-dsh.mjs 把 Cordis 事件翻成 cc 同构输入喂本文件
|
|
190
|
+
deepseek_harness: {
|
|
191
|
+
format: 'dsh-plugin',
|
|
192
|
+
processNames: ['dsh', 'DeepSeek Harness'],
|
|
193
|
+
titleSources: ['oc-session-title', 'first-prompt'],
|
|
194
|
+
turnEvents: ['UserPromptSubmit'],
|
|
195
|
+
events: {
|
|
196
|
+
UserPromptSubmit: 'watch',
|
|
197
|
+
PreToolUse: 'watch',
|
|
198
|
+
PostToolUse: 'watch',
|
|
199
|
+
Stop: 'done',
|
|
200
|
+
PermissionRequest: 'ask_approval',
|
|
201
|
+
},
|
|
202
|
+
lifecycle: { SessionEnd: 'remove' },
|
|
203
|
+
},
|
|
150
204
|
opencode: {
|
|
151
205
|
format: 'oc-plugin',
|
|
152
206
|
processNames: ['opencode'],
|
|
@@ -754,6 +808,8 @@ function writeEnded(s, now) {
|
|
|
754
808
|
const rec = { sessionId: sid, tool: s.tool || '', cwd: s.cwd || '', interactive: s.interactive === true, endedAt: t }
|
|
755
809
|
// 轮数随记录留下:--resume 同一个会话时接着数(窗口对话轮数 spec §2.1)
|
|
756
810
|
if (turnsOf(s) !== undefined) rec.turns = s.turns
|
|
811
|
+
// 首句也留下:会话结束后再被续跑(--resume)时,标题还用它,不被续跑那句话顶掉(Buglist 161)
|
|
812
|
+
if (typeof s.firstPrompt === 'string' && s.firstPrompt) rec.firstPrompt = s.firstPrompt
|
|
757
813
|
const file = path.join(dir, sid + '.json')
|
|
758
814
|
fs.writeFileSync(file + '.tmp', JSON.stringify(rec), { encoding: 'utf8', mode: 0o600 })
|
|
759
815
|
fs.renameSync(file + '.tmp', file)
|
|
@@ -770,19 +826,41 @@ function writeEnded(s, now) {
|
|
|
770
826
|
}
|
|
771
827
|
}
|
|
772
828
|
|
|
773
|
-
/**
|
|
774
|
-
function
|
|
829
|
+
/** 已结束记录(与 src/core/ended.ts 的 readEnded 同一有效期);没有 / 过期返回 null */
|
|
830
|
+
function readEndedRecord(sessionId) {
|
|
775
831
|
const sid = String(sessionId || '')
|
|
776
|
-
if (!/^[A-Za-z0-9_-]{1,64}$/.test(sid)) return
|
|
832
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(sid)) return null
|
|
777
833
|
try {
|
|
778
834
|
const r = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'ended', sid + '.json'), 'utf8'))
|
|
779
|
-
if (!r || r.sessionId !== sid || typeof r.endedAt !== 'number' || Date.now() - r.endedAt > ENDED_TTL_MS) return
|
|
780
|
-
return
|
|
835
|
+
if (!r || r.sessionId !== sid || typeof r.endedAt !== 'number' || Date.now() - r.endedAt > ENDED_TTL_MS) return null
|
|
836
|
+
return r
|
|
781
837
|
} catch (e) {
|
|
782
|
-
return
|
|
838
|
+
return null
|
|
783
839
|
}
|
|
784
840
|
}
|
|
785
841
|
|
|
842
|
+
/** 已结束记录里留下的轮数;没有返回 undefined */
|
|
843
|
+
function endedTurns(sessionId) {
|
|
844
|
+
return turnsOf(readEndedRecord(sessionId))
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** 已结束记录里留下的首句;没有返回 undefined */
|
|
848
|
+
function endedFirstPrompt(sessionId) {
|
|
849
|
+
const r = readEndedRecord(sessionId)
|
|
850
|
+
return r && typeof r.firstPrompt === 'string' && r.firstPrompt ? r.firstPrompt : undefined
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* DeskBuddy 自己注入的话(daemon 续跑时发给智能体的「你在设备上已批准…」,与 src/daemon/index.ts 的 INTERNAL_PROMPT_MARK 一致):
|
|
855
|
+
* 不是用户说的,不当会话首句(标题)、不算一轮(Buglist 161)。老 daemon(beta.14 及以前)发的没有标记,按固定开头认
|
|
856
|
+
*/
|
|
857
|
+
const INTERNAL_PROMPT_MARK = '[DeskBuddy]'
|
|
858
|
+
function isInternalPrompt(text) {
|
|
859
|
+
if (typeof text !== 'string') return false
|
|
860
|
+
const t = text.trim()
|
|
861
|
+
return t.startsWith(INTERNAL_PROMPT_MARK) || t.startsWith('(你在设备上已批准:')
|
|
862
|
+
}
|
|
863
|
+
|
|
786
864
|
/** 会话结束:先写已结束记录,再删状态(删状态的地方都走这里,/clear 除外) */
|
|
787
865
|
function retireState(s, now) {
|
|
788
866
|
writeEnded(s, now)
|
|
@@ -801,6 +879,22 @@ function removeState(sessionId) {
|
|
|
801
879
|
* 读取某工具下所有【进程还活着】的会话,顺手清理已死的。
|
|
802
880
|
* SessionEnd 极不可靠(用户叉掉窗口时根本不触发),进程存活才是唯一硬信号。
|
|
803
881
|
*/
|
|
882
|
+
/** 与 src/core/state.ts 的 STALE_NO_PID_MS / STALE_NO_PID_LOG / isStaleNoPid 一致 */
|
|
883
|
+
const STALE_NO_PID_MS = 2 * 60 * 60 * 1000
|
|
884
|
+
const STALE_NO_PID_LOG = '无进程号且 2 小时无事件,按已结束处理'
|
|
885
|
+
function pendingApprovalSessions() {
|
|
886
|
+
try {
|
|
887
|
+
const recs = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'approvals.json'), 'utf8'))
|
|
888
|
+
return new Set(Array.isArray(recs) ? recs.filter((r) => r && r.status === 'pending' && typeof r.sessionId === 'string').map((r) => r.sessionId) : [])
|
|
889
|
+
} catch (e) {
|
|
890
|
+
return new Set()
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function isStaleNoPid(s, now, pending) {
|
|
894
|
+
if (s.pid || (s.state !== 'watch' && s.state !== 'ask_approval') || !(now - s.updatedAt > STALE_NO_PID_MS)) return false
|
|
895
|
+
return !(pending || pendingApprovalSessions)().has(s.sessionId)
|
|
896
|
+
}
|
|
897
|
+
|
|
804
898
|
function readAliveStates(tool) {
|
|
805
899
|
let files
|
|
806
900
|
try {
|
|
@@ -827,6 +921,16 @@ function readAliveStates(tool) {
|
|
|
827
921
|
retireState(s) // 窗口已关:记下已结束、剔除并自我清理,state 目录不再无限增长
|
|
828
922
|
continue
|
|
829
923
|
}
|
|
924
|
+
// 没记进程号、2 小时没新事件的「进行中」会话按已结束处理(与 src/core/state.ts isStaleNoPid 一致)
|
|
925
|
+
if (isStaleNoPid(s, Date.now())) {
|
|
926
|
+
try {
|
|
927
|
+
fs.appendFileSync(path.join(deskbuddyHome(), 'daemon.log'), `${new Date().toISOString()} 会话 ${String(s.sessionId).slice(0, 8)} ${STALE_NO_PID_LOG}\n`, 'utf8')
|
|
928
|
+
} catch (e) {
|
|
929
|
+
/* 记不下不影响 */
|
|
930
|
+
}
|
|
931
|
+
retireState(s)
|
|
932
|
+
continue
|
|
933
|
+
}
|
|
830
934
|
alive.push(s)
|
|
831
935
|
}
|
|
832
936
|
return alive
|
|
@@ -1062,6 +1166,10 @@ function isInteractiveArgs(args) {
|
|
|
1062
1166
|
if (tokens[0] === 'exec' && /codex/i.test(path.basename(all[0]))) return false
|
|
1063
1167
|
// opencode 的非交互是 `opencode run …`(二期 P9);TUI 是 `opencode [目录]`
|
|
1064
1168
|
if (tokens[0] === 'run' && /opencode/i.test(path.basename(all[0]))) return false
|
|
1169
|
+
// DeepSeek Harness:`node …/dsh <profile>` 或 `dsh --profile <profile> …`;headless / sdk / acp 是程序在用,其余(web 等)是窗口。
|
|
1170
|
+
// 🔴 在 -p 判据之前返回:一次性任务的文本是整段 argv,按空白切开后里面可能正好有个 -p
|
|
1171
|
+
const dsh = dshProfile(all)
|
|
1172
|
+
if (dsh !== null) return !DSH_NON_WINDOW[dsh]
|
|
1065
1173
|
return !tokens.some(
|
|
1066
1174
|
(t) =>
|
|
1067
1175
|
t === '--print' ||
|
|
@@ -1071,6 +1179,24 @@ function isInteractiveArgs(args) {
|
|
|
1071
1179
|
)
|
|
1072
1180
|
}
|
|
1073
1181
|
|
|
1182
|
+
/** 与 hook/deskbuddy-dsh.mjs 的 NON_WINDOW_PROFILES 一致 */
|
|
1183
|
+
const DSH_NON_WINDOW = { headless: true, sdk: true, 'sdk-minimal': true, acp: true }
|
|
1184
|
+
|
|
1185
|
+
/** 命令行是 dsh 的话返回它的 profile 名(取不到返回 ''),不是 dsh 返回 null */
|
|
1186
|
+
function dshProfile(all) {
|
|
1187
|
+
let i = -1
|
|
1188
|
+
for (let k = 0; k < Math.min(all.length, 2); k++) {
|
|
1189
|
+
const t = String(all[k]).replace(/\\/g, '/')
|
|
1190
|
+
// npm 的 bin 链接叫 dsh;Windows 的 dsh.cmd 起的是 node …/@deepseek-ai/dsh/lib/bin.js
|
|
1191
|
+
if (path.posix.basename(t) === 'dsh' || /(^|\/)@deepseek-ai\/dsh\/lib\/bin\.js$/.test(t)) i = k
|
|
1192
|
+
}
|
|
1193
|
+
if (i < 0) return null
|
|
1194
|
+
const rest = all.slice(i + 1)
|
|
1195
|
+
const j = rest.indexOf('--profile')
|
|
1196
|
+
if (j >= 0) return rest[j + 1] || ''
|
|
1197
|
+
return rest.find((t) => t && t[0] !== '-') || ''
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1074
1200
|
/**
|
|
1075
1201
|
* 嵌入宿主(插件 / 桌面版 / SDK 长连):`--input-format stream-json`(含 `=` 写法)且不带 -p / --print。
|
|
1076
1202
|
* 这类会话 isInteractiveArgs 判 false(不是终端窗口),但用户就在宿主里一轮轮发话:没有唤醒等待者(wake.cjs 铁律③),
|
|
@@ -1468,6 +1594,229 @@ function setTurns(next, disk, prev, isTurn, sessionId) {
|
|
|
1468
1594
|
else next.turns = v
|
|
1469
1595
|
}
|
|
1470
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* Codex 的 hook 输入长相:几乎每个事件都带 turn_id;transcript 在 ~/.codex/sessions/…/rollout-*.jsonl。
|
|
1599
|
+
* 🔴 至少两个特征同时成立才算(评审):单看一个可能误伤将来的 Claude Code(例如它也给更多事件加 turn_id)
|
|
1600
|
+
*/
|
|
1601
|
+
function looksLikeCodex(payload) {
|
|
1602
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : ''
|
|
1603
|
+
let n = 0
|
|
1604
|
+
if (typeof payload.turn_id === 'string' && payload.turn_id && payload.hook_event_name !== 'MessageDisplay') n++
|
|
1605
|
+
if (/(^|[\\/])rollout-[^\\/]*\.jsonl$/i.test(tp)) n++
|
|
1606
|
+
if (/[\\/]\.codex[\\/]sessions[\\/]/.test(tp)) n++
|
|
1607
|
+
return n >= 2
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/** 排查用:被 foreignHost 拦下时记一行到 ~/.deskbuddy/hook-debug.log(不写 stderr:有的宿主会把 hook 的 stderr 当输入)。不记任何文本 */
|
|
1611
|
+
function noteForeign(tool, payload) {
|
|
1612
|
+
try {
|
|
1613
|
+
const p = path.join(deskbuddyHome(), 'hook-debug.log')
|
|
1614
|
+
fs.mkdirSync(deskbuddyHome(), { recursive: true })
|
|
1615
|
+
try {
|
|
1616
|
+
if (fs.statSync(p).size > 256 * 1024) fs.truncateSync(p, 0)
|
|
1617
|
+
} catch (e) {
|
|
1618
|
+
/* 还没有 */
|
|
1619
|
+
}
|
|
1620
|
+
const sid = String(payload.session_id || '').slice(0, 8)
|
|
1621
|
+
const ev = String(payload.hook_event_name || '').slice(0, 40)
|
|
1622
|
+
fs.appendFileSync(p, `${new Date().toISOString()} 宿主不是 ${tool},忽略 event=${ev} session=${sid}\n`, 'utf8')
|
|
1623
|
+
} catch (e) {
|
|
1624
|
+
/* 记不下不影响 */
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** Claude Code 的 hook 输入长相:transcript 在 <配置目录>/projects/<目录名>/<会话号>.jsonl,没有 turn_id */
|
|
1629
|
+
function looksLikeClaude(payload) {
|
|
1630
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : ''
|
|
1631
|
+
return !payload.turn_id && /[\\/]projects[\\/][^\\/]+[\\/][0-9a-f-]{36}\.jsonl$/i.test(tp)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* 命令行上的工具标识与真正触发 hook 的宿主对不上(2026-10-02 现场):同一台电脑 Codex 会话(UUIDv7)同时以 tool=claude_code
|
|
1636
|
+
* 上报,设备上多出一张「进行中」卡挂了 20 分钟。新版 Codex 能从 Claude Code 导入配置(/import「import setup … from Claude Code」,
|
|
1637
|
+
* 含 ~/.claude/settings.json 里的 hooks),导进去的是我们给 Claude 装的 `hook.cjs claude_code`,于是在 Codex 会话里也按 claude_code 报。
|
|
1638
|
+
* 反过来 Claude 触发 codex 的条目同理。认出来就静默退出:那个会话由它自己工具的 hook 正常上报
|
|
1639
|
+
*/
|
|
1640
|
+
function foreignHost(tool, payload) {
|
|
1641
|
+
if (!payload || typeof payload !== 'object') return false
|
|
1642
|
+
if (tool === 'claude_code') return looksLikeCodex(payload)
|
|
1643
|
+
if (tool === 'codex') return looksLikeClaude(payload)
|
|
1644
|
+
if (tool === 'qoder' || tool === 'qoder_cn') {
|
|
1645
|
+
const host = qoderHostOf(payload)
|
|
1646
|
+
return !!host && host !== tool
|
|
1647
|
+
}
|
|
1648
|
+
return false
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Qoder 两版各自的配置目录(Bug 165:同一会话同时以 qoder 与 qoder_cn 上报):transcript 写在配置目录下的 projects/ 里,
|
|
1653
|
+
* 国内版 `.qoder-cn`(桌面 App 也把 worker 的 QODERCN_CONFIG_DIR 指到 ~/.qoder-cn,0.4.1 读 app 源码核对),国际版 `.qoder`。
|
|
1654
|
+
* 认不出(自定义了配置目录等)返回 '',不判
|
|
1655
|
+
*/
|
|
1656
|
+
function qoderHostOf(payload) {
|
|
1657
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : ''
|
|
1658
|
+
if (/[\\/]\.qoder-cn[\\/]projects[\\/]/i.test(tp)) return 'qoder_cn'
|
|
1659
|
+
if (/[\\/]\.qoder[\\/]projects[\\/]/i.test(tp)) return 'qoder'
|
|
1660
|
+
return ''
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* 保险(设备答选择题,Qoder 待实机确认,发版前联调必验):设备在 Qoder 上答了选择题(回显是「已在…上回答」),
|
|
1665
|
+
* 可这一步跑完的 PostToolUse(AskUserQuestion) 输入里却没有 answers —— 说明 Qoder 没采用 hook 回填的答案。
|
|
1666
|
+
* 记一行到 daemon.log,方便联调时发现
|
|
1667
|
+
*/
|
|
1668
|
+
function checkQoderAnswerAdopted(tool, event, payload, notice) {
|
|
1669
|
+
try {
|
|
1670
|
+
if ((tool !== 'qoder_cn' && tool !== 'qoder') || event !== 'PostToolUse' || payload.tool_name !== 'AskUserQuestion') return
|
|
1671
|
+
if (typeof notice !== 'string' || !/已在.*上回答/.test(notice)) return
|
|
1672
|
+
const a = payload.tool_input && payload.tool_input.answers
|
|
1673
|
+
if (a && typeof a === 'object' && Object.keys(a).length > 0) return
|
|
1674
|
+
const line = `${new Date().toISOString()} Qoder 未采用设备答案(tool=${tool} session=${String(payload.session_id || '').slice(0, 8)})\n`
|
|
1675
|
+
fs.mkdirSync(deskbuddyHome(), { recursive: true })
|
|
1676
|
+
fs.appendFileSync(path.join(deskbuddyHome(), 'daemon.log'), line, 'utf8')
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
/* 记不下不影响 */
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* daemon 代智能体起的 hook(百度搭子:DumateWatch 直连 HTTP,把事件喂给本文件)往上找不到智能体进程,
|
|
1684
|
+
* daemon 用 DESKBUDDY_AGENT_PID 告诉我们是哪个(spec 2026-10-03 §3.2)。
|
|
1685
|
+
* 🔴 只在父进程就是 daemon(gate.json 里记的 pid)时采纳:别的进程设这个变量不能冒充
|
|
1686
|
+
*/
|
|
1687
|
+
function envAgentPid() {
|
|
1688
|
+
const v = process.env.DESKBUDDY_AGENT_PID || ''
|
|
1689
|
+
if (!/^[1-9]\d{0,9}$/.test(v)) return null
|
|
1690
|
+
const pid = Number(v)
|
|
1691
|
+
// 宿主进程里的插件自己起的 hook(DeepSeek Harness:插件跑在 dsh 进程里,进程名只是 node,按名字往上找不到):
|
|
1692
|
+
// 带 DESKBUDDY_AGENT_SELF=1 声明「我就是你的父进程」—— 只认直接父进程,冒充不了别的进程
|
|
1693
|
+
if (process.env.DESKBUDDY_AGENT_SELF === '1') return pid === process.ppid && pidExists(pid) ? pid : null
|
|
1694
|
+
let conf
|
|
1695
|
+
try {
|
|
1696
|
+
conf = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
return null
|
|
1699
|
+
}
|
|
1700
|
+
if (!conf || !Number.isInteger(conf.pid) || conf.pid !== process.ppid) return null
|
|
1701
|
+
return pidExists(pid) ? pid : null
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* daemon 开窗的 WorkBuddy 窗口(Bug 175):启动器(deskbuddy wrap --launch)给 TUI 子进程带上自己的进程号和工具名
|
|
1706
|
+
* (wrap/promptLaunch.ts promptEnv)。WorkBuddy 的 CLI 是 node 跑的 codebuddy 脚本,进程名对不上 processNames('WorkBuddy'),
|
|
1707
|
+
* resolveAgentPid 找不到 → 没有进程号、interactive 判成 false。这里沿父进程往上找「父进程正是启动器」的那一个,就是窗口里的智能体。
|
|
1708
|
+
* 🔴 工具名必须对得上:窗口里的智能体再起别的智能体(Bash 里跑 claude)时变量会被继承,那边不能认成这个窗口
|
|
1709
|
+
*/
|
|
1710
|
+
function launchedAgentPid(tool) {
|
|
1711
|
+
const t = process.env.DESKBUDDY_LAUNCHED_TOOL || ''
|
|
1712
|
+
const v = process.env.DESKBUDDY_LAUNCHER_PID || ''
|
|
1713
|
+
if (!t || t !== tool || !/^[1-9]\d{0,9}$/.test(v)) return null
|
|
1714
|
+
const launcher = Number(v)
|
|
1715
|
+
if (!pidExists(launcher)) return null
|
|
1716
|
+
// 🔴 只看最近两层(hook 的父进程,或经一层 shell 的祖父进程):智能体在窗口里再起一个 `codebuddy -p`(Bash 工具)时
|
|
1717
|
+
// 变量会被它继承,那时启动器在更上面好几层 —— 不能把嵌套的那个会话认成这个窗口(评审建议 f)
|
|
1718
|
+
let pid = process.ppid
|
|
1719
|
+
for (let i = 0; i < 2 && pid > 1; i++) {
|
|
1720
|
+
const pp = parentPid(pid)
|
|
1721
|
+
if (!pp) return null
|
|
1722
|
+
if (pp === launcher) return pid
|
|
1723
|
+
pid = pp
|
|
1724
|
+
}
|
|
1725
|
+
return null
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* daemon 开窗的 WorkBuddy 窗口(Bug 176):启动参数 --settings 里注入的 Notification hook 带 --window-notify 跑这里。
|
|
1730
|
+
* 授权框 / 选择题弹出之后 CodeBuddy 发 Notification(permission_prompt)(不等 hook),这里把它连同答复通道的窗口号
|
|
1731
|
+
* 发给 daemon(不挂起),daemon 推设备卡;设备答了经通道写成按键。只认启动器给的环境变量(工具名 + 窗口号),
|
|
1732
|
+
* 用户自己开的 WorkBuddy 没有这些变量 → 什么都不做
|
|
1733
|
+
*/
|
|
1734
|
+
function launchedWindowNotify(tool, payload) {
|
|
1735
|
+
if (process.env.DESKBUDDY_LAUNCHED_TOOL !== tool) return null
|
|
1736
|
+
const win = process.env.DESKBUDDY_LAUNCHED_WINDOW || ''
|
|
1737
|
+
if (!/^[a-f0-9]{32}$/.test(win)) return null
|
|
1738
|
+
if (!payload || payload.hook_event_name !== 'Notification' || payload.notification_type !== 'permission_prompt') return null
|
|
1739
|
+
// 必须正是启动器直接起的那个 TUI 发的(嵌套的 codebuddy 继承了变量也不算,见 launchedAgentPid)
|
|
1740
|
+
if (!launchedAgentPid(tool)) return null
|
|
1741
|
+
return Object.assign({}, payload, { deskbuddy_window: win })
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** 千问办公推卡通知的超时:hook 卡着 PreToolUse,界面在它返回前不出题,必须短 */
|
|
1745
|
+
const QWENWORK_NOTIFY_MS = 800
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* 千问办公(spec 2026-10-03 §3.1):选择题 / 授权框要推设备时返回种类,否则 ''。
|
|
1749
|
+
* 选择题是 PreToolUse(AskUserQuestion);授权只有 Notification(permission_prompt)(PermissionRequest 不触发)
|
|
1750
|
+
*/
|
|
1751
|
+
function qwenworkNotifyKind(tool, event, payload) {
|
|
1752
|
+
if (tool !== 'qwenwork' || !payload || typeof payload !== 'object') return ''
|
|
1753
|
+
if (event === 'PreToolUse' && payload.tool_name === 'AskUserQuestion') return 'question'
|
|
1754
|
+
if (event === 'Notification' && payload.notification_type === 'permission_prompt') return 'permission'
|
|
1755
|
+
return ''
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* 告诉 daemon「有一道题 / 一个授权框」(POST /gate mode=notify),🔴 不等设备:daemon 收到立刻回 200,
|
|
1760
|
+
* 设备答复由 daemon 经 App 内置 MCP 回填,界面照常弹。daemon 不在 / 超时 / 出错一律静默(resolve false)
|
|
1761
|
+
*/
|
|
1762
|
+
function notifyDaemon(tool, payload, timeoutMs) {
|
|
1763
|
+
return new Promise((resolve) => {
|
|
1764
|
+
let done = false
|
|
1765
|
+
const finish = (v) => {
|
|
1766
|
+
if (done) return
|
|
1767
|
+
done = true
|
|
1768
|
+
resolve(v)
|
|
1769
|
+
}
|
|
1770
|
+
let conf
|
|
1771
|
+
try {
|
|
1772
|
+
conf = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
return finish(false)
|
|
1775
|
+
}
|
|
1776
|
+
if (!conf || !Number.isInteger(conf.port) || typeof conf.token !== 'string') return finish(false)
|
|
1777
|
+
let body
|
|
1778
|
+
try {
|
|
1779
|
+
body = JSON.stringify({ mode: 'notify', tool: tool, hook: payload })
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
return finish(false)
|
|
1782
|
+
}
|
|
1783
|
+
let req
|
|
1784
|
+
try {
|
|
1785
|
+
req = http.request(
|
|
1786
|
+
{
|
|
1787
|
+
host: '127.0.0.1',
|
|
1788
|
+
port: conf.port,
|
|
1789
|
+
path: '/gate',
|
|
1790
|
+
method: 'POST',
|
|
1791
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'x-gate-token': conf.token },
|
|
1792
|
+
},
|
|
1793
|
+
(res) => {
|
|
1794
|
+
res.resume()
|
|
1795
|
+
res.on('end', () => finish(res.statusCode === 200))
|
|
1796
|
+
res.on('error', () => finish(false))
|
|
1797
|
+
}
|
|
1798
|
+
)
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
return finish(false)
|
|
1801
|
+
}
|
|
1802
|
+
const timer = setTimeout(() => {
|
|
1803
|
+
try {
|
|
1804
|
+
req.destroy()
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
/* 已经断了 */
|
|
1807
|
+
}
|
|
1808
|
+
finish(false)
|
|
1809
|
+
}, timeoutMs)
|
|
1810
|
+
if (timer.unref) timer.unref()
|
|
1811
|
+
req.on('error', () => finish(false))
|
|
1812
|
+
req.on('close', () => {
|
|
1813
|
+
clearTimeout(timer)
|
|
1814
|
+
finish(false)
|
|
1815
|
+
})
|
|
1816
|
+
req.end(body)
|
|
1817
|
+
})
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1471
1820
|
async function main(argv) {
|
|
1472
1821
|
// 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
|
|
1473
1822
|
const tool = (argv && argv[2]) || 'claude_code'
|
|
@@ -1475,6 +1824,17 @@ async function main(argv) {
|
|
|
1475
1824
|
const event = payload.hook_event_name || ''
|
|
1476
1825
|
const sessionId = payload.session_id || ''
|
|
1477
1826
|
if (!sessionId) return
|
|
1827
|
+
// 宿主对不上就当没来过:不写状态、不上报、不输出(见 foreignHost)
|
|
1828
|
+
if (foreignHost(tool, payload)) {
|
|
1829
|
+
noteForeign(tool, payload)
|
|
1830
|
+
return
|
|
1831
|
+
}
|
|
1832
|
+
// 开窗 WorkBuddy 的授权框 / 选择题通知(Bug 176):只发通知,不写状态(状态由用户 settings 里那条 hook 报)
|
|
1833
|
+
if (argv && argv[3] === '--window-notify') {
|
|
1834
|
+
const n = launchedWindowNotify(tool, payload)
|
|
1835
|
+
if (n) await notifyDaemon(tool, n, QWENWORK_NOTIFY_MS)
|
|
1836
|
+
return
|
|
1837
|
+
}
|
|
1478
1838
|
// 快速路径(切片 C):Codex 的同步 UserPromptSubmit 条目 / opencode 插件的 chat.message 调。
|
|
1479
1839
|
// 只读 appended → 输出 JSON → 删;不查进程、不上报(同步 hook 卡着用户的输入)
|
|
1480
1840
|
if (argv && argv[3] === '--appended') {
|
|
@@ -1486,8 +1846,13 @@ async function main(argv) {
|
|
|
1486
1846
|
// 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
|
|
1487
1847
|
const eventAt = Date.now()
|
|
1488
1848
|
|
|
1849
|
+
// 千问办公:选择题 / 授权框推设备(不挂起,见 notifyDaemon)。状态照常往下走。
|
|
1850
|
+
// 🔴 放在取 eventAt 之后:通知最多耗 800ms,不能把这次事件的时间往后推(并发的 Stop 会被它压掉)
|
|
1851
|
+
if (qwenworkNotifyKind(tool, event, payload)) await notifyDaemon(tool, payload, QWENWORK_NOTIFY_MS)
|
|
1852
|
+
|
|
1489
1853
|
if (NOTICE_EVENTS[event]) {
|
|
1490
1854
|
const notice = takeNotice(sessionId)
|
|
1855
|
+
checkQoderAnswerAdopted(tool, event, payload, notice)
|
|
1491
1856
|
// 🔴 writeSync:macOS 上 stdout 是管道时 process.stdout.write 是异步的,进程先退就丢了
|
|
1492
1857
|
if (notice) {
|
|
1493
1858
|
try {
|
|
@@ -1553,9 +1918,15 @@ async function main(argv) {
|
|
|
1553
1918
|
const now = Date.now()
|
|
1554
1919
|
if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
|
|
1555
1920
|
// 窗口对话轮数(spec 2026-10-02 §2.1):一轮开始的事件加 1
|
|
1556
|
-
const
|
|
1921
|
+
const internal = event === 'UserPromptSubmit' && isInternalPrompt(payload.prompt)
|
|
1922
|
+
const isTurn = !internal && turnEvents(registry, tool).indexOf(event) >= 0
|
|
1557
1923
|
// 首条指令(标题来源 first-prompt):只记第一次
|
|
1558
|
-
|
|
1924
|
+
// 状态没了(会话结束后被续跑)就接已结束记录里的首句;DeskBuddy 注入的话不算首句
|
|
1925
|
+
const firstPrompt =
|
|
1926
|
+
(prev && prev.firstPrompt) ||
|
|
1927
|
+
(prev ? undefined : endedFirstPrompt(sessionId)) ||
|
|
1928
|
+
(event === 'UserPromptSubmit' && !internal ? cleanPrompt(payload.prompt) : '') ||
|
|
1929
|
+
undefined
|
|
1559
1930
|
if (superseded(prev, eventAt)) {
|
|
1560
1931
|
// 被并发的更晚事件(多半是同一轮的 Stop)抢先写了:状态不覆盖,但首条指令要补上,不然标题就成了第二条指令
|
|
1561
1932
|
if (patchSuperseded(prev, firstPrompt, isTurn)) await reportIfAuthorized(tool, Date.now())
|
|
@@ -1564,7 +1935,9 @@ async function main(argv) {
|
|
|
1564
1935
|
// 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
|
|
1565
1936
|
// 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
|
|
1566
1937
|
const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
|
|
1567
|
-
|
|
1938
|
+
// 开窗的 WorkBuddy 窗口:沿用的状态已判过交互就不再查(Windows 上查进程表要起 PowerShell)
|
|
1939
|
+
const launched = prevAlive && prev.interactive === true ? null : launchedAgentPid(tool)
|
|
1940
|
+
const pid = (prevAlive && prev.pid) || envAgentPid() || launched || resolveAgentPid(process.pid, agentNames(registry, tool)) || undefined
|
|
1568
1941
|
// 两个判定沿用规则相同:进程还活着且上次判过就沿用,否则查一次命令行(只查一次)
|
|
1569
1942
|
let host = null
|
|
1570
1943
|
const hostFlag = (k) => {
|
|
@@ -1592,12 +1965,17 @@ async function main(argv) {
|
|
|
1592
1965
|
pid: pid,
|
|
1593
1966
|
pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
|
|
1594
1967
|
// 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
|
|
1595
|
-
|
|
1968
|
+
// 开窗的 WorkBuddy 窗口是交互窗口(Bug 175):设备回复按「用户窗口」处理(忙就排队、空闲才续跑),不能并发续跑
|
|
1969
|
+
interactive: windowId || launched ? true : hostFlag('interactive'),
|
|
1596
1970
|
// 嵌入宿主(插件 / 桌面版,见 isEmbeddedArgs):daemon 据此按用户窗口处理设备回复。只落本机、不上报
|
|
1597
1971
|
embedded: hostFlag('embedded'),
|
|
1598
1972
|
// 会话开在 DeskBuddy 窗口里(spec 2026-09-30 §3.2):服务端据此放行 8348,指令由包装层写进窗口
|
|
1599
1973
|
// Codex(切片 B):会话加载在 daemon 连着的 app-server 里 = 窗口开着且能往里发
|
|
1600
|
-
|
|
1974
|
+
// DeepSeek Harness:插件开着下发信箱(web / Desktop 里,DESKBUDDY_DSH_MAILBOX)= 设备回复能直接 followup 进这个会话
|
|
1975
|
+
remoteWindow:
|
|
1976
|
+
windowId || (tool === 'codex' && codexRemoteHas(sessionId)) || (tool === 'deepseek_harness' && process.env.DESKBUDDY_DSH_MAILBOX === '1')
|
|
1977
|
+
? true
|
|
1978
|
+
: undefined,
|
|
1601
1979
|
lastReportAt: prev ? prev.lastReportAt : undefined,
|
|
1602
1980
|
// daemon 靠它给窗口待批准撤卡(S10),与 src/core/state.ts 的 recent 同构
|
|
1603
1981
|
recent: ((prev && Array.isArray(prev.recent) ? prev.recent : []).concat([
|
|
@@ -1609,6 +1987,9 @@ async function main(argv) {
|
|
|
1609
1987
|
},
|
|
1610
1988
|
])).slice(-RECENT_MAX),
|
|
1611
1989
|
firstPrompt: firstPrompt,
|
|
1990
|
+
// transcript 路径(Claude Code 的 hook 输入带):daemon 据此认 Esc 打断(Bug 表 160)。🔴 只落本机、不上报
|
|
1991
|
+
transcriptPath:
|
|
1992
|
+
typeof payload.transcript_path === 'string' && payload.transcript_path ? payload.transcript_path : (prev && prev.transcriptPath) || undefined,
|
|
1612
1993
|
}
|
|
1613
1994
|
|
|
1614
1995
|
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
@@ -1720,6 +2101,14 @@ module.exports = {
|
|
|
1720
2101
|
pairClear,
|
|
1721
2102
|
shouldReport,
|
|
1722
2103
|
superseded,
|
|
2104
|
+
foreignHost,
|
|
2105
|
+
qoderHostOf,
|
|
2106
|
+
checkQoderAnswerAdopted,
|
|
2107
|
+
qwenworkNotifyKind,
|
|
2108
|
+
envAgentPid,
|
|
2109
|
+
launchedAgentPid,
|
|
2110
|
+
launchedWindowNotify,
|
|
2111
|
+
notifyDaemon,
|
|
1723
2112
|
sign,
|
|
1724
2113
|
titleSources,
|
|
1725
2114
|
writeState,
|
|
@@ -1734,6 +2123,10 @@ module.exports = {
|
|
|
1734
2123
|
setTurns,
|
|
1735
2124
|
patchSuperseded,
|
|
1736
2125
|
endedTurns,
|
|
2126
|
+
endedFirstPrompt,
|
|
2127
|
+
isInternalPrompt,
|
|
2128
|
+
isStaleNoPid,
|
|
2129
|
+
STALE_NO_PID_MS,
|
|
1737
2130
|
WINDOW_EVENTS,
|
|
1738
2131
|
noteWindow,
|
|
1739
2132
|
readWindow,
|