Files
2026-06-30 10:33:56 +08:00

198 lines
8.9 KiB
JavaScript

/**
* Sync Stitch shared project "4端1.0版本" prototypes to pages/
* Usage: STITCH_API_KEY=xxx node scripts/sync-stitch.mjs [--force]
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const PAGES = path.join(ROOT, 'pages');
const PROJECT_ID = process.env.STITCH_PROJECT_ID || '9513024892894556686';
const FORCE = process.argv.includes('--force');
const API_KEY =
process.env.STITCH_API_KEY ||
(() => {
try {
const mcp = JSON.parse(fs.readFileSync(path.join(process.env.USERPROFILE || '', '.cursor', 'mcp.json'), 'utf8'));
return mcp?.mcpServers?.stitch?.headers?.['X-Goog-Api-Key'];
} catch {
return null;
}
})();
if (!API_KEY) {
console.error('Missing STITCH_API_KEY');
process.exit(1);
}
async function mcpCall(tool, args = {}) {
const res = await fetch('https://stitch.googleapis.com/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': API_KEY },
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: { name: tool, arguments: args },
}),
});
const json = await res.json();
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
if (json.result?.isError) throw new Error(json.result.content?.[0]?.text || 'MCP error');
return json.result.structuredContent ?? JSON.parse(json.result.content?.[0]?.text || '{}');
}
async function download(url, dest) {
if (!url) return false;
const res = await fetch(url);
if (!res.ok) return false;
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
return true;
}
function classify(title) {
const t = title || '';
if (/\.md$|logo\.png|全案交付|Stitch提示词/i.test(t)) return 'asset';
if (/总部|开城管理|开城页面|城市参数|商品管理页|拦截配送|数据报表|预警详情|补发处理页面|跨城发货处理|财务结算中心|财务对账|合同管理|子账号|我的员工|推广码|数据周报|总部管理|在线客服对话页.*总部|客服中心.*跨城|申请打款|资产明细|账单明细详情|超时订单|订单售后详情.*补发与退款|补发订单详情|添加商品|编辑城市页面|门店审核管理|门店审核记录|添加子账号|新增开城|生成推广码|推广码详情|推广码管理|合伙人贡献榜|合伙人待确认账单.*(?!合伙人中心)/i.test(t)) {
if (/合伙人待确认账单|合伙人中心|合伙人订单|录入新门店|合伙人门店|合伙人工作台|城市合伙人登录|城市合伙人快捷|城市合伙人订单/.test(t)) {
// keep partner-specific
} else if (/总部|开城|拦截|数据报表|预警|补发处理页|跨城发货|财务结算|财务对账|合同|子账号|我的员工|推广码|数据周报|申请打款|资产明细|账单明细|超时订单|添加商品|编辑城市|门店审核管理|门店审核记录|添加子账号|新增开城|商品管理页/.test(t)) {
return 'skip';
}
}
if (/总部|开城管理|拦截配送管理|数据报表中心|预警详情|补发处理页面|跨城发货处理|财务结算中心|财务对账|合同管理|子账号|我的员工|推广码管理|数据周报|总部管理|申请打款|资产明细|账单明细详情|超时订单详情|添加商品页面|编辑城市页面|门店审核管理|门店审核记录|添加子账号|新增开城|生成推广码|推广码详情|合伙人贡献榜|商品管理页|订单中心管理页|订单售后详情|补发订单详情|在线客服对话页|总部客服|总部端订单|总部管理中心|总部管理登录|总部管理一键|拦截配送|城市参数配置/i.test(t)) {
return 'skip';
}
if (/门店端|门店管理|S-0|门店核销|门店营业|门店登录|门店管理登录|门店管理首页|门店管理一键|门店核销成功|门店核销确认|门店核销记录/i.test(t) && !/合伙人门店|录入新门店/.test(t)) {
return 'shop';
}
if (/合伙人|录入新门店|城市合伙人/i.test(t)) return 'partner';
if (/C端|小程序|用户登录|商品详情|确认订单|地址|我的订单|订单详情|好客权益|个人中心|餐券核销|核销码|可用门店|门店详情|微信支付|定位授权|微信|核销成功及评价|核销成功.*评价/i.test(t)) {
return 'user';
}
if (/门店详情页$/.test(t) && !/编辑/.test(t)) return 'user';
if (/门店详情编辑/.test(t)) return 'partner';
return 'skip';
}
function slug(title, screenId) {
const base = (title || 'screen')
.replace(/[^\w\u4e00-\u9fa5]+/g, '_')
.replace(/^_|_$/g, '')
.slice(0, 40);
return `${base}_${screenId.slice(0, 8)}`;
}
async function main() {
console.log('Fetching project...');
const project = await mcpCall('get_project', { name: `projects/${PROJECT_ID}` });
const designMd = project.designTheme?.designMd;
if (designMd) {
const designPath = path.join(PAGES, 'stitch_4_1.0', 'DESIGN.md');
fs.mkdirSync(path.dirname(designPath), { recursive: true });
fs.writeFileSync(designPath, designMd, 'utf8');
console.log('Updated DESIGN.md');
}
console.log('Listing screens...');
const { screens = [] } = await mcpCall('list_screens', { projectId: PROJECT_ID });
console.log(`Found ${screens.length} screens`);
const manifest = {
projectId: PROJECT_ID,
projectTitle: '4端1.0版本',
syncedAt: new Date().toISOString(),
screens: [],
};
let idx = { user: 0, shop: 0, partner: 0, skip: 0, asset: 0 };
for (const screen of screens) {
const screenId = screen.name?.replace(/.*\/screens\//, '') || screen.screenId;
const title = screen.title || screen.displayName || '';
const category = classify(title);
idx[category] = (idx[category] || 0) + 1;
const entry = {
screenId,
title,
category,
route: null,
priority: category === 'skip' || category === 'asset' ? 'skip' : 'P1',
};
if (category === 'asset') {
const assetDir = path.join(PAGES, '_assets');
fs.mkdirSync(assetDir, { recursive: true });
manifest.screens.push(entry);
continue;
}
if (category === 'skip') {
const archiveDir = path.join(PAGES, '_archive', slug(title, screenId));
fs.mkdirSync(archiveDir, { recursive: true });
entry.dir = path.relative(ROOT, archiveDir);
manifest.screens.push(entry);
if (FORCE || !fs.existsSync(path.join(archiveDir, 'meta.json'))) {
try {
const detail = await mcpCall('get_screen', { name: `projects/${PROJECT_ID}/screens/${screenId}`, projectId: PROJECT_ID, screenId });
fs.writeFileSync(path.join(archiveDir, 'meta.json'), JSON.stringify({ title, screenId, syncedAt: new Date().toISOString() }, null, 2));
if (detail.htmlCode?.downloadUrl) await download(detail.htmlCode.downloadUrl, path.join(archiveDir, 'code.html'));
if (detail.screenshot?.downloadUrl) await download(detail.screenshot.downloadUrl, path.join(archiveDir, 'screen.png'));
} catch (e) {
console.warn(` skip download failed: ${title}`, e.message);
}
}
continue;
}
const num = String(++idx[category]).padStart(2, '0');
const dir = path.join(PAGES, category, `${num}_${slug(title, screenId)}`);
fs.mkdirSync(dir, { recursive: true });
entry.dir = path.relative(ROOT, dir);
const metaPath = path.join(dir, 'meta.json');
if (!FORCE && fs.existsSync(metaPath) && fs.existsSync(path.join(dir, 'code.html'))) {
manifest.screens.push(entry);
continue;
}
try {
const detail = await mcpCall('get_screen', {
name: `projects/${PROJECT_ID}/screens/${screenId}`,
projectId: PROJECT_ID,
screenId,
});
const meta = { title, screenId, category, syncedAt: new Date().toISOString() };
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
if (detail.htmlCode?.downloadUrl) {
const ok = await download(detail.htmlCode.downloadUrl, path.join(dir, 'code.html'));
if (!ok) console.warn(` HTML download failed: ${title}`);
}
if (detail.screenshot?.downloadUrl) {
await download(detail.screenshot.downloadUrl, path.join(dir, 'screen.png'));
}
console.log(` [${category}] ${title}`);
} catch (e) {
console.warn(` Failed: ${title}`, e.message);
}
manifest.screens.push(entry);
}
fs.writeFileSync(path.join(PAGES, 'SCREEN_MAP.json'), JSON.stringify(manifest, null, 2), 'utf8');
console.log('\nDone.');
console.log(` user: ${manifest.screens.filter((s) => s.category === 'user').length}`);
console.log(` shop: ${manifest.screens.filter((s) => s.category === 'shop').length}`);
console.log(` partner: ${manifest.screens.filter((s) => s.category === 'partner').length}`);
console.log(` skip/archive: ${manifest.screens.filter((s) => s.category === 'skip').length}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});