185 lines
10 KiB
JavaScript
185 lines
10 KiB
JavaScript
// Recover only literal locale data from the retained REA source; never execute target scripts.
|
|
import fs from 'node:fs'
|
|
import crypto from 'node:crypto'
|
|
import { parse } from '@babel/parser'
|
|
|
|
const EVIDENCE_ID = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
|
const SOURCE_SHA256 = '6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'
|
|
const BUNDLE_PATH = '.tmp/rea-runtime-scripts.json'
|
|
const OUTPUT_PATH = 'src/renderer/src/workbuddy/conversation/source-labels.ts'
|
|
const MANIFEST_PATH = '.tmp/rea-modern-conversation-labels-manifest.json'
|
|
|
|
const bundle = JSON.parse(fs.readFileSync(BUNDLE_PATH, 'utf8'))
|
|
if (bundle.evidence_id !== EVIDENCE_ID) throw new Error(`Unexpected REA Evidence: ${bundle.evidence_id}`)
|
|
const sourceItems = bundle.items.filter((item) => item.file_path?.replaceAll('\\', '/').endsWith('/renderer/assets/src-DbE1_mgM.js'))
|
|
if (sourceItems.length !== 1 || !sourceItems[0].source?.included) throw new Error('Expected exactly one included REA source artifact src-DbE1_mgM.js')
|
|
const artifact = sourceItems[0].source.artifact
|
|
const source = artifact.text
|
|
const digest = crypto.createHash('sha256').update(source).digest('hex')
|
|
if (artifact.sha256 !== SOURCE_SHA256 || digest !== SOURCE_SHA256) throw new Error(`REA source identity mismatch: ${digest}`)
|
|
|
|
const ast = parse(source, { sourceType: 'unambiguous', errorRecovery: true, plugins: ['jsx', 'decorators-legacy'] })
|
|
if (ast.errors.length) throw new Error(`Babel reported parse errors: ${ast.errors.map((error) => error.message).join('; ')}`)
|
|
|
|
const parents = new WeakMap()
|
|
const objectAssignments = []
|
|
function visit(node, parent) {
|
|
if (!node || typeof node !== 'object' || typeof node.type !== 'string') return
|
|
if (parent) parents.set(node, parent)
|
|
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && node.right.type === 'ObjectExpression') {
|
|
objectAssignments.push({ binding: node.left.name, object: node.right, assignment: node })
|
|
}
|
|
for (const value of Object.values(node)) {
|
|
if (Array.isArray(value)) value.forEach((child) => visit(child, node))
|
|
else if (value && typeof value === 'object' && typeof value.type === 'string') visit(value, node)
|
|
}
|
|
}
|
|
visit(ast.program)
|
|
|
|
function staticKey(node) {
|
|
if (node?.type === 'Identifier') return node.name
|
|
if (node?.type === 'StringLiteral') return node.value
|
|
return undefined
|
|
}
|
|
|
|
function staticString(node) {
|
|
if (node?.type === 'StringLiteral') return node.value
|
|
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) return node.quasis[0].value.cooked
|
|
return undefined
|
|
}
|
|
|
|
function propertiesOf(object) {
|
|
return new Map(object.properties
|
|
.filter((property) => property.type === 'ObjectProperty')
|
|
.map((property) => [staticKey(property.key), property.value])
|
|
.filter(([key]) => key !== undefined))
|
|
}
|
|
|
|
const englishAnchors = {
|
|
'tool.command.confirm': 'Run this command?',
|
|
'tool.command.run': 'Run',
|
|
'tool.command.skip': 'Skip',
|
|
'tool.command.reject': 'Reject',
|
|
'tool.writeFile.editing': 'Editing',
|
|
'tool.writeFile.create': 'Create',
|
|
'tool.writeFile.accept': 'Accept',
|
|
'fold.completedWithDuration': 'Completed {duration}',
|
|
'chatInput.modelSelector.thinkingMode': 'Thinking mode'
|
|
}
|
|
const chineseAnchors = {
|
|
'tool.command.confirm': '确认执行命令?',
|
|
'tool.command.run': '运行',
|
|
'tool.command.skip': '跳过',
|
|
'tool.command.reject': '拒绝',
|
|
'tool.writeFile.editing': '修改中',
|
|
'tool.writeFile.create': '创建',
|
|
'tool.writeFile.accept': '接受',
|
|
'fold.completedWithDuration': '已完成 {duration}',
|
|
'chatInput.modelSelector.thinkingMode': '思考模式'
|
|
}
|
|
|
|
function anchoredAssignment(binding, anchors) {
|
|
const candidates = objectAssignments.filter((record) => {
|
|
if (record.binding !== binding) return false
|
|
const properties = propertiesOf(record.object)
|
|
return Object.entries(anchors).every(([key, value]) => staticString(properties.get(key)) === value)
|
|
})
|
|
if (candidates.length !== 1) throw new Error(`Expected one ${binding} locale object matching its literal anchors, found ${candidates.length}`)
|
|
return candidates[0]
|
|
}
|
|
|
|
const localeMappings = []
|
|
for (const record of objectAssignments) {
|
|
const properties = propertiesOf(record.object)
|
|
const simplified = properties.get('zh-CN')
|
|
const english = properties.get('en-US')
|
|
if (simplified?.type !== 'Identifier' || english?.type !== 'Identifier') continue
|
|
try {
|
|
const zh = anchoredAssignment(simplified.name, chineseAnchors)
|
|
const en = anchoredAssignment(english.name, englishAnchors)
|
|
localeMappings.push({ record, properties, zh, en })
|
|
} catch {
|
|
// Other locale-shaped objects in the bundle are unrelated to these source anchors.
|
|
}
|
|
}
|
|
if (localeMappings.length !== 1) throw new Error(`Expected one locale map bound to the anchored zh-CN/en-US objects, found ${localeMappings.length}`)
|
|
const { record: localeMap, properties: localeProperties, zh: zhAssignment, en: enAssignment } = localeMappings[0]
|
|
|
|
let defaultLocale
|
|
let defaultLocaleBinding
|
|
let cursor = parents.get(localeMap.object)
|
|
while (cursor && cursor.type !== 'SequenceExpression') cursor = parents.get(cursor)
|
|
if (cursor) {
|
|
const mapExpression = parents.get(localeMap.object)
|
|
const mapIndex = cursor.expressions.indexOf(mapExpression)
|
|
const precedingLocale = cursor.expressions.slice(0, mapIndex).findLast((expression) =>
|
|
expression.type === 'AssignmentExpression' && expression.left.type === 'Identifier' && staticString(expression.right) === 'zh-CN'
|
|
)
|
|
if (precedingLocale) {
|
|
defaultLocale = staticString(precedingLocale.right)
|
|
defaultLocaleBinding = precedingLocale.left.name
|
|
}
|
|
}
|
|
if (defaultLocale !== 'zh-CN') throw new Error(`Could not verify zh-CN from the source locale initializer; found ${defaultLocale ?? 'none'}`)
|
|
if (localeProperties.get(defaultLocale)?.name !== zhAssignment.binding) throw new Error('Source default locale is not bound to the anchored zh-CN dictionary')
|
|
|
|
function readDictionary(record, inherited = new Map()) {
|
|
const dictionary = new Map(inherited)
|
|
for (const property of record.object.properties) {
|
|
if (property.type === 'SpreadElement') {
|
|
if (property.argument.type !== 'Identifier' || property.argument.name !== enAssignment.binding) {
|
|
throw new Error(`Unsupported locale dictionary spread at source offset ${property.start}`)
|
|
}
|
|
for (const [key, value] of readDictionary(enAssignment)) dictionary.set(key, value)
|
|
continue
|
|
}
|
|
if (property.type !== 'ObjectProperty') throw new Error(`Unsupported locale property at source offset ${property.start}`)
|
|
const key = staticKey(property.key)
|
|
const value = staticString(property.value)
|
|
if (key === undefined || value === undefined) throw new Error(`Non-literal locale entry at source offset ${property.start}`)
|
|
dictionary.set(key, value)
|
|
}
|
|
return dictionary
|
|
}
|
|
|
|
const enUS = readDictionary(enAssignment)
|
|
const zhCN = readDictionary(zhAssignment)
|
|
if (enUS.size !== 971 || zhCN.size !== 971) throw new Error(`Unexpected key count: en-US=${enUS.size}, zh-CN=${zhCN.size}`)
|
|
const englishKeys = [...enUS.keys()].sort()
|
|
const chineseKeys = [...zhCN.keys()].sort()
|
|
if (englishKeys.some((key, index) => key !== chineseKeys[index])) throw new Error('zh-CN and en-US locale key sets differ')
|
|
for (const [key, expected] of Object.entries(englishAnchors)) {
|
|
if (enUS.get(key) !== expected) throw new Error(`English anchor changed: ${key}`)
|
|
}
|
|
for (const [key, expected] of Object.entries(chineseAnchors)) {
|
|
if (zhCN.get(key) !== expected) throw new Error(`Chinese anchor changed: ${key}`)
|
|
}
|
|
|
|
const enObject = Object.fromEntries(enUS)
|
|
const zhObject = Object.fromEntries(zhCN)
|
|
const sourceComment = `// Generated from REA ${EVIDENCE_ID}; source SHA-256 ${SOURCE_SHA256}.\n// Locale map ${localeMap.binding} at UTF-16 offset ${localeMap.object.start}; en-US ${enAssignment.binding} at ${enAssignment.object.start}; zh-CN ${zhAssignment.binding} at ${zhAssignment.object.start}.\n// The source default is ${defaultLocale} (${defaultLocaleBinding}); fallback order is requested locale, en-US, zh-CN, then the key.\n`
|
|
const output = `${sourceComment}const enUS = ${JSON.stringify(enObject, null, 2)} as const\nconst zhCN = ${JSON.stringify(zhObject, null, 2)} as const\n\nexport const SOURCE_CONVERSATION_DEFAULT_LOCALE = '${defaultLocale}' as const\nexport const SOURCE_CONVERSATION_LOCALES = ['zh-CN', 'en-US'] as const\nexport type SourceConversationLocale = typeof SOURCE_CONVERSATION_LOCALES[number]\nexport type SourceConversationLabelKey = keyof typeof enUS & keyof typeof zhCN\n\nexport function sourceConversationLabel(\n key: SourceConversationLabelKey,\n params?: Readonly<Record<string, string | number>>,\n locale: SourceConversationLocale = SOURCE_CONVERSATION_DEFAULT_LOCALE\n): string {\n const localized = locale === 'en-US' ? enUS[key] : zhCN[key]\n const template = localized ?? enUS[key] ?? zhCN[key] ?? key\n if (!params) return template\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = params[name]\n return value == null ? match : String(value)\n })\n}\n`
|
|
fs.writeFileSync(OUTPUT_PATH, output)
|
|
|
|
const manifest = {
|
|
evidenceId: EVIDENCE_ID,
|
|
artifact: 'renderer/assets/src-DbE1_mgM.js',
|
|
sha256: SOURCE_SHA256,
|
|
parser: 'Babel AST; source was not executed',
|
|
sourceOffsetsUtf16: {
|
|
englishDictionary: { binding: enAssignment.binding, start: enAssignment.object.start, endExclusive: enAssignment.object.end },
|
|
simplifiedChineseDictionary: { binding: zhAssignment.binding, start: zhAssignment.object.start, endExclusive: zhAssignment.object.end },
|
|
localeMap: { binding: localeMap.binding, start: localeMap.object.start, endExclusive: localeMap.object.end },
|
|
defaultLocale: { binding: defaultLocaleBinding, value: defaultLocale }
|
|
},
|
|
localeBindings: { 'zh-CN': zhAssignment.binding, 'en-US': enAssignment.binding },
|
|
keyCount: enUS.size,
|
|
matchingKeySets: true,
|
|
anchors: Object.fromEntries(Object.keys(englishAnchors).map((key) => [key, { 'en-US': enUS.get(key), 'zh-CN': zhCN.get(key) }])),
|
|
interpolation: { sourcePattern: String.raw`\{(\w+)\}`, missingParameter: 'preserve placeholder' },
|
|
fallbackOrder: ['requested locale', 'en-US', 'zh-CN', 'key'],
|
|
generatedFile: OUTPUT_PATH
|
|
}
|
|
fs.writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
console.log(JSON.stringify({ generatedFile: OUTPUT_PATH, manifest: MANIFEST_PATH, evidenceId: EVIDENCE_ID, sha256: SOURCE_SHA256, sourceOffsetsUtf16: manifest.sourceOffsetsUtf16, keyCount: enUS.size }))
|