const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const docDir = __dirname; const tmpDir = path.join(docDir, '_eid_docx_tmp'); const docxPath = path.join(docDir, 'E证通小程序申请与配置指南.docx'); function escapeXml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function p(text, style, bold) { const styleXml = style ? `` : ''; const boldXml = bold ? '' : ''; return `${styleXml}${boldXml}${escapeXml(text)}`; } function h1(text) { return p(text, 'Heading1', true); } function h2(text) { return p(text, 'Heading2', true); } function body(text) { return p(text); } function bullet(text) { return `${escapeXml(text)}`; } const sections = [ h1('E证通小程序申请与配置指南'), body('适用项目:灵活用工微信小程序(uni-app)'), body('文档版本:v1.0'), body('编写日期:2026年7月8日'), body(''), h2('一、概述'), body('E证通(腾讯云慧眼人脸核身)在微信小程序中的接入方式,官方称为「E证通小程序 SDK」,并非微信公众平台「插件管理」中搜索添加的普通插件。'), body('整体接入分为两部分:'), bullet('腾讯云侧:申请商户 ID、下载 SDK、后端对接 GetEidToken / GetEidResult'), bullet('微信小程序后台:配置服务器域名、可选半屏小程序等'), body('官方文档:'), bullet('接入流程说明:https://cloud.tencent.com/document/product/1007/108009'), bullet('小程序集成:https://cloud.tencent.com/document/product/1007/108011'), body(''), h2('二、整体流程'), body('1. 腾讯云申请商户 ID(约 3 个工作日)'), body('2. 下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'), body('3. 后端接入 GetEidToken / GetEidResult 接口'), body('4. 微信小程序后台配置域名、半屏(可选)'), body('5. 小程序内集成 SDK 并调用 startEid 发起核身'), body(''), h2('三、腾讯云侧:申请商户 ID(必做)'), body('步骤 1:登录腾讯云慧眼 · 人脸核身控制台'), bullet('控制台地址:https://console.cloud.tencent.com/faceid'), body('步骤 2:进入「自助接入 → E证通服务 → 申请商户 ID」'), body('步骤 3:填写并提交申请资料'), bullet('小程序 AppID(manifest.json 中 mp-weixin.appid,不能为空)'), bullet('小程序名称'), bullet('业务场景说明(例如:临时工/企业注册实人认证)'), bullet('按提示上传业务域名校验文件(若需要返回身份信息等)'), body('步骤 4:等待审核,一般约 3 个工作日'), body('步骤 5:审核通过后'), bullet('获得商户 ID(MerchantId)'), bullet('可下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'), body('后端使用商户 ID 调用 GetEidToken 生成核身 Token,再传给前端使用。'), body(''), h2('四、微信小程序后台配置'), body('登录微信公众平台:https://mp.weixin.qq.com/ ,进入对应小程序。'), body(''), body('4.1 服务器域名(必配)'), body('路径:开发 → 开发管理 → 开发设置 → 服务器域名 → request 合法域名'), body('添加域名:https://eid.faceid.qq.com'), body('说明:开发阶段可在微信开发者工具勾选「不校验合法域名」,但上线前必须配置。'), body(''), body('4.2 业务域名(视 SDK 版本而定)'), bullet('SDK V1.0.7 及以上:一般不需要配置业务域名'), bullet('更低版本:可能需要将 eid.faceid.qq.com 加入业务域名,校验文件在申请商户 ID 时一并提交审核'), body(''), body('4.3 半屏打开 eID 小程序(可选,体验更好)'), body('E证通核身时会跳转到「eID 数字身份」小程序,AppID:wx0e2cb0b052a91c92'), body('配置路径:设置 → 第三方设置 → 半屏小程序管理 → 添加'), bullet('每个小程序最多可添加 10 个半屏小程序'), bullet('还需向 E证通侧申请半屏权限,审核通过后才会生效'), bullet('不支持半屏时会自动降级为全屏跳转'), body('半屏模式还需在 app.json 中配置(uni-app 对应 pages.json / manifest.json):'), body('"embeddedAppIdList": ["wx0e2cb0b052a91c92"]'), body(''), body('4.4 关于「插件」的说明'), body('E证通不需要在「设置 → 第三方设置 → 插件管理」中搜索添加插件。正确做法是:下载 SDK 放入项目,在 App.onLaunch 中调用 initEid,页面中通过按钮点击调用 startEid 并传入 token。'), body(''), h2('五、小程序 SDK 集成要点'), body('5.1 下载并放置 SDK'), body('在腾讯云控制台下载 E证通小程序 SDK,将 mp_ecard_sdk 文件夹放在小程序根目录下(uni-app 项目建议放在 app/ 目录下)。'), body(''), body('5.2 初始化 SDK(App.onLaunch)'), body('在 App.js / App.vue 的 onLaunch 中引入并调用 initEid。'), body('在 pages.json 中注册 SDK 自带页面路径(以 SDK 版本说明为准)。'), body(''), body('5.3 调用核身(必须通过按钮点击触发)'), body('SDK v1.0.5 及以上版本,startEid 必须通过按钮点击形式发起(内部使用 wx.navigateToMiniProgram 拉起 eID 数字身份小程序)。'), body('主要参数说明:'), bullet('data.token:后端返回的 EidToken(必填)'), bullet('data.enableEmbedded:是否半屏打开(SDK v1.0.9+,默认 false)'), bullet('data.allowFullScreen:半屏后是否允许全屏(SDK v1.0.9+,默认 true)'), bullet('verifyDoneCallback:核身完成回调,含 token 与 verifyDone 标志'), body(''), body('5.4 从 eID 小程序返回时的 onShow 处理'), body('用户从 eID 数字身份小程序返回时,会同时触发接入方 App.onShow。若 onShow 中有登录跳转等逻辑,需排除此场景:'), body('判断条件:scene === 1038 且 referrerInfo.appId === "wx0e2cb0b052a91c92" 时直接 return,不执行原有逻辑。'), body(''), h2('六、后端接口要求'), body('商户 ID 审核通过后,后端需至少提供以下能力:'), bullet('GetEidToken:根据姓名、身份证号等信息向腾讯云换取 EidToken,返回给前端'), bullet('GetEidResult:核身结束后查询核验结果,以前端回调为辅、后端查询为准'), body('默认情况下不会返回姓名、身份证等明文信息;如需加密返回身份信息,需在控制台单独申请开通。'), body('建议后端封装接口示例:'), bullet('POST /api/v1/mp/face/init — 登记提交后获取 EidToken'), bullet('GET /api/v1/mp/face/result — 核身完成后查询结果并更新 faceVerified 状态'), body(''), h2('七、与本项目的对应关系'), bullet('小程序 AppID:manifest.json → mp-weixin.appid(当前需填写)'), bullet('SDK 目录:app/mp_ecard_sdk/'), bullet('初始化:App.vue 的 onLaunch 中 initEid'), bullet('页面注册:pages.json 增加 SDK 页面路径'), bullet('业务串联:企业/临时工登记页提交 → 人脸校验页 → startEid → 后端确认 → 进入业务首页'), body('建议业务流程:'), body('登记页提交 → 后端保存资料并 GetEidToken → 跳转人脸校验页 → 用户点击按钮 startEid(token) → 进入 eID 数字身份小程序 → 返回本小程序 → 后端 GetEidResult 确认 → faceVerified = true → 进入首页'), body(''), h2('八、调试与注意事项'), bullet('请在微信开发者工具中使用手机「预览」模式调试,官方建议不要使用「真机调试」'), bullet('E证通 SDK 通过 wx.onAppShow 监听从 eID 小程序返回的事件,触发 verifyDoneCallback'), bullet('半屏接入是较新能力,部分机型存在兼容性问题,上线前需充分测试'), body(''), h2('九、申请前 Checklist'), bullet('小程序已注册,AppID 已填入 manifest.json'), bullet('腾讯云账号已实名认证'), bullet('人脸核身 / E证通 服务已开通'), bullet('商户 ID 已申请(约 3 工作日)'), bullet('E证通小程序 SDK 已下载'), bullet('微信后台 request 域名已添加 eid.faceid.qq.com'), bullet('(可选)半屏小程序已配置 wx0e2cb0b052a91c92'), bullet('后端 GetEidToken / GetEidResult 接口已联调通过'), body(''), h2('十、常见问题'), body('Q1:找不到「E证通插件」怎么办?'), body('A:E证通不是插件市场插件,需在腾讯云下载 SDK 集成,并在微信后台配置域名。'), body(''), body('Q2:调起核身失败?'), body('A:检查 AppID 是否为空、商户 ID 是否审核通过、是否已 initEid、Token 是否有效。'), body(''), body('Q3:域名请求报错?'), body('A:确认已在微信后台添加 https://eid.faceid.qq.com 到 request 合法域名。'), body(''), body('Q4:真机调试异常?'), body('A:改用开发者工具「预览」扫码测试,不要使用「真机调试」。'), body(''), body('Q5:返回小程序后页面乱跳?'), body('A:在 App.onShow 中排除从 eID 小程序返回的场景(scene=1038)。'), ]; const documentXml = ` ${sections.join('\n ')} `; const files = { '[Content_Types].xml': ` `, '_rels/.rels': ` `, 'word/document.xml': documentXml, 'word/_rels/document.xml.rels': ` `, 'word/numbering.xml': ` `, 'word/styles.xml': ` `, }; if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); fs.mkdirSync(tmpDir, { recursive: true }); for (const [relPath, content] of Object.entries(files)) { const fullPath = path.join(tmpDir, relPath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content, 'utf8'); } if (fs.existsSync(docxPath)) fs.unlinkSync(docxPath); const ps1Path = path.join(docDir, '_zip-eid-doc.ps1'); const ps1 = ` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.IO.Compression.FileSystem $tmp = '${tmpDir.replace(/'/g, "''")}' $out = '${docxPath.replace(/'/g, "''")}' if (Test-Path -LiteralPath $out) { Remove-Item -LiteralPath $out -Force } [System.IO.Compression.ZipFile]::CreateFromDirectory($tmp, $out) Write-Host "OK" `; fs.writeFileSync(ps1Path, '\ufeff' + ps1, 'utf8'); execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`, { stdio: 'inherit' }); fs.unlinkSync(ps1Path); fs.rmSync(tmpDir, { recursive: true, force: true }); if (!fs.existsSync(docxPath)) { console.error('Failed to create docx'); process.exit(1); } console.log('Generated:', docxPath); console.log('Size:', fs.statSync(docxPath).size, 'bytes');