perf: optimize app startup and background lifecycles
This commit is contained in:
@@ -11,6 +11,7 @@ type AgentBrowserBody = {
|
||||
url?: unknown;
|
||||
action?: unknown;
|
||||
visible?: unknown;
|
||||
enabled?: unknown;
|
||||
bounds?: unknown;
|
||||
method?: unknown;
|
||||
params?: unknown;
|
||||
@@ -237,6 +238,20 @@ export async function handleAgentBrowserRoutes(
|
||||
bounds: parseBounds(body.bounds),
|
||||
});
|
||||
await ensureProjectStillActive(ctx, project);
|
||||
emitState(ctx, 'agent-browser:state', browser);
|
||||
sendJson(res, 200, { success: true, browser });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/agent-browser/diagnostics' && req.method === 'POST') {
|
||||
requireRendererPresentation(req);
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const browser = await service.setDiagnostics({
|
||||
projectPath: project.path,
|
||||
enabled: body.enabled === true,
|
||||
});
|
||||
await ensureProjectStillActive(ctx, project);
|
||||
sendJson(res, 200, { success: true, browser });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1089,6 +1089,10 @@ async function handlePublishProjectSource(
|
||||
}
|
||||
|
||||
let prepared: Awaited<ReturnType<typeof prepareProjectRelease>> | null = null;
|
||||
const releaseLease = ctx.lifecycle?.acquireLease({
|
||||
id: `release:${projectId}:${randomUUID()}`,
|
||||
kind: 'release-build',
|
||||
});
|
||||
try {
|
||||
if (!ctx.agentBrowser) throw new ProjectReleaseBuildError('LOCAL_BUILD_RUNTIME_UNAVAILABLE');
|
||||
prepared = await prepareProjectRelease({ projectPath: localProject.path, clientVersion: app.getVersion() });
|
||||
@@ -1242,10 +1246,77 @@ async function handlePublishProjectSource(
|
||||
...(publishedMetadataPreserved ? { metadata_disposition: PUBLISHED_METADATA_PRESERVED } : {}),
|
||||
});
|
||||
} finally {
|
||||
releaseLease?.();
|
||||
await prepared?.dispose().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartReleaseJob(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
ctx: HostApiContext,
|
||||
): Promise<void> {
|
||||
if (!ctx.releaseJobs) {
|
||||
sendJson(res, 503, { success: false, code: 'RELEASE_JOBS_UNAVAILABLE', error: '发布构建暂不可用。' });
|
||||
return;
|
||||
}
|
||||
const body = await parseJsonBody<{ projectId?: unknown }>(req);
|
||||
const projectId = readOptionalString(body.projectId);
|
||||
if (!projectId) {
|
||||
sendJson(res, 400, { success: false, code: 'PROJECT_ID_REQUIRED', error: '缺少项目标识。' });
|
||||
return;
|
||||
}
|
||||
const project = (await ctx.opencodeProjectStore.listProjects()).find((candidate) => candidate.id === projectId);
|
||||
if (!project) {
|
||||
sendJson(res, 404, { success: false, code: 'PROJECT_NOT_FOUND', error: '本地项目不存在。' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const job = ctx.releaseJobs.start({
|
||||
projectId,
|
||||
projectPath: project.path,
|
||||
clientVersion: app.getVersion(),
|
||||
});
|
||||
sendJson(res, 202, { success: true, job });
|
||||
} catch (error) {
|
||||
if (error instanceof ProjectReleaseBuildError) {
|
||||
sendJson(res, error.code === 'LOCAL_BUILD_BUSY' ? 409 : 400, {
|
||||
success: false,
|
||||
code: error.code,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReleaseJobStatus(
|
||||
res: ServerResponse,
|
||||
ctx: HostApiContext,
|
||||
jobId: string,
|
||||
): void {
|
||||
const job = ctx.releaseJobs?.get(jobId);
|
||||
if (!job) {
|
||||
sendJson(res, 404, { success: false, code: 'RELEASE_JOB_NOT_FOUND', error: '发布任务不存在。' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, { success: true, job });
|
||||
}
|
||||
|
||||
function handleCancelReleaseJob(
|
||||
res: ServerResponse,
|
||||
ctx: HostApiContext,
|
||||
jobId: string,
|
||||
): void {
|
||||
const job = ctx.releaseJobs?.cancel(jobId);
|
||||
if (!job) {
|
||||
sendJson(res, 404, { success: false, code: 'RELEASE_JOB_NOT_FOUND', error: '发布任务不存在。' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, { success: true, job });
|
||||
}
|
||||
|
||||
function createSpeechTranscriptionFormData(body: SpeechTranscriptionInput): FormData {
|
||||
const audioBase64 = readRequiredString(body.audioBase64, 'audioBase64');
|
||||
const fileName = readOptionalString(body.fileName) ?? 'voice.wav';
|
||||
@@ -1404,6 +1475,39 @@ export async function handleWorksRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/projects/release-jobs' && req.method === 'POST') {
|
||||
if (!hasRendererCapability(req)) {
|
||||
sendJson(res, 403, {
|
||||
success: false,
|
||||
status: 403,
|
||||
code: 'RENDERER_CAPABILITY_REQUIRED',
|
||||
error: 'Renderer capability required',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await handleStartReleaseJob(req, res, ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
const releaseJobMatch = url.pathname.match(/^\/api\/works\/projects\/release-jobs\/([^/]+)(?:\/(cancel))?$/);
|
||||
if (releaseJobMatch && req.method === 'GET' && !releaseJobMatch[2]) {
|
||||
handleReleaseJobStatus(res, ctx, decodeURIComponent(releaseJobMatch[1]));
|
||||
return true;
|
||||
}
|
||||
if (releaseJobMatch && req.method === 'POST' && (releaseJobMatch[2] === 'cancel' || url.searchParams.get('action') === 'cancel')) {
|
||||
if (!hasRendererCapability(req)) {
|
||||
sendJson(res, 403, {
|
||||
success: false,
|
||||
status: 403,
|
||||
code: 'RENDERER_CAPABILITY_REQUIRED',
|
||||
error: 'Renderer capability required',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
handleCancelReleaseJob(res, ctx, decodeURIComponent(releaseJobMatch[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/projects/mine' && req.method === 'GET') {
|
||||
await handleListMyProjects(req, res, url);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user