/** * 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); });