diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java index 76d9ef6a..6cbb491f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java @@ -897,26 +897,8 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi return errorOutput(errorOutput, conversationDto); } - if (agentConfigDto.getModelComponentConfig().getTargetConfig() == null) { - String errorMsg = I18nUtil.systemMessage("Backend.Chat.Error.AgentModelNotConfiguredOrOffline"); - errorOutput.setError(errorMsg); - agentExecuteResult.setError(errorMsg); - return errorOutput(errorOutput, conversationDto); - } - // 用户数据权限 UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId()); - - //判断agent是否被管控,若被管控需判断有没有权限 - if (agentConfigDto.getAccessControl() != null && agentConfigDto.getAccessControl().equals(YesOrNoEnum.Y.getKey()) && !agentConfigDto.getCreatorId().equals(RequestContext.get().getUserId())) { - if (userDataPermission.getAgentIds() == null || !userDataPermission.getAgentIds().contains(agentConfigDto.getId())) { - String errorMsg = I18nUtil.systemMessage("Backend.Chat.Error.NoAgentPermission"); - errorOutput.setError(errorMsg); - agentExecuteResult.setError(errorMsg); - return errorOutput(errorOutput, conversationDto); - } - } - Object targetConfig = agentConfigDto.getModelComponentConfig().getTargetConfig(); // 模型变化后是否需要重启智能体(通用智能体需要重启生效);全局模型在开启代理的情况下无需重启 String selectedKey = "agent.model.selected:" + RequestContext.get().getUserId() + ":" + agentConfigDto.getId(); @@ -959,6 +941,23 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi } } + if (targetConfig == null) { + String errorMsg = I18nUtil.systemMessage("Backend.Chat.Error.AgentModelNotConfiguredOrOffline"); + errorOutput.setError(errorMsg); + agentExecuteResult.setError(errorMsg); + return errorOutput(errorOutput, conversationDto); + } + + //判断agent是否被管控,若被管控需判断有没有权限 + if (agentConfigDto.getAccessControl() != null && agentConfigDto.getAccessControl().equals(YesOrNoEnum.Y.getKey()) && !agentConfigDto.getCreatorId().equals(RequestContext.get().getUserId())) { + if (userDataPermission.getAgentIds() == null || !userDataPermission.getAgentIds().contains(agentConfigDto.getId())) { + String errorMsg = I18nUtil.systemMessage("Backend.Chat.Error.NoAgentPermission"); + errorOutput.setError(errorMsg); + agentExecuteResult.setError(errorMsg); + return errorOutput(errorOutput, conversationDto); + } + } + //付费检查,评估是否可以继续执行,当前只要积分大于零,且订阅过智能体就可以执行 PriceEstimate priceEstimate = estimatePrice(agentConfigDto, tryReqDto, Objects.equals(conversationDto.getDevMode(), YesOrNoEnum.Y.getKey()), isDefaultModel); if (!priceEstimate.isPass()) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java index 82792cf8..f07c4966 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java @@ -174,7 +174,7 @@ public class SandboxAgentClient { SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(agentContext.getTenantConfig(), agentContext.getUserId(), agentContext.getConversation().getSandboxServerId()); // Start sandbox service if (sandboxServer.getScope() == SandboxScopeEnum.USER) { - checkAgentIfAlive(agentContext, sandboxServer) + continueAfterAgentStatusCheck(checkAgentIfAlive(agentContext, sandboxServer)) .doOnSuccess(res0 -> { // agent not started, append prompt try { @@ -183,7 +183,6 @@ public class SandboxAgentClient { sink.error(e); } }) - .onErrorResume(throwable -> Mono.just(false)) .subscribe(); } else { startSandbox(agentContext, sandboxServer) @@ -196,7 +195,7 @@ public class SandboxAgentClient { } return; } - checkAgentIfAlive(agentContext, sandboxServer) + continueAfterAgentStatusCheck(checkAgentIfAlive(agentContext, sandboxServer)) .doOnSuccess(res0 -> { // agent not started, append prompt try { @@ -205,7 +204,6 @@ public class SandboxAgentClient { sink.error(e); } }) - .onErrorResume(throwable -> Mono.just(false)) .subscribe(); }) .onErrorResume(Mono::error).doOnError(sink::error) @@ -232,6 +230,10 @@ public class SandboxAgentClient { return sseFlux; } + static Mono continueAfterAgentStatusCheck(Mono statusProbe) { + return statusProbe.onErrorResume(throwable -> Mono.just(false)); + } + private static Mono checkAgentIfAlive(AgentContext agentContext, SandboxServerConfig.SandboxServer sandboxServer) { return Mono.create(sink -> { //Query agent startup status/computer/agent/status diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/test/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClientTest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/test/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClientTest.java new file mode 100644 index 00000000..68703bc3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/test/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClientTest.java @@ -0,0 +1,15 @@ +package com.xspaceagi.agent.core.infra.component.agent; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class SandboxAgentClientTest { + + @Test + void statusProbeFailureContinuesAsNotAlive() { + StepVerifier.create(SandboxAgentClient.continueAfterAgentStatusCheck(Mono.error(new RuntimeException("probe failed")))) + .expectNext(false) + .verifyComplete(); + } +} diff --git a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-claude-code-acp-ts.js b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-claude-code-acp-ts.js index 5ebdf438..179bace8 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-claude-code-acp-ts.js +++ b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-claude-code-acp-ts.js @@ -35,9 +35,30 @@ function exec(cmd, opts = {}) { execSync(cmd, { stdio: 'inherit', ...opts }); } +function getLocalSourceDir() { + const candidates = [ + process.env.CLAUDE_CODE_ACP_TS_SOURCE_DIR, + path.resolve(electronClientRoot, '..', '..', '..', 'claude-code-acp-ts'), + ].filter(Boolean); + + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'package.json'))) { + return candidate; + } + } + + return null; +} + function main() { + const localSourceDir = getLocalSourceDir(); + let sourceDir = SOURCE_DIR; + // 1. 克隆或更新源码 - if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) { + if (localSourceDir) { + sourceDir = localSourceDir; + console.log(`[prepare-claude-code-acp-ts] 使用本地源码: ${sourceDir}`); + } else if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) { console.log('[prepare-claude-code-acp-ts] 克隆源码...'); exec(`git clone --branch ${GIT_BRANCH} ${GIT_REPO} "${SOURCE_DIR}"`); } else { @@ -46,31 +67,32 @@ function main() { } // 2. 检查构建产物是否存在 - const hasBuild = fs.existsSync(path.join(SOURCE_DIR, 'dist')); - const hasNodeModules = fs.existsSync(path.join(SOURCE_DIR, 'node_modules')); + const hasBuild = fs.existsSync(path.join(sourceDir, 'dist')); + const hasNodeModules = fs.existsSync(path.join(sourceDir, 'node_modules')); if (!hasBuild || !hasNodeModules) { // 清理旧的 node_modules(若有) - if (fs.existsSync(path.join(SOURCE_DIR, 'node_modules'))) { + const nodeModulesDir = path.join(sourceDir, 'node_modules'); + if (fs.existsSync(nodeModulesDir)) { console.log('[prepare-claude-code-acp-ts] 清理旧的 node_modules...'); - exec(`rm -rf "${path.join(SOURCE_DIR, 'node_modules')}"`); + fs.rmSync(nodeModulesDir, { recursive: true, force: true }); } // 3. 安装依赖 console.log('[prepare-claude-code-acp-ts] 安装依赖...'); - exec(`cd "${SOURCE_DIR}" && npm install --ignore-scripts`); + exec('npm install --ignore-scripts', { cwd: sourceDir }); // 4. 构建 // 注意:不用 npm run build,因为其 build 脚本可能是 ./node_modules/.bin/tsc(Unix 风格),Windows 不认识 //改用 npx tsc 替代,可跨平台 console.log('[prepare-claude-code-acp-ts] 构建项目...'); - exec(`cd "${SOURCE_DIR}" && npx tsc`); + exec('npx tsc', { cwd: sourceDir }); } else { console.log('[prepare-claude-code-acp-ts] 构建产物已就绪,跳过构建'); } // 5. 读取版本 - const srcPkg = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8')); + const srcPkg = JSON.parse(fs.readFileSync(path.join(sourceDir, 'package.json'), 'utf8')); console.log(`[prepare-claude-code-acp-ts] 源码版本: ${srcPkg.name}@${srcPkg.version}`); // 6. 清理并创建目标目录 @@ -81,20 +103,20 @@ function main() { // 7. 复制 dist/ console.log('[prepare-claude-code-acp-ts] 复制 dist/...'); - exec(`cp -R "${path.join(SOURCE_DIR, 'dist')}" "${destDir}/"`); + fs.cpSync(path.join(sourceDir, 'dist'), path.join(destDir, 'dist'), { recursive: true }); // 8. 复制 package.json fs.copyFileSync( - path.join(SOURCE_DIR, 'package.json'), + path.join(sourceDir, 'package.json'), path.join(destDir, 'package.json') ); // 9. 复制 node_modules/ console.log('[prepare-claude-code-acp-ts] 复制 node_modules/...'); - exec(`cp -R "${path.join(SOURCE_DIR, 'node_modules')}" "${destDir}/"`); + fs.cpSync(path.join(sourceDir, 'node_modules'), path.join(destDir, 'node_modules'), { recursive: true }); // 10. 复制 LICENSE - const licenseSrc = path.join(SOURCE_DIR, 'LICENSE'); + const licenseSrc = path.join(sourceDir, 'LICENSE'); if (fs.existsSync(licenseSrc)) { fs.copyFileSync(licenseSrc, path.join(destDir, 'LICENSE')); } diff --git a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qiming-file-server.js b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qiming-file-server.js index ac901f53..c551dd97 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qiming-file-server.js +++ b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qiming-file-server.js @@ -35,9 +35,30 @@ function exec(cmd, opts = {}) { execSync(cmd, { stdio: 'inherit', ...opts }); } +function getLocalSourceDir() { + const candidates = [ + process.env.QIMING_FILE_SERVER_SOURCE_DIR, + path.resolve(electronClientRoot, '..', '..', '..', 'qiming-file-server'), + ].filter(Boolean); + + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'package.json'))) { + return candidate; + } + } + + return null; +} + function main() { + const localSourceDir = getLocalSourceDir(); + let sourceDir = SOURCE_DIR; + // 1. 克隆或更新源码 - if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) { + if (localSourceDir) { + sourceDir = localSourceDir; + console.log(`[prepare-qiming-file-server] 使用本地源码: ${sourceDir}`); + } else if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) { console.log('[prepare-qiming-file-server] 克隆源码...'); exec(`git clone --branch ${GIT_BRANCH} ${GIT_REPO} "${SOURCE_DIR}"`); } else { @@ -46,29 +67,30 @@ function main() { } // 2. 检查构建产物是否存在 - const hasBuild = fs.existsSync(path.join(SOURCE_DIR, 'dist')); - const hasNodeModules = fs.existsSync(path.join(SOURCE_DIR, 'node_modules')); + const hasBuild = fs.existsSync(path.join(sourceDir, 'dist')); + const hasNodeModules = fs.existsSync(path.join(sourceDir, 'node_modules')); if (!hasBuild || !hasNodeModules) { // 清理旧的 node_modules(若有) - if (fs.existsSync(path.join(SOURCE_DIR, 'node_modules'))) { + const nodeModulesDir = path.join(sourceDir, 'node_modules'); + if (fs.existsSync(nodeModulesDir)) { console.log('[prepare-qiming-file-server] 清理旧的 node_modules...'); - exec(`rm -rf "${path.join(SOURCE_DIR, 'node_modules')}"`); + fs.rmSync(nodeModulesDir, { recursive: true, force: true }); } // 3. 安装依赖 console.log('[prepare-qiming-file-server] 安装依赖...'); - exec(`cd "${SOURCE_DIR}" && npm install --ignore-scripts`); + exec('npm install --ignore-scripts', { cwd: sourceDir }); // 4. 构建 console.log('[prepare-qiming-file-server] 构建项目...'); - exec(`cd "${SOURCE_DIR}" && npm run build`); + exec('npm run build', { cwd: sourceDir }); } else { console.log('[prepare-qiming-file-server] 构建产物已就绪,跳过构建'); } // 5. 读取版本 - const srcPkg = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8')); + const srcPkg = JSON.parse(fs.readFileSync(path.join(sourceDir, 'package.json'), 'utf8')); console.log(`[prepare-qiming-file-server] 源码版本: ${srcPkg.name}@${srcPkg.version}`); // 6. 清理并创建目标目录 @@ -79,20 +101,20 @@ function main() { // 7. 复制 dist/ console.log('[prepare-qiming-file-server] 复制 dist/...'); - exec(`cp -R "${path.join(SOURCE_DIR, 'dist')}" "${destDir}/"`); + fs.cpSync(path.join(sourceDir, 'dist'), path.join(destDir, 'dist'), { recursive: true }); // 8. 复制 package.json fs.copyFileSync( - path.join(SOURCE_DIR, 'package.json'), + path.join(sourceDir, 'package.json'), path.join(destDir, 'package.json') ); // 9. 复制 node_modules/ console.log('[prepare-qiming-file-server] 复制 node_modules/...'); - exec(`cp -R "${path.join(SOURCE_DIR, 'node_modules')}" "${destDir}/"`); + fs.cpSync(path.join(sourceDir, 'node_modules'), path.join(destDir, 'node_modules'), { recursive: true }); // 10. 复制 LICENSE - const licenseSrc = path.join(SOURCE_DIR, 'LICENSE'); + const licenseSrc = path.join(sourceDir, 'LICENSE'); if (fs.existsSync(licenseSrc)) { fs.copyFileSync(licenseSrc, path.join(destDir, 'LICENSE')); } diff --git a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js index f38efe42..1df7f681 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js +++ b/qimingclaw/crates/agent-electron-client/scripts/prepare/prepare-qimingcode.js @@ -28,7 +28,7 @@ const { URL } = require('url'); const { execSync, execFileSync } = require('child_process'); const { getProjectRoot } = require('../utils/project-paths'); -const QIMINGCODE_VERSION = '1.1.97'; +const QIMINGCODE_VERSION = '1.2.1'; const QIMINGCODE_REPO = process.env.QIMINGCODE_REPO || 'qiming-ai/qimingcode'; const projectRoot = getProjectRoot(); @@ -140,10 +140,7 @@ const FORCE_REFRESH_ON_MATCH = true; // ==================== 模式 1: 本地 dist 复制 ==================== function copyFromDist(key) { - const qimingcodeDist = process.env.QIMINGCODE_DIST_DIR || path.join( - process.env.HOME || '/root', - 'workspace/qimingcode/packages/opencode/dist', - ); + const qimingcodeDist = getLocalDistDir(); const distName = PLATFORM_MAP[key]; if (!distName) { console.error(`[prepare-qimingcode] 不支持的平台: ${key}`); @@ -152,12 +149,19 @@ function copyFromDist(key) { const resourceKey = getResourcePlatformKey(key); const binary = getBinaryName(key); - const srcPath = path.join(qimingcodeDist, distName, 'bin', binary); const destDir = path.join(resDir, resourceKey, 'bin'); const destPath = path.join(destDir, binary); + const srcBinDir = path.join(qimingcodeDist, distName, 'bin'); + const srcBinary = getBinaryCandidates(key) + .map((candidate) => path.join(srcBinDir, candidate)) + .find((candidatePath) => fs.existsSync(candidatePath)); - if (!fs.existsSync(srcPath)) { - console.warn(`[prepare-qimingcode] ${key}: 构建产物不存在 ${srcPath}`); + if (!srcBinary) { + console.warn( + `[prepare-qimingcode] ${key}: 构建产物不存在,已检查 ${getBinaryCandidates(key) + .map((candidate) => path.join(srcBinDir, candidate)) + .join(', ')}`, + ); return false; } @@ -195,8 +199,11 @@ function copyFromDist(key) { resetDestBinDir(destDir); // 复制整个 bin 目录(包含二进制 + assets 等) - const srcBinDir = path.join(qimingcodeDist, distName, 'bin'); fs.cpSync(srcBinDir, destDir, { recursive: true }); + const copiedBinaryPath = path.join(destDir, path.basename(srcBinary)); + if (copiedBinaryPath !== destPath) { + fs.renameSync(copiedBinaryPath, destPath); + } ensureModelJson(destDir, QIMINGCODE_VERSION); fs.chmodSync(destPath, 0o755); @@ -218,6 +225,16 @@ function copyFromDist(key) { return true; } +function getLocalDistDir() { + const candidates = [ + process.env.QIMINGCODE_DIST_DIR, + path.resolve(projectRoot, '..', '..', '..', 'qimingcode', 'packages', 'opencode', 'dist'), + path.join(process.env.HOME || '/root', 'workspace/qimingcode/packages/opencode/dist'), + ].filter(Boolean); + + return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0]; +} + // ==================== 模式 2: GitHub Release 下载 ==================== /** @@ -577,7 +594,7 @@ function codesign(binaryPath, key) { async function main() { const allPlatforms = process.argv.includes('--all') || process.argv.includes('--all-platforms'); - const useLocalDist = !!process.env.QIMINGCODE_DIST_DIR; + const useLocalDist = !!process.env.QIMINGCODE_DIST_DIR || fs.existsSync(getLocalDistDir()); const mode = useLocalDist ? '本地 dist 复制' : 'GitHub Release 下载'; fs.mkdirSync(resDir, { recursive: true }); diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts index 7a4e92b5..add82515 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.test.ts @@ -296,6 +296,40 @@ describe("AcpEngine.prompt", () => { const event = onPromptEnd.mock.calls.at(-1)?.[0]; expect(event.reason).toBe("mcp_reconnecting"); }); + + it("qimingcode prompt 长时间无响应时结束会话等待", async () => { + const { engine, sessionId, session, acpConnection } = + setupEngine("qimingcode"); + const onPromptEnd = vi.fn(); + const onSessionError = vi.fn(); + engine.on("computer:promptEnd", onPromptEnd); + engine.on("session.error", onSessionError); + + acpConnection.prompt.mockReturnValueOnce(new Promise(() => {})); + + vi.useFakeTimers(); + const promptPromise = engine.prompt( + sessionId, + [{ type: "text", text: "hi" }], + { messageID: "rid-timeout-001" }, + ); + + await vi.advanceTimersByTimeAsync(60_001); + await expect(promptPromise).resolves.toBeDefined(); + + expect(onPromptEnd).toHaveBeenCalledTimes(1); + expect(onPromptEnd.mock.calls[0][0]).toMatchObject({ + sessionId, + reason: "error", + }); + expect(onPromptEnd.mock.calls[0][0].description).toContain( + "ACP prompt timed out", + ); + expect(onSessionError).toHaveBeenCalledTimes(1); + expect(session.status).toBe("idle"); + expect((engine as any).activePromptSessions.has(sessionId)).toBe(false); + expect((engine as any).activePromptRejects.has(sessionId)).toBe(false); + }); }); describe("AcpEngine.handleAcpSessionUpdate", () => { diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.ts b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.ts index 60212cc8..86810e92 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/engines/acp/acpEngine.ts @@ -79,7 +79,7 @@ import { import { processRegistry } from "../../system/processRegistry"; import { t } from "../../i18n"; import type { DetailedSession } from "@shared/types/sessions"; -import { ACP_ABORT_TIMEOUT } from "@shared/constants"; +import { ACP_ABORT_TIMEOUT, ACP_PROMPT_TIMEOUT } from "@shared/constants"; import { APP_DATA_DIR_NAME } from "../../constants"; import { perfEmitter } from "../perf/perfEmitter"; import { firstTokenTrace } from "../perf/firstTokenTrace"; @@ -1520,8 +1520,20 @@ export class AcpEngine extends EventEmitter { const maxAttempts = this.engineName === "qimingcode" ? 2 : 1; let attempt = 1; while (true) { + let promptTimeout: ReturnType | undefined; try { - const res = await this.acpConnection!.prompt(promptParams); + const res = await Promise.race([ + this.acpConnection!.prompt(promptParams), + new Promise((_, reject) => { + promptTimeout = setTimeout(() => { + reject( + new Error( + `ACP prompt timed out after ${ACP_PROMPT_TIMEOUT}ms`, + ), + ); + }, ACP_PROMPT_TIMEOUT); + }), + ]); log.info( `${this.logTag} 📥 ACP prompt resolved (${Date.now() - promptStartTime}ms, attempt=${attempt}):`, safeStringify(res), @@ -1529,9 +1541,20 @@ export class AcpEngine extends EventEmitter { return res; } catch (err) { const errMsg = this.toErrorMessage(err); + if (errMsg.includes("ACP prompt timed out")) { + log.error( + `${this.logTag} 📥 ACP prompt timed out (${Date.now() - promptStartTime}ms, attempt=${attempt})`, + { + sessionId, + acpSessionId: session.acpSessionId, + requestId: _opts?.messageID, + }, + ); + } const canRetry = attempt < maxAttempts && !this.isPromptCancellation(err) && + !errMsg.includes("ACP prompt timed out") && this.isMcpReconnectFailure(errMsg); if (!canRetry) { @@ -1569,6 +1592,10 @@ export class AcpEngine extends EventEmitter { ); await this.sleep(MCP_RETRY_DELAY_MS); attempt += 1; + } finally { + if (promptTimeout !== undefined) { + clearTimeout(promptTimeout); + } } } }; diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/system/dependencies.ts b/qimingclaw/crates/agent-electron-client/src/main/services/system/dependencies.ts index 604c17d4..fd475ab7 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/system/dependencies.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/system/dependencies.ts @@ -1166,7 +1166,7 @@ export function getSetupRequiredDependencies(): LocalDependencyConfig[] { description: t(I18N_KEYS.Pages.Dependencies.DESC_QIMINGCODE), required: true, binName: "qimingcode", - installVersion: "1.1.97", + installVersion: "1.2.1", }, { name: "claude-code-acp-ts", diff --git a/qimingclaw/crates/agent-electron-client/src/shared/constants.test.ts b/qimingclaw/crates/agent-electron-client/src/shared/constants.test.ts index 2c863a6a..cc8ee75a 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/constants.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/shared/constants.test.ts @@ -36,6 +36,7 @@ import { CLEANUP_TIMEOUT, PROCESS_KILL_ESCALATION_TIMEOUT, ACP_ABORT_TIMEOUT, + ACP_PROMPT_TIMEOUT, ENGINE_DESTROY_TIMEOUT, DEPS_SYNC_TIMEOUT, NPM_MIRRORS, @@ -151,6 +152,7 @@ describe("Constants", () => { expect(CLEANUP_TIMEOUT).toBeGreaterThan(0); expect(PROCESS_KILL_ESCALATION_TIMEOUT).toBeGreaterThan(0); expect(ACP_ABORT_TIMEOUT).toBeGreaterThan(0); + expect(ACP_PROMPT_TIMEOUT).toBeGreaterThan(0); expect(ENGINE_DESTROY_TIMEOUT).toBeGreaterThan(0); expect(DEPS_SYNC_TIMEOUT).toBeGreaterThan(0); }); diff --git a/qimingclaw/crates/agent-electron-client/src/shared/constants.ts b/qimingclaw/crates/agent-electron-client/src/shared/constants.ts index 2df90990..b14f9631 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/constants.ts +++ b/qimingclaw/crates/agent-electron-client/src/shared/constants.ts @@ -151,6 +151,9 @@ export const PROCESS_KILL_ESCALATION_TIMEOUT = 5000; /** ACP 会话取消超时 (ms) */ export const ACP_ABORT_TIMEOUT = 15_000; +/** ACP prompt 等待响应超时 (ms) */ +export const ACP_PROMPT_TIMEOUT = 60_000; + /** * 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。 * `message` 经主进程 `t()` 本地化后,`isPromptCancellation` 仍靠此字段识别,避免误触发 MCP 重试。 diff --git a/qimingcode/bun.lock b/qimingcode/bun.lock index ee8d8c9c..92cae7ee 100644 --- a/qimingcode/bun.lock +++ b/qimingcode/bun.lock @@ -4069,8 +4069,6 @@ "nth-check": ["nth-check@2.1.1", "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "qimingcode": ["qimingcode@workspace:packages/opencode"], - "nypm": ["nypm@0.6.5", "https://registry.npmmirror.com/nypm/-/nypm-0.6.5.tgz", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -4333,6 +4331,8 @@ "pure-rand": ["pure-rand@8.4.0", "https://registry.npmmirror.com/pure-rand/-/pure-rand-8.4.0.tgz", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "qimingcode": ["qimingcode@workspace:packages/opencode"], + "qs": ["qs@6.15.1", "https://registry.npmmirror.com/qs/-/qs-6.15.1.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], "quansync": ["quansync@0.2.11", "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -5929,14 +5929,6 @@ "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "qimingcode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.71.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], - - "qimingcode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.53.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], - - "qimingcode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.41.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - - "qimingcode/minimatch": ["minimatch@10.0.3", "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], - "nypm/citty": ["citty@0.2.2", "https://registry.npmmirror.com/citty/-/citty-0.2.2.tgz", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "nypm/tinyexec": ["tinyexec@1.1.1", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.1.tgz", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], @@ -6001,6 +5993,14 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "qimingcode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.71.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], + + "qimingcode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.53.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], + + "qimingcode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.41.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + + "qimingcode/minimatch": ["minimatch@10.0.3", "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "readable-stream/buffer": ["buffer@6.0.3", "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],