deskbuddy 0.2.0-beta.2 → 0.2.0-beta.21
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 +2 -2
- package/dist/cli.js +15566 -4613
- package/dist/deskbuddy-dsh.mjs +706 -0
- package/dist/gate.cjs +411 -35
- package/dist/hook.cjs +1283 -44
- package/dist/launcher.cjs +229 -0
- package/dist/ocTranslate.mjs +144 -0
- package/dist/opencode.mjs +204 -53
- package/dist/wake.cjs +769 -0
- package/docs/INSTALL.md +24 -10
- package/hook/deskbuddy-dsh.d.mts +24 -0
- package/hook/deskbuddy-dsh.mjs +706 -0
- package/hook/gate.cjs +411 -35
- package/hook/hook-debug.log +68 -0
- package/hook/hook.cjs +1283 -44
- package/hook/launcher.cjs +229 -0
- package/hook/ocTranslate.d.mts +9 -0
- package/hook/ocTranslate.mjs +144 -0
- package/hook/opencode.d.mts +10 -1
- package/hook/opencode.mjs +204 -53
- package/hook/wake.cjs +769 -0
- 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/hook/gate.cjs
CHANGED
|
@@ -101,16 +101,26 @@ function argTool(argv) {
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* 按智能体判断是不是交互窗口。
|
|
104
|
-
* - Qoder CN(2026-09-27 实测):桌面端弹授权框时 hook 的父进程就是 `Qoder CN.app`
|
|
105
|
-
* daemon
|
|
104
|
+
* - Qoder CN(qoder_cn,2026-09-27 实测):桌面端弹授权框时 hook 的父进程就是 `Qoder CN.app` 主进程。
|
|
105
|
+
* ⚠️ daemon 起的远程任务也会被判成窗口:worker 是用 `Qoder CN.app` 里的可执行文件加 ELECTRON_RUN_AS_NODE=1 跑的
|
|
106
|
+
* (daemon/locate.ts qoderWorker),祖先链里同样有 app。目前安全靠这三点,缺一不可:
|
|
107
|
+
* ① 远程任务先走 PreToolUse 的 `--run` 关卡(daemon/executor.ts qoderPlan),每一步都经设备授权;
|
|
108
|
+
* ② 远程任务开着 bypass_permissions,不触发 PermissionRequest,本窗口关卡根本不会被调;
|
|
109
|
+
* ③ 万一被调到,coordinator.ts #onWindow 会跳过 daemon 自己起的会话(按 #runs 的 sessionId)
|
|
110
|
+
* - Qoder 国际版(qoder):同一套代码,app 叫 `Qoder.app` ⚠️ 未实机核对
|
|
111
|
+
* - Qoder 命令行窗口(Bug 204):终端里的 `qoderclicn`(国内版)/ `qodercli`(国际版),参数判据同 Claude
|
|
112
|
+
* (2026-10-03 qoderclicn 1.1.59 实测:PermissionRequest hook 的父进程就是 qoderclicn,回 allow 后不再弹框)
|
|
106
113
|
* - Claude Code:见 isInteractiveClaude
|
|
107
114
|
*/
|
|
108
115
|
function isInteractiveAgent(tool) {
|
|
109
116
|
if (process.platform === 'win32') return isInteractiveAgentWin(tool)
|
|
110
|
-
if (tool === '
|
|
117
|
+
if (tool === 'qoder_cn') return isInteractiveQoder(tool, '/Qoder CN.app/')
|
|
118
|
+
if (tool === 'qoder') return isInteractiveQoder(tool, '/Qoder.app/')
|
|
111
119
|
if (tool === 'claude_code') return isInteractiveClaude()
|
|
112
120
|
// opencode 的窗口确认由插件发起,插件已排除 `opencode run`(hook/opencode.mjs isHeadless)
|
|
113
121
|
if (tool === 'opencode') return true
|
|
122
|
+
// DeepSeek Harness 同理:插件只在 web / Desktop 这类有人在电脑前的 profile 里发起(hook/deskbuddy-dsh.mjs isWindowProfile)
|
|
123
|
+
if (tool === 'deepseek_harness') return true
|
|
114
124
|
return false
|
|
115
125
|
}
|
|
116
126
|
|
|
@@ -121,15 +131,76 @@ function claudeArgsInteractive(argsAfterArgv0) {
|
|
|
121
131
|
)
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Qoder 命令行:只看第一个独立 `--` 之前的参数。daemon 开窗新建 Qoder CN 时首句放在 `--` 之后(wrap/promptLaunch.ts),
|
|
136
|
+
* 首句里有个 ` -p ` 的词不能把窗口判成非交互(spec 2026-10-04 qodercn-visible §3.1;与 hook.cjs isInteractiveArgs 一致)
|
|
137
|
+
*/
|
|
138
|
+
function qoderArgsInteractive(argsAfterArgv0) {
|
|
139
|
+
const dd = argsAfterArgv0.indexOf('--')
|
|
140
|
+
return claudeArgsInteractive(dd >= 0 ? argsAfterArgv0.slice(0, dd) : argsAfterArgv0)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 进程名是不是这个工具的 Qoder 命令行(Bug 204)。国内版 `qoderclicn`,国际版 `qodercli`(装出来的文件名带版本号:qodercli-1.0.12)。
|
|
145
|
+
* 🔴 qoderclicn 也以 qodercli 开头,国际版要排除它
|
|
146
|
+
*/
|
|
147
|
+
function qoderCliMatches(tool, name) {
|
|
148
|
+
const b = path.basename(String(name || '')).toLowerCase().replace(/\.exe$/, '')
|
|
149
|
+
if (tool === 'qoder_cn') return b.startsWith('qoderclicn')
|
|
150
|
+
if (tool === 'qoder') return b.startsWith('qodercli') && !b.startsWith('qoderclicn')
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 进程表查询(ps)。取不到返回 null */
|
|
155
|
+
const PS = {
|
|
156
|
+
/** '<ppid> <comm>' */
|
|
157
|
+
line(pid) {
|
|
158
|
+
try {
|
|
159
|
+
return execFileSync('ps', ['-o', 'ppid=,comm=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
160
|
+
} catch (_) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
args(pid) {
|
|
165
|
+
try {
|
|
166
|
+
return execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
167
|
+
} catch (_) {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Qoder:祖先里先碰到桌面端 app → 交互;先碰到命令行 → 看参数(-p / stream-json 是程序在用)。
|
|
175
|
+
* ps / startPid 只给测试注入
|
|
176
|
+
*/
|
|
177
|
+
function isInteractiveQoder(tool, appMark, ps, startPid) {
|
|
178
|
+
const p = ps || PS
|
|
179
|
+
let pid = startPid || process.ppid
|
|
180
|
+
for (let i = 0; i < 8 && pid > 1; i++) {
|
|
181
|
+
const line = p.line(pid)
|
|
182
|
+
const m = line && line.match(/^(\d+)\s+(.*)$/)
|
|
183
|
+
if (!m) return false
|
|
184
|
+
if (m[2].includes(appMark)) return true
|
|
185
|
+
if (qoderCliMatches(tool, m[2])) {
|
|
186
|
+
const args = p.args(pid)
|
|
187
|
+
if (args === null) return false
|
|
188
|
+
return qoderArgsInteractive(args.trim().split(/\s+/).slice(1))
|
|
189
|
+
}
|
|
190
|
+
pid = Number(m[1])
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
124
195
|
/**
|
|
125
196
|
* Windows 版(没有 ps):进程表借 hook.cjs 的实现(同目录,随它一起装),判据同上。
|
|
126
|
-
* - Qoder CN 桌面端:祖先里有 `Qoder CN.exe`
|
|
197
|
+
* - Qoder CN 桌面端:祖先里有 `Qoder CN.exe`;国际版:`Qoder.exe`
|
|
127
198
|
* - Claude Code:原生版是 `claude.exe`,npm 版是 node 跑 `claude-code/cli.js`
|
|
128
199
|
* 取不到进程表 = 判不出来 = 不表态(窗口模式的兜底方向)
|
|
129
200
|
*/
|
|
130
201
|
function isInteractiveAgentWin(tool) {
|
|
131
|
-
if (tool === 'opencode') return true
|
|
132
|
-
if (tool !== 'qoder' && tool !== 'claude_code') return false
|
|
202
|
+
if (tool === 'opencode' || tool === 'deepseek_harness') return true
|
|
203
|
+
if (tool !== 'qoder_cn' && tool !== 'qoder' && tool !== 'claude_code') return false
|
|
133
204
|
let winProc
|
|
134
205
|
try {
|
|
135
206
|
winProc = require(path.join(__dirname, 'hook.cjs')).winProc
|
|
@@ -142,7 +213,10 @@ function isInteractiveAgentWin(tool) {
|
|
|
142
213
|
seen.add(pid)
|
|
143
214
|
const p = winProc(pid)
|
|
144
215
|
if (!p) return false
|
|
145
|
-
if (tool === '
|
|
216
|
+
if (tool === 'qoder_cn' && /qoder cn/i.test(p.name)) return true
|
|
217
|
+
if (tool === 'qoder' && /^qoder(\.exe)?$/i.test(p.name)) return true
|
|
218
|
+
// 命令行窗口(Bug 204)⚠️ Windows 未实机核对
|
|
219
|
+
if (qoderCliMatches(tool, p.name)) return qoderArgsInteractive(String(p.args || '').split(' ').slice(1))
|
|
146
220
|
if (tool === 'claude_code' && (/^claude(\.exe)?$/i.test(p.name) || p.name === 'claude-code/cli.js')) {
|
|
147
221
|
return claudeArgsInteractive(p.args.split(' ').slice(1))
|
|
148
222
|
}
|
|
@@ -151,21 +225,294 @@ function isInteractiveAgentWin(tool) {
|
|
|
151
225
|
return false
|
|
152
226
|
}
|
|
153
227
|
|
|
154
|
-
|
|
155
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Qoder CN 的宿主界面(授权选项透传:桌面 GUI 与终端里的 worker 选项集不同,notes 2026-10-03 §2):
|
|
230
|
+
* 1. 环境变量 QODER_AGENT_SDK_DAEMON=1:桌面端经 agent SDK 起的 worker(SDK 起 worker 时设的,hook 继承)→ gui
|
|
231
|
+
* 2. 沿父进程找第一个 Qoder CN 应用里的进程(macOS 路径含 `/Qoder CN.app/`,Windows 进程名含 `Qoder CN`)或 `qoderclicn` 命令行
|
|
232
|
+
* (与 isInteractiveQoder 同一顺序,Bug 204):
|
|
233
|
+
* - 不是 worker(参数里没有 qoder-worker-runtime)→ 桌面主进程直接起的 hook → gui
|
|
234
|
+
* - 是 worker 且参数带 `--sdk` → gui
|
|
235
|
+
* - 是 worker / 命令行:再往上找,跳过 sh / zsh / bash 这类中间 shell(GUI 若经 shell 起 worker 不误判,评审),
|
|
236
|
+
* 下一个还在应用里 → gui,否则(终端)→ tui
|
|
237
|
+
* 取不到 / 认不出返回 ''(不发 options)。info / startPid / platform 只给测试注入(info(pid) → {ppid, comm|name, args})
|
|
238
|
+
*/
|
|
239
|
+
const SHELL_NAMES = /^(-?(sh|bash|zsh|dash|fish|ksh)|cmd\.exe|cmd|powershell\.exe|powershell|pwsh\.exe|pwsh)$/i
|
|
240
|
+
|
|
241
|
+
/** 参数串里第一个独立 `--` 之前的部分 */
|
|
242
|
+
function beforeDashDashStr(args) {
|
|
243
|
+
const t = String(args || '').split(/\s+/)
|
|
244
|
+
const i = t.indexOf('--')
|
|
245
|
+
return i >= 0 ? t.slice(0, i).join(' ') : t.join(' ')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function qoderUi(tool, info, startPid, platform) {
|
|
249
|
+
if (tool !== 'qoder_cn') return ''
|
|
250
|
+
if (!info && process.env.QODER_AGENT_SDK_DAEMON === '1') return 'gui'
|
|
251
|
+
const win = (platform || process.platform) === 'win32'
|
|
252
|
+
const get = info || (win ? winProcInfo : procInfo)
|
|
253
|
+
const inApp = (p) => (win ? /qoder cn/i.test(p.name) : p.comm.includes('/Qoder CN.app/'))
|
|
254
|
+
const base = (p) => path.basename(win ? p.name : p.comm)
|
|
255
|
+
let pid = startPid || process.ppid
|
|
156
256
|
for (let i = 0; i < 8 && pid > 1; i++) {
|
|
157
|
-
|
|
257
|
+
const p = get(pid)
|
|
258
|
+
if (!p) return ''
|
|
259
|
+
// 与 isInteractiveQoder 同一顺序:祖先里先碰到桌面端 app 还是先碰到命令行(Bug 204:终端里的 qoderclicn)
|
|
260
|
+
const cli = qoderCliMatches(tool, win ? p.name : p.comm)
|
|
261
|
+
if (inApp(p) || cli) {
|
|
262
|
+
// 桌面主进程直接起的 hook(不是 worker / 命令行)→ gui
|
|
263
|
+
if (!cli && !/qoder-worker-runtime/.test(p.args)) return 'gui'
|
|
264
|
+
// 只看第一个独立 `--` 之前的参数(与 qoderArgsInteractive 同一判据):开窗新建把首句放在 `--` 之后,首句里的 --sdk 不算
|
|
265
|
+
if (/(^|\s)--sdk(\s|$)/.test(beforeDashDashStr(p.args))) return 'gui'
|
|
266
|
+
// worker / 命令行:跳过中间 shell 往上看,还在 app 里 = 桌面端起的 → gui,否则(终端)→ tui
|
|
267
|
+
let up = p.ppid
|
|
268
|
+
for (let j = 0; j < 6 && up > 1; j++) {
|
|
269
|
+
const q = get(up)
|
|
270
|
+
if (!q) return ''
|
|
271
|
+
if (SHELL_NAMES.test(base(q))) {
|
|
272
|
+
up = q.ppid
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
return inApp(q) ? 'gui' : 'tui'
|
|
276
|
+
}
|
|
277
|
+
return ''
|
|
278
|
+
}
|
|
279
|
+
pid = p.ppid
|
|
280
|
+
}
|
|
281
|
+
return ''
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function procInfo(pid) {
|
|
285
|
+
try {
|
|
286
|
+
const line = execFileSync('ps', ['-o', 'ppid=,comm=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
287
|
+
const m = line.match(/^(\d+)\s+(.*)$/)
|
|
288
|
+
if (!m) return null
|
|
289
|
+
const args = execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
290
|
+
return { ppid: Number(m[1]), comm: m[2], args }
|
|
291
|
+
} catch (_) {
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function winProcInfo(pid) {
|
|
297
|
+
try {
|
|
298
|
+
const p = require(path.join(__dirname, 'hook.cjs')).winProc(pid)
|
|
299
|
+
return p ? { ppid: p.ppid, name: p.name, comm: p.name, args: p.args || '' } : null
|
|
300
|
+
} catch (_) {
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Qoder CN 桌面端的界面语言(app.asar 0.4.1 渲染层):localStorage `loop.language`(zh / en),没设按系统首选语言(zh* → zh,其余 en)。
|
|
307
|
+
* userData = appData/<product.json dataDirectoryName>:macOS ~/Library/Application Support/com.qodercn.app.stable,
|
|
308
|
+
* Windows %APPDATA%\com.qodercn.app.stable(主进程 setPath('userData', appData/dataDirectoryName),不是 "Qoder CN")。
|
|
309
|
+
* 🔴 只读:直接按字节扫 LevelDB 的 .ldb / .log(不开库、不加锁)。读不出返回 ''(不发 options)
|
|
310
|
+
*/
|
|
311
|
+
const QODER_CN_DATA_DIR = 'com.qodercn.app.stable'
|
|
312
|
+
|
|
313
|
+
function qoderCnLang() {
|
|
314
|
+
const forced = process.env.DESKBUDDY_GATE_QODER_LANG
|
|
315
|
+
if (forced === 'zh' || forced === 'en' || forced === 'none') return forced === 'none' ? '' : forced
|
|
316
|
+
let dir = process.env.DESKBUDDY_GATE_QODER_DATA || ''
|
|
317
|
+
if (!dir) {
|
|
318
|
+
const appData = process.platform === 'win32' ? process.env.APPDATA : process.platform === 'darwin' ? path.join(os.homedir(), 'Library', 'Application Support') : ''
|
|
319
|
+
if (!appData) return ''
|
|
320
|
+
dir = path.join(appData, QODER_CN_DATA_DIR)
|
|
321
|
+
}
|
|
322
|
+
const stored = readLoopLanguage(path.join(dir, 'Local Storage', 'leveldb'))
|
|
323
|
+
if (stored) return stored
|
|
324
|
+
return process.env.DESKBUDDY_GATE_QODER_DATA ? '' : systemLang()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 按字节找 `loop.language` 的值;新的覆盖旧的(.ldb 按文件号、再 .log 按文件号、同文件按位置)。只认 zh / en */
|
|
328
|
+
function readLoopLanguage(dir) {
|
|
329
|
+
let names
|
|
330
|
+
try {
|
|
331
|
+
names = fs.readdirSync(dir)
|
|
332
|
+
} catch (_) {
|
|
333
|
+
return ''
|
|
334
|
+
}
|
|
335
|
+
const num = (n) => Number((n.match(/^(\d+)\./) || [])[1] || 0)
|
|
336
|
+
const files = [
|
|
337
|
+
...names.filter((n) => /^\d+\.ldb$/.test(n)).sort((a, b) => num(a) - num(b)),
|
|
338
|
+
...names.filter((n) => /^\d+\.log$/.test(n)).sort((a, b) => num(a) - num(b)),
|
|
339
|
+
]
|
|
340
|
+
let last = ''
|
|
341
|
+
// .log:key 后紧跟值长度 3 + "\x01zh";.ldb:key 后是 8 字节 tag(首字节 1 = 写入)再跟值
|
|
342
|
+
const re = /loop\.language(?:\x03\x01(zh|en)|\x01[\s\S]{7}\x01(zh|en))/g
|
|
343
|
+
for (const f of files) {
|
|
344
|
+
let buf
|
|
158
345
|
try {
|
|
159
|
-
|
|
346
|
+
const full = path.join(dir, f)
|
|
347
|
+
if (fs.statSync(full).size > 16 * 1024 * 1024) continue
|
|
348
|
+
buf = fs.readFileSync(full).toString('latin1')
|
|
160
349
|
} catch (_) {
|
|
161
|
-
|
|
350
|
+
continue
|
|
162
351
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
352
|
+
let m
|
|
353
|
+
while ((m = re.exec(buf))) last = m[1] || m[2]
|
|
354
|
+
}
|
|
355
|
+
return last
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 系统首选语言(渲染层 navigator.languages[0] 的近似):macOS 读 AppleLanguages,Windows 用 ICU 的默认区域 */
|
|
359
|
+
function systemLang() {
|
|
360
|
+
try {
|
|
361
|
+
let first = ''
|
|
362
|
+
if (process.platform === 'darwin') {
|
|
363
|
+
const out = execFileSync('defaults', ['read', '-g', 'AppleLanguages'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 })
|
|
364
|
+
const m = out.match(/"?([A-Za-z]{2,3}(?:[-_][A-Za-z0-9]+)*)"?/)
|
|
365
|
+
first = m ? m[1] : ''
|
|
366
|
+
} else if (process.platform === 'win32') {
|
|
367
|
+
first = Intl.DateTimeFormat().resolvedOptions().locale || ''
|
|
368
|
+
}
|
|
369
|
+
if (!first) return ''
|
|
370
|
+
return first.toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
371
|
+
} catch (_) {
|
|
372
|
+
return ''
|
|
167
373
|
}
|
|
168
|
-
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Claude Code 的 auto 模式能不能在设备上给(授权选项透传:用户拍板「切 auto 模式」上设备;hook 回 setMode auto 会绕过
|
|
378
|
+
* disableAutoMode,实测)。🔴 白名单:只有确认没有任何别的策略来源、且看得到的设置都没关时才返回 'on',其余一律 'off'。
|
|
379
|
+
* 来源(2.1.280 二进制核对):
|
|
380
|
+
* - userSettings:$CLAUDE_CONFIG_DIR/settings.json(缺省 ~/.claude)
|
|
381
|
+
* - projectSettings / localSettings:工作目录及各级上级的 .claude/settings.json、.claude/settings.local.json(宁可多看几级)
|
|
382
|
+
* 以上读到 permissions.disableAutoMode / disableAutoMode 为 "disable"(或 true)→ off
|
|
383
|
+
* - 托管策略,存在即 off、不解析:managed-settings.json 与 managed-settings.d/(非空)——macOS /Library/Application Support/ClaudeCode,
|
|
384
|
+
* Linux /etc/claude-code,Windows C:\Program Files\ClaudeCode;macOS MDM:/Library/Managed Preferences/[<用户>/]com.anthropic.claudecode.plist,
|
|
385
|
+
* 以及 `defaults read com.anthropic.claudecode` 读得到;Windows 注册表 HKLM / HKCU\SOFTWARE\Policies\ClaudeCode(查询失败也算存在)
|
|
386
|
+
* - 服务端下发的组织策略:落地缓存 $CLAUDE_CONFIG_DIR/remote-settings.json,存在即 off。
|
|
387
|
+
* ⚠️ 已知限制:CC 也可能把它存进「storage backend」而不落这个文件,那时看不出来,组织策略禁用 auto 时可能误显示
|
|
388
|
+
* - claude 启动参数带 --settings(文件内容不追)→ off;拿不到 claude 的命令行 → off
|
|
389
|
+
* 任一项读取失败 / 超时 / 报错 → off
|
|
390
|
+
*/
|
|
391
|
+
const CC_POLICY_DOMAIN = 'com.anthropic.claudecode'
|
|
392
|
+
const CC_REG_KEYS = ['HKLM\\SOFTWARE\\Policies\\ClaudeCode', 'HKCU\\SOFTWARE\\Policies\\ClaudeCode']
|
|
393
|
+
|
|
394
|
+
function ccManagedDir(platform) {
|
|
395
|
+
if (platform === 'darwin') return '/Library/Application Support/ClaudeCode'
|
|
396
|
+
if (platform === 'win32') return 'C:\\Program Files\\ClaudeCode'
|
|
397
|
+
return '/etc/claude-code'
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function defaultCcDeps() {
|
|
401
|
+
return {
|
|
402
|
+
platform: process.platform,
|
|
403
|
+
env: process.env,
|
|
404
|
+
home: os.homedir(),
|
|
405
|
+
cwd: process.cwd(),
|
|
406
|
+
username: (() => {
|
|
407
|
+
try {
|
|
408
|
+
return os.userInfo().username
|
|
409
|
+
} catch (_) {
|
|
410
|
+
return ''
|
|
411
|
+
}
|
|
412
|
+
})(),
|
|
413
|
+
readFile: (f) => fs.readFileSync(f, 'utf8'),
|
|
414
|
+
listDir: (d) => fs.readdirSync(d),
|
|
415
|
+
exec: (cmd, args) => {
|
|
416
|
+
const r = require('node:child_process').spawnSync(cmd, args, { encoding: 'utf8', timeout: 3000, windowsHide: true })
|
|
417
|
+
return { status: r.error ? -1 : r.status, stdout: r.stdout || '', stderr: r.stderr || '' }
|
|
418
|
+
},
|
|
419
|
+
claudeArgs: () => claudeArgs(),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 文件不存在 → null;存在 → 内容;别的错误照抛(调用方按 off) */
|
|
424
|
+
function readIfExists(d, f) {
|
|
425
|
+
try {
|
|
426
|
+
return d.readFile(f)
|
|
427
|
+
} catch (e) {
|
|
428
|
+
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return null
|
|
429
|
+
throw e
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function ccAutoMode(hook, deps) {
|
|
434
|
+
const forced = process.env.DESKBUDDY_GATE_CC_AUTO
|
|
435
|
+
if (!deps && (forced === 'on' || forced === 'off')) return forced
|
|
436
|
+
const d = deps || defaultCcDeps()
|
|
437
|
+
try {
|
|
438
|
+
return ccAutoModeOrThrow(hook, d)
|
|
439
|
+
} catch (_) {
|
|
440
|
+
return 'off'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function ccAutoModeOrThrow(hook, d) {
|
|
445
|
+
const join = d.platform === 'win32' ? path.win32.join : path.posix.join
|
|
446
|
+
const dirname = d.platform === 'win32' ? path.win32.dirname : path.posix.dirname
|
|
447
|
+
const configDir = d.env.CLAUDE_CONFIG_DIR || join(d.home, '.claude')
|
|
448
|
+
const off = (v) => v === 'disable' || v === true
|
|
449
|
+
|
|
450
|
+
// 1. 用户 / 项目 / 本地设置:读到「关」或读坏 → off
|
|
451
|
+
const files = [join(configDir, 'settings.json')]
|
|
452
|
+
let dir = hook && typeof hook.cwd === 'string' && hook.cwd ? hook.cwd : d.cwd
|
|
453
|
+
for (let i = 0; i < 64; i++) {
|
|
454
|
+
files.push(join(dir, '.claude', 'settings.json'), join(dir, '.claude', 'settings.local.json'))
|
|
455
|
+
const up = dirname(dir)
|
|
456
|
+
if (up === dir) break
|
|
457
|
+
dir = up
|
|
458
|
+
}
|
|
459
|
+
for (const f of files) {
|
|
460
|
+
const raw = readIfExists(d, f)
|
|
461
|
+
if (raw === null) continue
|
|
462
|
+
const j = JSON.parse(raw)
|
|
463
|
+
if (j && typeof j === 'object' && (off(j.disableAutoMode) || (j.permissions && off(j.permissions.disableAutoMode)))) return 'off'
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 2. 托管策略:存在即 off
|
|
467
|
+
const managed = ccManagedDir(d.platform)
|
|
468
|
+
if (readIfExists(d, join(managed, 'managed-settings.json')) !== null) return 'off'
|
|
469
|
+
try {
|
|
470
|
+
if (d.listDir(join(managed, 'managed-settings.d')).length > 0) return 'off'
|
|
471
|
+
} catch (e) {
|
|
472
|
+
if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) return 'off'
|
|
473
|
+
}
|
|
474
|
+
if (d.platform === 'darwin') {
|
|
475
|
+
const plists = [`/Library/Managed Preferences/${CC_POLICY_DOMAIN}.plist`]
|
|
476
|
+
if (d.username) plists.unshift(`/Library/Managed Preferences/${d.username}/${CC_POLICY_DOMAIN}.plist`)
|
|
477
|
+
for (const f of plists) if (readIfExists(d, f) !== null) return 'off'
|
|
478
|
+
const r = d.exec('defaults', ['read', CC_POLICY_DOMAIN])
|
|
479
|
+
if (r.status === 0) return 'off'
|
|
480
|
+
if (!/does not exist/i.test(r.stderr)) return 'off'
|
|
481
|
+
}
|
|
482
|
+
if (d.platform === 'win32') {
|
|
483
|
+
for (const key of CC_REG_KEYS) {
|
|
484
|
+
const r = d.exec('reg', ['query', key])
|
|
485
|
+
if (r.status === 0) return 'off'
|
|
486
|
+
// 只有「找不到」才算没有;其余(含本地化报错认不出)一律当存在
|
|
487
|
+
if (!(r.status === 1 && /unable to find|找不到|cannot find/i.test(r.stderr + r.stdout))) return 'off'
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 3. 服务端下发的组织策略的落地缓存
|
|
492
|
+
if (readIfExists(d, join(configDir, 'remote-settings.json')) !== null) return 'off'
|
|
493
|
+
|
|
494
|
+
// 4. 启动参数:拿不到 claude 的命令行 / 带 --settings → off
|
|
495
|
+
const args = d.claudeArgs()
|
|
496
|
+
if (!Array.isArray(args)) return 'off'
|
|
497
|
+
if (args.some((t) => t === '--settings' || t.startsWith('--settings='))) return 'off'
|
|
498
|
+
return 'on'
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 沿父进程找 claude,返回它 argv[0] 之后的参数;找不到返回 null。Windows 用 hook.cjs 的进程表 */
|
|
502
|
+
function claudeArgs() {
|
|
503
|
+
const fake = process.env.DESKBUDDY_GATE_CC_ARGS
|
|
504
|
+
if (typeof fake === 'string') return fake === '!none' ? null : fake.split(/\s+/).filter(Boolean)
|
|
505
|
+
const win = process.platform === 'win32'
|
|
506
|
+
let pid = process.ppid
|
|
507
|
+
for (let i = 0; i < 8 && pid > 1; i++) {
|
|
508
|
+
const p = win ? winProcInfo(pid) : procInfo(pid)
|
|
509
|
+
if (!p) return null
|
|
510
|
+
const name = win ? p.name : path.basename(p.comm)
|
|
511
|
+
if (/^claude(\.exe)?$/i.test(name)) return p.args.trim().split(/\s+/).slice(1)
|
|
512
|
+
if (win && name === 'claude-code/cli.js') return p.args.trim().split(/\s+/).slice(2)
|
|
513
|
+
pid = p.ppid
|
|
514
|
+
}
|
|
515
|
+
return null
|
|
169
516
|
}
|
|
170
517
|
|
|
171
518
|
function isInteractiveClaude() {
|
|
@@ -206,17 +553,35 @@ async function windowMain() {
|
|
|
206
553
|
const conf = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
|
|
207
554
|
if (!conf || !Number.isInteger(conf.port) || typeof conf.token !== 'string' || !conf.token) return fail()
|
|
208
555
|
const hook = JSON.parse(await readStdin())
|
|
209
|
-
|
|
556
|
+
// DESKBUDDY_GATE_UI 只给测试用(tui / gui),其余真去查进程
|
|
557
|
+
const forcedUi = process.env.DESKBUDDY_GATE_UI
|
|
558
|
+
const ui = forcedUi === 'tui' || forcedUi === 'gui' ? forcedUi : qoderUi(tool)
|
|
559
|
+
const lang = ui === 'gui' ? qoderCnLang() : ''
|
|
560
|
+
const extra = {
|
|
561
|
+
...(ui ? { ui } : {}),
|
|
562
|
+
...(lang ? { lang } : {}),
|
|
563
|
+
...(tool === 'claude_code' ? { ccAutoMode: ccAutoMode(hook) } : {}),
|
|
564
|
+
}
|
|
565
|
+
const res = await post(conf.port, conf.token, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
|
|
210
566
|
if (res.status !== 200) return fail()
|
|
211
567
|
const out = JSON.parse(res.data)
|
|
212
568
|
if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
569
|
+
let decision
|
|
570
|
+
if (out.decision === 'allow') {
|
|
571
|
+
decision = { behavior: 'allow' }
|
|
572
|
+
// 选择题作答(设备答选择题 spec §3.4):daemon 用本地原始 tool_input 按下标拼好的,原样交回
|
|
573
|
+
if (out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) decision.updatedInput = out.updatedInput
|
|
574
|
+
if (Array.isArray(out.updatedPermissions) && out.updatedPermissions.length > 0) decision.updatedPermissions = out.updatedPermissions
|
|
575
|
+
} else decision = { behavior: 'deny', message: (typeof out.reason === 'string' && out.reason) || '用户在设备上拒绝了这一步' }
|
|
219
576
|
const body = { hookSpecificOutput: { hookEventName: 'PermissionRequest', decision } }
|
|
577
|
+
// opencode 插件回填选择题要的是每题的 label 数组(它不认 updatedInput);Claude 系不带,免得 hook 输出校验不认这个字段
|
|
578
|
+
if ((tool === 'opencode' || tool === 'deepseek_harness') && out.decision === 'allow' && Array.isArray(out.answerLists)) body.deskbuddyAnswers = out.answerLists
|
|
579
|
+
// opencode 授权选项(once / always / reject)与「拒绝并说明」的那段话:插件据此回填(hook/opencode.mjs ocReplyOf)
|
|
580
|
+
if (tool === 'opencode') {
|
|
581
|
+
const reply = out.native && typeof out.native === 'object' ? out.native.reply : undefined
|
|
582
|
+
if (reply === 'once' || reply === 'always' || reply === 'reject') body.deskbuddyReply = reply
|
|
583
|
+
if (out.decision === 'deny' && typeof out.feedback === 'string' && out.feedback.trim()) body.deskbuddyFeedback = out.feedback
|
|
584
|
+
}
|
|
220
585
|
// 窗口里告诉人是在哪台设备上处理的
|
|
221
586
|
if (typeof out.notice === 'string' && out.notice) body.systemMessage = out.notice.slice(0, 300)
|
|
222
587
|
process.stdout.write(JSON.stringify(body), () => process.exit(0))
|
|
@@ -270,18 +635,29 @@ async function main() {
|
|
|
270
635
|
return
|
|
271
636
|
}
|
|
272
637
|
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
638
|
+
const spec = {
|
|
639
|
+
hookEventName: 'PreToolUse',
|
|
640
|
+
permissionDecision: out.decision,
|
|
641
|
+
permissionDecisionReason: reason,
|
|
642
|
+
}
|
|
643
|
+
// 远程选择题作答(WorkBuddy 的 codebuddy -p):带上答案。codebuddy 的命令 hook 读 hookSpecificOutput.modifiedInput,
|
|
644
|
+
// 合并多个 hook 结果时读 updatedInput(Claude 兼容字段)—— 两个都带(codebuddy WorkBuddy 内置版源码核对)
|
|
645
|
+
if (out.decision === 'allow' && out.updatedInput && typeof out.updatedInput === 'object' && !Array.isArray(out.updatedInput)) {
|
|
646
|
+
spec.updatedInput = out.updatedInput
|
|
647
|
+
spec.modifiedInput = out.updatedInput
|
|
648
|
+
}
|
|
649
|
+
const payload = JSON.stringify({ hookSpecificOutput: spec })
|
|
280
650
|
// 等写完再退:管道写入是异步的,写一半就 exit 的话 allow / defer 会丢
|
|
281
651
|
process.stdout.write(payload, (err) => (err ? fail('写 hook 输出失败') : process.exit(0)))
|
|
282
652
|
}
|
|
283
653
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
process.on('
|
|
287
|
-
|
|
654
|
+
if (require.main === module) {
|
|
655
|
+
// 兜住一切没想到的异常 —— 见顶部铁律
|
|
656
|
+
process.on('uncaughtException', (e) => fail(`内部错误 ${(e && e.message) || e}`))
|
|
657
|
+
process.on('unhandledRejection', (e) => fail(`内部错误 ${(e && e.message) || e}`))
|
|
658
|
+
main().catch((e) => fail(`内部错误 ${(e && e.message) || e}`))
|
|
659
|
+
} else {
|
|
660
|
+
// 只给单测(智能体永远是 `node gate.cjs` 直接跑)。
|
|
661
|
+
// 🔴 禁止通过 require 调用本关卡:main 不跑,进程会 exit 0 = 放行
|
|
662
|
+
module.exports = { qoderCliMatches, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS }
|
|
663
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
2026-10-03T12:47:47.235Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
2
|
+
2026-10-03T12:49:41.785Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
3
|
+
2026-10-03T12:50:52.692Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
4
|
+
2026-10-03T13:21:49.856Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
5
|
+
2026-10-03T13:27:31.507Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
6
|
+
2026-10-03T15:56:44.816Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
7
|
+
2026-10-04T02:02:45.414Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
8
|
+
2026-10-04T02:05:17.361Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
9
|
+
2026-10-04T03:44:32.326Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
10
|
+
2026-10-04T03:44:32.839Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
11
|
+
2026-10-04T03:44:32.839Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
12
|
+
2026-10-04T03:44:33.014Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
13
|
+
2026-10-04T03:44:33.014Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
14
|
+
2026-10-04T03:55:44.720Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
15
|
+
2026-10-04T03:55:45.175Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
16
|
+
2026-10-04T03:55:45.175Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
17
|
+
2026-10-04T03:55:45.327Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
18
|
+
2026-10-04T03:55:45.327Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
19
|
+
2026-10-04T06:58:28.544Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
20
|
+
2026-10-04T06:58:28.973Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
21
|
+
2026-10-04T06:58:28.973Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
22
|
+
2026-10-04T06:58:29.082Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
23
|
+
2026-10-04T06:58:29.082Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
24
|
+
2026-10-04T07:08:32.294Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
25
|
+
2026-10-04T07:08:32.795Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
26
|
+
2026-10-04T07:08:32.795Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
27
|
+
2026-10-04T07:08:32.908Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
28
|
+
2026-10-04T07:08:32.909Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
29
|
+
2026-10-04T07:38:35.702Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
30
|
+
2026-10-04T07:38:36.121Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
31
|
+
2026-10-04T07:38:36.121Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
32
|
+
2026-10-04T07:38:36.211Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
33
|
+
2026-10-04T07:38:36.211Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
34
|
+
2026-10-04T11:03:50.679Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
35
|
+
2026-10-04T11:03:51.107Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
36
|
+
2026-10-04T11:03:51.107Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
37
|
+
2026-10-04T11:03:51.207Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
38
|
+
2026-10-04T11:03:51.207Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
39
|
+
2026-10-04T11:12:40.872Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
40
|
+
2026-10-04T11:12:42.480Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
41
|
+
2026-10-04T11:12:42.481Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
42
|
+
2026-10-04T11:12:42.866Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
43
|
+
2026-10-04T11:12:44.878Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
44
|
+
2026-10-04T11:14:37.601Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
45
|
+
2026-10-04T11:14:38.045Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
46
|
+
2026-10-04T11:14:38.045Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
47
|
+
2026-10-04T11:14:38.171Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
48
|
+
2026-10-04T11:14:38.171Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
49
|
+
2026-10-04T12:00:44.405Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
50
|
+
2026-10-04T12:00:44.879Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
51
|
+
2026-10-04T12:00:44.879Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
52
|
+
2026-10-04T12:00:45.021Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
53
|
+
2026-10-04T12:00:45.022Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
54
|
+
2026-10-04T12:08:19.397Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
55
|
+
2026-10-04T12:08:19.805Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
56
|
+
2026-10-04T12:08:19.805Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
57
|
+
2026-10-04T12:08:19.911Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
58
|
+
2026-10-04T12:08:19.912Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
59
|
+
2026-10-05T02:15:10.472Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
60
|
+
2026-10-05T02:15:10.898Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
61
|
+
2026-10-05T02:15:10.899Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
62
|
+
2026-10-05T02:15:11.004Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
63
|
+
2026-10-05T02:15:11.004Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
64
|
+
2026-10-05T02:23:41.382Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
65
|
+
2026-10-05T02:23:41.811Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
66
|
+
2026-10-05T02:23:41.812Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
67
|
+
2026-10-05T02:23:41.898Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
68
|
+
2026-10-05T02:23:41.898Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|