feat: integrate pixel teacher presence and resilient classroom preview

This commit is contained in:
鲨鱼辣椒
2026-09-23 18:23:53 +08:00
parent 12d800ec51
commit 7951cca700
60 changed files with 3313 additions and 180 deletions

View File

@@ -2,11 +2,14 @@ import { realpath } from 'node:fs/promises';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { AgentBrowserBounds, AgentBrowserFaultShape } from '../../../shared/agent-browser';
import { normalizeCodingProjectPath } from '../../coding-projects/project-store';
import { CodingWorkPreviewService } from '../../coding-runtime/work-preview';
import type { HostApiContext } from '../context';
import { hasRendererCapability } from '../renderer-capability';
import { parseJsonBody, sendJson } from '../route-utils';
type AgentBrowserBody = {
conversation_id?: unknown;
request_id?: unknown;
project_id?: unknown;
project_path?: unknown;
url?: unknown;
@@ -28,6 +31,23 @@ type AgentBrowserBody = {
max_bytes?: unknown;
};
const workPreviewServices = new WeakMap<HostApiContext, CodingWorkPreviewService>();
function workPreviewService(ctx: HostApiContext): CodingWorkPreviewService | undefined {
if (!ctx.codingProducts || !ctx.agentBrowser) return undefined;
let preview = workPreviewServices.get(ctx);
if (!preview) {
preview = new CodingWorkPreviewService({
browser: ctx.agentBrowser,
conversations: ctx.codingProducts.conversations,
runtime: ctx.codingProducts.runtime,
ensureActive: (target) => ensureProjectStillActive(ctx, target),
});
workPreviewServices.set(ctx, preview);
}
return preview;
}
class AgentBrowserRouteError extends Error {
constructor(
readonly code: AgentBrowserFaultShape['code'],
@@ -236,6 +256,27 @@ export async function handleAgentBrowserRoutes(
try {
const service = requireService(ctx);
if (url.pathname === '/api/agent-browser/ensure-work' && req.method === 'POST') {
requireRendererPresentation(req);
const body = await readBody(req);
const project = await resolveActiveProject(ctx, undefined, body.project_id, true);
const requestId = nonEmptyString(body.request_id);
if (!requestId || !/^[a-zA-Z0-9-]{1,80}$/.test(requestId)) {
throw new AgentBrowserRouteError('INVALID_REQUEST', '作品请求无效。');
}
if (!ctx.codingProducts) throw new AgentBrowserRouteError('CLOSED', '本地编程服务暂时不可用。', 503);
const preview = workPreviewService(ctx)!;
const release = ctx.lifecycle?.acquireLease({ id: `work-preview:${project.id}:${requestId}`, kind: 'work-preview' });
try {
const result = await preview.ensure(project, requestId, nonEmptyString(body.conversation_id));
await ensureProjectStillActive(ctx, project);
sendJson(res, 200, { success: true, ...result });
} finally {
release?.();
}
return true;
}
if (url.pathname === '/api/agent-browser/state' && req.method === 'GET') {
const project = await resolveActiveProject(
ctx,
@@ -272,6 +313,7 @@ export async function handleAgentBrowserRoutes(
...(body.inject_project_data === true ? { injectProjectData: true } : {}),
});
await ensureProjectStillActive(ctx, project);
workPreviewService(ctx)?.remember(project, browser);
emitState(ctx, 'agent-browser:show', browser);
sendJson(res, 200, { success: true, browser });
return true;
@@ -318,6 +360,7 @@ export async function handleAgentBrowserRoutes(
url: nonEmptyString(body.url),
});
await ensureProjectStillActive(ctx, project);
workPreviewService(ctx)?.remember(project, browser);
emitState(ctx, 'agent-browser:state', browser);
sendJson(res, 200, { success: true, browser });
return true;