feat: separate Chinese display name and English packaging name

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 08:54:43 +08:00
parent aef449e0e4
commit edf2bdada6
11 changed files with 104 additions and 28 deletions

View File

@@ -15,6 +15,7 @@ export type BuildStatus =
export interface BuildRecord {
id: string;
app_name: string;
app_name_en: string;
app_identifier: string;
status: BuildStatus;
workflow_run_id: number | null;
@@ -36,19 +37,31 @@ export function getDb(): Database.Database {
db = new Database(dbPath);
const schema = fs.readFileSync(path.join(__dirname, "schema.sql"), "utf8");
db.exec(schema);
migrateBuildsTable(db);
return db;
}
function migrateBuildsTable(database: Database.Database) {
const columns = database
.prepare("PRAGMA table_info(builds)")
.all() as Array<{ name: string }>;
if (!columns.some((column) => column.name === "app_name_en")) {
database.exec("ALTER TABLE builds ADD COLUMN app_name_en TEXT NOT NULL DEFAULT ''");
}
}
export function insertBuild(record: {
id: string;
appName: string;
appNameEn: string;
appIdentifier: string;
}): BuildRecord {
const database = getDb();
database
.prepare(
`INSERT INTO builds (id, app_name, app_identifier, status)
VALUES (@id, @appName, @appIdentifier, 'pending')`,
`INSERT INTO builds (id, app_name, app_name_en, app_identifier, status)
VALUES (@id, @appName, @appNameEn, @appIdentifier, 'pending')`,
)
.run(record);
@@ -105,6 +118,7 @@ export function toPublicBuild(record: BuildRecord) {
return {
id: record.id,
appName: record.app_name,
appNameEn: record.app_name_en || record.app_name,
appIdentifier: record.app_identifier,
status: record.status,
workflowRunId: record.workflow_run_id,

View File

@@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY,
app_name TEXT NOT NULL,
app_name_en TEXT NOT NULL DEFAULT '',
app_identifier TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
workflow_run_id INTEGER,

View File

@@ -19,6 +19,8 @@ import {
import {
normalizeAppIdentifier,
slugifyIdentifier,
validateChineseAppName,
validateEnglishAppName,
validateZipBuffer,
ZipValidationError,
} from "../services/zip.js";
@@ -54,14 +56,14 @@ buildsRouter.get("/:id", async (req, res) => {
buildsRouter.post("/", upload.single("file"), async (req, res) => {
try {
const appName = String(req.body.appName ?? "").trim();
const appNameZh = validateChineseAppName(
String(req.body.appNameZh ?? req.body.appName ?? ""),
);
const appNameEn = validateEnglishAppName(
String(req.body.appNameEn ?? ""),
);
const identifierInput = String(req.body.identifier ?? "").trim();
if (!appName) {
res.status(400).json({ error: "appName is required" });
return;
}
if (!req.file) {
res.status(400).json({ error: "file is required" });
return;
@@ -74,19 +76,25 @@ buildsRouter.post("/", upload.single("file"), async (req, res) => {
const { normalizedBuffer } = validateZipBuffer(req.file.buffer, maxUploadBytes);
const jobId = nanoid(10);
const slug = slugifyIdentifier(appName) || jobId.toLowerCase();
const slug = slugifyIdentifier(appNameEn) || jobId.toLowerCase();
const appIdentifier = normalizeAppIdentifier(
identifierInput || `com.web2app.${slug}`,
);
insertBuild({ id: jobId, appName, appIdentifier });
insertBuild({
id: jobId,
appName: appNameZh,
appNameEn,
appIdentifier,
});
await uploadSiteZip(jobId, normalizedBuffer);
updateBuild(jobId, { status: "queued" });
const workflowRunId = await triggerBuildWorkflow({
jobId,
appName,
appName: appNameZh,
appNameEn,
appIdentifier,
});

View File

@@ -61,6 +61,7 @@ export async function uploadSiteZip(jobId: string, zipBuffer: Buffer): Promise<v
export async function triggerBuildWorkflow(input: {
jobId: string;
appName: string;
appNameEn: string;
appIdentifier: string;
}): Promise<number> {
const { owner, repo } = getGitHubConfig();
@@ -74,6 +75,7 @@ export async function triggerBuildWorkflow(input: {
inputs: {
job_id: input.jobId,
app_name: input.appName,
app_name_en: input.appNameEn,
app_identifier: input.appIdentifier,
},
});

View File

@@ -154,6 +154,27 @@ function sanitizeSegment(segment: string): string {
return value.slice(0, 48);
}
export function validateEnglishAppName(input: string): string {
const value = input.trim();
if (!value) {
throw new ZipValidationError("应用英文名不能为空");
}
if (!/^[a-zA-Z][a-zA-Z0-9 _.-]*$/.test(value)) {
throw new ZipValidationError(
"应用英文名需以字母开头,仅支持英文字母、数字、空格、下划线和连字符",
);
}
return value;
}
export function validateChineseAppName(input: string): string {
const value = input.trim();
if (!value) {
throw new ZipValidationError("应用中文名不能为空");
}
return value;
}
export function slugifyIdentifier(input: string): string {
return input
.toLowerCase()