在 Makelore 构建并预检静态发布产物

This commit is contained in:
2026-08-12 21:19:39 +08:00
parent 3a80625fe2
commit 5b44864265
26 changed files with 1275 additions and 217 deletions

View File

@@ -11,10 +11,12 @@ import type {
AgentBrowserPayloadChunk,
AgentBrowserSnapshot,
} from '../../shared/agent-browser';
import type { StaticArtifactSnapshot } from '../services/static-release-server';
export type WorksSubmissionBindingStore = ReturnType<typeof createWorksSubmissionBindingStore>;
export interface AgentBrowserService {
preflightStaticArtifact(snapshot: StaticArtifactSnapshot): Promise<{ ok: true }>;
preflightCurrentProject(projectPath: string): Promise<{ ok: true }>;
getSnapshot(projectPath?: string): Promise<AgentBrowserSnapshot> | AgentBrowserSnapshot;
open(input: {

View File

@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'http';
import { randomUUID } from 'node:crypto';
import { lstat, mkdir, mkdtemp, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { app } from 'electron';
import { lstat, mkdir, open, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
@@ -10,9 +10,9 @@ import { trustedWorksProjectPlayUrl } from '../works-play-url';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import {
createStaticProjectPackage,
ProjectPackageError,
} from '../../services/project-packager';
import { prepareProjectRelease, ProjectReleaseBuildError } from '../../services/project-release-builder';
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
import { logger } from '../../utils/logger';
@@ -126,14 +126,12 @@ function sendPublishSourceFailure(
}
const SAFE_PUBLISH_PREFLIGHT_CODES = new Set([
'PREVIEW_REQUIRED',
'PUBLISH_PREFLIGHT_LOAD_FAILED',
'PUBLISH_PREFLIGHT_RUNTIME_ERROR',
'PUBLISH_PREFLIGHT_BLANK',
'PUBLISH_PREFLIGHT_TIMEOUT',
]);
const SAFE_PUBLISH_PREFLIGHT_MESSAGES: Record<string, string> = {
PREVIEW_REQUIRED: '请先在 Makelore 内置浏览器中打开当前项目预览。',
PUBLISH_PREFLIGHT_LOAD_FAILED: '作品主页无法打开。',
PUBLISH_PREFLIGHT_RUNTIME_ERROR: '作品打开时发生了运行错误。',
PUBLISH_PREFLIGHT_BLANK: '作品打开后没有可见内容。',
@@ -157,7 +155,17 @@ async function sendPublishSourceUpstreamError(
): Promise<void> {
let code = fallbackCode;
let error = fallbackMessage;
if (response.status === 401) {
let payload: unknown = null;
try { payload = await readResponsePayload(response); } catch { /* project only stable codes */ }
const detail = isRecord(payload) && isRecord(payload.detail) ? payload.detail : null;
const upstreamCode = isRecord(payload)
? readOptionalString(payload.code) ?? (detail ? readOptionalString(detail.code) : undefined)
: undefined;
if (upstreamCode === 'CLIENT_BUILD_PROTOCOL_REQUIRED') {
code = upstreamCode;
error = '请升级 Makelore 并重新提交。';
}
else if (response.status === 401) {
code = 'AUTH_REQUIRED';
error = '登录状态已失效,请重新登录。';
} else if (response.status === 403) {
@@ -748,6 +756,9 @@ async function readAutomaticVersionName(projectPath: string): Promise<string> {
function createSourceUploadForm(
archiveBytes: Buffer,
archiveName: string,
builtArchiveBytes: Buffer,
builtArchiveName: string,
artifactContract: object,
versionName: string,
): FormData {
const archiveBlob = new Blob([new Uint8Array(archiveBytes)], { type: 'application/zip' });
@@ -755,6 +766,8 @@ function createSourceUploadForm(
form.set('version_name', versionName);
form.set('change_log', SOURCE_PUBLISH_CHANGE_LOG);
form.set('archive', archiveBlob, archiveName);
form.set('built_archive', new Blob([new Uint8Array(builtArchiveBytes)], { type: 'application/zip' }), builtArchiveName);
form.set('artifact_contract', JSON.stringify(artifactContract));
return form;
}
@@ -763,6 +776,9 @@ async function uploadSourceProjectVersion(input: {
appId: string;
archiveBytes: Buffer;
archiveName: string;
builtArchiveBytes: Buffer;
builtArchiveName: string;
artifactContract: object;
versionName: string;
idempotencyKey: string;
}): Promise<Response> {
@@ -780,6 +796,9 @@ async function uploadSourceProjectVersion(input: {
body: createSourceUploadForm(
input.archiveBytes,
input.archiveName,
input.builtArchiveBytes,
input.builtArchiveName,
input.artifactContract,
input.versionName,
),
},
@@ -847,24 +866,11 @@ async function handlePublishProjectSource(
return;
}
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'makelore-publish-'));
const archivePath = join(temporaryDirectory, 'project.zip');
let prepared: Awaited<ReturnType<typeof prepareProjectRelease>> | null = null;
try {
if (!ctx.agentBrowser) {
sendPublishSourceFailure(
res,
400,
'PREVIEW_REQUIRED',
'请先在 Makelore 内置浏览器中打开当前项目预览。',
);
return;
}
await ctx.agentBrowser.preflightCurrentProject(localProject.path);
const packageSummary = await createStaticProjectPackage({
projectPath: localProject.path,
archivePath,
});
const archiveBytes = await readFile(archivePath);
if (!ctx.agentBrowser) throw new ProjectReleaseBuildError('LOCAL_BUILD_RUNTIME_UNAVAILABLE');
prepared = await prepareProjectRelease({ projectPath: localProject.path, clientVersion: app.getVersion() });
await ctx.agentBrowser.preflightStaticArtifact(prepared.staticArtifact);
const versionName = await readAutomaticVersionName(localProject.path);
const idempotencyKey = `makelore-${randomUUID()}`;
@@ -910,8 +916,11 @@ async function handlePublishProjectSource(
const uploadResponse = await uploadSourceProjectVersion({
accessToken,
appId,
archiveBytes,
archiveName: packageSummary.archiveName,
archiveBytes: prepared.sourceArchive.bytes,
archiveName: prepared.sourceArchive.name,
builtArchiveBytes: prepared.builtArchive.bytes,
builtArchiveName: prepared.builtArchive.name,
artifactContract: prepared.contract,
versionName,
idempotencyKey,
});
@@ -943,7 +952,7 @@ async function handlePublishProjectSource(
versionId: uploadPayload.version_id,
versionName,
reviewStatus: uploadPayload.review_status,
zipSha256: packageSummary.sha256,
zipSha256: prepared.contract.source_digest,
});
} catch {
logger.warn('[works] One-click submission succeeded, but local preview mapping could not be saved');
@@ -952,7 +961,7 @@ async function handlePublishProjectSource(
} else {
bindingWarning = LOCAL_PREVIEW_BINDING_WARNING;
}
const { archivePath: _archivePath, ...rendererPackageSummary } = packageSummary;
const { archivePath: _archivePath, ...rendererPackageSummary } = prepared.sourceArchive.summary;
sendJson(res, uploadResponse.status, {
success: true,
package: rendererPackageSummary,
@@ -960,7 +969,7 @@ async function handlePublishProjectSource(
...(bindingWarning ? { binding_warning: bindingWarning } : {}),
});
} finally {
await rm(temporaryDirectory, { recursive: true, force: true }).catch(() => undefined);
await prepared?.dispose().catch(() => undefined);
}
}
@@ -1147,12 +1156,15 @@ export async function handleWorksRoutes(
const isProjectStatus = /^\/api\/works\/projects\/mine\/[^/]+\/status$/.test(url.pathname);
if (isProjectSourcePublish) {
const isPackageError = error instanceof ProjectPackageError;
const isLocalBuildError = error instanceof ProjectReleaseBuildError;
sendPublishSourceFailure(
res,
isPackageError || isSafePublishPreflightError(error) ? 400 : 503,
isPackageError || isSafePublishPreflightError(error) ? error.code : 'WORKS_SQUARE_UNAVAILABLE',
isPackageError || isLocalBuildError || isSafePublishPreflightError(error) ? 400 : 503,
isPackageError || isLocalBuildError || isSafePublishPreflightError(error) ? error.code : 'WORKS_SQUARE_UNAVAILABLE',
isPackageError
? error.message
: isLocalBuildError
? error.code
: isSafePublishPreflightError(error)
? SAFE_PUBLISH_PREFLIGHT_MESSAGES[error.code]
: '发布服务暂时不可用,请稍后重试。',