198 lines
8.9 KiB
JavaScript
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);
|
|
});
|