chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,240 @@
|
||||
import './wb-legacy-disabled.mjs'
|
||||
|
||||
/**
|
||||
* WorkBuddy 运行时样式采集(CDP)。
|
||||
*
|
||||
* 用途:拉取真实 WorkBuddy 界面的元素矩形与 computed style,作为 YCode 视觉对齐依据。
|
||||
* 只读页面,不发送消息、不执行工具、不读取 cookies / localStorage。
|
||||
*
|
||||
* 用法:node scripts/wb-capture.mjs <port> <outDir>
|
||||
*/
|
||||
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const port = Number(process.argv[2] ?? 9223)
|
||||
const outDir = process.argv[3] ?? 'docs/workbuddy-capture'
|
||||
|
||||
/** 要采集的元素:侧栏、对话区、输入区 */
|
||||
const SELECTORS = [
|
||||
// 布局骨架
|
||||
'body',
|
||||
'#root',
|
||||
'body > div',
|
||||
'[class*="app-shell"]',
|
||||
'[class*="workbench"]',
|
||||
'[class*="layout"]',
|
||||
// 左侧栏
|
||||
'.cb-sidebar-nav',
|
||||
'.cb-sidebar-nav__header',
|
||||
'.cb-sidebar-nav__header-title',
|
||||
'.cb-sidebar-nav__header-subtitle',
|
||||
'.cb-sidebar-nav__content',
|
||||
'.cb-sidebar-nav__section',
|
||||
'.cb-sidebar-nav__section-title',
|
||||
'.cb-sidebar-nav__item',
|
||||
'.cb-sidebar-nav__item--selected',
|
||||
'.cb-sidebar-nav__item-icon',
|
||||
'.cb-sidebar-nav__item-title',
|
||||
'.cb-sidebar-nav__item-subtitle',
|
||||
'.cb-sidebar-nav__item-badge',
|
||||
'.cb-sidebar-nav__footer',
|
||||
'[class*="sidebar"]',
|
||||
// 对话区
|
||||
'[class*="conversation"]',
|
||||
'[class*="message-list"]',
|
||||
'.cr-message-list',
|
||||
'.cr-message-list__content',
|
||||
'.cr-agent',
|
||||
'.cr-agent__header',
|
||||
'.cr-self-message',
|
||||
'.cr-self-bubble',
|
||||
'.cr-markdown',
|
||||
'.cr-markdown p',
|
||||
'.cr-collapse',
|
||||
'.cr-collapse__header',
|
||||
// 输入区
|
||||
'.cr-input-box',
|
||||
'.cr-input-box__main',
|
||||
'.cr-input-container',
|
||||
'.cr-input-editor-host',
|
||||
'.cr-input-toolbar',
|
||||
'.cr-input-toolbar__right',
|
||||
'.cr-input-footer',
|
||||
'.cr-send-button',
|
||||
'[class*="input-container"]',
|
||||
'[class*="input-area-container"]',
|
||||
// 右栏 / 面板
|
||||
'.detail-sidebar',
|
||||
'[class*="detail-panel"]',
|
||||
'[class*="artifact"]'
|
||||
]
|
||||
|
||||
/** 精选属性,避免一次性导出 300+ 属性难以阅读 */
|
||||
const PROPS = [
|
||||
'display',
|
||||
'position',
|
||||
'flex-direction',
|
||||
'flex-grow',
|
||||
'flex-shrink',
|
||||
'align-items',
|
||||
'justify-content',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'padding-top',
|
||||
'padding-right',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'margin-top',
|
||||
'margin-bottom',
|
||||
'margin-left',
|
||||
'margin-right',
|
||||
'gap',
|
||||
'row-gap',
|
||||
'column-gap',
|
||||
'background-color',
|
||||
'background-image',
|
||||
'color',
|
||||
'border-top-width',
|
||||
'border-top-color',
|
||||
'border-radius',
|
||||
'box-shadow',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-weight',
|
||||
'line-height',
|
||||
'letter-spacing',
|
||||
'text-transform',
|
||||
'opacity',
|
||||
'overflow-y',
|
||||
'z-index'
|
||||
]
|
||||
|
||||
async function main() {
|
||||
const listRes = await fetch(`http://127.0.0.1:${port}/json/list`)
|
||||
const targets = await listRes.json()
|
||||
const pages = targets.filter((t) => t.type === 'page')
|
||||
if (pages.length === 0) {
|
||||
console.error('未找到 page 目标:', targets.map((t) => `${t.type}`).join(', '))
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('页面目标:')
|
||||
for (const p of pages) console.log(` - ${p.title} | ${p.url.split('?')[0].slice(0, 90)}`)
|
||||
|
||||
// 优先主对话窗口(排除 search 等辅助窗口)
|
||||
const main = pages.find((p) => !/windowKind=/.test(p.url)) ?? pages[0]
|
||||
console.log('选定目标:', main.title)
|
||||
|
||||
const ws = new WebSocket(main.webSocketDebuggerUrl)
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
const send = (method, params = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const msgId = ++id
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(msgId)
|
||||
reject(new Error(`CDP 超时: ${method}`))
|
||||
}, 20000)
|
||||
pending.set(msgId, (v) => {
|
||||
clearTimeout(timer)
|
||||
resolve(v)
|
||||
})
|
||||
ws.send(JSON.stringify({ id: msgId, method, params }))
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('WebSocket 连接超时')), 10000)
|
||||
ws.addEventListener('open', () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
})
|
||||
ws.addEventListener('error', (e) => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error(`WebSocket 错误: ${e?.message ?? 'unknown'}`))
|
||||
})
|
||||
})
|
||||
ws.addEventListener('message', (ev) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
pending.get(msg.id)(msg.result)
|
||||
pending.delete(msg.id)
|
||||
}
|
||||
})
|
||||
console.log('CDP 已连接')
|
||||
|
||||
const expression = `(() => {
|
||||
const SELECTORS = ${JSON.stringify(SELECTORS)};
|
||||
const PROPS = ${JSON.stringify(PROPS)};
|
||||
const clean = (v) => (v == null ? null : String(v).replace(/\\s+/g, ' ').trim().slice(0, 220));
|
||||
const pick = (el) => {
|
||||
const cs = getComputedStyle(el);
|
||||
const out = {};
|
||||
for (const p of PROPS) {
|
||||
const v = clean(cs.getPropertyValue(p));
|
||||
if (v && v !== 'none' && v !== 'normal' && v !== 'auto' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') out[p] = v;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const rect = (el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
|
||||
};
|
||||
const result = {};
|
||||
for (const sel of SELECTORS) {
|
||||
let nodes = [];
|
||||
try { nodes = [...document.querySelectorAll(sel)]; } catch { nodes = []; }
|
||||
if (nodes.length === 0) continue;
|
||||
result[sel] = {
|
||||
count: nodes.length,
|
||||
samples: nodes.slice(0, 3).map((el) => ({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
cls: clean(el.className).slice(0, 160),
|
||||
text: clean(el.textContent).slice(0, 120),
|
||||
rect: rect(el),
|
||||
styles: pick(el)
|
||||
}))
|
||||
};
|
||||
}
|
||||
// 顶层结构:从 body 往下三层,输出带尺寸的容器
|
||||
const tree = [];
|
||||
const walk = (el, depth) => {
|
||||
if (depth > 4 || tree.length > 120) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 8 || r.height < 8) return;
|
||||
tree.push({ depth, tag: el.tagName.toLowerCase(), cls: clean(el.className).slice(0, 90), rect: rect(el) });
|
||||
for (const c of el.children) walk(c, depth + 1);
|
||||
};
|
||||
for (const c of document.body.children) walk(c, 0);
|
||||
return JSON.stringify({
|
||||
title: document.title,
|
||||
viewport: { w: innerWidth, h: innerHeight, dpr: devicePixelRatio, zoom: (window.outerWidth / innerWidth).toFixed(3) },
|
||||
htmlAttrs: { theme: document.documentElement.dataset.theme || null, class: clean(document.documentElement.className) },
|
||||
bodyBg: getComputedStyle(document.body).backgroundColor,
|
||||
selectors: result,
|
||||
layoutTree: tree
|
||||
});
|
||||
})()`
|
||||
|
||||
const evaluated = await send('Runtime.evaluate', { expression, returnByValue: true })
|
||||
const data = JSON.parse(evaluated.result.value)
|
||||
|
||||
await mkdir(outDir, { recursive: true })
|
||||
await writeFile(join(outDir, 'styles.json'), JSON.stringify(data, null, 2), 'utf8')
|
||||
console.log('已写出 styles.json,命中选择器', Object.keys(data.selectors).length, '个')
|
||||
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' })
|
||||
if (shot?.data) {
|
||||
await writeFile(join(outDir, 'page.png'), Buffer.from(shot.data, 'base64'))
|
||||
console.log('已写出 page.png')
|
||||
}
|
||||
ws.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('采集失败:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in new issue
Block a user