feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '../route-utils';
|
||||
import { TeacherError } from '../../coding-teacher/config-client';
|
||||
import type { TeacherScope } from '../../coding-teacher/service';
|
||||
import type { TeacherSend } from '../../../shared/coding-teacher';
|
||||
import type { TeacherCheckInInput, TeacherSend } from '../../../shared/coding-teacher';
|
||||
import { takeTeacherPreviewRevision } from '../../main/app-deep-link';
|
||||
|
||||
export async function handleCodingTeacherRoutes(
|
||||
@@ -26,11 +26,12 @@ export async function handleCodingTeacherRoutes(
|
||||
const projectTopics = url.pathname.match(
|
||||
/^\/api\/coding\/projects\/([^/]+)\/(teacher|friend)-topics(?:\/([^/]+))?(?:\/(messages|events|save|requests\/([^/]+)\/cancel))?$/
|
||||
);
|
||||
const checkIn = url.pathname.match(/^\/api\/coding\/projects\/([^/]+)\/teacher-check-in$/);
|
||||
const role = projectTopics?.[2] === 'friend' || url.pathname === '/api/coding/friend/config' ? 'friend' : 'teacher';
|
||||
const config = url.pathname === '/api/coding/teacher/config' || url.pathname === '/api/coding/friend/config';
|
||||
const draft = url.pathname === '/api/coding/teacher-preview';
|
||||
const pending = url.pathname === '/api/coding/teacher-preview/pending-link';
|
||||
if (!source && !preview && !projectTopics && !config && !draft && !pending) return false;
|
||||
if (!source && !preview && !projectTopics && !checkIn && !config && !draft && !pending) return false;
|
||||
if ((config || draft || pending) && req.method !== 'GET') {
|
||||
sendJson(res, 405, { error: '不支持此操作。' });
|
||||
return true;
|
||||
@@ -45,6 +46,14 @@ export async function handleCodingTeacherRoutes(
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (checkIn) {
|
||||
if (req.method !== 'POST') sendJson(res, 405, { error: '不支持此操作。' });
|
||||
else sendJson(res, 200, await service.checkIn(
|
||||
{ projectId: decodeURIComponent(checkIn[1]), sourceId: 'project', role: 'teacher' },
|
||||
await parseJsonBody<TeacherCheckInInput>(req)
|
||||
));
|
||||
return true;
|
||||
}
|
||||
if (config && req.method === 'GET') {
|
||||
sendJson(res, 200, await service.definition(role));
|
||||
return true;
|
||||
@@ -74,7 +83,7 @@ export async function handleCodingTeacherRoutes(
|
||||
return true;
|
||||
}
|
||||
if (id && !action && req.method === 'GET') {
|
||||
sendJson(res, 200, await service.read(scope, id));
|
||||
sendJson(res, 200, await service.read(scope, id, url.searchParams.get('select') !== 'false'));
|
||||
return true;
|
||||
}
|
||||
if (id && action === 'messages' && req.method === 'POST') {
|
||||
|
||||
Reference in New Issue
Block a user