diff --git a/apps/backend/src/services/session/session-proof-beijing-time.js b/apps/backend/src/services/session/session-proof-beijing-time.js
new file mode 100644
index 00000000..31213cfc
--- /dev/null
+++ b/apps/backend/src/services/session/session-proof-beijing-time.js
@@ -0,0 +1,31 @@
+// @ts-nocheck
+
+import {
+ BAIDU_BEIJING_TIME_URL,
+ BEIJING_TIME_PROOF_CLIP,
+ BEIJING_TIME_PROOF_VIEWPORT,
+} from './session-proof-constants.js'
+
+export async function captureBeijingTimeProof(browserContext, { screenshotPath }) {
+ const proofPage = await browserContext.newPage()
+
+ try {
+ await proofPage.setViewportSize(BEIJING_TIME_PROOF_VIEWPORT)
+ await proofPage.goto(BAIDU_BEIJING_TIME_URL, { waitUntil: 'domcontentloaded' })
+ await proofPage.waitForTimeout(3_000)
+ await proofPage.evaluate(() => {
+ window.scrollTo(0, 0)
+ })
+ await proofPage.screenshot({
+ path: screenshotPath,
+ clip: BEIJING_TIME_PROOF_CLIP,
+ })
+
+ return {
+ screenshotPath,
+ pageUrl: BAIDU_BEIJING_TIME_URL,
+ }
+ } finally {
+ await proofPage.close().catch(() => null)
+ }
+}
diff --git a/apps/backend/src/services/session/session-proof-html.js b/apps/backend/src/services/session/session-proof-html.js
new file mode 100644
index 00000000..afe0eb3a
--- /dev/null
+++ b/apps/backend/src/services/session/session-proof-html.js
@@ -0,0 +1,168 @@
+export function buildProofHtml({ redeemImageBase64, timeImageBase64, redeemMessage, timePageUrl }) {
+ const capturedAt = new Date().toLocaleString('zh-CN', {
+ hour12: false,
+ timeZone: 'Asia/Shanghai',
+ })
+
+ return `
+
+
+
+
+ 兑换与北京时间凭证
+
+
+
+
+
+
+
Tencent Redeem Proof
+
兑换截图与北京时间截图
+
用于留存兑换结果与北京时间检索页的组合凭证。
+
+
+ Captured At
+ ${escapeHtml(capturedAt)}
+
+
+
+ 兑换结果截图
+ ${escapeHtml(redeemMessage || '兑换完成')}
+
+

+
+
+
+ 百度检索“北京时间”截图
+ 单独标签页打开百度搜索结果并截图。
+ ${escapeHtml(timePageUrl)}
+
+

+
+
+
+
+`
+}
+
+export function escapeHtml(value) {
+ return String(value || '')
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+}
diff --git a/apps/backend/src/services/session/session-proof-renderer.js b/apps/backend/src/services/session/session-proof-renderer.js
new file mode 100644
index 00000000..97a88f49
--- /dev/null
+++ b/apps/backend/src/services/session/session-proof-renderer.js
@@ -0,0 +1,108 @@
+// @ts-nocheck
+
+import fs from 'node:fs/promises'
+
+import { buildProofHtml } from './session-proof-html.js'
+
+export async function showResultDialog(page, message) {
+ await page.evaluate((text) => {
+ document.querySelectorAll('iframe').forEach((element) => element.remove())
+ document.querySelectorAll('.pop').forEach((element) => {
+ element.style.display = 'none'
+ })
+
+ let card = document.getElementById('codex-redeem-result')
+
+ if (!card) {
+ card = document.createElement('div')
+ card.id = 'codex-redeem-result'
+ card.innerHTML = `
+ Tencent Browser Session
+ 兑换结果
+
+ `
+ document.body.appendChild(card)
+ }
+
+ const messageNode = card.querySelector('.codex-redeem-result__message')
+
+ if (messageNode) {
+ messageNode.textContent = text
+ }
+
+ Object.assign(card.style, {
+ position: 'fixed',
+ left: '50%',
+ top: '50%',
+ transform: 'translate(-50%, -50%)',
+ zIndex: '99999',
+ width: 'min(560px, calc(100vw - 80px))',
+ padding: '32px 36px',
+ borderRadius: '24px',
+ background: 'rgba(10, 18, 28, 0.92)',
+ boxShadow: '0 24px 80px rgba(0, 0, 0, 0.35)',
+ border: '1px solid rgba(109, 241, 202, 0.25)',
+ color: '#f4fbff',
+ fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
+ })
+
+ const styleId = 'codex-redeem-result-style'
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style')
+ style.id = styleId
+ style.textContent = `
+ #codex-redeem-result .codex-redeem-result__eyebrow {
+ font-size: 12px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #6df1ca;
+ margin-bottom: 12px;
+ }
+ #codex-redeem-result .codex-redeem-result__title {
+ font-size: 34px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ }
+ #codex-redeem-result .codex-redeem-result__message {
+ font-size: 24px;
+ line-height: 1.5;
+ color: #ffffff;
+ }
+ `
+ document.head.appendChild(style)
+ }
+
+ window.scrollTo(0, 0)
+ }, message)
+
+ await page.waitForTimeout(600)
+}
+
+export async function composeProofScreenshot(
+ browserContext,
+ { outputPath, redeemImagePath, timeImagePath, redeemMessage, timePageUrl },
+) {
+ const [redeemImageBase64, timeImageBase64] = await Promise.all([
+ fs.readFile(redeemImagePath, 'base64'),
+ fs.readFile(timeImagePath, 'base64'),
+ ])
+
+ const composePage = await browserContext.newPage()
+
+ try {
+ await composePage.setViewportSize({ width: 1520, height: 1900 })
+ await composePage.setContent(
+ buildProofHtml({
+ redeemImageBase64,
+ timeImageBase64,
+ redeemMessage,
+ timePageUrl,
+ }),
+ { waitUntil: 'domcontentloaded' },
+ )
+ await composePage.screenshot({ path: outputPath, fullPage: true })
+ } finally {
+ await composePage.close().catch(() => null)
+ }
+}
diff --git a/apps/backend/src/services/session/session-proof.js b/apps/backend/src/services/session/session-proof.js
index fdd68dad..1d89df16 100644
--- a/apps/backend/src/services/session/session-proof.js
+++ b/apps/backend/src/services/session/session-proof.js
@@ -4,10 +4,10 @@ import fs from 'node:fs/promises'
import {
BAIDU_BEIJING_TIME_URL,
- BEIJING_TIME_PROOF_CLIP,
- BEIJING_TIME_PROOF_VIEWPORT,
DEFAULT_REDEEM_PROOF_MODE,
} from './session-proof-constants.js'
+import { captureBeijingTimeProof } from './session-proof-beijing-time.js'
+import { showResultDialog, composeProofScreenshot } from './session-proof-renderer.js'
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof-mode.js'
import { buildArtifactPaths } from './session-proof-paths.js'
import { writeRedeemResultFile } from './session-proof-result-writer.js'
@@ -143,299 +143,3 @@ export async function writeRedeemArtifactFiles({
}
export { resolveRedeemProofMode, shouldCaptureBeijingTimeProof }
-
-async function showResultDialog(page, message) {
- await page.evaluate((text) => {
- document.querySelectorAll('iframe').forEach((element) => element.remove())
- document.querySelectorAll('.pop').forEach((element) => {
- element.style.display = 'none'
- })
-
- let card = document.getElementById('codex-redeem-result')
-
- if (!card) {
- card = document.createElement('div')
- card.id = 'codex-redeem-result'
- card.innerHTML = `
- Tencent Browser Session
- 兑换结果
-
- `
- document.body.appendChild(card)
- }
-
- const messageNode = card.querySelector('.codex-redeem-result__message')
-
- if (messageNode) {
- messageNode.textContent = text
- }
-
- Object.assign(card.style, {
- position: 'fixed',
- left: '50%',
- top: '50%',
- transform: 'translate(-50%, -50%)',
- zIndex: '99999',
- width: 'min(560px, calc(100vw - 80px))',
- padding: '32px 36px',
- borderRadius: '24px',
- background: 'rgba(10, 18, 28, 0.92)',
- boxShadow: '0 24px 80px rgba(0, 0, 0, 0.35)',
- border: '1px solid rgba(109, 241, 202, 0.25)',
- color: '#f4fbff',
- fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
- })
-
- const styleId = 'codex-redeem-result-style'
-
- if (!document.getElementById(styleId)) {
- const style = document.createElement('style')
- style.id = styleId
- style.textContent = `
- #codex-redeem-result .codex-redeem-result__eyebrow {
- font-size: 12px;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: #6df1ca;
- margin-bottom: 12px;
- }
- #codex-redeem-result .codex-redeem-result__title {
- font-size: 34px;
- font-weight: 700;
- margin-bottom: 16px;
- }
- #codex-redeem-result .codex-redeem-result__message {
- font-size: 24px;
- line-height: 1.5;
- color: #ffffff;
- }
- `
- document.head.appendChild(style)
- }
-
- window.scrollTo(0, 0)
- }, message)
-
- await page.waitForTimeout(600)
-}
-
-async function captureBeijingTimeProof(browserContext, { screenshotPath }) {
- const proofPage = await browserContext.newPage()
-
- try {
- await proofPage.setViewportSize(BEIJING_TIME_PROOF_VIEWPORT)
- await proofPage.goto(BAIDU_BEIJING_TIME_URL, { waitUntil: 'domcontentloaded' })
- await proofPage.waitForTimeout(3_000)
- await proofPage.evaluate(() => {
- window.scrollTo(0, 0)
- })
- await proofPage.screenshot({
- path: screenshotPath,
- clip: BEIJING_TIME_PROOF_CLIP,
- })
-
- return {
- screenshotPath,
- pageUrl: BAIDU_BEIJING_TIME_URL,
- }
- } finally {
- await proofPage.close().catch(() => null)
- }
-}
-
-async function composeProofScreenshot(
- browserContext,
- { outputPath, redeemImagePath, timeImagePath, redeemMessage, timePageUrl },
-) {
- const [redeemImageBase64, timeImageBase64] = await Promise.all([
- fs.readFile(redeemImagePath, 'base64'),
- fs.readFile(timeImagePath, 'base64'),
- ])
-
- const composePage = await browserContext.newPage()
-
- try {
- await composePage.setViewportSize({ width: 1520, height: 1900 })
- await composePage.setContent(
- buildProofHtml({
- redeemImageBase64,
- timeImageBase64,
- redeemMessage,
- timePageUrl,
- }),
- { waitUntil: 'domcontentloaded' },
- )
- await composePage.screenshot({ path: outputPath, fullPage: true })
- } finally {
- await composePage.close().catch(() => null)
- }
-}
-
-function buildProofHtml({ redeemImageBase64, timeImageBase64, redeemMessage, timePageUrl }) {
- const capturedAt = new Date().toLocaleString('zh-CN', {
- hour12: false,
- timeZone: 'Asia/Shanghai',
- })
-
- return `
-
-
-
-
- 兑换与北京时间凭证
-
-
-
-
-
-
-
Tencent Redeem Proof
-
兑换截图与北京时间截图
-
用于留存兑换结果与北京时间检索页的组合凭证。
-
-
- Captured At
- ${escapeHtml(capturedAt)}
-
-
-
- 兑换结果截图
- ${escapeHtml(redeemMessage || '兑换完成')}
-
-

-
-
-
- 百度检索“北京时间”截图
- 单独标签页打开百度搜索结果并截图。
- ${escapeHtml(timePageUrl)}
-
-

-
-
-
-
-`
-}
-
-function escapeHtml(value) {
- return String(value || '')
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
-}
diff --git a/apps/backend/src/services/session/session-proof.test.js b/apps/backend/src/services/session/session-proof.test.js
index 7e3430aa..ff4ef220 100644
--- a/apps/backend/src/services/session/session-proof.test.js
+++ b/apps/backend/src/services/session/session-proof.test.js
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict'
import path from 'node:path'
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof.js'
+import { buildProofHtml } from './session-proof-html.js'
import { buildArtifactPaths } from './session-proof-paths.js'
test('resolveRedeemProofMode normalizes configured modes and falls back to full', () => {
@@ -62,3 +63,19 @@ test('buildArtifactPaths keeps redeem proof artifact filenames stable', () => {
assert.equal(artifactPaths.htmlPath, path.join('/tmp/redeem-proof', 'page.html'))
assert.equal(artifactPaths.resultPath, path.join('/tmp/redeem-proof', 'result.json'))
})
+
+test('buildProofHtml escapes injected text while keeping proof sections stable', () => {
+ const html = buildProofHtml({
+ redeemImageBase64: 'redeem-base64',
+ timeImageBase64: 'time-base64',
+ redeemMessage: '兑换成功 & 已到账',
+ timePageUrl: 'https://example.com/?q=<北京时间>',
+ })
+
+ assert.match(html, /Tencent Redeem Proof/)
+ assert.match(html, /兑换截图与北京时间截图/)
+ assert.match(html, /<b>兑换成功 & 已到账<\/b>/)
+ assert.match(html, /https:\/\/example\.com\/\?q=<北京时间>/)
+ assert.match(html, /data:image\/png;base64,redeem-base64/)
+ assert.match(html, /data:image\/png;base64,time-base64/)
+})