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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// opencode 系事件 → cc-hook 同构输入的翻译(spec 2026-10-03 §3.2「共用翻译」)。
|
|
2
|
+
// opencode 插件(opencode.mjs,跑在 opencode 自带的 bun 里)与 daemon 的 DumateWatch(百度搭子 = 改名的 opencode)共用这一份:
|
|
3
|
+
// 插件 import 同目录的本文件(随 hook.cjs 一起同步到 ~/.deskbuddy/),daemon 经 src/core/ocTranslate.ts 打包进去。
|
|
4
|
+
// 🔴 零依赖纯函数:不能 import 任何东西(插件那边同目录只有我们自己的文件)
|
|
5
|
+
|
|
6
|
+
/** opencode 工具 → cc-hook 同构的 tool_name / tool_input(daemon 的 classify 按 Claude Code 的工具名判断) */
|
|
7
|
+
export function toCcTool(tool, args) {
|
|
8
|
+
const a = args && typeof args === 'object' ? args : {}
|
|
9
|
+
const s = (v) => (typeof v === 'string' ? v : '')
|
|
10
|
+
switch (tool) {
|
|
11
|
+
case 'bash':
|
|
12
|
+
return { tool_name: 'Bash', tool_input: { command: s(a.command) } }
|
|
13
|
+
case 'read':
|
|
14
|
+
return { tool_name: 'Read', tool_input: { file_path: s(a.filePath) } }
|
|
15
|
+
case 'edit':
|
|
16
|
+
return { tool_name: 'Edit', tool_input: { file_path: s(a.filePath) } }
|
|
17
|
+
case 'write':
|
|
18
|
+
return { tool_name: 'Write', tool_input: { file_path: s(a.filePath) } }
|
|
19
|
+
case 'glob':
|
|
20
|
+
return { tool_name: 'Glob', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
21
|
+
case 'grep':
|
|
22
|
+
return { tool_name: 'Grep', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
23
|
+
case 'list':
|
|
24
|
+
return { tool_name: 'LS', tool_input: { path: s(a.path) } }
|
|
25
|
+
case 'webfetch':
|
|
26
|
+
return { tool_name: 'WebFetch', tool_input: { url: s(a.url) } }
|
|
27
|
+
case 'task':
|
|
28
|
+
return { tool_name: 'Task', tool_input: {} }
|
|
29
|
+
case 'todowrite':
|
|
30
|
+
case 'todoread':
|
|
31
|
+
return { tool_name: 'TodoWrite', tool_input: {} }
|
|
32
|
+
case 'patch':
|
|
33
|
+
case 'apply_patch':
|
|
34
|
+
return { tool_name: 'apply_patch', tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } }
|
|
35
|
+
default:
|
|
36
|
+
// 不认识的工具(MCP 等)原名原参数交出去:classify 对不认识的一律推设备
|
|
37
|
+
return { tool_name: String(tool || ''), tool_input: a }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** permission.asked → PermissionRequest 的 tool_name / tool_input,给设备卡片生成摘要 */
|
|
42
|
+
export function askToCc(props) {
|
|
43
|
+
const md = (props && props.metadata) || {}
|
|
44
|
+
if (typeof md.command === 'string' && md.command) return { tool_name: 'Bash', tool_input: { command: md.command } }
|
|
45
|
+
const file = md.filepath || md.filePath
|
|
46
|
+
if (typeof file === 'string' && file) return { tool_name: 'Edit', tool_input: { file_path: file } }
|
|
47
|
+
const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(' ') : ''
|
|
48
|
+
return { tool_name: String((props && props.permission) || 'permission'), tool_input: { path: patterns } }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 授权请求里 opencode 自己给的「以后都允许」模式(permission.asked 的 always,如 bash 的 `npm *`、edit 的 `*`)
|
|
53
|
+
* → 一条 session 级 addRules 建议(与 Claude Code 的 permission_suggestions 同形),daemon 据此在设备上给「允许并记住」。
|
|
54
|
+
* 设备选了记住,gate 输出带 updatedPermissions,插件回 always(opencode 在本次运行期间记住这些模式)。没有 always 返回 undefined
|
|
55
|
+
*/
|
|
56
|
+
export function alwaysSuggestions(props) {
|
|
57
|
+
const perm = String((props && props.permission) || '').trim()
|
|
58
|
+
const always = props && Array.isArray(props.always) ? props.always.filter((x) => typeof x === 'string' && x.trim() !== '') : []
|
|
59
|
+
if (!perm || always.length === 0) return undefined
|
|
60
|
+
return [{ type: 'addRules', rules: always.map((x) => ({ toolName: perm, ruleContent: `${perm} ${x.trim()}` })), behavior: 'allow', destination: 'session' }]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 这次提问来自哪个工具:question.asked 只带 tool.callID,工具名要从 tool.execute.before 记下的调用里查。
|
|
65
|
+
* 🔴 只推 question 工具本身的提问:plan_exit 也走 question.asked(「计划完成,是否切到 build」Yes/No),
|
|
66
|
+
* 推到设备 = 盲批计划,违背 ExitPlanMode 不推的原则(设备答选择题 spec §3.7)。认不出来源的一律不推
|
|
67
|
+
*/
|
|
68
|
+
export function isPlainQuestion(props, callTool) {
|
|
69
|
+
if (callTool !== 'question') return false
|
|
70
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : []
|
|
71
|
+
// 兜底:plan_exit 的固定题面 / header
|
|
72
|
+
return !qs.some((q) => q && (q.header === 'Build Agent' || (typeof q.question === 'string' && /^Plan at .* is complete\./.test(q.question))))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* question.asked(opencode 的 question 工具 / plan_exit 等向用户提问)→ AskUserQuestion 形状的 tool_input,
|
|
77
|
+
* 统一走 daemon 的选择题链路(设备答选择题 spec §3.4)。`multiple` → multiSelect;`custom:false` 原样带上(不许自己填)
|
|
78
|
+
*/
|
|
79
|
+
export function questionToCc(props) {
|
|
80
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : null
|
|
81
|
+
if (!qs || qs.length === 0) return null
|
|
82
|
+
return {
|
|
83
|
+
tool_name: 'AskUserQuestion',
|
|
84
|
+
tool_input: {
|
|
85
|
+
questions: qs.map((q) => ({
|
|
86
|
+
question: q && q.question,
|
|
87
|
+
header: (q && q.header) || '',
|
|
88
|
+
multiSelect: !!(q && q.multiple),
|
|
89
|
+
options: q && Array.isArray(q.options) ? q.options.map((o) => ({ label: o && o.label, description: (o && o.description) || '' })) : [],
|
|
90
|
+
...(q && q.custom === false ? { custom: false } : {}),
|
|
91
|
+
})),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** opencode 起标题前的占位名(1.18.32 isDefaultTitle:`New session - ` / `Child session - ` + ISO 时间),不当标题 */
|
|
97
|
+
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$/
|
|
98
|
+
|
|
99
|
+
/** session.updated 的 info → 记下自动标题。子会话(subagent,带 parentID)、占位名不记 */
|
|
100
|
+
export function rememberTitle(titles, info) {
|
|
101
|
+
if (!info || typeof info.id !== 'string' || !info.id || info.parentID) return
|
|
102
|
+
const t = typeof info.title === 'string' ? info.title.trim() : ''
|
|
103
|
+
if (!t || OC_DEFAULT_TITLE_RE.test(t)) return
|
|
104
|
+
titles.set(info.id, t.slice(0, 100))
|
|
105
|
+
}
|
|
106
|
+
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// hook/deskbuddy-dsh.mjs 的类型声明(只给测试用;插件本体是零依赖 JS,跑在 dsh 进程里)
|
|
2
|
+
type Cc = { tool_name: string; tool_input: Record<string, unknown> }
|
|
3
|
+
export const TOOL: string
|
|
4
|
+
export const SUPPORTED_VERSION_RE: RegExp
|
|
5
|
+
export function toCcTool(tool: unknown, args: unknown): Cc
|
|
6
|
+
export function parseArgs(raw: unknown): Record<string, unknown>
|
|
7
|
+
export function questionsToCc(questions: unknown): { tool_name: string; tool_input: { questions: Array<Record<string, unknown>> } } | null
|
|
8
|
+
export function answersFromLists(questions: unknown, lists: unknown): { answers: Array<{ id: string; selected: string[]; custom?: string }> } | null
|
|
9
|
+
export function skippedAnswers(questions: unknown): { answers: Array<{ id: string; selected: string[] }> } | null
|
|
10
|
+
export function profileName(ctxProfile: unknown, argv?: string[]): string
|
|
11
|
+
export function isWindowProfile(name: unknown): boolean
|
|
12
|
+
export function gatingAllowed(version: string): boolean
|
|
13
|
+
export function dshVersion(argv?: string[]): string
|
|
14
|
+
export function parseGateOut(r: unknown): { decision: 'allow' | 'deny'; answers: string[][] | null } | null
|
|
15
|
+
export function race<T>(next: () => Promise<T> | T, device: Promise<T | null>, cancelDevice: () => void, abortNative: () => void): Promise<T>
|
|
16
|
+
export function spliceSignal(req: { signal?: AbortSignal }, ac: AbortController): boolean
|
|
17
|
+
export const name: string
|
|
18
|
+
export function apply(ctx: unknown, config?: { node?: string }): void
|
|
19
|
+
export function runIdFromEnv(env?: Record<string, string | undefined>): string
|
|
20
|
+
export function mailboxHome(env?: Record<string, string | undefined>): string
|
|
21
|
+
export function runGateAllowed(r: unknown): boolean
|
|
22
|
+
export function rootSession(parents: Map<string, string>, sid: string): string
|
|
23
|
+
export const STALE_LETTER_MS: number
|
|
24
|
+
export const LETTER_RE: RegExp
|