feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+205
View File
@@ -0,0 +1,205 @@
import { promises as fs } from 'node:fs'
import { basename, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(scriptDir, '..', '..')
const tmpDir = join(repoRoot, '.tmp')
const evidenceFiles = {
home: join(tmpDir, 'rea-four-modules-library-stable-20261017.json'),
local: join(tmpDir, 'rea-four-modules-library-local-20261017.json')
}
const prefixes = [
'knowledge-sidebar',
'main-layout-content',
'workspace-tree',
'wb-home',
'doc-feed',
'wb-feed',
'my-files'
]
const styleKeys = [
'display',
'position',
'font-family',
'font-size',
'font-weight',
'line-height',
'color',
'background-color',
'border-radius',
'border-top-width',
'border-top-color',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'gap',
'align-items',
'justify-content',
'box-shadow',
'overflow',
'max-width',
'min-height',
'transition-property',
'transition-duration',
'transition-timing-function',
'opacity',
'transform'
]
function stripComments(source) {
return source.replace(/\/\*[\s\S]*?\*\//g, '')
}
function cssRules(source) {
const css = stripComments(source)
const stack = []
const rules = []
let boundary = 0
let quote = ''
let escaped = false
for (let index = 0; index < css.length; index += 1) {
const char = css[index]
if (quote) {
if (escaped) escaped = false
else if (char === '\\') escaped = true
else if (char === quote) quote = ''
continue
}
if (char === '"' || char === "'") {
quote = char
continue
}
if (char === '{') {
const prelude = css.slice(boundary, index).trim()
stack.push({ prelude, bodyStart: index + 1 })
boundary = index + 1
continue
}
if (char === '}') {
const entry = stack.pop()
if (entry) {
const body = css.slice(entry.bodyStart, index).trim()
if (body && !entry.prelude.startsWith('@')) {
rules.push({
context: stack.map((item) => item.prelude).filter((item) => item.startsWith('@')),
selector: entry.prelude,
body
})
}
}
boundary = index + 1
continue
}
if (char === ';' && stack.length === 0) boundary = index + 1
}
return rules
}
function matchesPrefix(selector) {
return prefixes.some((prefix) => selector.includes(prefix))
}
function relevantNode(node) {
const bounds = node.bounds ?? []
if (bounds.length < 4 || bounds[2] <= 0 || bounds[3] <= 0) return false
if (!node.attributes || !Object.keys(node.attributes).length) return false
return true
}
function layoutNode(node) {
const styles = node.styles ?? {}
return {
nodeIndex: node.node_index,
parentIndex: node.parent_index,
tag: node.node_name,
attributes: node.attributes,
bounds: node.bounds,
styles: Object.fromEntries(styleKeys.map((key) => [key, styles[key]]))
}
}
async function readEvidence(path) {
return JSON.parse(await fs.readFile(path, 'utf8'))
}
async function main() {
const evidence = {}
for (const [name, path] of Object.entries(evidenceFiles)) {
evidence[name] = await readEvidence(path)
}
const seenRules = new Set()
const recovered = []
const styleSources = new Map()
for (const [name, entry] of Object.entries(evidence)) {
for (const sheet of entry.normalized_result.styles.items ?? []) {
const text = sheet.source?.artifact?.text
if (!text || !prefixes.some((prefix) => text.includes(prefix))) continue
const sheetName = basename(sheet.file_path ?? 'inline.css')
styleSources.set(sheetName, {
path: sheet.file_path,
bytes: sheet.source.artifact.bytes,
sha256: sheet.source.artifact.sha256
})
for (const rule of cssRules(text)) {
if (!matchesPrefix(rule.selector)) continue
const key = `${rule.context.join('>')}|${rule.selector}|${rule.body}`
if (seenRules.has(key)) continue
seenRules.add(key)
recovered.push({
evidence: name,
sheet: sheetName,
...rule
})
}
}
}
const css = recovered
.map((rule) => {
const prefix = rule.context.length ? `${rule.context.join(' { ')} { ` : ''
const suffix = rule.context.length ? ` ${'}'.repeat(rule.context.length)}` : ''
return `/* ${rule.evidence}:${rule.sheet} */\n${prefix}${rule.selector} { ${rule.body} }${suffix}`
})
.join('\n\n')
const layout = {}
for (const [name, entry] of Object.entries(evidence)) {
layout[name] = (entry.normalized_result.visuals.layout.documents ?? [])
.flatMap((document) => document.nodes ?? [])
.filter(relevantNode)
.map(layoutNode)
}
await fs.mkdir(tmpDir, { recursive: true })
await Promise.all([
fs.writeFile(join(tmpDir, 'library-recovered-rules.css'), `${css}\n`, 'utf8'),
fs.writeFile(join(tmpDir, 'library-layout-summary.json'), JSON.stringify({
styleSources: Object.fromEntries(styleSources),
layout
}, null, 2), 'utf8')
])
process.stdout.write(JSON.stringify({
rules: recovered.length,
css: join(tmpDir, 'library-recovered-rules.css'),
layout: join(tmpDir, 'library-layout-summary.json'),
styleSources
}, null, 2))
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})