fix: harden agent prompt handling
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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<Boolean> continueAfterAgentStatusCheck(Mono<Boolean> statusProbe) {
|
||||
return statusProbe.onErrorResume(throwable -> Mono.just(false));
|
||||
}
|
||||
|
||||
private static Mono<Boolean> checkAgentIfAlive(AgentContext agentContext, SandboxServerConfig.SandboxServer sandboxServer) {
|
||||
return Mono.create(sink -> {
|
||||
//Query agent startup status/computer/agent/status
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<typeof setTimeout> | undefined;
|
||||
try {
|
||||
const res = await this.acpConnection!.prompt(promptParams);
|
||||
const res = await Promise.race([
|
||||
this.acpConnection!.prompt(promptParams),
|
||||
new Promise<never>((_, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 重试。
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
Reference in New Issue
Block a user