Merge origin/main and preserve nonblocking project entry
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { accessSync, constants } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { AgentBrowserModule } from '../agent-browser';
|
||||
import type { AgentBrowserSnapshot } from '../../shared/agent-browser';
|
||||
import { CodingAttachmentStore } from '../coding-projects/attachment-store';
|
||||
import { createCodingConversationStore } from '../coding-projects/conversation-store';
|
||||
import { CodingProjectService } from '../coding-projects/project-service';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from '../coding-projects/project-store';
|
||||
import { CodingConversationService } from '../coding-runtime/conversation-service';
|
||||
import { PiManagedExtensionHost } from '../coding-runtime/pi/extension-host';
|
||||
import { PiProjectWriteLeaseCoordinator } from '../coding-runtime/pi/write-lease';
|
||||
import { PiAgentServerProcess } from '../coding-runtime/pi/agent-server-process';
|
||||
import { PiManagedInputRevisionCoordinator } from '../coding-runtime/pi/managed-input-revision';
|
||||
import { PiProductTools } from '../coding-runtime/pi/product-tools';
|
||||
@@ -46,6 +48,10 @@ import {
|
||||
import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service';
|
||||
import { createGameResourcePluginAdapter } from '../coding-plugins/adapters/game-resource';
|
||||
import { GameResourceClient } from '../services/game-resource-client';
|
||||
import {
|
||||
GameResourceDeliveryCoordinator,
|
||||
GameResourceDeliveryReceiptStore,
|
||||
} from '../services/game-resource-delivery';
|
||||
import { AccountPluginCache } from '../coding-plugins/account-plugin-cache';
|
||||
import {
|
||||
createMarketplaceClient,
|
||||
@@ -97,6 +103,8 @@ export interface CreateCodingCompositionOptions {
|
||||
accountCache?: AccountPluginCache;
|
||||
clientVersion?: string;
|
||||
policyClient?: PluginPolicyClient;
|
||||
requestAgentBrowserPresentation?(snapshot: AgentBrowserSnapshot): void;
|
||||
publishAgentBrowserState?(snapshot: AgentBrowserSnapshot): void;
|
||||
}
|
||||
|
||||
type PiWorkerExecutableProbe = (candidate: string) => boolean;
|
||||
@@ -229,12 +237,15 @@ export function createCodingComposition(
|
||||
onGenerationChanged: async () => await invalidateDeviceResources(),
|
||||
});
|
||||
const devicePackageTools = new DevicePackageTools(devicePackageManager);
|
||||
const projectWriteLeases = new PiProjectWriteLeaseCoordinator();
|
||||
const productTools = new PiProductTools({
|
||||
browser: options.browser,
|
||||
attachments,
|
||||
bundledSkillsDir: options.paths.bundledSkillsDir,
|
||||
modelToolRegistry,
|
||||
devicePackageTools,
|
||||
requestAgentBrowserPresentation: options.requestAgentBrowserPresentation,
|
||||
publishAgentBrowserState: options.publishAgentBrowserState,
|
||||
pluginSkillSources,
|
||||
getPluginSkillSources: async () => effectiveResolver
|
||||
? (await effectiveResolver.getSkillSources()).map((source) => ({
|
||||
@@ -245,7 +256,7 @@ export function createCodingComposition(
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
const extensionHost = new PiManagedExtensionHost();
|
||||
const extensionHost = new PiManagedExtensionHost(projectWriteLeases);
|
||||
extensionHost.configureProductTools(productTools);
|
||||
const conversationStores = new Map<string, ReturnType<typeof createCodingConversationStore>>();
|
||||
const conversationStoreForProject = (projectPath: string) => {
|
||||
@@ -302,8 +313,26 @@ export function createCodingComposition(
|
||||
});
|
||||
const dataService = createDataServiceOperations({ projects });
|
||||
const dataServiceAdapter = createDataServicePluginAdapter(dataService);
|
||||
const gameResourceClient = new GameResourceClient();
|
||||
const gameResourceDelivery = new GameResourceDeliveryCoordinator({
|
||||
client: gameResourceClient,
|
||||
receipts: new GameResourceDeliveryReceiptStore(path.join(
|
||||
options.paths.userDataDir,
|
||||
'coding-runtime',
|
||||
'game-resource',
|
||||
'receipts.json',
|
||||
)),
|
||||
leases: projectWriteLeases,
|
||||
recordTouchedPaths: async (conversationId, runId, paths) => {
|
||||
await productTools.recordTouchedPaths(conversationId, runId, paths);
|
||||
},
|
||||
...(options.acquireBackgroundLease
|
||||
? { acquireBackgroundLease: options.acquireBackgroundLease }
|
||||
: {}),
|
||||
});
|
||||
const gameResourceAdapter = createGameResourcePluginAdapter({
|
||||
client: new GameResourceClient(),
|
||||
client: gameResourceClient,
|
||||
delivery: gameResourceDelivery,
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
@@ -474,8 +503,10 @@ export function createCodingComposition(
|
||||
await invalidateManagedResources();
|
||||
},
|
||||
});
|
||||
void gameResourceDelivery.resumePending().catch(() => undefined);
|
||||
const unsubscribeMarketplaceSession = subscribeWorksSquareSession(() => {
|
||||
void invalidateManagedResources();
|
||||
void gameResourceDelivery.resumePending().catch(() => undefined);
|
||||
});
|
||||
const host = createCodingProductHost({
|
||||
projects,
|
||||
@@ -517,9 +548,13 @@ export function createCodingComposition(
|
||||
runtime.dispose(conversationId, reason)
|
||||
)));
|
||||
if (reason === 'background_sleep' && runtime.hasActiveWork()) return;
|
||||
if (reason === 'background_sleep') {
|
||||
await options.browser.close().catch(() => undefined);
|
||||
}
|
||||
await agentServer.stop();
|
||||
},
|
||||
async shutdown() {
|
||||
gameResourceDelivery.dispose();
|
||||
previewDataSession?.dispose();
|
||||
if (typeof options.browser.configurePreviewDataSession === 'function') {
|
||||
options.browser.configurePreviewDataSession(undefined);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { hasRendererCapability } from '../renderer-capability';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
type AgentBrowserBody = {
|
||||
project_id?: unknown;
|
||||
project_path?: unknown;
|
||||
url?: unknown;
|
||||
action?: unknown;
|
||||
@@ -71,7 +72,12 @@ function parseStringArray(value: unknown): string[] | undefined {
|
||||
return value.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
async function resolveActiveProject(ctx: HostApiContext, requestedPath?: unknown) {
|
||||
async function resolveActiveProject(
|
||||
ctx: HostApiContext,
|
||||
requestedPath?: unknown,
|
||||
requestedId?: unknown,
|
||||
rendererPresentation = false,
|
||||
) {
|
||||
const activeProject = await ctx.codingProjectStore.getActiveProject();
|
||||
if (!activeProject) {
|
||||
throw new AgentBrowserRouteError('PROJECT_NOT_ACTIVE', '请先打开一个项目。', 409);
|
||||
@@ -84,6 +90,24 @@ async function resolveActiveProject(ctx: HostApiContext, requestedPath?: unknown
|
||||
throw new AgentBrowserRouteError('PROJECT_NOT_ACTIVE', '当前项目目录不可用。', 409);
|
||||
}
|
||||
|
||||
const projectId = nonEmptyString(requestedId);
|
||||
if (projectId) {
|
||||
if (!rendererPresentation) {
|
||||
throw new AgentBrowserRouteError(
|
||||
'TARGET_DENIED',
|
||||
'项目 ID 只能由 Makelore 界面用于浏览器操作。',
|
||||
403,
|
||||
);
|
||||
}
|
||||
if (projectId !== activeProject.id) {
|
||||
throw new AgentBrowserRouteError('PROJECT_MISMATCH', '智能体只能调试当前项目。', 403);
|
||||
}
|
||||
return {
|
||||
...activeProject,
|
||||
path: activeRealPath,
|
||||
};
|
||||
}
|
||||
|
||||
const requested = nonEmptyString(requestedPath);
|
||||
if (!requested) {
|
||||
throw new AgentBrowserRouteError('INVALID_REQUEST', '缺少当前项目路径。');
|
||||
@@ -188,6 +212,19 @@ async function readBody(req: IncomingMessage): Promise<AgentBrowserBody> {
|
||||
return await parseJsonBody<AgentBrowserBody>(req);
|
||||
}
|
||||
|
||||
function resolveRequestProject(
|
||||
req: IncomingMessage,
|
||||
ctx: HostApiContext,
|
||||
body: AgentBrowserBody,
|
||||
) {
|
||||
return resolveActiveProject(
|
||||
ctx,
|
||||
body.project_path,
|
||||
body.project_id,
|
||||
hasRendererCapability(req),
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleAgentBrowserRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -200,7 +237,12 @@ export async function handleAgentBrowserRoutes(
|
||||
const service = requireService(ctx);
|
||||
|
||||
if (url.pathname === '/api/agent-browser/state' && req.method === 'GET') {
|
||||
const project = await resolveActiveProject(ctx, url.searchParams.get('project_path'));
|
||||
const project = await resolveActiveProject(
|
||||
ctx,
|
||||
url.searchParams.get('project_path'),
|
||||
url.searchParams.get('project_id'),
|
||||
hasRendererCapability(req),
|
||||
);
|
||||
const browser = await service.getSnapshot(project.path);
|
||||
await ensureProjectStillActive(ctx, project);
|
||||
sendJson(res, 200, { success: true, browser });
|
||||
@@ -209,8 +251,13 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/open' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const rendererPresentation = hasRendererCapability(req);
|
||||
const project = await resolveActiveProject(
|
||||
ctx,
|
||||
body.project_path,
|
||||
body.project_id,
|
||||
rendererPresentation,
|
||||
);
|
||||
if (body.bounds !== undefined && !rendererPresentation) {
|
||||
requireRendererPresentation(req);
|
||||
}
|
||||
@@ -233,7 +280,7 @@ export async function handleAgentBrowserRoutes(
|
||||
if (url.pathname === '/api/agent-browser/present' && req.method === 'POST') {
|
||||
requireRendererPresentation(req);
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveActiveProject(ctx, body.project_path, body.project_id, true);
|
||||
const browser = await service.present({
|
||||
projectPath: project.path,
|
||||
visible: body.visible === true,
|
||||
@@ -248,7 +295,7 @@ export async function handleAgentBrowserRoutes(
|
||||
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 project = await resolveActiveProject(ctx, body.project_path, body.project_id, true);
|
||||
const browser = await service.setDiagnostics({
|
||||
projectPath: project.path,
|
||||
enabled: body.enabled === true,
|
||||
@@ -260,7 +307,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/navigate' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const action = nonEmptyString(body.action);
|
||||
if (action !== 'url' && action !== 'back' && action !== 'forward' && action !== 'reload') {
|
||||
throw new AgentBrowserRouteError('INVALID_REQUEST', '浏览器导航动作无效。');
|
||||
@@ -278,7 +325,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/cdp/send' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const method = nonEmptyString(body.method);
|
||||
if (!method) throw new AgentBrowserRouteError('INVALID_REQUEST', '缺少 CDP method。');
|
||||
const params = body.params === undefined
|
||||
@@ -302,7 +349,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/cdp/events' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const page = await service.readEvents({
|
||||
projectPath: project.path,
|
||||
after: finiteInteger(body.after),
|
||||
@@ -317,7 +364,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/payload/read' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const handle = nonEmptyString(body.handle);
|
||||
if (!handle) throw new AgentBrowserRouteError('INVALID_REQUEST', '缺少 payload handle。');
|
||||
const chunk = await service.readPayload({
|
||||
@@ -333,7 +380,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/close' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const browser = await service.close(project.path);
|
||||
await ensureProjectStillActive(ctx, project);
|
||||
emitState(ctx, 'agent-browser:state', {
|
||||
@@ -347,7 +394,7 @@ export async function handleAgentBrowserRoutes(
|
||||
|
||||
if (url.pathname === '/api/agent-browser/reset-profile' && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
const project = await resolveActiveProject(ctx, body.project_path);
|
||||
const project = await resolveRequestProject(req, ctx, body);
|
||||
const browser = await service.resetProfile(project.path);
|
||||
await ensureProjectStillActive(ctx, project);
|
||||
emitState(ctx, 'agent-browser:state', {
|
||||
|
||||
@@ -60,6 +60,27 @@ const AGENT_AVATAR_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp'
|
||||
const MAX_PROJECT_COVER_BYTES = 10 * 1024 * 1024;
|
||||
const PROJECT_COVER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
||||
const SAFE_PROJECT_STATUSES = new Set(['draft', 'published']);
|
||||
const TOKEN_POINT_FIELDS = [
|
||||
'weekly_allowance',
|
||||
'weekly_used',
|
||||
'weekly_reserved',
|
||||
'weekly_remaining',
|
||||
'permanent_total',
|
||||
'permanent_used',
|
||||
'permanent_reserved',
|
||||
'permanent_remaining',
|
||||
'total_remaining',
|
||||
] as const;
|
||||
const TOKEN_POINT_METADATA_FIELDS = [
|
||||
'plan_code',
|
||||
'plan_name',
|
||||
'cycle_start',
|
||||
'cycle_end',
|
||||
'next_refresh_at',
|
||||
] as const;
|
||||
const TOKEN_POINT_ENTITLEMENT_SOURCES = new Set(['self', 'family_owner', 'shared_group']);
|
||||
const TOKEN_POINT_UPGRADE_ACTIONS = new Set(['self_service', 'contact_family_owner']);
|
||||
const TOKEN_POINT_VALUE_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/u;
|
||||
|
||||
function readRequiredString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
@@ -267,6 +288,62 @@ function readNullableStringField(
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
function readNullablePointField(
|
||||
source: Record<string, unknown>,
|
||||
field: string,
|
||||
): string | null | undefined {
|
||||
const value = source[field];
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== 'string') return undefined;
|
||||
const normalized = value.trim();
|
||||
return TOKEN_POINT_VALUE_PATTERN.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function projectSafeTokenPointBalance(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const entitlementSource = readOptionalString(value.entitlement_source);
|
||||
const upgradeAction = readOptionalString(value.upgrade_action);
|
||||
if (
|
||||
!entitlementSource
|
||||
|| !TOKEN_POINT_ENTITLEMENT_SOURCES.has(entitlementSource)
|
||||
|| typeof value.family_shared !== 'boolean'
|
||||
|| typeof value.can_manage_membership !== 'boolean'
|
||||
|| !upgradeAction
|
||||
|| !TOKEN_POINT_UPGRADE_ACTIONS.has(upgradeAction)
|
||||
) return null;
|
||||
|
||||
if (
|
||||
value.shared_available !== undefined
|
||||
&& value.shared_available !== null
|
||||
&& typeof value.shared_available !== 'boolean'
|
||||
) return null;
|
||||
|
||||
const canManageMembership = value.can_manage_membership;
|
||||
if (!canManageMembership && typeof value.shared_available !== 'boolean') return null;
|
||||
|
||||
const projected: Record<string, unknown> = {};
|
||||
for (const field of TOKEN_POINT_METADATA_FIELDS) {
|
||||
const fieldValue = canManageMembership ? readNullableStringField(value, field) : null;
|
||||
if (fieldValue === undefined) return null;
|
||||
projected[field] = fieldValue;
|
||||
}
|
||||
for (const field of TOKEN_POINT_FIELDS) {
|
||||
const fieldValue = canManageMembership ? readNullablePointField(value, field) : null;
|
||||
if (fieldValue === undefined) return null;
|
||||
projected[field] = fieldValue;
|
||||
}
|
||||
|
||||
return {
|
||||
...projected,
|
||||
entitlement_source: entitlementSource,
|
||||
family_shared: value.family_shared,
|
||||
can_manage_membership: canManageMembership,
|
||||
upgrade_action: upgradeAction,
|
||||
shared_available: canManageMembership ? null : value.shared_available,
|
||||
};
|
||||
}
|
||||
|
||||
function projectSafeProject(
|
||||
value: unknown,
|
||||
options: { requireStatus?: boolean } = {},
|
||||
@@ -614,12 +691,12 @@ async function handleListMyProjects(
|
||||
sendJson(res, response.status, { success: true, page });
|
||||
}
|
||||
|
||||
async function handleGetBillingTokenUsage(
|
||||
async function handleGetBillingTokenPoints(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/billing/token-usage').toString(), {
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/billing/points').toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@@ -627,11 +704,16 @@ async function handleGetBillingTokenUsage(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await sendUpstreamError(res, response, `Works Square token usage failed (${response.status})`);
|
||||
await sendUpstreamError(res, response, `Works Square token points failed (${response.status})`);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(res, response.status, { success: true, usage: await readResponsePayload(response) });
|
||||
const points = projectSafeTokenPointBalance(await readResponsePayload(response));
|
||||
if (!points) {
|
||||
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid token point balance' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, response.status, { success: true, points });
|
||||
}
|
||||
|
||||
async function handleAgentProfile(
|
||||
@@ -1413,8 +1495,8 @@ export async function handleWorksRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/billing/token-usage' && req.method === 'GET') {
|
||||
await handleGetBillingTokenUsage(req, res);
|
||||
if (url.pathname === '/api/works/billing/points' && req.method === 'GET') {
|
||||
await handleGetBillingTokenPoints(req, res);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user