// Local-only documentation export. Never print or copy a device key into the repository. const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); const { pathToFileURL } = require('node:url'); async function main() { const input = path.resolve(process.argv[2] || ''); if (!process.argv[2] || path.basename(input) !== 'device-init.private.json') { throw Error('Provide the local device-init.private.json path'); } const repo = path.resolve(__dirname, '..'); const folder = path.dirname(input); const relative = path.relative(repo, folder); if (!relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)) { throw Error('Private export must be outside the repository'); } const init = JSON.parse(fs.readFileSync(input, 'utf8').replace(/^\uFEFF/, '')); if (init.factory_id !== 'KP01-020000260914' || init.origin !== 'https://82.157.201.114' || !/^[0-9a-f]{64}$/i.test(init.device_key_hex)) { throw Error('Unexpected private identity; values not printed'); } const fields = ['factory_id', 'model', 'origin', 'device_key_hex', 'key_version']; const values = Object.fromEntries(fields.map(key => [key, init[key]])); if (Object.values(values).some(value => value === undefined)) throw Error('Missing initialization field'); const source = path.join(repo, 'docs/embedded-device-business-guide-20260914.md'); const publicMarkdown = fs.readFileSync(source, 'utf8').replace(/\r\n/g, '\n'); if (publicMarkdown.includes(init.device_key_hex)) throw Error('Private key found in public source'); const sectionStart = publicMarkdown.indexOf('## 1. 设备需要烧录的字段'); const sectionEnd = publicMarkdown.indexOf('## 2. 蓝牙协议与名称约定'); if (sectionStart < 0 || sectionEnd < sectionStart) throw Error('Unexpected source structure'); const actualHeader = [ '# VoiceLink 设备完整接口文档(私有交接)', '', '2026-09-15 · 设备接口专用版 · 蓝牙配网、绑定、语音聊天、MQTT 心跳、OTA', '', '## 1. 实际需要烧录的内容', '', '以下 5 项是平台已登记的这台独立联调设备的实际值,不是占位符。只给一台联调设备使用;这是测试身份,不是测得的实物 MAC,也不是最终硬件型号。不同实物需分别在平台登记。', '', '```json', JSON.stringify(values, null, 2), '```', '', '**本文件含真实设备密钥,仅在本机/受控交接中使用,不上传公开仓库或公开网页。**', '', '| 烧录字段 | 中文解释与用途 |', '|---|---|', '| factory_id | 设备唯一编号;设备接口的 device_id、请求头 X-Device-ID 使用它 |', '| model | 平台登记的设备型号;广播名称和 OTA 型号核对使用它 |', '| origin | 平台 HTTPS 地址 |', '| device_key_hex | 解码成 32 字节设备密钥;用于现有 HTTP 请求签名,不是火山密钥 |', '| key_version | 上述设备密钥的版本,当前为 1 |', '', `本设备蓝牙名称:\`vl-${values.model}-${values.factory_id.slice(-8)}\`。由型号和设备编号拼接,不是额外烧录字段。`, '', '固件另外带实际运行的 `firmware_version` 和数字 `version_code`;未对应平台发布记录时顺序号填 0。它们属于固件构建信息。hardware_revision 仅是库存信息,新设备接口无需烧录或上报。', '', '不用烧录 Wi-Fi、账号、老人 ID、智能体 ID、火山凭据或 MQTT 密码。Wi-Fi 和 bind_token 由小程序发送;绑定成功后平台直接下发火山专属凭据;MQTT 配置联网获取。', '', '平台:`https://82.157.201.114`。聊天继续按每通 `session_id` 关联火山回调,不改成固定老人 ID。', '', '本文只保留设备直接参与的通信。阅读顺序:第 2–5 节配网绑定 → 附录 B 本地初始化 SDK(不请求平台登记)→ 第 6–7 节聊天 → 第 8–10 节 MQTT 心跳与 OTA。设备平台请求统一按附录 A 封装。示例时间、版本、任务 ID 和下载链接不能写死。', '', ].join('\n'); const cFolder = path.join(folder, 'device-protocol-c'); fs.mkdirSync(cFolder, { recursive: true }); const codeBlocks = ['vl_protocol.h', 'vl_protocol.c'].map(filename => { const code = fs.readFileSync(path.join(repo, 'docs/device-protocol-c', filename), 'utf8'); fs.writeFileSync(path.join(cFolder, filename), code); return '### ' + filename + '\n\n```c\n' + code.trimEnd() + '\n```\n'; }).join('\n'); const markdown = (actualHeader + '\n' + publicMarkdown.slice(sectionEnd)) .replace('', codeBlocks); const { marked } = await import(pathToFileURL(require.resolve('marked')).href); let jsonExamples = 0, tables = 0, headings = 0; marked.walkTokens(marked.lexer(markdown), token => { if (token.type === 'code' && token.lang === 'json') { try { JSON.parse(token.text); } catch { throw Error('Invalid JSON example; content not printed'); } jsonExamples++; } if (token.type === 'table') { tables++; if (token.rows.some(row => row.length !== token.header.length)) throw Error('Malformed table'); } if (token.type === 'heading' && token.depth === 2) headings++; if (token.type === 'link' && !/^(https?:|#)/.test(token.href)) { if (!fs.existsSync(path.join(folder, decodeURIComponent(token.href.split('#')[0])))) { throw Error('Missing handoff reference'); } } }); const requiredRoutes = ['ble-bind', 'ble-state', 'voice-config', 'chat/start', 'chat/end', 'mqtt-config', 'ota/report']; for (const route of requiredRoutes) { if (!markdown.includes('/api/v1/device/' + route)) throw Error('Missing required device route: ' + route); } if (/\/api\/v1\/(?:devices\/|ops\/)|X-Login-Portal|X-Tenant-Id|第 8–12 节/.test(markdown)) { throw Error('Non-device interfaces or stale references found'); } if (/\/api\/v1\/device\/(register|time-challenges|voice-runtime|activation-acks)|"boot_id"|CFG1|TIME1/.test(markdown)) { throw Error('Removed startup handshake reappeared'); } if (headings !== 13 || jsonExamples < 31 || !markdown.includes('/callbacks/v1/volcengine/chat?session_id=') || markdown.includes('固定老人回调方案调整')) throw Error('Incomplete or stale document'); const firstJSON = /```json\n([\s\S]*?)\n```/.exec(markdown); if (!firstJSON || JSON.stringify(JSON.parse(firstJSON[1])) !== JSON.stringify(values)) { throw Error('Actual initialization must be first JSON block'); } const output = path.join(folder, '设备完整接口文档-20260915.private.md'); fs.writeFileSync(output, markdown, { mode: 0o600 }); if (fs.readFileSync(output, 'utf8') !== markdown) throw Error('Export mismatch'); // Refresh the old private entry point too, so it cannot retain the discarded elder callback proposal. const shortOutput = path.join(folder, '设备实际烧录信息.private.md'); fs.writeFileSync(shortOutput, actualHeader + '\n完整流程见 [设备完整接口文档](设备完整接口文档-20260915.private.md)。\n', { mode: 0o600 }); console.log(JSON.stringify({ output, shortOutput, headings, jsonExamples, tables, actualInitializationFirst: true, callbackMode: 'session_id', cHelpersIncluded: true, sha256: crypto.createHash('sha256').update(markdown).digest('hex') })); } main().catch(() => { console.error('Private handoff export failed; no private values logged.'); process.exitCode = 1; });