From 899d898a8eb9d7d27afb6c52f3225dc9f635da5c Mon Sep 17 00:00:00 2001 From: yml Date: Mon, 4 May 2026 13:27:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=85=91=E6=8D=A2=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E6=B8=B2=E6=9F=93=E4=B8=8E=E6=88=AA=E5=9B=BE=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/session-proof-beijing-time.js | 31 ++ .../services/session/session-proof-html.js | 168 ++++++++++ .../session/session-proof-renderer.js | 108 +++++++ .../src/services/session/session-proof.js | 300 +----------------- .../services/session/session-proof.test.js | 17 + 5 files changed, 326 insertions(+), 298 deletions(-) create mode 100644 apps/backend/src/services/session/session-proof-beijing-time.js create mode 100644 apps/backend/src/services/session/session-proof-html.js create mode 100644 apps/backend/src/services/session/session-proof-renderer.js 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/) +})