收敛客户端静态发布链路
This commit is contained in:
@@ -2,7 +2,7 @@ import type { BrowserWindow } from 'electron';
|
||||
import type { OpencodeManager } from '../opencode/manager';
|
||||
import type { OpencodeProjectStore } from '../opencode/project-store';
|
||||
import type { HostEventBus } from './event-bus';
|
||||
import type { createWorksCloudDeployment } from '../services/works-cloud-deployment';
|
||||
import type { createWorksSubmissionBindingStore } from '../services/works-submission-binding';
|
||||
import type { DesignWorkspaceModule } from '../image-workspace/module';
|
||||
import type {
|
||||
AgentBrowserBounds,
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
AgentBrowserSnapshot,
|
||||
} from '../../shared/agent-browser';
|
||||
|
||||
export type WorksCloudDeploymentCoordinator = ReturnType<typeof createWorksCloudDeployment>;
|
||||
export type WorksSubmissionBindingStore = ReturnType<typeof createWorksSubmissionBindingStore>;
|
||||
|
||||
export interface AgentBrowserService {
|
||||
getSnapshot(projectPath?: string): Promise<AgentBrowserSnapshot> | AgentBrowserSnapshot;
|
||||
@@ -64,6 +64,6 @@ export interface HostApiContext {
|
||||
eventBus: HostEventBus;
|
||||
mainWindow: BrowserWindow | null;
|
||||
agentBrowser?: AgentBrowserService;
|
||||
worksCloudDeployment?: WorksCloudDeploymentCoordinator;
|
||||
worksSubmissionBinding?: WorksSubmissionBindingStore;
|
||||
imageWorkspace?: DesignWorkspaceModule;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { hasRendererCapability } from '../renderer-capability';
|
||||
import { WORKS_SQUARE_CONFIG } from '../works-config';
|
||||
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
|
||||
import { proxyAwareFetch } from '../../utils/proxy-fetch';
|
||||
import type { WorksCloudDeploymentRecord } from '../../../shared/works-cloud-deployment';
|
||||
import type { WorksSubmissionBindingRecord } from '../../../shared/works-submission-binding';
|
||||
import type { DevicePreviewSnapshot } from '../../../shared/device-preview';
|
||||
|
||||
type RemoteProjectSnapshot = {
|
||||
@@ -66,7 +66,7 @@ function projectFromPayload(payload: unknown): RemoteProjectSnapshot | null {
|
||||
return {
|
||||
appId: readString(project.app_id),
|
||||
playable: typeof project.playable === 'boolean' ? project.playable : null,
|
||||
runtimeUrl: readString(project.runtime_url) ?? readString(project.play_url),
|
||||
runtimeUrl: readString(project.play_url) ?? readString(project.runtime_url),
|
||||
runtimeVersionName: readString(project.version_name),
|
||||
latestVersionId: readString(latestVersion?.id) ?? readString(latestVersion?.version_id),
|
||||
latestVersionName: readString(latestVersion?.version_name),
|
||||
@@ -214,7 +214,7 @@ async function createOwnedReleasePreview(
|
||||
|
||||
function localSnapshot(
|
||||
projectId: string,
|
||||
deployment: WorksCloudDeploymentRecord | null,
|
||||
deployment: WorksSubmissionBindingRecord | null,
|
||||
): Omit<DevicePreviewSnapshot, 'state' | 'message'> {
|
||||
return {
|
||||
projectId,
|
||||
@@ -226,19 +226,13 @@ function localSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function buildingMessage(status: WorksCloudDeploymentRecord['status']): string {
|
||||
if (status === 'waiting_for_login') return '正在等待登录状态恢复,恢复后会继续提交预览版本。';
|
||||
if (status === 'uploading') return '预览包正在上传到服务端。';
|
||||
return '正在等待新的预览包与安全检查完成。';
|
||||
}
|
||||
|
||||
function normalizedReviewStatus(value: string | null): string | null {
|
||||
return value?.trim().toLowerCase() || null;
|
||||
}
|
||||
|
||||
async function resolveDevicePreview(
|
||||
projectId: string,
|
||||
deployment: WorksCloudDeploymentRecord | null,
|
||||
deployment: WorksSubmissionBindingRecord | null,
|
||||
): Promise<DevicePreviewSnapshot> {
|
||||
if (!deployment) {
|
||||
return {
|
||||
@@ -258,19 +252,11 @@ async function resolveDevicePreview(
|
||||
|
||||
const base = localSnapshot(projectId, deployment);
|
||||
|
||||
if (deployment.status === 'failed') {
|
||||
if (deployment.status === 'legacy_retired') {
|
||||
return {
|
||||
...base,
|
||||
state: 'unavailable',
|
||||
message: deployment.error || '预览版本生成失败,请重新生成。',
|
||||
};
|
||||
}
|
||||
|
||||
if (deployment.status !== 'submitted') {
|
||||
return {
|
||||
...base,
|
||||
state: 'building',
|
||||
message: buildingMessage(deployment.status),
|
||||
message: deployment.message || '旧版自动部署任务已停用,请重新提交审核。',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -438,8 +424,8 @@ export async function handleDevicePreviewRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
const deployment = ctx.worksCloudDeployment
|
||||
? await ctx.worksCloudDeployment.get(projectId)
|
||||
const deployment = ctx.worksSubmissionBinding
|
||||
? await ctx.worksSubmissionBinding.get(projectId)
|
||||
: null;
|
||||
const preview = await resolveDevicePreview(projectId, deployment);
|
||||
sendJson(res, 200, { success: true, preview });
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
type RevertOpencodeSessionMessageInput,
|
||||
type SendOpencodeSessionMessageInput,
|
||||
} from '../../opencode/client';
|
||||
import { readWorksPublishFile } from '../../opencode/works-publish-file';
|
||||
import { readWorksDeployCheck } from '../../opencode/works-square-deploy-check';
|
||||
import {
|
||||
buildOpencodeRuntimeConfigSummaryFromNianCodeProviders,
|
||||
type OpencodeRuntimeConfigSummary,
|
||||
@@ -1128,67 +1126,6 @@ export async function handleOpencodeRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
const worksPublishMatch = url.pathname.match(/^\/api\/opencode\/projects\/([^/]+)\/works-publish$/);
|
||||
if (worksPublishMatch && req.method === 'GET') {
|
||||
try {
|
||||
const projectId = decodeURIComponent(worksPublishMatch[1] ?? '');
|
||||
if (!projectId) throw new Error('Missing project id');
|
||||
const project = await findProjectById(ctx, projectId);
|
||||
if (!project) throw new Error('Project not found');
|
||||
sendJson(res, 200, await readWorksPublishFile(project.path));
|
||||
} catch (error) {
|
||||
sendJson(res, 500, {
|
||||
status: 'invalid',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const worksDeployCheckMatch = url.pathname.match(/^\/api\/opencode\/projects\/([^/]+)\/works-deploy-check$/);
|
||||
if (worksDeployCheckMatch && req.method === 'GET') {
|
||||
try {
|
||||
const projectId = decodeURIComponent(worksDeployCheckMatch[1] ?? '');
|
||||
if (!projectId) throw new Error('Missing project id');
|
||||
const project = await findProjectById(ctx, projectId);
|
||||
if (!project) throw new Error('Project not found');
|
||||
const publish = await readWorksPublishFile(project.path);
|
||||
sendJson(
|
||||
res,
|
||||
200,
|
||||
await readWorksDeployCheck(project.path, publish.status === 'ready' ? publish.publish : null),
|
||||
);
|
||||
} catch (error) {
|
||||
sendJson(res, 500, {
|
||||
status: 'invalid',
|
||||
filePath: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const worksCloudDeployMatch = url.pathname.match(/^\/api\/opencode\/projects\/([^/]+)\/works-cloud-deploy$/);
|
||||
if (worksCloudDeployMatch && (req.method === 'GET' || req.method === 'POST')) {
|
||||
try {
|
||||
const projectId = decodeURIComponent(worksCloudDeployMatch[1] ?? '');
|
||||
if (!projectId) throw new Error('Missing project id');
|
||||
if (!ctx.worksCloudDeployment) throw new Error('Cloud deployment coordinator is unavailable');
|
||||
if (req.method === 'POST') {
|
||||
const deployment = await ctx.worksCloudDeployment.arm(projectId);
|
||||
sendJson(res, 202, { success: true, deployment });
|
||||
} else {
|
||||
sendJson(res, 200, { success: true, deployment: await ctx.worksCloudDeployment.get(projectId) });
|
||||
}
|
||||
} catch (error) {
|
||||
sendJson(res, 500, {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/opencode/projects/remove' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await parseJsonBody<{ projectId?: string }>(req);
|
||||
|
||||
@@ -2,13 +2,11 @@ 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 { basename, extname, isAbsolute, join, normalize, resolve } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
import { proxyAwareFetch } from '../../utils/proxy-fetch';
|
||||
import { WORKS_SQUARE_CONFIG } from '../works-config';
|
||||
import { readWorksDeployCheck } from '../../opencode/works-square-deploy-check';
|
||||
import { readWorksPublishFile } from '../../opencode/works-publish-file';
|
||||
import {
|
||||
createStaticProjectPackage,
|
||||
ProjectPackageError,
|
||||
@@ -21,14 +19,6 @@ type CreateProjectInput = {
|
||||
project?: unknown;
|
||||
};
|
||||
|
||||
type UploadProjectVersionInput = {
|
||||
accessToken?: unknown;
|
||||
projectId?: unknown;
|
||||
versionName?: unknown;
|
||||
changeLog?: unknown;
|
||||
zipFilePath?: unknown;
|
||||
};
|
||||
|
||||
type PublishProjectSourceInput = {
|
||||
projectId?: unknown;
|
||||
project?: unknown;
|
||||
@@ -62,10 +52,6 @@ function readOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function resolveProjectFilePath(projectPath: string, value: string): string {
|
||||
return isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) ? value : resolve(projectPath, value);
|
||||
}
|
||||
|
||||
function readRequiredHeader(req: IncomingMessage, name: string): string {
|
||||
const value = req.headers[name.toLowerCase()];
|
||||
const firstValue = Array.isArray(value) ? value[0] : value;
|
||||
@@ -209,6 +195,7 @@ function projectSafeProject(value: unknown): Record<string, unknown> | null {
|
||||
'difficulty',
|
||||
'status',
|
||||
'updated_at',
|
||||
'play_url',
|
||||
'runtime_url',
|
||||
'creator_name',
|
||||
'buddy_name',
|
||||
@@ -629,76 +616,6 @@ async function handleGetMyProjectStatus(
|
||||
sendJson(res, response.status, { success: true, status: statusPayload });
|
||||
}
|
||||
|
||||
async function createArchiveFormData(
|
||||
body: UploadProjectVersionInput,
|
||||
ctx: HostApiContext,
|
||||
appId: string,
|
||||
): Promise<FormData> {
|
||||
const versionName = readRequiredString(body.versionName, 'versionName');
|
||||
const changeLog = readRequiredString(body.changeLog, 'changeLog');
|
||||
const zipFilePath = readRequiredString(body.zipFilePath, 'zipFilePath');
|
||||
const projectId = readRequiredString(body.projectId, 'projectId');
|
||||
const project = (await ctx.opencodeProjectStore.listProjects()).find((item) => item.id === projectId);
|
||||
if (!project) throw new Error('Project not found');
|
||||
const publish = await readWorksPublishFile(project.path);
|
||||
if (publish.status !== 'ready') throw new Error('BLOCKED: works-publish.json is missing or invalid');
|
||||
if (publish.publish.app_id !== appId) throw new Error('BLOCKED: upload app id does not match project publish data');
|
||||
const resolvedZipPath = resolveProjectFilePath(project.path, zipFilePath);
|
||||
const publishZipPath = resolveProjectFilePath(project.path, publish.publish.zip_file_path);
|
||||
if (normalize(publishZipPath).toLowerCase() !== normalize(resolvedZipPath).toLowerCase()) {
|
||||
throw new Error('BLOCKED: upload zip path does not match project publish data');
|
||||
}
|
||||
const deployCheck = await readWorksDeployCheck(project.path, publish.publish);
|
||||
if (deployCheck.status !== 'pass' && deployCheck.status !== 'warning') {
|
||||
throw new Error(`BLOCKED: ${deployCheck.error || 'deployment checks did not pass'}`);
|
||||
}
|
||||
if (extname(resolvedZipPath).toLowerCase() !== '.zip') {
|
||||
throw new Error('zipFilePath must point to a .zip file');
|
||||
}
|
||||
|
||||
const archiveStat = await stat(resolvedZipPath);
|
||||
if (!archiveStat.isFile()) {
|
||||
throw new Error('zipFilePath must point to a file');
|
||||
}
|
||||
|
||||
const archiveBytes = await readFile(resolvedZipPath);
|
||||
const archiveBlob = new Blob([new Uint8Array(archiveBytes)], { type: 'application/zip' });
|
||||
const form = new FormData();
|
||||
form.set('version_name', versionName);
|
||||
form.set('change_log', changeLog);
|
||||
form.set('archive', archiveBlob, basename(resolvedZipPath));
|
||||
return form;
|
||||
}
|
||||
|
||||
async function handleUploadProjectVersion(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
appId: string,
|
||||
ctx: HostApiContext,
|
||||
): Promise<void> {
|
||||
const body = await parseJsonBody<UploadProjectVersionInput>(req);
|
||||
const accessToken = readRequiredString(body.accessToken, 'accessToken');
|
||||
const form = await createArchiveFormData(body, ctx, appId);
|
||||
|
||||
const response = await proxyAwareFetch(
|
||||
createWorksUrl(`/api/projects/${encodeURIComponent(appId)}/versions/upload`).toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: form,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
await sendUpstreamError(res, response, `Works Square upload failed (${response.status})`);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(res, response.status, { success: true, upload: await readResponsePayload(response) });
|
||||
}
|
||||
|
||||
const SOURCE_PUBLISH_CHANGE_LOG = '通过 Makelore 一键提交';
|
||||
const RETRYABLE_SOURCE_UPLOAD_STATUSES = new Set([408, 502, 503, 504]);
|
||||
const VERSION_FILE_MAX_BYTES = 64 * 1024;
|
||||
@@ -924,9 +841,9 @@ async function handlePublishProjectSource(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (ctx.worksCloudDeployment) {
|
||||
if (ctx.worksSubmissionBinding) {
|
||||
try {
|
||||
await ctx.worksCloudDeployment.recordSubmitted(projectId, {
|
||||
await ctx.worksSubmissionBinding.recordSubmitted(projectId, {
|
||||
appId,
|
||||
versionId: uploadPayload.version_id,
|
||||
versionName,
|
||||
@@ -1115,12 +1032,6 @@ export async function handleWorksRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
const uploadMatch = url.pathname.match(/^\/api\/works\/projects\/([^/]+)\/versions\/upload$/);
|
||||
if (uploadMatch && req.method === 'POST') {
|
||||
await handleUploadProjectVersion(req, res, decodeURIComponent(uploadMatch[1]), ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendJson(res, 404, { success: false, error: `No route for ${req.method} ${url.pathname}` });
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user