Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

449
src/lib/works-square.ts Normal file
View File

@@ -0,0 +1,449 @@
import { hostApiFetch } from '@/lib/host-api';
import type { WorksCloudDeploymentRecord } from '../../shared/works-cloud-deployment';
export type ProjectPublic = {
app_id: string;
title: string;
summary: string;
cover_url?: string | null;
category?: string | null;
age_band?: string | null;
difficulty?: string | null;
status?: string | null;
updated_at?: string | null;
playable?: boolean;
runtime_url?: string | null;
creator_name?: string | null;
buddy_name?: string | null;
buddy_sprite_url?: string | null;
buddy_pose_url?: string | null;
version_name?: string | null;
testing_ask?: string | null;
update_note?: string | null;
remix_note?: string | null;
how_to?: string | null;
learning_note?: string | null;
visual_alt?: string | null;
};
export type ProjectPage = {
items: ProjectPublic[];
next_cursor: string | null;
limit: number;
};
export type WorksAssetPublic = {
slug: string;
title: string;
summary: string;
category: string;
source: string;
source_url?: string | null;
license?: string | null;
license_url?: string | null;
series?: string | null;
tags: string[];
preview_url?: string | null;
image_urls: string[];
archive_size_bytes?: number | null;
extracted_file_count?: number | null;
published_at?: string | null;
updated_at?: string | null;
};
export type WorksAssetPage = {
items: WorksAssetPublic[];
next_cursor: string | null;
limit: number;
};
export type WorksAssetDownloadInput = {
slug: string;
projectId: string;
};
export type WorksAssetDownloadResult = {
slug: string;
filePath: string;
relativePath: string;
bytesWritten: number;
};
export type WorksProjectMetadataInput = {
app_id: string;
title: string;
summary: string;
cover_url?: string | null;
category?: string | null;
age_band?: string | null;
difficulty?: string | null;
};
export type WorksProjectVersionUpload = {
version_id: string;
review_status: string;
};
export type WorksProjectVersion = {
id: string;
version_name: string;
review_status: string;
change_log: string;
build_job_id: string | null;
created_at: string;
};
export type WorksProjectVersionsPage = {
items: WorksProjectVersion[];
};
export type WorksProjectStatusDetail = {
project: ProjectPublic;
latest_version: WorksProjectVersion | null;
versions: WorksProjectVersion[];
};
export type WorksProjectUploadInput = {
accessToken: string;
appId: string;
projectId: string;
versionName: string;
changeLog: string;
zipFilePath: string;
};
export type WorksSpeechTranscription = {
text: string;
model: string;
};
export type WorksSpeechTranscriptionInput = {
accessToken: string;
audioBase64: string;
fileName: string;
mimeType: string;
language?: string;
prompt?: string;
model?: string;
};
export type WorksTokenUsage = {
five_hour_remaining_percent: number | null;
five_hour_refresh_at?: string | null;
weekly_remaining_percent: number | null;
weekly_refresh_at?: string | null;
plan_code?: string | null;
plan_name?: string | null;
};
export type PlazaCard = {
id: string;
title: string;
summary: string;
coverImage: string | null;
type: string | null;
ageBand: string | null;
difficulty: string | null;
updatedAt: string | null;
playable: boolean;
runtimeUrl: string | null;
};
type WorksActionResponse<TField extends string, TValue> = {
success: boolean;
status?: number;
error?: string;
} & {
[key in TField]?: TValue;
};
type FetchWorksProjectsInput = {
q?: string;
category?: string;
limit?: number;
};
type FetchWorksAssetsInput = {
q?: string;
category?: string;
tag?: string;
source?: string;
limit?: number;
};
type FetchMyWorksProjectsInput = {
limit?: number;
};
export class WorksSquareApiError extends Error {
readonly statusCode?: number;
constructor(message: string, statusCode?: number) {
super(message);
this.name = 'WorksSquareApiError';
this.statusCode = statusCode;
}
}
function assertSuccess<TField extends string, TValue>(
response: WorksActionResponse<TField, TValue>,
field: TField,
fallback: string,
): TValue {
if (!response.success || response[field] === undefined) {
throw new WorksSquareApiError(response.error || fallback, response.status);
}
return response[field] as TValue;
}
function appendTrimmedParam(params: URLSearchParams, name: string, value: string | undefined): void {
const trimmed = value?.trim();
if (trimmed) {
params.set(name, trimmed);
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function readNonEmptyString(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function normalizeSpeechTranscription(value: unknown): WorksSpeechTranscription | null {
const directText = readNonEmptyString(value);
if (directText) {
return { text: directText, model: '' };
}
if (!isRecord(value)) {
return null;
}
const text = readNonEmptyString(value.text);
const model = readNonEmptyString(value.model) ?? '';
if (text) {
return { text, model };
}
for (const key of ['transcription', 'data', 'result', 'payload']) {
const nested = normalizeSpeechTranscription(value[key]);
if (nested) {
return {
text: nested.text,
model: nested.model || model,
};
}
}
return null;
}
export function toPlazaCard(project: ProjectPublic): PlazaCard {
return {
id: project.app_id,
title: project.title,
summary: project.summary,
coverImage: project.cover_url ?? null,
type: project.category ?? null,
ageBand: project.age_band ?? null,
difficulty: project.difficulty ?? null,
updatedAt: project.updated_at ?? null,
playable: Boolean(project.playable),
runtimeUrl: project.runtime_url ?? null,
};
}
export async function fetchWorksProjects(input: FetchWorksProjectsInput = {}): Promise<ProjectPage> {
const params = new URLSearchParams();
appendTrimmedParam(params, 'q', input.q);
appendTrimmedParam(params, 'category', input.category);
params.set('limit', String(input.limit ?? 24));
const query = params.toString();
const response = await hostApiFetch<WorksActionResponse<'page', ProjectPage>>(
`/api/works/projects${query ? `?${query}` : ''}`,
);
return assertSuccess(response, 'page', 'Failed to load Works Square projects');
}
export async function fetchWorksProject(appId: string): Promise<ProjectPublic> {
const response = await hostApiFetch<WorksActionResponse<'project', ProjectPublic>>(
`/api/works/projects/${encodeURIComponent(appId)}`,
);
return assertSuccess(response, 'project', 'Failed to load Works Square project');
}
export async function fetchWorksAssets(input: FetchWorksAssetsInput = {}): Promise<WorksAssetPage> {
const params = new URLSearchParams();
appendTrimmedParam(params, 'q', input.q);
appendTrimmedParam(params, 'category', input.category);
appendTrimmedParam(params, 'tag', input.tag);
appendTrimmedParam(params, 'source', input.source);
params.set('limit', String(input.limit ?? 24));
const query = params.toString();
const response = await hostApiFetch<WorksActionResponse<'assets', WorksAssetPage>>(
`/api/works/assets${query ? `?${query}` : ''}`,
);
return assertSuccess(response, 'assets', 'Failed to load Works Square assets');
}
export async function fetchWorksAsset(slug: string): Promise<WorksAssetPublic> {
const response = await hostApiFetch<WorksActionResponse<'asset', WorksAssetPublic>>(
`/api/works/assets/${encodeURIComponent(slug)}`,
);
return assertSuccess(response, 'asset', 'Failed to load Works Square asset');
}
export async function downloadWorksAssetToProject(
input: WorksAssetDownloadInput,
): Promise<WorksAssetDownloadResult> {
const response = await hostApiFetch<WorksActionResponse<'download', WorksAssetDownloadResult>>(
`/api/works/assets/${encodeURIComponent(input.slug)}/download`,
{
method: 'POST',
body: JSON.stringify({ projectId: input.projectId }),
},
);
return assertSuccess(response, 'download', 'Failed to download Works Square asset');
}
export async function createWorksProject(
accessToken: string,
project: WorksProjectMetadataInput,
): Promise<ProjectPublic> {
const response = await hostApiFetch<WorksActionResponse<'project', ProjectPublic>>('/api/works/projects', {
method: 'POST',
body: JSON.stringify({ accessToken, project }),
});
return assertSuccess(response, 'project', 'Failed to create Works Square project');
}
export async function fetchMyWorksProjects(
accessToken: string,
input: FetchMyWorksProjectsInput = {},
): Promise<ProjectPage> {
const params = new URLSearchParams();
params.set('limit', String(input.limit ?? 50));
const response = await hostApiFetch<WorksActionResponse<'page', ProjectPage>>(
`/api/works/projects/mine?${params.toString()}`,
{
headers: {
'X-NianCode-Access-Token': accessToken,
},
},
);
return assertSuccess(response, 'page', 'Failed to load your Works Square projects');
}
export async function fetchWorksTokenUsage(accessToken: string): Promise<WorksTokenUsage> {
const response = await hostApiFetch<WorksActionResponse<'usage', WorksTokenUsage>>(
'/api/works/billing/token-usage',
{
headers: {
'X-NianCode-Access-Token': accessToken,
},
},
);
return assertSuccess(response, 'usage', 'Failed to load Works Square token usage');
}
export async function fetchMyWorksProjectStatus(
accessToken: string,
appId: string,
): Promise<WorksProjectStatusDetail> {
const response = await hostApiFetch<WorksActionResponse<'status', WorksProjectStatusDetail>>(
`/api/works/projects/mine/${encodeURIComponent(appId)}/status`,
{
headers: {
'X-NianCode-Access-Token': accessToken,
},
},
);
return assertSuccess(response, 'status', 'Failed to load Works Square project status');
}
export async function uploadWorksProjectZip(
input: WorksProjectUploadInput,
): Promise<WorksProjectVersionUpload> {
const response = await hostApiFetch<WorksActionResponse<'upload', WorksProjectVersionUpload>>(
`/api/works/projects/${encodeURIComponent(input.appId)}/versions/upload`,
{
method: 'POST',
body: JSON.stringify({
accessToken: input.accessToken,
projectId: input.projectId,
versionName: input.versionName,
changeLog: input.changeLog,
zipFilePath: input.zipFilePath,
}),
},
);
return assertSuccess(response, 'upload', 'Failed to upload Works Square project zip');
}
export async function armWorksCloudDeployment(projectId: string): Promise<WorksCloudDeploymentRecord> {
const response = await hostApiFetch<WorksActionResponse<'deployment', WorksCloudDeploymentRecord>>(
`/api/opencode/projects/${encodeURIComponent(projectId)}/works-cloud-deploy`,
{ method: 'POST', body: JSON.stringify({}) },
);
return assertSuccess(response, 'deployment', 'Failed to arm cloud deployment');
}
export async function fetchWorksCloudDeployment(
projectId: string,
): Promise<WorksCloudDeploymentRecord | null> {
const response = await hostApiFetch<{
success: boolean;
deployment?: WorksCloudDeploymentRecord | null;
error?: string;
}>(`/api/opencode/projects/${encodeURIComponent(projectId)}/works-cloud-deploy`);
if (!response.success) {
throw new WorksSquareApiError(response.error || 'Failed to read cloud deployment status');
}
return response.deployment ?? null;
}
export async function fetchWorksProjectVersions(
accessToken: string,
appId: string,
): Promise<WorksProjectVersionsPage> {
const response = await hostApiFetch<WorksActionResponse<'versions', WorksProjectVersionsPage>>(
`/api/works/projects/${encodeURIComponent(appId)}/versions`,
{
headers: {
'X-NianCode-Access-Token': accessToken,
},
},
);
return assertSuccess(response, 'versions', 'Failed to load Works Square uploaded versions');
}
export async function transcribeWorksSpeech(
input: WorksSpeechTranscriptionInput,
): Promise<WorksSpeechTranscription> {
const response = await hostApiFetch<WorksActionResponse<'transcription', unknown>>(
'/api/works/speech/transcriptions',
{
method: 'POST',
body: JSON.stringify({
accessToken: input.accessToken,
audioBase64: input.audioBase64,
fileName: input.fileName,
mimeType: input.mimeType,
language: input.language,
prompt: input.prompt,
model: input.model,
}),
},
);
const transcriptionPayload = assertSuccess(response, 'transcription', 'Failed to transcribe speech');
const transcription = normalizeSpeechTranscription(transcriptionPayload);
if (!transcription) {
throw new WorksSquareApiError(response.error || 'Speech transcription response did not contain text', response.status);
}
return transcription;
}