From edf2bdada6e5ae920643caf78ed6c957cad4f25e Mon Sep 17 00:00:00 2001 From: shumengya Date: Thu, 28 May 2026 08:54:43 +0800 Subject: [PATCH] feat: separate Chinese display name and English packaging name Co-authored-by: Cursor --- .github/scripts/prepare-template.mjs | 15 +++++++++------ .github/workflows/build-app.yml | 9 +++++++-- README.md | 5 +++-- server/db/index.ts | 18 ++++++++++++++++-- server/db/schema.sql | 1 + server/routes/builds.ts | 26 +++++++++++++++++--------- server/services/github.ts | 2 ++ server/services/zip.ts | 21 +++++++++++++++++++++ web/src/api.ts | 1 + web/src/pages/JobStatus.tsx | 6 +++++- web/src/pages/Upload.tsx | 28 ++++++++++++++++++++++------ 11 files changed, 104 insertions(+), 28 deletions(-) diff --git a/.github/scripts/prepare-template.mjs b/.github/scripts/prepare-template.mjs index 0e18ba0..ed72183 100644 --- a/.github/scripts/prepare-template.mjs +++ b/.github/scripts/prepare-template.mjs @@ -7,11 +7,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, "../.."); const jobId = process.env.JOB_ID; -const appName = process.env.APP_NAME; +const appNameZh = process.env.APP_NAME; +const appNameEn = process.env.APP_NAME_EN; const appIdentifier = process.env.APP_IDENTIFIER; -if (!jobId || !appName || !appIdentifier) { - console.error("JOB_ID, APP_NAME, and APP_IDENTIFIER are required"); +if (!jobId || !appNameZh || !appNameEn || !appIdentifier) { + console.error( + "JOB_ID, APP_NAME, APP_NAME_EN, and APP_IDENTIFIER are required", + ); process.exit(1); } @@ -49,13 +52,13 @@ function toSafeBinaryName(name, fallback) { function patchTauriConfig(filePath) { const conf = JSON.parse(fs.readFileSync(filePath, "utf8")); - const safeBinaryName = toSafeBinaryName(appName, `App${jobId}`); - conf.productName = appName; + const safeBinaryName = toSafeBinaryName(appNameEn, `App${jobId}`); + conf.productName = appNameZh; conf.mainBinaryName = safeBinaryName; conf.identifier = appIdentifier; conf.version = "1.0.0"; if (conf.app?.windows?.[0]) { - conf.app.windows[0].title = appName; + conf.app.windows[0].title = appNameZh; } fs.writeFileSync(filePath, `${JSON.stringify(conf, null, 2)}\n`); } diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 88c63b4..6c496cf 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -8,7 +8,11 @@ on: required: true type: string app_name: - description: Application display name + description: Application display name (Chinese) + required: true + type: string + app_name_en: + description: Application English name for packaging required: true type: string app_identifier: @@ -22,6 +26,7 @@ permissions: env: JOB_ID: ${{ inputs.job_id }} APP_NAME: ${{ inputs.app_name }} + APP_NAME_EN: ${{ inputs.app_name_en }} APP_IDENTIFIER: ${{ inputs.app_identifier }} NDK_VERSION: "27.2.12479018" @@ -168,7 +173,7 @@ jobs: tag_name: build-${{ inputs.job_id }} name: Web2App ${{ inputs.app_name }} (${{ inputs.job_id }}) body: | - Automated build for **${{ inputs.app_name }}** (`${{ inputs.app_identifier }}`). + Automated build for **${{ inputs.app_name }}** (English: `${{ inputs.app_name_en }}`, ID: `${{ inputs.app_identifier }}`). - Windows desktop installer/portable binary - Android APK (demo signing, sideload only) diff --git a/README.md b/README.md index 1d3af00..a20f761 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ powershell Compress-Archive -Path * -DestinationPath ../sample-site.zip -Force | 方法 | 路径 | 说明 | |------|------|------| -| `POST` | `/api/builds` | `multipart/form-data`: `file`, `appName`, 可选 `identifier` | +| `POST` | `/api/builds` | `multipart/form-data`: `file`, `appNameZh`, `appNameEn`, 可选 `identifier` | | `GET` | `/api/builds/:id` | 查询构建状态与下载链接 | | `GET` | `/api/builds` | 最近 20 条构建记录 | @@ -103,7 +103,8 @@ powershell Compress-Archive -Path * -DestinationPath ../sample-site.zip -Force 在 GitHub 仓库 Actions 页选择 **Build App**,手动填写: - `job_id`: 任意已上传目录名(需先存在 `builds/{job_id}/site.zip`) -- `app_name`: Demo App +- `app_name`: 演示应用 +- `app_name_en`: Demo App - `app_identifier`: com.web2app.demo ## 本地数据 diff --git a/server/db/index.ts b/server/db/index.ts index 02918ce..3c3854b 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -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, diff --git a/server/db/schema.sql b/server/db/schema.sql index da8b10e..c23ab56 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -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, diff --git a/server/routes/builds.ts b/server/routes/builds.ts index c2e6688..ea0823e 100644 --- a/server/routes/builds.ts +++ b/server/routes/builds.ts @@ -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, }); diff --git a/server/services/github.ts b/server/services/github.ts index 379263d..a436a80 100644 --- a/server/services/github.ts +++ b/server/services/github.ts @@ -61,6 +61,7 @@ export async function uploadSiteZip(jobId: string, zipBuffer: Buffer): Promise { 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, }, }); diff --git a/server/services/zip.ts b/server/services/zip.ts index be85f60..92fb8d3 100644 --- a/server/services/zip.ts +++ b/server/services/zip.ts @@ -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() diff --git a/web/src/api.ts b/web/src/api.ts index be3c160..de65dda 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -8,6 +8,7 @@ export type BuildStatus = export interface Build { id: string; appName: string; + appNameEn: string; appIdentifier: string; status: BuildStatus; workflowRunId: number | null; diff --git a/web/src/pages/JobStatus.tsx b/web/src/pages/JobStatus.tsx index dddbd07..a294014 100644 --- a/web/src/pages/JobStatus.tsx +++ b/web/src/pages/JobStatus.tsx @@ -58,9 +58,13 @@ export default function JobStatus() {
{build.id}
-
应用名称
+
应用中文名
{build.appName}
+
+
应用英文名
+
{build.appNameEn}
+
Bundle ID
{build.appIdentifier}
diff --git a/web/src/pages/Upload.tsx b/web/src/pages/Upload.tsx index 10b1c8e..ba9cd03 100644 --- a/web/src/pages/Upload.tsx +++ b/web/src/pages/Upload.tsx @@ -4,7 +4,8 @@ import { createBuild } from "../api"; export default function Upload() { const navigate = useNavigate(); - const [appName, setAppName] = useState(""); + const [appNameZh, setAppNameZh] = useState(""); + const [appNameEn, setAppNameEn] = useState(""); const [identifier, setIdentifier] = useState(""); const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); @@ -23,7 +24,8 @@ export default function Upload() { try { const formData = new FormData(); formData.append("file", file); - formData.append("appName", appName); + formData.append("appNameZh", appNameZh); + formData.append("appNameEn", appNameEn); if (identifier.trim()) { formData.append("identifier", identifier.trim()); } @@ -40,15 +42,29 @@ export default function Upload() { return (

上传静态站点

-

压缩包需包含 index.html,大小默认不超过 50MB。

+

+ 压缩包需包含 index.html,大小默认不超过 50MB。中文名用于界面展示,英文名用于安装包与 + Bundle ID 生成。 +

+