迭代 ONE-OS 多原型:租赁/自营台账增强、租赁合同与条线说明,统一台账壳层与侧栏导航
- 新增业务部台账、收款记录、业务条线说明及 ledger-shared / vm-shared 公共壳层 - 租赁业务台账:字段计算、宽表交互、标注 PRD 与表头说明修复 - 租赁合同:列表/表单增强、客户资质与审批规则 - 侧栏调整:工作台移至 OneOS 根目录,台账与数据分析分组更新 - 补充 S3 批量发布脚本与弹窗明细表规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
289
scripts/publish-all-to-s3.mjs
Normal file
289
scripts/publish-all-to-s3.mjs
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 将项目原型页发布到 S3(阿里云 OSS)。
|
||||
* 流程:Make Admin export-html 构建静态包 → 按页面独立前缀上传到 OSS。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/publish-all-to-s3.mjs
|
||||
* node scripts/publish-all-to-s3.mjs --dry-run
|
||||
* node scripts/publish-all-to-s3.mjs --group prototypes
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import extract from 'extract-zip';
|
||||
import { readServerInfo } from './utils/serverInfo.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const CONFIG_PATH = path.join(projectRoot, '.axhub/make/axhub.config.json');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
projectId: 'oneos1.2',
|
||||
adminOrigin: '',
|
||||
dryRun: false,
|
||||
group: 'all',
|
||||
includeSource: true,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--project-id' && argv[i + 1]) args.projectId = argv[++i].trim();
|
||||
else if (arg === '--admin-origin' && argv[i + 1]) args.adminOrigin = argv[++i].trim();
|
||||
else if (arg === '--group' && argv[i + 1]) args.group = argv[++i].trim();
|
||||
else if (arg === '--dry-run') args.dryRun = true;
|
||||
else if (arg === '--no-source') args.includeSource = false;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function resolveAdminOrigin(explicit) {
|
||||
if (explicit) return explicit.replace(/\/+$/u, '');
|
||||
const info = readServerInfo(projectRoot, 'admin');
|
||||
if (info?.origin) return info.origin.replace(/\/+$/u, '');
|
||||
return 'http://localhost:53817';
|
||||
}
|
||||
|
||||
function readS3Config() {
|
||||
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
const s3 = raw?.cloudPublishing?.s3;
|
||||
if (!s3?.accessKeyId || !s3?.secretAccessKey || !s3?.bucket) {
|
||||
throw new Error('请在 .axhub/make/axhub.config.json 中配置 cloudPublishing.s3');
|
||||
}
|
||||
return {
|
||||
...s3,
|
||||
includeSource: raw?.cloudPublishing?.publishSettings?.includeSource !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function toPublishPath(resource) {
|
||||
const raw = resource.filePath || resource.sourcePath || '';
|
||||
return String(raw).replace(/\\/gu, '/').replace(/\/index\.tsx?$/iu, '');
|
||||
}
|
||||
|
||||
function resolveObjectPrefix(entry) {
|
||||
const normalized = entry.path.replace(/^\/+|\/+$/gu, '');
|
||||
const prototypeMatch = normalized.match(/^(?:src\/)?prototypes\/(.+)$/u);
|
||||
if (prototypeMatch?.[1]) return prototypeMatch[1];
|
||||
const themeMatch = normalized.match(/^(?:src\/)?themes\/(.+)$/u);
|
||||
if (themeMatch?.[1]) return `themes/${themeMatch[1]}`;
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
function resolvePublicUrl(baseUrl, prefix) {
|
||||
const base = String(baseUrl || '').replace(/\/?$/u, '/');
|
||||
const key = `${prefix.replace(/^\/+|\/+$/gu, '')}/index.html`;
|
||||
return `${base}${key.split('/').map(encodeURIComponent).join('/')}`;
|
||||
}
|
||||
|
||||
function guessContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const map = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.webp': 'image/webp',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.woff2': 'font/woff2',
|
||||
'.woff': 'font/woff',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
};
|
||||
return map[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
async function collectPublishEntries(adminOrigin, projectId, groupFilter) {
|
||||
const res = await fetch(`${adminOrigin}/api/projects/${encodeURIComponent(projectId)}/resources`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET /api/projects/${projectId}/resources failed: ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const groups = groupFilter === 'all' ? ['prototypes', 'themes'] : [groupFilter];
|
||||
const entries = [];
|
||||
for (const group of groups) {
|
||||
for (const item of data.resources?.[group] || []) {
|
||||
const publishPath = toPublishPath(item);
|
||||
if (!publishPath) continue;
|
||||
entries.push({
|
||||
group,
|
||||
id: item.id,
|
||||
title: item.title || item.id,
|
||||
path: publishPath,
|
||||
prefix: resolveObjectPrefix({ path: publishPath, id: item.id }),
|
||||
});
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function exportHtmlZip(adminOrigin, projectId, entry, includeSource) {
|
||||
const params = new URLSearchParams({
|
||||
path: entry.path,
|
||||
projectId,
|
||||
});
|
||||
if (includeSource) params.set('includeSource', 'true');
|
||||
const res = await fetch(`${adminOrigin}/api/export-html?${params.toString()}`);
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
let message = text;
|
||||
try {
|
||||
message = JSON.parse(text).error || text;
|
||||
} catch {
|
||||
// keep raw text
|
||||
}
|
||||
throw new Error(message || `export-html failed (${res.status})`);
|
||||
}
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
if (buffer.length < 4 || buffer[0] !== 0x50 || buffer[1] !== 0x4b) {
|
||||
throw new Error('export-html 未返回有效 ZIP');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function resolveS3Endpoint(s3Config) {
|
||||
const bucket = String(s3Config.bucket || '').trim();
|
||||
const endpoint = String(s3Config.endpoint || '').trim().replace(/\/+$/u, '');
|
||||
const region = String(s3Config.region || 'cn-hangzhou').trim();
|
||||
|
||||
if (endpoint) {
|
||||
// 配置里可能是 bucket 级域名(model-lnoneos.oss-...),AWS SDK 只需区域 endpoint。
|
||||
const bucketHostPrefix = bucket ? `${bucket}.` : '';
|
||||
if (bucketHostPrefix && endpoint.includes(bucketHostPrefix)) {
|
||||
return endpoint.replace(bucketHostPrefix, '');
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
return `https://oss-${region}.aliyuncs.com`;
|
||||
}
|
||||
|
||||
function createS3Client(s3Config) {
|
||||
return new S3Client({
|
||||
region: s3Config.region || 'cn-hangzhou',
|
||||
endpoint: resolveS3Endpoint(s3Config),
|
||||
forcePathStyle: false,
|
||||
credentials: {
|
||||
accessKeyId: s3Config.accessKeyId,
|
||||
secretAccessKey: s3Config.secretAccessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadDirectory(client, bucket, prefix, dir) {
|
||||
const uploaded = [];
|
||||
const walk = async (currentDir, relative = '') => {
|
||||
for (const name of fs.readdirSync(currentDir)) {
|
||||
const abs = path.join(currentDir, name);
|
||||
const rel = relative ? `${relative}/${name}` : name;
|
||||
if (fs.statSync(abs).isDirectory()) {
|
||||
await walk(abs, rel);
|
||||
continue;
|
||||
}
|
||||
const key = [prefix.replace(/^\/+|\/+$/gu, ''), rel].filter(Boolean).join('/');
|
||||
await client.send(new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: fs.readFileSync(abs),
|
||||
ContentType: guessContentType(abs),
|
||||
}));
|
||||
uploaded.push(key);
|
||||
}
|
||||
};
|
||||
await walk(dir);
|
||||
return uploaded;
|
||||
}
|
||||
|
||||
async function publishEntry(client, s3Config, adminOrigin, projectId, entry, includeSource) {
|
||||
const zipBuffer = await exportHtmlZip(adminOrigin, projectId, entry, includeSource);
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'axhub-publish-'));
|
||||
const zipPath = path.join(tempDir, 'bundle.zip');
|
||||
const extractDir = path.join(tempDir, 'site');
|
||||
fs.writeFileSync(zipPath, zipBuffer);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
await extract(zipPath, { dir: extractDir });
|
||||
const objectPrefix = s3Config.prefix
|
||||
? `${String(s3Config.prefix).replace(/^\/+|\/+$/gu, '')}/${entry.prefix}`
|
||||
: entry.prefix;
|
||||
const keys = await uploadDirectory(client, s3Config.bucket, objectPrefix, extractDir);
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
return {
|
||||
url: resolvePublicUrl(s3Config.baseUrl, objectPrefix),
|
||||
fileCount: keys.length,
|
||||
keys,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const adminOrigin = resolveAdminOrigin(args.adminOrigin);
|
||||
const s3Config = readS3Config();
|
||||
const includeSource = args.includeSource && s3Config.includeSource;
|
||||
const entries = await collectPublishEntries(adminOrigin, args.projectId, args.group);
|
||||
|
||||
console.log(`Admin: ${adminOrigin}`);
|
||||
console.log(`Bucket: ${s3Config.bucket}`);
|
||||
console.log(`Base URL: ${s3Config.baseUrl}`);
|
||||
console.log(`Pages to publish: ${entries.length}`);
|
||||
|
||||
if (args.dryRun) {
|
||||
for (const entry of entries) {
|
||||
console.log(`[dry-run] ${entry.group}/${entry.id} -> ${entry.prefix}/index.html`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createS3Client(s3Config);
|
||||
const results = [];
|
||||
let failed = 0;
|
||||
|
||||
for (let i = 0; i < entries.length; i += 1) {
|
||||
const entry = entries[i];
|
||||
const label = `[${i + 1}/${entries.length}] ${entry.group}/${entry.id}`;
|
||||
process.stdout.write(`${label} ... `);
|
||||
try {
|
||||
const result = await publishEntry(client, s3Config, adminOrigin, args.projectId, entry, includeSource);
|
||||
console.log(`OK ${result.url} (${result.fileCount} files)`);
|
||||
results.push({ ...entry, status: 'success', ...result });
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.log(`FAIL ${message}`);
|
||||
results.push({ ...entry, status: 'failed', error: message });
|
||||
}
|
||||
}
|
||||
|
||||
const reportPath = path.join(
|
||||
projectRoot,
|
||||
'.axhub/make/exports',
|
||||
`cloud.publish.s3-all-${new Date().toISOString().replace(/[:.]/gu, '-')}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
projectId: args.projectId,
|
||||
target: 's3',
|
||||
method: 'export-html+s3-upload',
|
||||
total: entries.length,
|
||||
success: entries.length - failed,
|
||||
failed,
|
||||
completedAt: new Date().toISOString(),
|
||||
results: results.map(({ keys, ...rest }) => ({
|
||||
...rest,
|
||||
fileCount: rest.fileCount ?? keys?.length ?? 0,
|
||||
})),
|
||||
}, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(`\nDone: ${entries.length - failed}/${entries.length} succeeded`);
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
console.log(`Report: ${reportPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -20,6 +20,12 @@ const XLL_MODULES = [
|
||||
{ slug: 'oneos-web-safety-training', title: '业务-司机安全培训', skipCodegen: true },
|
||||
];
|
||||
|
||||
/** 从 ONE-OS Web 合包拆出、由项目内手写 index 维护的独立原型 */
|
||||
const STANDALONE_MODULES = [
|
||||
{ slug: 'business-dept-ledger', title: '业务部台账', skipCodegen: true },
|
||||
{ slug: 'oneos-web-h2-station-site', title: '站点信息', skipCodegen: true },
|
||||
];
|
||||
|
||||
const MODULES = [
|
||||
{ slug: 'oneos-web-workbench', title: '工作台', files: ['工作台.jsx'] },
|
||||
{ slug: 'oneos-web-login', title: '登录', files: ['登录.jsx'] },
|
||||
@@ -29,8 +35,8 @@ const MODULES = [
|
||||
{ slug: 'oneos-web-finance', title: '财务管理', dir: '财务管理', excludeDirs: ['文档'] },
|
||||
{ slug: 'oneos-web-procurement', title: '采购管理', dir: '采购管理' },
|
||||
{ slug: 'oneos-web-lease-contract', title: '车辆租赁合同', dir: '车辆租赁合同' },
|
||||
{ slug: 'oneos-web-h2-station', title: '加氢站管理', dir: '加氢站管理', excludeDirs: ['export-tools', 'AI-加氢站站点信息-complete'] },
|
||||
{ slug: 'oneos-web-data-analysis', title: '数据分析', dir: '数据分析' },
|
||||
{ slug: 'oneos-web-h2-station', title: '加氢站管理', dir: '加氢站管理', excludeDirs: ['export-tools', 'AI-加氢站站点信息-complete'], excludeFiles: ['站点信息.jsx'] },
|
||||
{ slug: 'oneos-web-data-analysis', title: '数据分析', dir: '数据分析', excludeFiles: ['业务部台账.jsx'] },
|
||||
{ slug: 'oneos-web-ledger-data', title: '台账数据', dir: '台账数据', excludeDirs: ['docs'] },
|
||||
{ slug: 'oneos-web-business', title: '业务管理', dir: '业务管理', excludeDirs: ['文档', 'export-tools', 'AI-保险采购-complete'] },
|
||||
{ slug: 'oneos-web-ops', title: '运维管理', dir: '运维管理' },
|
||||
@@ -81,6 +87,7 @@ function collectJsxFiles(module) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith('.jsx')) continue;
|
||||
if (module.excludeFiles?.includes(entry.name)) continue;
|
||||
results.push({
|
||||
rel,
|
||||
abs: full,
|
||||
@@ -223,6 +230,15 @@ function toComponentName(slug) {
|
||||
.join('');
|
||||
}
|
||||
|
||||
const WORKBENCH_SIDEBAR_ITEM = {
|
||||
id: 'item:prototypes:oneos-web-workbench',
|
||||
kind: 'item',
|
||||
title: '工作台',
|
||||
itemKey: 'prototypes/oneos-web-workbench',
|
||||
};
|
||||
|
||||
const ONEOS_NAV_FOLDER_ID = 'folder-1782874576229-r8atbu';
|
||||
|
||||
function toSidebarItem(module) {
|
||||
return {
|
||||
id: `item:prototypes:${module.slug}`,
|
||||
@@ -232,6 +248,17 @@ function toSidebarItem(module) {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureWorkbenchInOneOsRoot(prototypes) {
|
||||
for (const item of prototypes) {
|
||||
if (item?.id !== ONEOS_NAV_FOLDER_ID || item.kind !== 'folder' || !Array.isArray(item.children)) {
|
||||
continue;
|
||||
}
|
||||
const rest = item.children.filter((child) => child.itemKey !== WORKBENCH_SIDEBAR_ITEM.itemKey);
|
||||
item.children = [WORKBENCH_SIDEBAR_ITEM, ...rest];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSidebar(webModules, xllModules) {
|
||||
const sidebar = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
|
||||
const prefix = 'item:prototypes:oneos-web-';
|
||||
@@ -244,6 +271,10 @@ function updateSidebar(webModules, xllModules) {
|
||||
&& !String(item.itemKey).startsWith('prototypes/oneos-web-'),
|
||||
);
|
||||
|
||||
ensureWorkbenchInOneOsRoot(existing);
|
||||
|
||||
const webModulesForFolder = webModules.filter((module) => module.slug !== 'oneos-web-workbench');
|
||||
|
||||
const xllFolder = {
|
||||
id: 'folder-prototypes-xll-miniapp',
|
||||
kind: 'folder',
|
||||
@@ -256,7 +287,7 @@ function updateSidebar(webModules, xllModules) {
|
||||
kind: 'folder',
|
||||
title: 'ONE-OS Web 端',
|
||||
defaultExpanded: true,
|
||||
children: webModules.map(toSidebarItem),
|
||||
children: webModulesForFolder.map(toSidebarItem),
|
||||
};
|
||||
|
||||
sidebar.prototypes = [...existing, xllFolder, oneosFolder];
|
||||
@@ -270,7 +301,7 @@ function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const allModules = [...XLL_MODULES, ...MODULES];
|
||||
const allModules = [...XLL_MODULES, ...STANDALONE_MODULES, ...MODULES];
|
||||
|
||||
for (const module of allModules) {
|
||||
if (module.skipCodegen) {
|
||||
|
||||
Reference in New Issue
Block a user