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,87 @@
|
||||
// Import an inert conversation from REA Evidence. Never reads WorkBuddy storage directly.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const evidencePath = args[0]
|
||||
const destination = args[1]
|
||||
const title = args[2] || '开始新会话'
|
||||
if (!evidencePath || !destination || !path.isAbsolute(destination)) {
|
||||
throw new Error('Usage: node scripts/import-workbuddy-session.mjs <REA export> <absolute YCode sessions.json> [title] [--apply]')
|
||||
}
|
||||
const evidence = JSON.parse((await fs.readFile(evidencePath, 'utf8')).replace(/^\uFEFF/, ''))
|
||||
if (evidence.operation !== 'export_selected_conversation_text' || evidence.provider?.id !== 'rea-selected-conversation-text' ||
|
||||
!/^ev_[a-f0-9]{64}$/.test(evidence.evidence_id)) throw new Error('Expected a REA selected transcript Evidence')
|
||||
const source = evidence.normalized_result.file
|
||||
const sha = data => createHash('sha256').update(data).digest('hex')
|
||||
if (sha(Buffer.from(source.text)) !== source.sha256 || Buffer.byteLength(source.text) !== source.bytes) {
|
||||
throw new Error('REA transcript digest/byte count does not match')
|
||||
}
|
||||
await build({ entryPoints: ['src/shared/workbuddy-import.ts'], bundle: true, platform: 'node', format: 'esm',
|
||||
outfile: '.tmp/workbuddy-import-converter.mjs', logLevel: 'silent' })
|
||||
const { convertWorkBuddyJsonl } = await import(pathToFileURL(path.resolve('.tmp/workbuddy-import-converter.mjs')).href)
|
||||
const originalBytes = await fs.readFile(destination)
|
||||
const existing = JSON.parse(originalBytes.toString('utf8').replace(/^\uFEFF/, ''))
|
||||
if (!Array.isArray(existing.sessions)) throw new Error('Invalid YCode session store')
|
||||
if (existing.sessions.some(session => session.messages?.some(message => message.status === 'streaming'))) {
|
||||
throw new Error('YCode has unfinished persisted messages; finish the active run before offline import')
|
||||
}
|
||||
const sourceId = evidence.normalized_result.session_id
|
||||
const duplicate = existing.sessions.find(session => session.importProvenance?.sourceSessionId === sourceId)
|
||||
if (duplicate) {
|
||||
console.log(JSON.stringify({ status: 'already-imported', sessionId: duplicate.id, title: duplicate.title }))
|
||||
process.exit(0)
|
||||
}
|
||||
const importedAt = Date.now()
|
||||
const result = convertWorkBuddyJsonl(source.text, { sessionId: randomUUID(), title,
|
||||
sourceSessionId: sourceId, evidenceId: evidence.evidence_id, sourceSha256: source.sha256,
|
||||
importedAt, branchMode: 'visible' })
|
||||
const exportedImages = new Map((evidence.normalized_result.attachments ?? []).map(image => {
|
||||
const bytes = Buffer.from(image.data_base64, 'base64')
|
||||
if (sha(bytes) !== image.sha256 || image.sha256 !== image.blob_id || bytes.length !== image.bytes ||
|
||||
!['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mime)) throw new Error('Invalid REA image artifact')
|
||||
return [image.blob_id, image]
|
||||
}))
|
||||
let copiedImages = 0
|
||||
for (const reference of result.provenance.referencedAttachments ?? []) {
|
||||
const attachment = result.messages.find(message => message.id === reference.messageId)?.attachments?.find(item => item.id === reference.attachmentId)
|
||||
const image = exportedImages.get(reference.sourceBlobId)
|
||||
if (!attachment || !image) throw new Error('A referenced image is unavailable in the REA export; no import performed')
|
||||
attachment.previewUrl = `data:${image.mime};base64,${image.data_base64}`
|
||||
copiedImages++
|
||||
}
|
||||
if (copiedImages === result.provenance.referencedAttachments.length) {
|
||||
result.warnings = result.warnings.filter(warning => !warning.startsWith('Image attachment references were retained'))
|
||||
}
|
||||
const session = { ...result.meta, updatedAt: importedAt, messages: result.messages, importProvenance: result.provenance }
|
||||
await fs.writeFile('.tmp/workbuddy-copy-session.json', JSON.stringify(session, null, 2))
|
||||
const report = { applied: false, title, sessionId: session.id, sourceSessionId: sourceId,
|
||||
evidenceId: evidence.evidence_id, sourceSha256: source.sha256, stats: { ...result.stats, copiedImages }, warnings: result.warnings,
|
||||
existingSessionCount: existing.sessions.length, destination }
|
||||
if (args.includes('--apply')) {
|
||||
if (process.platform === 'win32') {
|
||||
const executable = path.resolve('node_modules/electron/dist/electron.exe').replaceAll("'", "''")
|
||||
const processIds = execFileSync('powershell.exe', ['-NoProfile', '-Command',
|
||||
`Get-CimInstance Win32_Process -Filter "Name='electron.exe'" | Where-Object { $_.ExecutablePath -eq '${executable}' -and $_.CommandLine -notmatch '--type=' -and ($_.CommandLine -match '\\s\\.\\s*$' -or $_.CommandLine -match 'out[\\\\/]main') } | Select-Object -ExpandProperty ProcessId`],
|
||||
{ encoding: 'utf8', windowsHide: true }).trim()
|
||||
if (processIds) throw new Error('Close the YCode app before applying an offline import so its main-process cache cannot overwrite the copy')
|
||||
}
|
||||
const currentBytes = await fs.readFile(destination)
|
||||
if (sha(currentBytes) !== sha(originalBytes)) throw new Error('YCode store changed during import; no writes performed')
|
||||
const backup = `${destination}.before-workbuddy-${importedAt}.bak`
|
||||
await fs.copyFile(destination, backup, 1)
|
||||
const expected = { ...existing, sessions: [...existing.sessions, session] }
|
||||
const temp = `${destination}.import-${randomUUID()}.tmp`
|
||||
await fs.writeFile(temp, JSON.stringify(expected, null, 2))
|
||||
await fs.rename(temp, destination)
|
||||
const restored = JSON.parse(await fs.readFile(destination, 'utf8'))
|
||||
if (JSON.stringify(restored) !== JSON.stringify(expected)) throw new Error(`Import readback failed; backup: ${backup}`)
|
||||
report.applied = true
|
||||
report.backup = backup
|
||||
}
|
||||
await fs.writeFile('.tmp/workbuddy-copy-report.json', JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report))
|
||||
Reference in new issue
Block a user