deskbuddy 0.2.0-beta.13 → 0.2.0-beta.14
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 +2241 -359
- package/dist/gate.cjs +21 -13
- package/dist/hook.cjs +287 -9
- package/dist/ocTranslate.mjs +106 -0
- package/dist/opencode.mjs +100 -69
- package/hook/gate.cjs +21 -13
- package/hook/hook-debug.log +5 -0
- package/hook/hook.cjs +287 -9
- 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/opencode.mjs
CHANGED
|
@@ -21,8 +21,13 @@
|
|
|
21
21
|
// 🔴 窗口模式反过来:任何失败都不表态,授权框照常由人处理。
|
|
22
22
|
|
|
23
23
|
import { spawn } from 'node:child_process'
|
|
24
|
+
import { appendFileSync } from 'node:fs'
|
|
24
25
|
import { dirname, join } from 'node:path'
|
|
25
26
|
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { alwaysSuggestions, askToCc, isPlainQuestion, questionToCc, rememberTitle, toCcTool } from './ocTranslate.mjs'
|
|
28
|
+
|
|
29
|
+
// 翻译函数搬到了 ocTranslate.mjs(与 DumateWatch 共用),这里转出去保持原来的导出
|
|
30
|
+
export { alwaysSuggestions, askToCc, isPlainQuestion, questionToCc, rememberTitle, toCcTool }
|
|
26
31
|
|
|
27
32
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
28
33
|
const HOOK = join(HERE, 'hook.cjs')
|
|
@@ -62,63 +67,6 @@ export function isHeadless(argv = process.argv) {
|
|
|
62
67
|
return (end >= 0 ? args.slice(0, end) : args).includes('run')
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
/** opencode 工具 → cc-hook 同构的 tool_name / tool_input(daemon 的 classify 按 Claude Code 的工具名判断) */
|
|
66
|
-
export function toCcTool(tool, args) {
|
|
67
|
-
const a = args && typeof args === 'object' ? args : {}
|
|
68
|
-
const s = (v) => (typeof v === 'string' ? v : '')
|
|
69
|
-
switch (tool) {
|
|
70
|
-
case 'bash':
|
|
71
|
-
return { tool_name: 'Bash', tool_input: { command: s(a.command) } }
|
|
72
|
-
case 'read':
|
|
73
|
-
return { tool_name: 'Read', tool_input: { file_path: s(a.filePath) } }
|
|
74
|
-
case 'edit':
|
|
75
|
-
return { tool_name: 'Edit', tool_input: { file_path: s(a.filePath) } }
|
|
76
|
-
case 'write':
|
|
77
|
-
return { tool_name: 'Write', tool_input: { file_path: s(a.filePath) } }
|
|
78
|
-
case 'glob':
|
|
79
|
-
return { tool_name: 'Glob', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
80
|
-
case 'grep':
|
|
81
|
-
return { tool_name: 'Grep', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
82
|
-
case 'list':
|
|
83
|
-
return { tool_name: 'LS', tool_input: { path: s(a.path) } }
|
|
84
|
-
case 'webfetch':
|
|
85
|
-
return { tool_name: 'WebFetch', tool_input: { url: s(a.url) } }
|
|
86
|
-
case 'task':
|
|
87
|
-
return { tool_name: 'Task', tool_input: {} }
|
|
88
|
-
case 'todowrite':
|
|
89
|
-
case 'todoread':
|
|
90
|
-
return { tool_name: 'TodoWrite', tool_input: {} }
|
|
91
|
-
case 'patch':
|
|
92
|
-
case 'apply_patch':
|
|
93
|
-
return { tool_name: 'apply_patch', tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } }
|
|
94
|
-
default:
|
|
95
|
-
// 不认识的工具(MCP 等)原名原参数交出去:classify 对不认识的一律推设备
|
|
96
|
-
return { tool_name: String(tool || ''), tool_input: a }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** permission.asked → PermissionRequest 的 tool_name / tool_input,给设备卡片生成摘要 */
|
|
101
|
-
export function askToCc(props) {
|
|
102
|
-
const md = (props && props.metadata) || {}
|
|
103
|
-
if (typeof md.command === 'string' && md.command) return { tool_name: 'Bash', tool_input: { command: md.command } }
|
|
104
|
-
const file = md.filepath || md.filePath
|
|
105
|
-
if (typeof file === 'string' && file) return { tool_name: 'Edit', tool_input: { file_path: file } }
|
|
106
|
-
const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(' ') : ''
|
|
107
|
-
return { tool_name: String((props && props.permission) || 'permission'), tool_input: { path: patterns } }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 授权请求里 opencode 自己给的「以后都允许」模式(permission.asked 的 always,如 bash 的 `npm *`、edit 的 `*`)
|
|
112
|
-
* → 一条 session 级 addRules 建议(与 Claude Code 的 permission_suggestions 同形),daemon 据此在设备上给「允许并记住」。
|
|
113
|
-
* 设备选了记住,gate 输出带 updatedPermissions,插件回 always(opencode 在本次运行期间记住这些模式)。没有 always 返回 undefined
|
|
114
|
-
*/
|
|
115
|
-
export function alwaysSuggestions(props) {
|
|
116
|
-
const perm = String((props && props.permission) || '').trim()
|
|
117
|
-
const always = props && Array.isArray(props.always) ? props.always.filter((x) => typeof x === 'string' && x.trim() !== '') : []
|
|
118
|
-
if (!perm || always.length === 0) return undefined
|
|
119
|
-
return [{ type: 'addRules', rules: always.map((x) => ({ toolName: perm, ruleContent: `${perm} ${x.trim()}` })), behavior: 'allow', destination: 'session' }]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
70
|
/**
|
|
123
71
|
* 远程模式:关卡改由 tool.execute.before 把,放开权限。🔴 必须原地改(整对象替换不生效,O8)。
|
|
124
72
|
* - OC_GATED_KEYS 是 daemon 压成 deny 的,放成 allow(用户原来在这些键上写的值已被覆盖,找不回来 —— 每一步都要过设备)
|
|
@@ -184,17 +132,6 @@ export function injectAppended(output, raw) {
|
|
|
184
132
|
return o && typeof o.systemMessage === 'string' ? o.systemMessage : ''
|
|
185
133
|
}
|
|
186
134
|
|
|
187
|
-
/** opencode 起标题前的占位名(1.18.32 isDefaultTitle:`New session - ` / `Child session - ` + ISO 时间),不当标题 */
|
|
188
|
-
const OC_DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
189
|
-
|
|
190
|
-
/** session.updated 的 info → 记下自动标题。子会话(subagent,带 parentID)、占位名不记 */
|
|
191
|
-
export function rememberTitle(titles, info) {
|
|
192
|
-
if (!info || typeof info.id !== 'string' || !info.id || info.parentID) return
|
|
193
|
-
const t = typeof info.title === 'string' ? info.title.trim() : ''
|
|
194
|
-
if (!t || OC_DEFAULT_TITLE_RE.test(t)) return
|
|
195
|
-
titles.set(info.id, t.slice(0, 100))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
135
|
/** 活着的子进程;opencode 退出时一并结束,不留孤儿 gate 挂着设备卡片(评审) */
|
|
199
136
|
const LIVE = new Set()
|
|
200
137
|
process.on('exit', () => {
|
|
@@ -238,7 +175,18 @@ function startChild(node, script, args, input, killMs = 0) {
|
|
|
238
175
|
|
|
239
176
|
const runChild = (node, script, args, input, killMs) => startChild(node, script, args, input, killMs).done
|
|
240
177
|
|
|
178
|
+
/**
|
|
179
|
+
* 跑在百度搭子(DuMate,内核是改名的 opencode:dumate-opencode)里。那边的会话由 daemon 的 DumateWatch 直连 HTTP 上报和推卡,
|
|
180
|
+
* 插件再报一遍就是 tool=opencode 的第二份会话 + 双卡(spec 2026-10-03 §3.2「防双卡」)
|
|
181
|
+
*/
|
|
182
|
+
export function isDumateHost(env = process.env, execPath = process.execPath) {
|
|
183
|
+
return !!(env && env.DUMATE_INAPP_KEY) || /dumate-opencode/i.test(String(execPath || ''))
|
|
184
|
+
}
|
|
185
|
+
|
|
241
186
|
export function make(opts = {}) {
|
|
187
|
+
if (isDumateHost(opts.env, opts.execPath)) return async function DeskBuddyOff() {
|
|
188
|
+
return {}
|
|
189
|
+
}
|
|
242
190
|
return async function DeskBuddy(ctx) {
|
|
243
191
|
const node = process.env.DESKBUDDY_NODE || opts.node || 'node'
|
|
244
192
|
const client = ctx && ctx.client
|
|
@@ -325,6 +273,82 @@ export function make(opts = {}) {
|
|
|
325
273
|
if (typeof out.systemMessage === 'string' && out.systemMessage) await toast(out.systemMessage)
|
|
326
274
|
}
|
|
327
275
|
|
|
276
|
+
// ---- 选择题(question.asked):推设备作答,答案经 /question/{id}/reply 回填;设备跳过 → reject ----
|
|
277
|
+
const pendingQ = new Map() // requestID → gate 子进程
|
|
278
|
+
const debugLog = (line) => {
|
|
279
|
+
try {
|
|
280
|
+
appendFileSync(join(HERE, 'hook-debug.log'), `${new Date().toISOString()} [opencode] ${line}\n`, 'utf8')
|
|
281
|
+
} catch (_) {
|
|
282
|
+
/* 记不下不影响 */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** 返回是否成功(hey-api 客户端不抛错,失败在返回值的 error / response.status 里) */
|
|
286
|
+
const postRaw = async (url, path, body) => {
|
|
287
|
+
// 插件拿到的是 v1 SDK 客户端,没有 question 的方法:用它底下的 HTTP 客户端直接打(路径参数由客户端填)
|
|
288
|
+
const http = client && (client._client || client.client)
|
|
289
|
+
if (!http || typeof http.post !== 'function') {
|
|
290
|
+
debugLog(`回填选择题失败:客户端没有 post(${url})`)
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const r = await http.post({ url, path, ...(body ? { body, headers: { 'Content-Type': 'application/json' } } : {}) })
|
|
295
|
+
const status = r && r.response && typeof r.response.status === 'number' ? r.response.status : 0
|
|
296
|
+
if ((r && r.error) || status >= 400) {
|
|
297
|
+
debugLog(`回填选择题失败:${url} status=${status || '?'}`)
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
return true
|
|
301
|
+
} catch (e) {
|
|
302
|
+
debugLog(`回填选择题出错:${url} ${(e && e.message) || e}`)
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const onQuestion = async (p) => {
|
|
307
|
+
const { id, sessionID } = p || {}
|
|
308
|
+
if (!id || !sessionID || runId || headless || !client) return
|
|
309
|
+
const call = p.tool && p.tool.callID ? calls.get(p.tool.callID) : null
|
|
310
|
+
if (!isPlainQuestion(p, call && call.tool)) return
|
|
311
|
+
const cc = questionToCc(p)
|
|
312
|
+
if (!cc) return
|
|
313
|
+
const input = { hook_event_name: 'PermissionRequest', session_id: sessionID, cwd, ...cc }
|
|
314
|
+
const { child, done } = startChild(node, GATE, ['--window', '--tool', TOOL], input)
|
|
315
|
+
if (child) pendingQ.set(id, child)
|
|
316
|
+
const r = await done
|
|
317
|
+
pendingQ.delete(id)
|
|
318
|
+
if (r.code !== 0 || !r.out) return
|
|
319
|
+
let out
|
|
320
|
+
try {
|
|
321
|
+
out = JSON.parse(r.out)
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
const d = out && out.hookSpecificOutput && out.hookSpecificOutput.decision
|
|
326
|
+
if (!d) return
|
|
327
|
+
let ok
|
|
328
|
+
if (d.behavior === 'allow' && Array.isArray(out.deskbuddyAnswers)) {
|
|
329
|
+
ok = await postRaw('/question/{requestID}/reply', { requestID: id }, { answers: out.deskbuddyAnswers })
|
|
330
|
+
} else if (d.behavior === 'deny') {
|
|
331
|
+
ok = await postRaw('/question/{requestID}/reject', { requestID: id })
|
|
332
|
+
} else return
|
|
333
|
+
if (!ok) {
|
|
334
|
+
await toast('设备答案未能回填,请在窗口作答')
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (typeof out.systemMessage === 'string' && out.systemMessage) await toast(out.systemMessage)
|
|
338
|
+
}
|
|
339
|
+
const onQuestionDone = (p) => {
|
|
340
|
+
// 人先在窗口里答了 / 拒了:结束 gate 子进程,daemon 随之撤掉设备上的卡片
|
|
341
|
+
const child = p && pendingQ.get(p.requestID)
|
|
342
|
+
if (child) {
|
|
343
|
+
pendingQ.delete(p.requestID)
|
|
344
|
+
try {
|
|
345
|
+
child.kill('SIGTERM')
|
|
346
|
+
} catch (_) {
|
|
347
|
+
/* 已经退了 */
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
328
352
|
const onReplied = (p) => {
|
|
329
353
|
// 人先在窗口里答了:结束 gate 子进程,daemon 随之撤掉设备上的卡片
|
|
330
354
|
const child = p && pending.get(p.requestID)
|
|
@@ -362,7 +386,7 @@ export function make(opts = {}) {
|
|
|
362
386
|
|
|
363
387
|
'tool.execute.before': async (input, output) => {
|
|
364
388
|
const cc = toCcTool(input && input.tool, output && output.args)
|
|
365
|
-
if (input && input.callID) calls.set(input.callID, { cc, at: Date.now() })
|
|
389
|
+
if (input && input.callID) calls.set(input.callID, { cc, tool: input && input.tool, at: Date.now() })
|
|
366
390
|
report('PreToolUse', input && input.sessionID, cc)
|
|
367
391
|
if (!runId) return
|
|
368
392
|
// 远程关卡:等设备。任何失败都拒(见顶部)
|
|
@@ -398,6 +422,13 @@ export function make(opts = {}) {
|
|
|
398
422
|
case 'permission.replied':
|
|
399
423
|
onReplied(p)
|
|
400
424
|
break
|
|
425
|
+
case 'question.asked':
|
|
426
|
+
onQuestion(p).catch(() => {})
|
|
427
|
+
break
|
|
428
|
+
case 'question.replied':
|
|
429
|
+
case 'question.rejected':
|
|
430
|
+
onQuestionDone(p)
|
|
431
|
+
break
|
|
401
432
|
case 'session.idle':
|
|
402
433
|
report('Stop', p.sessionID)
|
|
403
434
|
break
|
package/hook/gate.cjs
CHANGED
|
@@ -213,13 +213,16 @@ async function windowMain() {
|
|
|
213
213
|
if (res.status !== 200) return fail()
|
|
214
214
|
const out = JSON.parse(res.data)
|
|
215
215
|
if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
let decision
|
|
217
|
+
if (out.decision === 'allow') {
|
|
218
|
+
decision = { behavior: 'allow' }
|
|
219
|
+
// 选择题作答(设备答选择题 spec §3.4):daemon 用本地原始 tool_input 按下标拼好的,原样交回
|
|
220
|
+
if (out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) decision.updatedInput = out.updatedInput
|
|
221
|
+
if (Array.isArray(out.updatedPermissions) && out.updatedPermissions.length > 0) decision.updatedPermissions = out.updatedPermissions
|
|
222
|
+
} else decision = { behavior: 'deny', message: (typeof out.reason === 'string' && out.reason) || '用户在设备上拒绝了这一步' }
|
|
222
223
|
const body = { hookSpecificOutput: { hookEventName: 'PermissionRequest', decision } }
|
|
224
|
+
// opencode 插件回填选择题要的是每题的 label 数组(它不认 updatedInput);Claude 系不带,免得 hook 输出校验不认这个字段
|
|
225
|
+
if (tool === 'opencode' && out.decision === 'allow' && Array.isArray(out.answerLists)) body.deskbuddyAnswers = out.answerLists
|
|
223
226
|
// 窗口里告诉人是在哪台设备上处理的
|
|
224
227
|
if (typeof out.notice === 'string' && out.notice) body.systemMessage = out.notice.slice(0, 300)
|
|
225
228
|
process.stdout.write(JSON.stringify(body), () => process.exit(0))
|
|
@@ -273,13 +276,18 @@ async function main() {
|
|
|
273
276
|
return
|
|
274
277
|
}
|
|
275
278
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
const spec = {
|
|
280
|
+
hookEventName: 'PreToolUse',
|
|
281
|
+
permissionDecision: out.decision,
|
|
282
|
+
permissionDecisionReason: reason,
|
|
283
|
+
}
|
|
284
|
+
// 远程选择题作答(WorkBuddy 的 codebuddy -p):带上答案。codebuddy 的命令 hook 读 hookSpecificOutput.modifiedInput,
|
|
285
|
+
// 合并多个 hook 结果时读 updatedInput(Claude 兼容字段)—— 两个都带(codebuddy WorkBuddy 内置版源码核对)
|
|
286
|
+
if (out.decision === 'allow' && out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) {
|
|
287
|
+
spec.updatedInput = out.updatedInput
|
|
288
|
+
spec.modifiedInput = out.updatedInput
|
|
289
|
+
}
|
|
290
|
+
const payload = JSON.stringify({ hookSpecificOutput: spec })
|
|
283
291
|
// 等写完再退:管道写入是异步的,写一半就 exit 的话 allow / defer 会丢
|
|
284
292
|
process.stdout.write(payload, (err) => (err ? fail('写 hook 输出失败') : process.exit(0)))
|
|
285
293
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
2026-10-02T13:19:27.363Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
2
|
+
2026-10-02T14:34:18.393Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
3
|
+
2026-10-03T02:01:37.783Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
4
|
+
2026-10-03T02:16:17.435Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
5
|
+
2026-10-03T02:19:56.607Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
package/hook/hook.cjs
CHANGED
|
@@ -147,6 +147,45 @@ 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
|
+
},
|
|
150
189
|
opencode: {
|
|
151
190
|
format: 'oc-plugin',
|
|
152
191
|
processNames: ['opencode'],
|
|
@@ -754,6 +793,8 @@ function writeEnded(s, now) {
|
|
|
754
793
|
const rec = { sessionId: sid, tool: s.tool || '', cwd: s.cwd || '', interactive: s.interactive === true, endedAt: t }
|
|
755
794
|
// 轮数随记录留下:--resume 同一个会话时接着数(窗口对话轮数 spec §2.1)
|
|
756
795
|
if (turnsOf(s) !== undefined) rec.turns = s.turns
|
|
796
|
+
// 首句也留下:会话结束后再被续跑(--resume)时,标题还用它,不被续跑那句话顶掉(Buglist 161)
|
|
797
|
+
if (typeof s.firstPrompt === 'string' && s.firstPrompt) rec.firstPrompt = s.firstPrompt
|
|
757
798
|
const file = path.join(dir, sid + '.json')
|
|
758
799
|
fs.writeFileSync(file + '.tmp', JSON.stringify(rec), { encoding: 'utf8', mode: 0o600 })
|
|
759
800
|
fs.renameSync(file + '.tmp', file)
|
|
@@ -770,19 +811,41 @@ function writeEnded(s, now) {
|
|
|
770
811
|
}
|
|
771
812
|
}
|
|
772
813
|
|
|
773
|
-
/**
|
|
774
|
-
function
|
|
814
|
+
/** 已结束记录(与 src/core/ended.ts 的 readEnded 同一有效期);没有 / 过期返回 null */
|
|
815
|
+
function readEndedRecord(sessionId) {
|
|
775
816
|
const sid = String(sessionId || '')
|
|
776
|
-
if (!/^[A-Za-z0-9_-]{1,64}$/.test(sid)) return
|
|
817
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(sid)) return null
|
|
777
818
|
try {
|
|
778
819
|
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
|
|
820
|
+
if (!r || r.sessionId !== sid || typeof r.endedAt !== 'number' || Date.now() - r.endedAt > ENDED_TTL_MS) return null
|
|
821
|
+
return r
|
|
781
822
|
} catch (e) {
|
|
782
|
-
return
|
|
823
|
+
return null
|
|
783
824
|
}
|
|
784
825
|
}
|
|
785
826
|
|
|
827
|
+
/** 已结束记录里留下的轮数;没有返回 undefined */
|
|
828
|
+
function endedTurns(sessionId) {
|
|
829
|
+
return turnsOf(readEndedRecord(sessionId))
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** 已结束记录里留下的首句;没有返回 undefined */
|
|
833
|
+
function endedFirstPrompt(sessionId) {
|
|
834
|
+
const r = readEndedRecord(sessionId)
|
|
835
|
+
return r && typeof r.firstPrompt === 'string' && r.firstPrompt ? r.firstPrompt : undefined
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* DeskBuddy 自己注入的话(daemon 续跑时发给智能体的「你在设备上已批准…」,与 src/daemon/index.ts 的 INTERNAL_PROMPT_MARK 一致):
|
|
840
|
+
* 不是用户说的,不当会话首句(标题)、不算一轮(Buglist 161)。老 daemon(beta.14 及以前)发的没有标记,按固定开头认
|
|
841
|
+
*/
|
|
842
|
+
const INTERNAL_PROMPT_MARK = '[DeskBuddy]'
|
|
843
|
+
function isInternalPrompt(text) {
|
|
844
|
+
if (typeof text !== 'string') return false
|
|
845
|
+
const t = text.trim()
|
|
846
|
+
return t.startsWith(INTERNAL_PROMPT_MARK) || t.startsWith('(你在设备上已批准:')
|
|
847
|
+
}
|
|
848
|
+
|
|
786
849
|
/** 会话结束:先写已结束记录,再删状态(删状态的地方都走这里,/clear 除外) */
|
|
787
850
|
function retireState(s, now) {
|
|
788
851
|
writeEnded(s, now)
|
|
@@ -801,6 +864,22 @@ function removeState(sessionId) {
|
|
|
801
864
|
* 读取某工具下所有【进程还活着】的会话,顺手清理已死的。
|
|
802
865
|
* SessionEnd 极不可靠(用户叉掉窗口时根本不触发),进程存活才是唯一硬信号。
|
|
803
866
|
*/
|
|
867
|
+
/** 与 src/core/state.ts 的 STALE_NO_PID_MS / STALE_NO_PID_LOG / isStaleNoPid 一致 */
|
|
868
|
+
const STALE_NO_PID_MS = 2 * 60 * 60 * 1000
|
|
869
|
+
const STALE_NO_PID_LOG = '无进程号且 2 小时无事件,按已结束处理'
|
|
870
|
+
function pendingApprovalSessions() {
|
|
871
|
+
try {
|
|
872
|
+
const recs = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'approvals.json'), 'utf8'))
|
|
873
|
+
return new Set(Array.isArray(recs) ? recs.filter((r) => r && r.status === 'pending' && typeof r.sessionId === 'string').map((r) => r.sessionId) : [])
|
|
874
|
+
} catch (e) {
|
|
875
|
+
return new Set()
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function isStaleNoPid(s, now, pending) {
|
|
879
|
+
if (s.pid || (s.state !== 'watch' && s.state !== 'ask_approval') || !(now - s.updatedAt > STALE_NO_PID_MS)) return false
|
|
880
|
+
return !(pending || pendingApprovalSessions)().has(s.sessionId)
|
|
881
|
+
}
|
|
882
|
+
|
|
804
883
|
function readAliveStates(tool) {
|
|
805
884
|
let files
|
|
806
885
|
try {
|
|
@@ -827,6 +906,16 @@ function readAliveStates(tool) {
|
|
|
827
906
|
retireState(s) // 窗口已关:记下已结束、剔除并自我清理,state 目录不再无限增长
|
|
828
907
|
continue
|
|
829
908
|
}
|
|
909
|
+
// 没记进程号、2 小时没新事件的「进行中」会话按已结束处理(与 src/core/state.ts isStaleNoPid 一致)
|
|
910
|
+
if (isStaleNoPid(s, Date.now())) {
|
|
911
|
+
try {
|
|
912
|
+
fs.appendFileSync(path.join(deskbuddyHome(), 'daemon.log'), `${new Date().toISOString()} 会话 ${String(s.sessionId).slice(0, 8)} ${STALE_NO_PID_LOG}\n`, 'utf8')
|
|
913
|
+
} catch (e) {
|
|
914
|
+
/* 记不下不影响 */
|
|
915
|
+
}
|
|
916
|
+
retireState(s)
|
|
917
|
+
continue
|
|
918
|
+
}
|
|
830
919
|
alive.push(s)
|
|
831
920
|
}
|
|
832
921
|
return alive
|
|
@@ -1468,6 +1557,170 @@ function setTurns(next, disk, prev, isTurn, sessionId) {
|
|
|
1468
1557
|
else next.turns = v
|
|
1469
1558
|
}
|
|
1470
1559
|
|
|
1560
|
+
/**
|
|
1561
|
+
* Codex 的 hook 输入长相:几乎每个事件都带 turn_id;transcript 在 ~/.codex/sessions/…/rollout-*.jsonl。
|
|
1562
|
+
* 🔴 至少两个特征同时成立才算(评审):单看一个可能误伤将来的 Claude Code(例如它也给更多事件加 turn_id)
|
|
1563
|
+
*/
|
|
1564
|
+
function looksLikeCodex(payload) {
|
|
1565
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : ''
|
|
1566
|
+
let n = 0
|
|
1567
|
+
if (typeof payload.turn_id === 'string' && payload.turn_id && payload.hook_event_name !== 'MessageDisplay') n++
|
|
1568
|
+
if (/(^|[\\/])rollout-[^\\/]*\.jsonl$/i.test(tp)) n++
|
|
1569
|
+
if (/[\\/]\.codex[\\/]sessions[\\/]/.test(tp)) n++
|
|
1570
|
+
return n >= 2
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/** 排查用:被 foreignHost 拦下时记一行到 ~/.deskbuddy/hook-debug.log(不写 stderr:有的宿主会把 hook 的 stderr 当输入)。不记任何文本 */
|
|
1574
|
+
function noteForeign(tool, payload) {
|
|
1575
|
+
try {
|
|
1576
|
+
const p = path.join(deskbuddyHome(), 'hook-debug.log')
|
|
1577
|
+
fs.mkdirSync(deskbuddyHome(), { recursive: true })
|
|
1578
|
+
try {
|
|
1579
|
+
if (fs.statSync(p).size > 256 * 1024) fs.truncateSync(p, 0)
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
/* 还没有 */
|
|
1582
|
+
}
|
|
1583
|
+
const sid = String(payload.session_id || '').slice(0, 8)
|
|
1584
|
+
const ev = String(payload.hook_event_name || '').slice(0, 40)
|
|
1585
|
+
fs.appendFileSync(p, `${new Date().toISOString()} 宿主不是 ${tool},忽略 event=${ev} session=${sid}\n`, 'utf8')
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
/* 记不下不影响 */
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** Claude Code 的 hook 输入长相:transcript 在 <配置目录>/projects/<目录名>/<会话号>.jsonl,没有 turn_id */
|
|
1592
|
+
function looksLikeClaude(payload) {
|
|
1593
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : ''
|
|
1594
|
+
return !payload.turn_id && /[\\/]projects[\\/][^\\/]+[\\/][0-9a-f-]{36}\.jsonl$/i.test(tp)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* 命令行上的工具标识与真正触发 hook 的宿主对不上(2026-10-02 现场):同一台电脑 Codex 会话(UUIDv7)同时以 tool=claude_code
|
|
1599
|
+
* 上报,设备上多出一张「进行中」卡挂了 20 分钟。新版 Codex 能从 Claude Code 导入配置(/import「import setup … from Claude Code」,
|
|
1600
|
+
* 含 ~/.claude/settings.json 里的 hooks),导进去的是我们给 Claude 装的 `hook.cjs claude_code`,于是在 Codex 会话里也按 claude_code 报。
|
|
1601
|
+
* 反过来 Claude 触发 codex 的条目同理。认出来就静默退出:那个会话由它自己工具的 hook 正常上报
|
|
1602
|
+
*/
|
|
1603
|
+
function foreignHost(tool, payload) {
|
|
1604
|
+
if (!payload || typeof payload !== 'object') return false
|
|
1605
|
+
if (tool === 'claude_code') return looksLikeCodex(payload)
|
|
1606
|
+
if (tool === 'codex') return looksLikeClaude(payload)
|
|
1607
|
+
return false
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* 保险(设备答选择题,Qoder 待实机确认,发版前联调必验):设备在 Qoder 上答了选择题(回显是「已在…上回答」),
|
|
1612
|
+
* 可这一步跑完的 PostToolUse(AskUserQuestion) 输入里却没有 answers —— 说明 Qoder 没采用 hook 回填的答案。
|
|
1613
|
+
* 记一行到 daemon.log,方便联调时发现
|
|
1614
|
+
*/
|
|
1615
|
+
function checkQoderAnswerAdopted(tool, event, payload, notice) {
|
|
1616
|
+
try {
|
|
1617
|
+
if ((tool !== 'qoder_cn' && tool !== 'qoder') || event !== 'PostToolUse' || payload.tool_name !== 'AskUserQuestion') return
|
|
1618
|
+
if (typeof notice !== 'string' || !/已在.*上回答/.test(notice)) return
|
|
1619
|
+
const a = payload.tool_input && payload.tool_input.answers
|
|
1620
|
+
if (a && typeof a === 'object' && Object.keys(a).length > 0) return
|
|
1621
|
+
const line = `${new Date().toISOString()} Qoder 未采用设备答案(tool=${tool} session=${String(payload.session_id || '').slice(0, 8)})\n`
|
|
1622
|
+
fs.mkdirSync(deskbuddyHome(), { recursive: true })
|
|
1623
|
+
fs.appendFileSync(path.join(deskbuddyHome(), 'daemon.log'), line, 'utf8')
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
/* 记不下不影响 */
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* daemon 代智能体起的 hook(百度搭子:DumateWatch 直连 HTTP,把事件喂给本文件)往上找不到智能体进程,
|
|
1631
|
+
* daemon 用 DESKBUDDY_AGENT_PID 告诉我们是哪个(spec 2026-10-03 §3.2)。
|
|
1632
|
+
* 🔴 只在父进程就是 daemon(gate.json 里记的 pid)时采纳:别的进程设这个变量不能冒充
|
|
1633
|
+
*/
|
|
1634
|
+
function envAgentPid() {
|
|
1635
|
+
const v = process.env.DESKBUDDY_AGENT_PID || ''
|
|
1636
|
+
if (!/^[1-9]\d{0,9}$/.test(v)) return null
|
|
1637
|
+
let conf
|
|
1638
|
+
try {
|
|
1639
|
+
conf = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
1640
|
+
} catch (e) {
|
|
1641
|
+
return null
|
|
1642
|
+
}
|
|
1643
|
+
if (!conf || !Number.isInteger(conf.pid) || conf.pid !== process.ppid) return null
|
|
1644
|
+
const pid = Number(v)
|
|
1645
|
+
return pidExists(pid) ? pid : null
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/** 千问办公推卡通知的超时:hook 卡着 PreToolUse,界面在它返回前不出题,必须短 */
|
|
1649
|
+
const QWENWORK_NOTIFY_MS = 800
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* 千问办公(spec 2026-10-03 §3.1):选择题 / 授权框要推设备时返回种类,否则 ''。
|
|
1653
|
+
* 选择题是 PreToolUse(AskUserQuestion);授权只有 Notification(permission_prompt)(PermissionRequest 不触发)
|
|
1654
|
+
*/
|
|
1655
|
+
function qwenworkNotifyKind(tool, event, payload) {
|
|
1656
|
+
if (tool !== 'qwenwork' || !payload || typeof payload !== 'object') return ''
|
|
1657
|
+
if (event === 'PreToolUse' && payload.tool_name === 'AskUserQuestion') return 'question'
|
|
1658
|
+
if (event === 'Notification' && payload.notification_type === 'permission_prompt') return 'permission'
|
|
1659
|
+
return ''
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* 告诉 daemon「有一道题 / 一个授权框」(POST /gate mode=notify),🔴 不等设备:daemon 收到立刻回 200,
|
|
1664
|
+
* 设备答复由 daemon 经 App 内置 MCP 回填,界面照常弹。daemon 不在 / 超时 / 出错一律静默(resolve false)
|
|
1665
|
+
*/
|
|
1666
|
+
function notifyDaemon(tool, payload, timeoutMs) {
|
|
1667
|
+
return new Promise((resolve) => {
|
|
1668
|
+
let done = false
|
|
1669
|
+
const finish = (v) => {
|
|
1670
|
+
if (done) return
|
|
1671
|
+
done = true
|
|
1672
|
+
resolve(v)
|
|
1673
|
+
}
|
|
1674
|
+
let conf
|
|
1675
|
+
try {
|
|
1676
|
+
conf = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
return finish(false)
|
|
1679
|
+
}
|
|
1680
|
+
if (!conf || !Number.isInteger(conf.port) || typeof conf.token !== 'string') return finish(false)
|
|
1681
|
+
let body
|
|
1682
|
+
try {
|
|
1683
|
+
body = JSON.stringify({ mode: 'notify', tool: tool, hook: payload })
|
|
1684
|
+
} catch (e) {
|
|
1685
|
+
return finish(false)
|
|
1686
|
+
}
|
|
1687
|
+
let req
|
|
1688
|
+
try {
|
|
1689
|
+
req = http.request(
|
|
1690
|
+
{
|
|
1691
|
+
host: '127.0.0.1',
|
|
1692
|
+
port: conf.port,
|
|
1693
|
+
path: '/gate',
|
|
1694
|
+
method: 'POST',
|
|
1695
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'x-gate-token': conf.token },
|
|
1696
|
+
},
|
|
1697
|
+
(res) => {
|
|
1698
|
+
res.resume()
|
|
1699
|
+
res.on('end', () => finish(res.statusCode === 200))
|
|
1700
|
+
res.on('error', () => finish(false))
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
} catch (e) {
|
|
1704
|
+
return finish(false)
|
|
1705
|
+
}
|
|
1706
|
+
const timer = setTimeout(() => {
|
|
1707
|
+
try {
|
|
1708
|
+
req.destroy()
|
|
1709
|
+
} catch (e) {
|
|
1710
|
+
/* 已经断了 */
|
|
1711
|
+
}
|
|
1712
|
+
finish(false)
|
|
1713
|
+
}, timeoutMs)
|
|
1714
|
+
if (timer.unref) timer.unref()
|
|
1715
|
+
req.on('error', () => finish(false))
|
|
1716
|
+
req.on('close', () => {
|
|
1717
|
+
clearTimeout(timer)
|
|
1718
|
+
finish(false)
|
|
1719
|
+
})
|
|
1720
|
+
req.end(body)
|
|
1721
|
+
})
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1471
1724
|
async function main(argv) {
|
|
1472
1725
|
// 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
|
|
1473
1726
|
const tool = (argv && argv[2]) || 'claude_code'
|
|
@@ -1475,6 +1728,11 @@ async function main(argv) {
|
|
|
1475
1728
|
const event = payload.hook_event_name || ''
|
|
1476
1729
|
const sessionId = payload.session_id || ''
|
|
1477
1730
|
if (!sessionId) return
|
|
1731
|
+
// 宿主对不上就当没来过:不写状态、不上报、不输出(见 foreignHost)
|
|
1732
|
+
if (foreignHost(tool, payload)) {
|
|
1733
|
+
noteForeign(tool, payload)
|
|
1734
|
+
return
|
|
1735
|
+
}
|
|
1478
1736
|
// 快速路径(切片 C):Codex 的同步 UserPromptSubmit 条目 / opencode 插件的 chat.message 调。
|
|
1479
1737
|
// 只读 appended → 输出 JSON → 删;不查进程、不上报(同步 hook 卡着用户的输入)
|
|
1480
1738
|
if (argv && argv[3] === '--appended') {
|
|
@@ -1486,8 +1744,13 @@ async function main(argv) {
|
|
|
1486
1744
|
// 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
|
|
1487
1745
|
const eventAt = Date.now()
|
|
1488
1746
|
|
|
1747
|
+
// 千问办公:选择题 / 授权框推设备(不挂起,见 notifyDaemon)。状态照常往下走。
|
|
1748
|
+
// 🔴 放在取 eventAt 之后:通知最多耗 800ms,不能把这次事件的时间往后推(并发的 Stop 会被它压掉)
|
|
1749
|
+
if (qwenworkNotifyKind(tool, event, payload)) await notifyDaemon(tool, payload, QWENWORK_NOTIFY_MS)
|
|
1750
|
+
|
|
1489
1751
|
if (NOTICE_EVENTS[event]) {
|
|
1490
1752
|
const notice = takeNotice(sessionId)
|
|
1753
|
+
checkQoderAnswerAdopted(tool, event, payload, notice)
|
|
1491
1754
|
// 🔴 writeSync:macOS 上 stdout 是管道时 process.stdout.write 是异步的,进程先退就丢了
|
|
1492
1755
|
if (notice) {
|
|
1493
1756
|
try {
|
|
@@ -1553,9 +1816,15 @@ async function main(argv) {
|
|
|
1553
1816
|
const now = Date.now()
|
|
1554
1817
|
if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
|
|
1555
1818
|
// 窗口对话轮数(spec 2026-10-02 §2.1):一轮开始的事件加 1
|
|
1556
|
-
const
|
|
1819
|
+
const internal = event === 'UserPromptSubmit' && isInternalPrompt(payload.prompt)
|
|
1820
|
+
const isTurn = !internal && turnEvents(registry, tool).indexOf(event) >= 0
|
|
1557
1821
|
// 首条指令(标题来源 first-prompt):只记第一次
|
|
1558
|
-
|
|
1822
|
+
// 状态没了(会话结束后被续跑)就接已结束记录里的首句;DeskBuddy 注入的话不算首句
|
|
1823
|
+
const firstPrompt =
|
|
1824
|
+
(prev && prev.firstPrompt) ||
|
|
1825
|
+
(prev ? undefined : endedFirstPrompt(sessionId)) ||
|
|
1826
|
+
(event === 'UserPromptSubmit' && !internal ? cleanPrompt(payload.prompt) : '') ||
|
|
1827
|
+
undefined
|
|
1559
1828
|
if (superseded(prev, eventAt)) {
|
|
1560
1829
|
// 被并发的更晚事件(多半是同一轮的 Stop)抢先写了:状态不覆盖,但首条指令要补上,不然标题就成了第二条指令
|
|
1561
1830
|
if (patchSuperseded(prev, firstPrompt, isTurn)) await reportIfAuthorized(tool, Date.now())
|
|
@@ -1564,7 +1833,7 @@ async function main(argv) {
|
|
|
1564
1833
|
// 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
|
|
1565
1834
|
// 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
|
|
1566
1835
|
const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
|
|
1567
|
-
const pid = (prevAlive && prev.pid) || resolveAgentPid(process.pid, agentNames(registry, tool)) || undefined
|
|
1836
|
+
const pid = (prevAlive && prev.pid) || envAgentPid() || resolveAgentPid(process.pid, agentNames(registry, tool)) || undefined
|
|
1568
1837
|
// 两个判定沿用规则相同:进程还活着且上次判过就沿用,否则查一次命令行(只查一次)
|
|
1569
1838
|
let host = null
|
|
1570
1839
|
const hostFlag = (k) => {
|
|
@@ -1720,6 +1989,11 @@ module.exports = {
|
|
|
1720
1989
|
pairClear,
|
|
1721
1990
|
shouldReport,
|
|
1722
1991
|
superseded,
|
|
1992
|
+
foreignHost,
|
|
1993
|
+
checkQoderAnswerAdopted,
|
|
1994
|
+
qwenworkNotifyKind,
|
|
1995
|
+
envAgentPid,
|
|
1996
|
+
notifyDaemon,
|
|
1723
1997
|
sign,
|
|
1724
1998
|
titleSources,
|
|
1725
1999
|
writeState,
|
|
@@ -1734,6 +2008,10 @@ module.exports = {
|
|
|
1734
2008
|
setTurns,
|
|
1735
2009
|
patchSuperseded,
|
|
1736
2010
|
endedTurns,
|
|
2011
|
+
endedFirstPrompt,
|
|
2012
|
+
isInternalPrompt,
|
|
2013
|
+
isStaleNoPid,
|
|
2014
|
+
STALE_NO_PID_MS,
|
|
1737
2015
|
WINDOW_EVENTS,
|
|
1738
2016
|
noteWindow,
|
|
1739
2017
|
readWindow,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// hook/ocTranslate.mjs 的类型声明(daemon 经 src/core/ocTranslate.ts 引用;插件本体是零依赖 JS)
|
|
2
|
+
export function toCcTool(tool: unknown, args: unknown): { tool_name: string; tool_input: Record<string, unknown> }
|
|
3
|
+
export function askToCc(props: unknown): { tool_name: string; tool_input: Record<string, unknown> }
|
|
4
|
+
export function alwaysSuggestions(props: unknown): Array<Record<string, unknown>> | undefined
|
|
5
|
+
export function isPlainQuestion(props: unknown, callTool: unknown): boolean
|
|
6
|
+
export function questionToCc(props: unknown): { tool_name: string; tool_input: { questions: Array<Record<string, unknown>> } } | null
|
|
7
|
+
export function rememberTitle(titles: Map<string, string>, info: unknown): void
|