From 5b9126c8a9ac3655724bdd51ee035c19c5a714d2 Mon Sep 17 00:00:00 2001 From: hbxnlsy Date: Thu, 2 Apr 2026 21:17:11 +0800 Subject: [PATCH] init --- Notion-bot | 354 ++++++++++++++ README.md | 1 + ngaccountant | 1275 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1630 insertions(+) create mode 100644 Notion-bot create mode 100644 README.md create mode 100644 ngaccountant diff --git a/Notion-bot b/Notion-bot new file mode 100644 index 0000000..a9ebae3 --- /dev/null +++ b/Notion-bot @@ -0,0 +1,354 @@ +/** + * 配置区域 + */ +const CONFIG = { + // Notion 配置 + NOTION_DATABASE_ID: "修改为对应的NOTION_DATABASE_ID", + NOTION_API_KEY: "修改为对应的NOTION_API_KEY", + + // Telegram 配置 + TELEGRAM_BOT_TOKEN: "修改为对应的TELEGRAM_BOT_TOKEN", + ALLOWED_USER_IDS: ["修改为对应的ALLOWED_USER_IDS"], + + // 基础配置 + CITY: "Portland", + STATE_FULL: "Oregon", + ZIP_CODE: "97212" +}; + +export default { + async fetch(request, env, ctx) { + if (request.method === "GET") { + const data = await handleRedfinAddress(null, true); + return new Response(JSON.stringify(data, null, 2), { + headers: { "Content-Type": "application/json; charset=utf-8" } + }); + } + + if (request.method !== "POST") return new Response("Bot is running!", { status: 200 }); + + try { + const update = await request.json(); + + // --- 1. 鉴权 --- + const userId = getUserId(update); + if (userId && !CONFIG.ALLOWED_USER_IDS.includes(String(userId))) { + return new Response("Unauthorized", { status: 200 }); + } + + // --- 2. 消息处理 --- + if (update.message && update.message.text) { + const text = update.message.text.trim(); + const chatId = update.message.chat.id; + + if (text === "/start") { + await sendWelcomeMenu(chatId); + } + else if (text === "📦 立即取号" || text === "取号" || text === "/get") { + await handleGetAccount(chatId); + } + else if (text === "📊 查询库存" || text === "库存" || text === "/stock") { + await handleCheckStock(chatId); + } + else if (text === "🏠 随机地址" || text === "地址" || text === "/address") { + await handleRedfinAddress(chatId); + } + } + + // --- 3. 按钮回调处理 --- + else if (update.callback_query) { + await handleCallback(update.callback_query); + } + + } catch (e) { + console.error(e); + } + + return new Response("OK", { status: 200 }); + } +}; + +// --- 辅助函数 --- +function getUserId(update) { + if (update.message) return update.message.from.id; + if (update.callback_query) return update.callback_query.from.id; + return null; +} + +// 菜单 +async function sendWelcomeMenu(chatId) { + const text = "👋 欢迎回来!\n\n请点击下方按钮开始操作:"; + const keyboard = { + keyboard: [ + [{ text: "📦 立即取号" }, { text: "📊 查询库存" }], + [{ text: "🏠 随机地址" }] + ], + resize_keyboard: true, + is_persistent: true + }; + await sendMessage(chatId, text, keyboard); +} + +// --- 核心业务逻辑 --- + +// 1. 处理地址抓取 (👑 终极降维打击版:Overpass API 数据库直连) +async function handleRedfinAddress(chatId, isJsonOnly = false) { + try { + // 💡 放弃搜索,改为“数据库框选” + // (45.530,-122.665,45.555,-122.615) 是完美覆盖 Portland 97212 的地理矩形坐标 + // 查询指令:在这个矩形内,找出所有带有 addr:housenumber (门牌号) 和 addr:street (街道) 的真实房屋节点,一次提取 300 个 + const bbox = "45.530,-122.665,45.555,-122.615"; + const overpassQuery = `[out:json];node(${bbox})["addr:housenumber"]["addr:street"];out 300;`; + + // 使用官方与备用节点,防止单点故障 + const endpoints = [ + "https://overpass-api.de/api/interpreter", + "https://lz4.overpass-api.de/api/interpreter" + ]; + const apiUrl = `${endpoints[Math.floor(Math.random() * endpoints.length)]}?data=${encodeURIComponent(overpassQuery)}`; + + const response = await fetch(apiUrl, { + headers: { + // 生成随机 User-Agent 防止被识别为同一来源限制 + "User-Agent": `TelegramAddressBot_CF_${Math.floor(Math.random()*100000)}`, + "Accept": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`API 拒绝响应: ${response.status}`); + } + + const data = await response.json(); + const houses = data.elements; + + if (!houses || houses.length === 0) { + throw new Error("区域内未提取到有效建筑数据"); + } + + // --- 成功获取!--- + // 从一次性拿到的 300 套真实房屋中,随机挑一套 + const randomHouse = houses[Math.floor(Math.random() * houses.length)]; + const tags = randomHouse.tags; + + // 精准组装地址 + const street = `${tags["addr:housenumber"]} ${tags["addr:street"]}`; + const city = tags["addr:city"] || CONFIG.CITY; + const state = "OR"; + const zip = tags["addr:postcode"] || CONFIG.ZIP_CODE; + + const fullAddr = `${street}, ${city}, ${state} ${zip}`; + + const result = { + address: street, + city: city, + state: state, + zip: zip, + fullAddress: fullAddr + }; + + if (!isJsonOnly && chatId) { + const msg = `🏠 随机房源地址\n\n📍 ${result.fullAddress}`; + await sendMessage(chatId, msg); + } + + return result; + + } catch (e) { + if (chatId) { + await sendMessage(chatId, `❌ 获取失败: ${e.message} (请稍后再试)`); + } + return { error: e.message }; + } +} + +// 2. 处理取号 +async function handleGetAccount(chatId) { + const [page, stockCount] = await Promise.all([ + queryNotionUnused(), + getStockCount() + ]); + + if (!page) { + return sendMessage(chatId, "⚠️ 库存不足:当前没有“未使用”的账号。"); + } + + const props = page.properties; + const pageId = page.id; + + const account = props["账号"]?.formula?.string || props["账号"]?.rich_text?.[0]?.plain_text || props["账号"]?.title?.[0]?.plain_text || "无账号"; + const password = props["密码"]?.formula?.string || props["密码"]?.rich_text?.[0]?.plain_text || "无密码"; + + // 读取创建时间 + let createTimeRaw = props["创建时间"]?.created_time || props["创建时间"]?.date?.start || "未知"; + let createTimeStr = "未知"; + + if (createTimeRaw !== "未知") { + const dateObj = new Date(createTimeRaw); + createTimeStr = dateObj.toLocaleString('zh-CN', { + timeZone: 'Asia/Shanghai', + hour12: false, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit' + }).replace(/\//g, '-'); + } + + const messageText = + `📦 提取成功! (📊 库存: ${stockCount})\n\n` + + `账号:${account}\n` + + `密码:${password}\n` + + `组合:${account}----${password}\n` + + `Xbox创建时间:${createTimeStr}\n\n` + + `👇 请选择操作:`; + + const inlineKeyboard = { + inline_keyboard: [ + [ + { text: "✅ 兑换码", callback_data: `redeem|${pageId}` }, + { text: "🔘 其他", callback_data: `other|${pageId}` } + ], + [ + { text: "❌ 取消", callback_data: `cancel|${pageId}` } + ] + ] + }; + + await sendMessage(chatId, messageText, inlineKeyboard); +} + +// 3. 处理库存查询 +async function handleCheckStock(chatId) { + const count = await getStockCount(); + const text = `📊 库存查询结果\n\n` + + `当前剩余可用白号:${count} 个`; + await sendMessage(chatId, text); +} + +// 4. 处理按钮回调 +async function handleCallback(query) { + const [action, pageId] = query.data.split("|"); + const chatId = query.message.chat.id; + const messageId = query.message.message_id; + + if (action === "cancel") { + await deleteMessage(chatId, messageId); + return; + } + + let originalText = query.message.text || ""; + originalText = originalText.replace(/👇.*/s, "").trim(); + + // 🔄 重新给账号、密码、组合、地址加上 标签 + originalText = originalText.replace(/(账号:)(.+)/, '$1$2'); + originalText = originalText.replace(/(密码:)(.+)/, '$1$2'); + originalText = originalText.replace(/(组合:)(.+)/, '$1$2'); + originalText = originalText.replace(/(📍)(.+)/, '$1$2'); + + let newStatus = action === "redeem" ? "兑换码" : "其他"; + let statusText = action === "redeem" ? "✅ 已标记为:兑换码" : "🔘 已标记为:其他"; + + const nowStr = new Date().toLocaleString('zh-CN', { + timeZone: 'Asia/Shanghai', + hour12: false + }); + + const success = await updateNotionPage(pageId, newStatus); + + if (success) { + const finalText = `${originalText}\n\n${statusText}\n🕒 使用时间:${nowStr}`; + await editMessage(chatId, messageId, finalText); + await answerCallback(query.id, "操作成功!"); + } else { + await answerCallback(query.id, "❌ Notion 更新失败"); + } +} + +// --- Notion API --- + +async function queryNotionUnused() { + const url = `https://api.notion.com/v1/databases/${CONFIG.NOTION_DATABASE_ID}/query`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`, + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "filter": { "property": "状态", "select": { "is_empty": true } }, // 仅筛选空状态 + "sorts": [{ "property": "创建时间", "direction": "ascending" }], + "page_size": 1 + }) + }); + const data = await response.json(); + if (data.results && data.results.length > 0) return data.results[0]; + return null; +} + +async function getStockCount() { + const url = `https://api.notion.com/v1/databases/${CONFIG.NOTION_DATABASE_ID}/query`; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`, + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "filter": { "property": "状态", "select": { "is_empty": true } }, // 仅筛选空状态 + "page_size": 100 + }) + }); + const data = await response.json(); + return data.results ? data.results.length : 0; + } catch (e) { + return "未知"; + } +} + +async function updateNotionPage(pageId, statusName) { + const url = `https://api.notion.com/v1/pages/${pageId}`; + const now = new Date().toISOString(); + const response = await fetch(url, { + method: "PATCH", + headers: { + "Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`, + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "properties": { + "状态": { "select": { "name": statusName } }, + "使用日期": { "date": { "start": now } } + } + }) + }); + return response.ok; +} + +// --- Telegram API --- + +async function sendMessage(chatId, text, replyMarkup = null) { + const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/sendMessage`; + const body = { chat_id: chatId, text: text, parse_mode: "HTML" }; + if (replyMarkup) body.reply_markup = replyMarkup; + await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); +} + +async function deleteMessage(chatId, messageId) { + const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/deleteMessage`; + await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, message_id: messageId }) }); +} + +async function editMessage(chatId, messageId, text) { + const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/editMessageText`; + await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, message_id: messageId, text: text, parse_mode: "HTML" }) }); +} + +async function answerCallback(id, text) { + const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/answerCallbackQuery`; + await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ callback_query_id: id, text: text }) }); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0460ec --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Cloudflare-worker \ No newline at end of file diff --git a/ngaccountant b/ngaccountant new file mode 100644 index 0000000..5129fc9 --- /dev/null +++ b/ngaccountant @@ -0,0 +1,1275 @@ +const BOT_TOKEN = 'xxxxxxxx'; +const ALLOWED_USER_ID = xxxxxxxx; +const VIEW_TOKEN = 'xbox123'; + +const MAX_TELEGRAM_TEXT = 4000; +const FETCH_TIMEOUT_MS = 8000; +const EXCHANGE_TIMEOUT_MS = 6000; +const MINI_CONCURRENCY = 5; // 每次 /run 只处理前 5 个 +const RUN_LOCK_TTL = 1800; +const BATCH_TIMEOUT_MS = 15000; + +// ========================= 工具函数 ========================= +function safeTelegramText(text) { + let s = String(text ?? ''); + if (s.length > MAX_TELEGRAM_TEXT) { + s = s.substring(0, MAX_TELEGRAM_TEXT) + '\n\n... (内容过长已截断)'; + } + return s; +} + +function escapeHTML(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function makeProgressBar(done, total, length = 10) { + if (total <= 0) return '□□□□□□□□□□'; + const ratio = Math.max(0, Math.min(1, done / total)); + const filled = Math.round(ratio * length); + return '■'.repeat(filled) + '□'.repeat(length - filled); +} + +function getPaginationKeyboard(currentPage) { + return { + inline_keyboard: [ + [ + { text: currentPage === 'page_1' ? '✅ 报价' : '🎮 报价', callback_data: 'page_1' }, + { text: currentPage === 'page_2' ? '✅ Surge' : '📦 Surge', callback_data: 'page_2' }, + { text: currentPage === 'page_3' ? '✅ 链接' : '🔗 链接', callback_data: 'page_3' } + ] + ] + }; +} + +async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + return await fetch(url, { + ...options, + signal: controller.signal + }); + } finally { + clearTimeout(timer); + } +} + +async function answerCallbackQuery(callbackQueryId, text = null) { + const url = `https://api.telegram.org/bot${BOT_TOKEN}/answerCallbackQuery`; + const body = { callback_query_id: callbackQueryId }; + + if (text) { + body.text = text; + body.show_alert = true; + } + + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + try { + resp.body?.cancel(); + } catch (_) {} +} + +async function sendTelegramMessage(chatId, text, replyMarkup = null) { + const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`; + + const payloadObj = { + chat_id: chatId, + text: safeTelegramText(text), + parse_mode: 'HTML', + disable_web_page_preview: true + }; + + if (replyMarkup) { + payloadObj.reply_markup = replyMarkup; + } + + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payloadObj) + }); + + try { + return await resp.json(); + } catch { + return null; + } +} + +async function editTelegramMessage(chatId, messageId, text, replyMarkup = null) { + const url = `https://api.telegram.org/bot${BOT_TOKEN}/editMessageText`; + + const payloadObj = { + chat_id: chatId, + message_id: messageId, + text: safeTelegramText(text), + parse_mode: 'HTML', + disable_web_page_preview: true + }; + + if (replyMarkup) { + payloadObj.reply_markup = replyMarkup; + } + + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payloadObj) + }); + + try { + resp.body?.cancel(); + } catch (_) {} +} + +function calculateProxyPrice(cnyPrices) { + const totalCNY = cnyPrices.reduce((acc, val) => acc + parseFloat(val), 0); + let proxyPrice = 0; + let expressionStr = ''; + const baseStr = cnyPrices.join('+'); + const multi = cnyPrices.length > 1; + + if (totalCNY < 40) { + proxyPrice = totalCNY + 12; + expressionStr = `${baseStr}+12=${proxyPrice.toFixed(2)}`; + } else if (totalCNY < 65) { + proxyPrice = totalCNY * 1.28; + expressionStr = `${multi ? '(' + baseStr + ')' : baseStr}*1.28=${proxyPrice.toFixed(2)}`; + } else { + proxyPrice = totalCNY * 1.27; + expressionStr = `${multi ? '(' + baseStr + ')' : baseStr}*1.27=${proxyPrice.toFixed(2)}`; + } + + return { totalCNY, proxyPrice, expressionStr }; +} + +function buildProgressText(total, processed, pendingCount, rate, extra = '') { + const bar = makeProgressBar(processed, total, 10); + + let text = + `⏳ 抓取进行中\n\n` + + `${bar}\n` + + `${processed}/${total}\n\n` + + `当前待处理队列: ${pendingCount} 个`; + + if (rate) { + text += `\n汇率: 1 NGN ≈ ${Number(rate).toFixed(6)} CNY`; + } + + if (extra) { + text += `\n\n${extra}`; + } + + return text; +} + +// ================== 实时汇率获取 ================== +async function getRealTimeExchangeRate() { + const apis = [ + { + name: 'Open Exchange Rates', + url: 'https://open.er-api.com/v6/latest/NGN', + parse: (data) => data?.rates?.CNY + }, + { + name: 'Fawaz Ahmed API', + url: 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/ngn.json', + parse: (data) => data?.ngn?.cny + }, + { + name: 'ExchangeRate-API', + url: 'https://api.exchangerate-api.com/v4/latest/NGN', + parse: (data) => data?.rates?.CNY + } + ]; + + for (const api of apis) { + try { + const response = await fetchWithTimeout(api.url, {}, EXCHANGE_TIMEOUT_MS); + if (!response.ok) continue; + + const data = await response.json(); + const rate = api.parse(data); + + if (rate && typeof rate === 'number') { + return rate; + } + } catch (_) {} + } + + throw new Error('所有备用实时汇率 API 均请求失败,请稍后再试。'); +} + +// ========================= KV Key ========================= +function getQueueKey(userId) { + return `xbox_urls_${userId}`; +} + +function getRunLockKey(userId) { + return `RUNNING_${userId}`; +} + +function getRunMetaKey(userId) { + return `RUN_META_${userId}`; +} + +function getStateKey(userId) { + return `SUCCESS_STATE_${userId}`; +} + +function getPagesKey(userId) { + return `LATEST_RESULT_${userId}`; +} + +function getSurgeQueueKey(userId) { + return `SURGE_GROUP_QUEUE_${userId}`; +} + +function getResolvedLinksKey(userId) { + return `RESOLVED_LINKS_${userId}`; +} + +// ========================= 状态读写 ========================= +async function loadPendingQueue(env, userId) { + const raw = await env.LINKS_KV.get(getQueueKey(userId)); + return raw ? JSON.parse(raw) : []; +} + +async function savePendingQueue(env, userId, queue) { + const uniqueQueue = [...new Set(queue)]; + if (uniqueQueue.length > 0) { + await env.LINKS_KV.put(getQueueKey(userId), JSON.stringify(uniqueQueue)); + } else { + await env.LINKS_KV.delete(getQueueKey(userId)); + } +} + +async function loadSuccessState(env, userId) { + const raw = await env.LINKS_KV.get(getStateKey(userId)); + if (!raw) { + return { + items: [], + lastRate: null, + updatedAt: null + }; + } + + try { + const parsed = JSON.parse(raw); + return { + items: Array.isArray(parsed.items) ? parsed.items : [], + lastRate: parsed.lastRate ?? null, + updatedAt: parsed.updatedAt ?? null + }; + } catch { + return { + items: [], + lastRate: null, + updatedAt: null + }; + } +} + +async function saveSuccessState(env, userId, state) { + await env.LINKS_KV.put( + getStateKey(userId), + JSON.stringify({ + items: state.items || [], + lastRate: state.lastRate ?? null, + updatedAt: new Date().toISOString() + }) + ); +} + +async function loadResolvedLinks(env, userId) { + const raw = await env.LINKS_KV.get(getResolvedLinksKey(userId)); + if (!raw) return []; + + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +async function saveResolvedLinks(env, userId, links) { + const safeLinks = Array.isArray(links) ? links : []; + await env.LINKS_KV.put(getResolvedLinksKey(userId), JSON.stringify(safeLinks)); +} + +async function appendResolvedLink(env, userId, linkInfo) { + const links = await loadResolvedLinks(env, userId); + const exists = links.some(x => x.sourceUrl === linkInfo.sourceUrl); + + if (!exists) { + links.push({ + sourceUrl: linkInfo.sourceUrl, + resolvedUrl: linkInfo.resolvedUrl, + bigId: linkInfo.bigId, + gameName: linkInfo.gameName, + createdAt: new Date().toISOString() + }); + await saveResolvedLinks(env, userId, links); + } +} + +async function loadRunMeta(env, userId) { + const raw = await env.LINKS_KV.get(getRunMetaKey(userId)); + return raw ? JSON.parse(raw) : null; +} + +async function saveRunMeta(env, userId, meta) { + await env.LINKS_KV.put(getRunMetaKey(userId), JSON.stringify(meta), { + expirationTtl: RUN_LOCK_TTL + }); +} + +async function clearRunMeta(env, userId) { + await env.LINKS_KV.delete(getRunMetaKey(userId)); +} + +async function isRunActive(env, userId, runId) { + const meta = await loadRunMeta(env, userId); + return !!(meta && meta.runId === runId); +} + +async function forceStopRunInternal(env, userId, runId) { + if (!(await isRunActive(env, userId, runId))) { + return false; + } + + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); + return true; +} + +// ========================= Surge 分组队列 ========================= +async function loadSurgeQueue(env, userId) { + const raw = await env.LINKS_KV.get(getSurgeQueueKey(userId)); + if (!raw) { + return { + nextGroupIndex: 1, + groups: [] + }; + } + + try { + const parsed = JSON.parse(raw); + return { + nextGroupIndex: typeof parsed.nextGroupIndex === 'number' ? parsed.nextGroupIndex : 1, + groups: Array.isArray(parsed.groups) ? parsed.groups : [] + }; + } catch { + return { + nextGroupIndex: 1, + groups: [] + }; + } +} + +async function saveSurgeQueue(env, userId, queue) { + const safeQueue = { + nextGroupIndex: typeof queue.nextGroupIndex === 'number' ? queue.nextGroupIndex : 1, + groups: Array.isArray(queue.groups) ? queue.groups : [] + }; + + await env.LINKS_KV.put(getSurgeQueueKey(userId), JSON.stringify(safeQueue)); + + if (safeQueue.groups.length > 0) { + await env.LINKS_KV.put('LATEST_XBOX_LIST', JSON.stringify(safeQueue.groups[0].products)); + } else { + await env.LINKS_KV.delete('LATEST_XBOX_LIST'); + } +} + +async function appendProductToSurgeQueue(env, userId, item) { + const queue = await loadSurgeQueue(env, userId); + + const productPayload = { + ProductId: item.bigId, + SkuId: item.targetSkuId, + AvailabilityId: item.targetAvailabilityId + }; + + let lastGroup = queue.groups[queue.groups.length - 1]; + + if (!lastGroup || lastGroup.count >= 15) { + lastGroup = { + groupIndex: queue.nextGroupIndex, + count: 0, + products: {} + }; + queue.groups.push(lastGroup); + queue.nextGroupIndex += 1; + } + + lastGroup.count += 1; + lastGroup.products[`product${lastGroup.count}`] = productPayload; + + await saveSurgeQueue(env, userId, queue); +} + +async function popFirstSurgeGroup(env, userId) { + const queue = await loadSurgeQueue(env, userId); + + if (!queue.groups.length) { + return { + ok: true, + cleared: false, + remainingGroups: 0, + nextGroupIndex: null, + nextGroupCount: 0 + }; + } + + const removed = queue.groups.shift(); + await saveSurgeQueue(env, userId, queue); + + return { + ok: true, + cleared: true, + clearedGroupIndex: removed.groupIndex, + clearedGroupCount: removed.count, + remainingGroups: queue.groups.length, + nextGroupIndex: queue.groups[0]?.groupIndex ?? null, + nextGroupCount: queue.groups[0]?.count ?? 0 + }; +} + +// ========================= 页面渲染 ========================= +function buildPagesFromState(state, resolvedLinks, surgeQueue) { + const items = Array.isArray(state.items) ? state.items : []; + const rate = state.lastRate; + + let page1 = ''; + let page2 = ''; + let page3 = ''; + let firstGroupJsonObj = {}; + let hasMultipleSurgeGroups = false; + + if (items.length > 0) { + page1 += '🎮 游戏比价及代购信息\n\n'; + if (rate) { + page1 += `💱 最近一次汇率: 1 NGN ≈ ${Number(rate).toFixed(6)} CNY\n\n`; + } + + let copyableText = ''; + let totalCurrentPriceNGN = 0; + + items.forEach((info, idx) => { + const displayIndex = idx + 1; + + copyableText += `游戏(${displayIndex})\n`; + copyableText += `名称: ${info.gameName}\n`; + + copyableText += `原价: ${info.originalPriceStr}`; + if (info.originalPriceCNY !== 'N/A') copyableText += ` (¥${info.originalPriceCNY})`; + copyableText += '\n'; + + copyableText += `现价: ${info.currentPriceStr}`; + if (info.currentPriceCNY !== 'N/A') copyableText += ` (¥${info.currentPriceCNY})`; + copyableText += '\n'; + + copyableText += '----------------------------------------\n'; + + if (typeof info.currentPriceNum === 'number') { + totalCurrentPriceNGN += info.currentPriceNum; + } + }); + + const cnyPrices = items.map(info => info.currentPriceCNY).filter(p => p !== 'N/A'); + + if (cnyPrices.length > 0) { + const calc = calculateProxyPrice(cnyPrices); + copyableText += `📊 游戏总价: NGN ${totalCurrentPriceNGN.toFixed(2)}\n`; + copyableText += `🛍️ 代购总价: ${calc.expressionStr}`; + } else { + copyableText += `📊 游戏总价: NGN ${totalCurrentPriceNGN.toFixed(2)}\n`; + copyableText += '🛍️ 代购总价: ¥0.00'; + } + + page1 += `
${escapeHTML(copyableText.trim())}
`; + } else { + page1 += '📭 目前还没有成功解析的游戏。'; + } + + page2 += '📦 Surge 分组队列\n\n'; + + const groups = Array.isArray(surgeQueue?.groups) ? surgeQueue.groups : []; + + if (groups.length > 0) { + page2 += `当前剩余 ${groups.length} 组待同步\n\n`; + + groups.forEach((group, idx) => { + page2 += `【 第 ${group.groupIndex} 组 】共 ${group.count} 个商品\n`; + page2 += `
${escapeHTML(JSON.stringify(group.products))}
\n\n`; + + if (idx === 0) { + firstGroupJsonObj = group.products; + } else { + hasMultipleSurgeGroups = true; + } + }); + } else { + page2 += '📭 当前没有待同步的 Surge 数据。'; + } + + page3 += '🔗 已解析链接记录\n\n'; + + if (resolvedLinks.length > 0) { + resolvedLinks.forEach((item, idx) => { + page3 += `游戏(${idx + 1})\n`; + page3 += `名称: ${item.gameName || 'Unknown Game'}\n`; + page3 += `bigId: ${item.bigId || 'N/A'}\n`; + page3 += `解析链接: ${escapeHTML(item.resolvedUrl || '')}\n`; + page3 += '----------------------------------------\n'; + }); + } else { + page3 += '📭 当前还没有已解析的链接记录。'; + } + + return { + page1, + page2, + page3, + firstGroupJsonObj, + hasMultipleSurgeGroups + }; +} + +async function persistRenderedPages(env, userId) { + const state = await loadSuccessState(env, userId); + const surgeQueue = await loadSurgeQueue(env, userId); + const resolvedLinks = await loadResolvedLinks(env, userId); + const pages = buildPagesFromState(state, resolvedLinks, surgeQueue); + + await env.LINKS_KV.put(getPagesKey(userId), JSON.stringify({ + page_1: pages.page1, + page_2: pages.page2, + page_3: pages.page3 + })); + + return pages; +} + +async function showQuotePage(env, chatId, userId, messageId = null) { + const pages = await persistRenderedPages(env, userId); + + if (messageId) { + try { + await editTelegramMessage( + chatId, + messageId, + pages.page1, + getPaginationKeyboard('page_1') + ); + return; + } catch (_) {} + } + + await sendTelegramMessage( + chatId, + pages.page1, + getPaginationKeyboard('page_1') + ); +} + +// ========================= 单链接解析 ========================= +async function processSingleXboxLink(startUrl, currentRate) { + const urlObj = new URL(startUrl); + urlObj.searchParams.set('r', 'en-us'); + const safeFetchUrl = urlObj.toString(); + + const redirectResponse = await fetchWithTimeout( + safeFetchUrl, + { redirect: 'follow' }, + FETCH_TIMEOUT_MS + ); + + const finalUrl = redirectResponse.url; + + try { + redirectResponse.body?.cancel(); + } catch (_) {} + + const idMatch = finalUrl.match(/\/([a-zA-Z0-9]{12})(?:[\/?#]|$)/); + + if (!idMatch) { + throw new Error('无法提取 bigId'); + } + + const bigId = idMatch[1]; + const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=NG&languages=en-ng&MS-CV=DUMMY.1`; + + const apiResponse = await fetchWithTimeout(apiUrl, {}, FETCH_TIMEOUT_MS); + if (!apiResponse.ok) { + throw new Error(`微软接口请求失败: ${apiResponse.status}`); + } + + const data = await apiResponse.json(); + if (!data.Products || data.Products.length === 0) { + throw new Error('Products 为空'); + } + + const product = data.Products[0]; + const gameName = product.LocalizedProperties?.[0]?.ProductTitle || 'Unknown Game'; + + let targetSkuId = ''; + let targetAvailabilityId = ''; + let originalPriceStr = 'N/A'; + let currentPriceStr = 'N/A'; + let originalPriceNum = null; + let currentPriceNum = null; + + for (const skuObj of product.DisplaySkuAvailabilities || []) { + if (skuObj.Sku && skuObj.Sku.SkuType === 'full') { + for (const avail of skuObj.Availabilities || []) { + if (avail.Actions && avail.Actions.includes('Purchase')) { + targetSkuId = skuObj.Sku.SkuId; + targetAvailabilityId = avail.AvailabilityId; + + if (avail.OrderManagementData?.Price) { + const priceData = avail.OrderManagementData.Price; + const currency = priceData.CurrencyCode; + originalPriceNum = priceData.MSRP; + currentPriceNum = priceData.ListPrice; + + originalPriceStr = `${currency} ${originalPriceNum}`; + currentPriceStr = `${currency} ${currentPriceNum}`; + } + break; + } + } + } + if (targetAvailabilityId) break; + } + + if (!targetAvailabilityId) { + throw new Error('没有找到可购买的 SKU / Availability'); + } + + return { + sourceUrl: startUrl, + resolvedUrl: finalUrl, + bigId, + gameName, + targetSkuId, + targetAvailabilityId, + originalPriceStr, + currentPriceStr, + originalPriceNum, + currentPriceNum, + originalPriceCNY: originalPriceNum !== null ? (originalPriceNum * currentRate).toFixed(2) : 'N/A', + currentPriceCNY: currentPriceNum !== null ? (currentPriceNum * currentRate).toFixed(2) : 'N/A' + }; +} + +// ========================= 运行主逻辑 ========================= + +// ★ 新增:构建"继续"按钮键盘 +function getContinueKeyboard() { + return { + inline_keyboard: [ + [{ text: '▶️ 继续抓取', callback_data: 'action_run' }] + ] + }; +} + +// ★ 修改:finalizeRun 增加 batchSuccess / batchFail 参数 +async function finalizeRun( + env, chatId, userId, runId, progressMessageId, + total, processed, reason = 'done', + batchSuccess = 0, batchFail = 0 +) { + if (!(await isRunActive(env, userId, runId))) { + return; + } + + // ★ 核心改动:'done' 分支根据剩余队列决定显示内容 + if (reason === 'done') { + const pendingAfter = await loadPendingQueue(env, userId); + const state = await loadSuccessState(env, userId); + + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); + + if (pendingAfter.length > 0) { + // 队列还有内容 → 只显示本批摘要 + 继续按钮 + const summaryText = + `✅ 本批执行完成\n\n` + + `成功: ${batchSuccess} 个\n` + + `失败: ${batchFail} 个(已保留在队列中)\n\n` + + `累计解析: ${state.items.length} 个\n` + + `队列剩余: ${pendingAfter.length} 个`; + + if (progressMessageId) { + try { + await editTelegramMessage(chatId, progressMessageId, summaryText, getContinueKeyboard()); + return; + } catch (_) {} + } + await sendTelegramMessage(chatId, summaryText, getContinueKeyboard()); + } else { + // 队列已清空 → 显示完整报价/Surge/链接页 + await showQuotePage(env, chatId, userId, progressMessageId); + } + + return; + } + + if (reason === 'error_rate') { + const finalText = + `❌ 任务终止\n\n` + + `获取汇率失败,已停止执行。\n` + + `当前待处理仍保留在队列中。`; + + if (progressMessageId) { + try { + await editTelegramMessage(chatId, progressMessageId, finalText, getPaginationKeyboard('page_1')); + } catch (_) { + await sendTelegramMessage(chatId, finalText, getPaginationKeyboard('page_1')); + } + } else { + await sendTelegramMessage(chatId, finalText, getPaginationKeyboard('page_1')); + } + + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); + return; + } + + if (reason === 'force_stopped') { + await showQuotePage(env, chatId, userId, progressMessageId); + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); + return; + } + + // 兜底(理论上不会走到这里) + const pendingQueue = await loadPendingQueue(env, userId); + const state = await loadSuccessState(env, userId); + const bar = makeProgressBar(processed, total, 10); + + const fallbackText = + `✅ 本轮执行完成\n\n` + + `${bar}\n` + + `${processed}/${total}\n\n` + + `当前待处理: ${pendingQueue.length} 个\n` + + `累计成功解析: ${state.items.length} 个`; + + if (progressMessageId) { + try { + await editTelegramMessage(chatId, progressMessageId, fallbackText, getPaginationKeyboard('page_1')); + } catch (_) { + await sendTelegramMessage(chatId, fallbackText, getPaginationKeyboard('page_1')); + } + } else { + await sendTelegramMessage(chatId, fallbackText, getPaginationKeyboard('page_1')); + } + + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); +} + +async function runQueueTask(env, chatId, userId, runId, progressMessageId, targets) { + const total = targets.length; + let processed = 0; + + try { + const rate = await getRealTimeExchangeRate(); + + if (!(await isRunActive(env, userId, runId))) { + return; + } + + const state = await loadSuccessState(env, userId); + state.lastRate = rate; + await saveSuccessState(env, userId, state); + await persistRenderedPages(env, userId); + + const pendingBefore = await loadPendingQueue(env, userId); + + if (progressMessageId) { + try { + await editTelegramMessage( + chatId, + progressMessageId, + buildProgressText( + total, + 0, + pendingBefore.length, + rate, + `本批处理中: ${targets.length} 个` + ) + ); + } catch (_) {} + } + + const batchPromise = Promise.allSettled( + targets.map(url => processSingleXboxLink(url, rate)) + ); + + const raceResult = await Promise.race([ + batchPromise, + new Promise(resolve => setTimeout(() => resolve('__BATCH_TIMEOUT__'), BATCH_TIMEOUT_MS)) + ]); + + if (raceResult === '__BATCH_TIMEOUT__') { + const stopped = await forceStopRunInternal(env, userId, runId); + + if (stopped) { + await showQuotePage(env, chatId, userId, progressMessageId); + } + + return; + } + + const results = raceResult; + + if (!(await isRunActive(env, userId, runId))) { + return; + } + + let batchSuccess = 0; + let batchFail = 0; + + for (let i = 0; i < targets.length; i++) { + if (!(await isRunActive(env, userId, runId))) { + return; + } + + const sourceUrl = targets[i]; + const result = results[i]; + + if (result.status === 'fulfilled') { + const item = result.value; + + const freshState = await loadSuccessState(env, userId); + const exists = freshState.items.some(x => x.sourceUrl === sourceUrl); + + if (!exists) { + freshState.items.push(item); + freshState.lastRate = rate; + await saveSuccessState(env, userId, freshState); + + if (!(await isRunActive(env, userId, runId))) { + return; + } + + await appendProductToSurgeQueue(env, userId, item); + await appendResolvedLink(env, userId, { + sourceUrl: item.sourceUrl, + resolvedUrl: item.resolvedUrl, + bigId: item.bigId, + gameName: item.gameName + }); + } + + if (!(await isRunActive(env, userId, runId))) { + return; + } + + // 成功:从待处理队列中移除 + const latestPending = await loadPendingQueue(env, userId); + const newPending = latestPending.filter(x => x !== sourceUrl); + await savePendingQueue(env, userId, newPending); + + batchSuccess++; + } else { + // ★ 失败:URL 保留在待处理队列中(未移除),计入失败数 + batchFail++; + } + } + + if (!(await isRunActive(env, userId, runId))) { + return; + } + + processed = targets.length; + + const latestMeta = await loadRunMeta(env, userId); + if (latestMeta && latestMeta.runId === runId) { + latestMeta.processed = processed; + await saveRunMeta(env, userId, latestMeta); + } + + await persistRenderedPages(env, userId); + + const pendingAfter = await loadPendingQueue(env, userId); + + if (progressMessageId) { + try { + await editTelegramMessage( + chatId, + progressMessageId, + buildProgressText( + total, + processed, + pendingAfter.length, + rate, + `本批完成: ${targets.length} 个\n成功: ${batchSuccess} 个\n失败: ${batchFail} 个` + ) + ); + } catch (_) {} + } + + // ★ 将 batchSuccess / batchFail 传入 finalizeRun + await finalizeRun( + env, chatId, userId, runId, progressMessageId, + total, processed, 'done', + batchSuccess, batchFail + ); + } catch (error) { + if (String(error?.message || '').includes('汇率')) { + await finalizeRun(env, chatId, userId, runId, progressMessageId, total, processed, 'error_rate'); + return; + } + + if (await isRunActive(env, userId, runId)) { + await sendTelegramMessage( + chatId, + `❌ 执行异常\n\n${escapeHTML(error?.message || '未知错误')}\n\n已完成的数据不会丢失。` + ); + + await env.LINKS_KV.delete(getRunLockKey(userId)); + await clearRunMeta(env, userId); + } + } +} + +export default { + async fetch(request, env, ctx) { + const requestUrl = new URL(request.url); + + // ================= GET:syncxbox.com / Surge 前端读取当前组 ================= + if (request.method === 'GET') { + if (requestUrl.searchParams.get('token') === VIEW_TOKEN) { + const headers = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }; + + if (requestUrl.searchParams.get('action') === 'clear') { + const result = await popFirstSurgeGroup(env, ALLOWED_USER_ID); + return new Response(JSON.stringify(result), { + status: 200, + headers + }); + } + + const queue = await loadSurgeQueue(env, ALLOWED_USER_ID); + + if (!queue.groups.length) { + return new Response(JSON.stringify({ + ok: true, + currentGroupIndex: null, + currentGroupCount: 0, + remainingGroups: 0, + remainingAfterCurrent: 0, + currentGroup: {} + }), { + status: 200, + headers + }); + } + + const current = queue.groups[0]; + + return new Response(JSON.stringify({ + ok: true, + currentGroupIndex: current.groupIndex, + currentGroupCount: current.count, + remainingGroups: queue.groups.length, + remainingAfterCurrent: Math.max(queue.groups.length - 1, 0), + currentGroup: current.products + }), { + status: 200, + headers + }); + } + + return new Response('Xbox Bot is running.', { status: 200 }); + } + + // ================= Telegram webhook ================= + try { + const payload = await request.json(); + + let chatId, userId, text; + let cbId = null; + let callbackMessageId = null; + + if (payload.callback_query) { + chatId = payload.callback_query.message.chat.id; + userId = payload.callback_query.from.id; + text = payload.callback_query.data; + cbId = payload.callback_query.id; + callbackMessageId = payload.callback_query.message.message_id; + + if (text.startsWith('page_')) { + const storedResult = await env.LINKS_KV.get(getPagesKey(userId)); + + if (storedResult) { + const pagesData = JSON.parse(storedResult); + + if (pagesData[text]) { + try { + await editTelegramMessage( + chatId, + callbackMessageId, + pagesData[text], + getPaginationKeyboard(text) + ); + await answerCallbackQuery(cbId); + } catch (_) { + await answerCallbackQuery(cbId); + } + } else { + await answerCallbackQuery(cbId, '⚠️ 页面不存在'); + } + } else { + await answerCallbackQuery(cbId, '⚠️ 页面数据为空'); + } + + return new Response('OK', { status: 200 }); + } + + if (text === 'action_run') { + await answerCallbackQuery(cbId); + // ★ action_run 回调直接复用 /run 逻辑,text 已经是 'action_run',下面统一处理 + } + } else if (payload.message && payload.message.text) { + chatId = payload.message.chat.id; + userId = payload.message.from.id; + text = payload.message.text.trim(); + } else { + return new Response('OK', { status: 200 }); + } + + if (userId !== ALLOWED_USER_ID) { + return new Response('Unauthorized', { status: 200 }); + } + + const queueKey = getQueueKey(userId); + const lockKey = getRunLockKey(userId); + const stateKey = getStateKey(userId); + const pagesKey = getPagesKey(userId); + const runMetaKey = getRunMetaKey(userId); + const surgeQueueKey = getSurgeQueueKey(userId); + const resolvedLinksKey = getResolvedLinksKey(userId); + + if (text.toLowerCase() === '/view_product') { + const queue = await loadSurgeQueue(env, userId); + + if (!queue.groups.length) { + await sendTelegramMessage(chatId, '📭 当前没有待同步的 Surge 分组数据。'); + } else { + const current = queue.groups[0]; + let msg = + `📦 当前待同步的 Surge 分组队列\n\n` + + `剩余组数: ${queue.groups.length}\n` + + `当前组编号: ${current.groupIndex}\n` + + `当前组商品数: ${current.count}\n\n` + + `
${escapeHTML(JSON.stringify(current.products))}
`; + + await sendTelegramMessage(chatId, msg); + } + + return new Response('OK', { status: 200 }); + } + + if (text.toLowerCase().startsWith('/update_product')) { + const jsonStr = text.substring('/update_product'.length).trim(); + + if (!jsonStr) { + await sendTelegramMessage(chatId, '⚠️ 请在命令后附带合法的 JSON 字符串。'); + return new Response('OK', { status: 200 }); + } + + try { + const parsedData = JSON.parse(jsonStr); + const keys = Object.keys(parsedData || {}); + const queue = { + nextGroupIndex: 2, + groups: keys.length > 0 ? [{ + groupIndex: 1, + count: keys.length, + products: parsedData + }] : [] + }; + await saveSurgeQueue(env, userId, queue); + await persistRenderedPages(env, userId); + await sendTelegramMessage(chatId, '✅ 成功覆盖更新 Surge 分组数据!'); + } catch (e) { + await sendTelegramMessage(chatId, `❌ JSON 格式错误:\n${escapeHTML(e.message)}`); + } + + return new Response('OK', { status: 200 }); + } + + if (text.toLowerCase() === '/view_list') { + const urls = await loadPendingQueue(env, userId); + + if (urls.length === 0) { + await sendTelegramMessage(chatId, '📭 当前待处理链接队列为空。'); + } else { + let listText = `📋 当前待处理链接(共 ${urls.length} 个):\n\n`; + urls.forEach((url, index) => { + listText += `${index + 1}. ${escapeHTML(url)}\n`; + }); + await sendTelegramMessage(chatId, listText); + } + return new Response('OK', { status: 200 }); + } + + if (text.toLowerCase() === '/force_stop') { + const meta = await loadRunMeta(env, userId); + + if (!meta) { + await sendTelegramMessage(chatId, 'ℹ️ 当前没有正在执行的任务。'); + return new Response('OK', { status: 200 }); + } + + await env.LINKS_KV.delete(lockKey); + await clearRunMeta(env, userId); + await showQuotePage(env, chatId, userId, meta.progressMessageId); + + return new Response('OK', { status: 200 }); + } + + if (text.toLowerCase() === '/clear') { + await env.LINKS_KV.delete(queueKey); + await env.LINKS_KV.delete(stateKey); + await env.LINKS_KV.delete(pagesKey); + await env.LINKS_KV.delete(runMetaKey); + await env.LINKS_KV.delete(surgeQueueKey); + await env.LINKS_KV.delete(resolvedLinksKey); + await env.LINKS_KV.delete('LATEST_XBOX_LIST'); + await env.LINKS_KV.delete(lockKey); + + await sendTelegramMessage( + chatId, + '🗑️ 已彻底清空待处理队列成功解析记录已解析链接记录分页缓存与云端 Product 数据。' + ); + return new Response('OK', { status: 200 }); + } + + // ★ /run 和 action_run 回调统一处理 + if (text.toLowerCase() === '/run' || text === 'action_run') { + let isRunning = await env.LINKS_KV.get(lockKey); + + if (isRunning) { + const meta = await loadRunMeta(env, userId); + if (!meta) { + await env.LINKS_KV.delete(lockKey); + isRunning = null; + } + } + + if (isRunning) { + await sendTelegramMessage(chatId, '⏳ 当前已有任务正在执行中,请先 /force_stop。'); + return new Response('OK', { status: 200 }); + } + + const pendingQueue = await loadPendingQueue(env, userId); + if (pendingQueue.length === 0) { + await sendTelegramMessage(chatId, '📭 链接队列为空,请先发送游戏链接。'); + return new Response('OK', { status: 200 }); + } + + // 每次只处理前 5 个 + const targets = pendingQueue.slice(0, MINI_CONCURRENCY); + const runTotal = targets.length; + + const initText = + `⏳ 准备开始抓取\n\n` + + `本次处理: ${runTotal} 个\n` + + `队列剩余总数: ${pendingQueue.length} 个\n\n` + + `${makeProgressBar(0, runTotal)}\n0/${runTotal}`; + + // ★ 如果来自回调按钮,直接编辑那条消息;否则发新消息 + let progressMessageId; + if (callbackMessageId) { + await editTelegramMessage(chatId, callbackMessageId, initText, null); + progressMessageId = callbackMessageId; + } else { + const startMsg = await sendTelegramMessage(chatId, initText); + progressMessageId = startMsg?.result?.message_id || null; + } + + const runId = crypto.randomUUID(); + + await env.LINKS_KV.put(lockKey, runId, { expirationTtl: RUN_LOCK_TTL }); + await saveRunMeta(env, userId, { + runId, + chatId, + progressMessageId, + total: runTotal, + processed: 0, + startedAt: new Date().toISOString() + }); + + ctx.waitUntil(runQueueTask(env, chatId, userId, runId, progressMessageId, targets)); + return new Response('OK', { status: 200 }); + } + + const urlRegex = /(https?:\/\/[^\s]+)/g; + const newUrls = text.match(urlRegex); + + if (newUrls && newUrls.length > 0) { + const pendingQueue = await loadPendingQueue(env, userId); + const state = await loadSuccessState(env, userId); + const resolvedLinks = await loadResolvedLinks(env, userId); + + const successUrlSet = new Set((state.items || []).map(x => x.sourceUrl)); + const resolvedUrlSet = new Set((resolvedLinks || []).map(x => x.sourceUrl)); + const pendingSet = new Set(pendingQueue); + + const reallyNewUrls = newUrls.filter( + url => !successUrlSet.has(url) && !resolvedUrlSet.has(url) && !pendingSet.has(url) + ); + + const mergedQueue = [...pendingQueue, ...reallyNewUrls]; + await savePendingQueue(env, userId, mergedQueue); + await persistRenderedPages(env, userId); + + const inlineKeyboard = { + inline_keyboard: [ + [ + { text: '🚀 一键抓取执行', callback_data: 'action_run' } + ] + ] + }; + + if (reallyNewUrls.length > 0) { + await sendTelegramMessage( + chatId, + `✅ 成功记录 ${reallyNewUrls.length} 个新链接。\n\n当前待处理队列共 ${mergedQueue.length} 个。`, + inlineKeyboard + ); + } else { + await sendTelegramMessage( + chatId, + `ℹ️ 这些链接已经在待处理队列中,或之前已成功解析过。\n\n当前待处理队列共 ${mergedQueue.length} 个。`, + inlineKeyboard + ); + } + } + + return new Response('OK', { status: 200 }); + } catch (_) { + return new Response('Error', { status: 200 }); + } + } +};