Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/askaralim_nba-stats-api.json
T

1 line
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "askaralim/nba-stats-api", "files": {"services/openaiService.js": {"3c344400e2280bb0b6142ec4a254b6e3d993b81f": {"t": 1785768947.0557935, "n": 16276, "h": "cae0c3c274a4cc3e", "c": "/**\n * OpenAI Service for Game Summary Generation\n * Uses api2d for OpenAI API access\n * Two-step process: 1) Analyze data → JSON, 2) Generate summary from JSON\n */\n\nconst axios = require('axios');\nconst logger = require('../utils/logger');\n\nclass OpenAIService {\n constructor() {\n this.apiKey = process.env.OPENAI_API_KEY;\n this.baseURL = process.env.OPENAI_BASE_URL || 'https://openai.api2d.net/v1';\n this.model = process.env.GPT_MODEL || 'gpt-3.5-turbo';\n this.maxTokens = parseInt(process.env.GPT_MAX_TOKENS || '500');\n this.temperature = parseFloat(process.env.GPT_TEMPERATURE || '0.7');\n }\n\n /**\n * Step 1: Analyze game data and return structured JSON\n * @param {Object} gameFacts - Structured game facts\n * @returns {Promise<Object>} Analysis JSON\n */\n async analyzeGameData(gameFacts) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const prompt = this.buildAnalysisPrompt(gameFacts);\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个比赛数据分析引擎,而不是解说员。'\n },\n {\n role: 'user',\n content: prompt\n }\n ],\n temperature: 0.3, // Lower temperature for more deterministic analysis\n max_tokens: 800,\n response_format: { type: 'json_object' }\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n const content = response.data?.choices?.[0]?.message?.content?.trim();\n if (!content) {\n throw new Error('Empty response from OpenAI');\n }\n\n // Parse JSON response\n const analysis = JSON.parse(content);\n return analysis;\n } catch (error) {\n logger.error({ component: 'openai', task: 'analysis', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI analysis error');\n throw error;\n }\n }\n\n /**\n * Step 2: Generate summary text from analysis JSON\n * @param {Object} analysis - Analysis JSON from step 1\n * @returns {Promise<string>} Generated summary text\n */\n async generateSummaryFromAnalysis(analysis) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const prompt = this.buildSummaryPrompt(analysis);\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一名数据型比赛解说员。'\n },\n {\n role: 'user',\n content: prompt\n }\n ],\n temperature: this.temperature,\n max_tokens: this.maxTokens\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n const summary = response.data?.choices?.[0]?.message?.content?.trim();\n if (!summary) {\n throw new Error('Empty response from OpenAI');\n }\n\n return summary;\n } catch (error) {\n logger.error({ component: 'openai', task: 'summary', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI summary error');\n throw error;\n }\n }\n\n /**\n * Generate game summary using two-step process\n * @param {Object} gameFacts - Structured game facts\n * @returns {Promise<string>} Generated summary text\n */\n async generateGameSummary(gameFacts) {\n try {\n // Step 1: Analyze data and get JSON\n const analysis = await this.analyzeGameData(gameFacts);\n \n // Step 2: Generate summary from analysis\n const summary = await this.generateSummaryFromAnalysis(analysis);\n \n return summary;\n } catch (error) {\n logger.error({ component: 'openai', task: 'twoStepSummary', errorMessage: error.message }, 'Two-step AI summary generation failed');\n throw error;\n }\n }\n\n /**\n * Build Step 1 prompt: Data analysis\n * @param {Object} gameFacts - Structured game facts\n * @returns {string} Formatted prompt\n */\n buildAnalysisPrompt(gameFacts) {\n const {\n home_team,\n away_team,\n home_score,\n away_score,\n home_half,\n away_half,\n q1,\n q2,\n q3,\n q4,\n fg_home_made,\n fg_home_attempted,\n fg_home_percent,\n fg_away_made,\n fg_away_attempted,\n fg_away_percent,\n three_home_made,\n three_home_attempted,\n three_home_percent,\n three_away_made,\n three_away_attempted,\n three_away_percent,\n ft_home_made,\n ft_home_attempted,\n ft_home_percent,\n ft_away_made,\n ft_away_attempted,\n ft_away_percent,\n to_home,\n to_away,\n reb_home,\n reb_home_offensive,\n reb_home_defensive,\n reb_away,\n reb_away_offensive,\n reb_away_defensive,\n has_overtime,\n overtime_periods,\n largest_lead_home,\n largest_lead_away,\n foul_home,\n foul_away,\n points_in_paint_home,\n points_in_paint_away,\n fast_break_points_home,\n fast_break_points_away,\n turnover_points_home,\n turnover_points_away,\n halftime_leader,\n winner,\n top_scorer_home,\n top_scorer_away,\n top_points_home,\n top_points_away,\n } = gameFacts;\n\n // Build overtime section if applicable\n let overtimeSection = '';\n if (has_overtime && overtime_periods && overtime_periods.length > 0) {\n const otScores = overtime_periods.map(ot => `OT${ot.period}: ${ot.score}`).join('\\n ');\n overtimeSection = `- 加时赛比分:\\n ${otScores}\\n`;\n }\n\n return `请根据以下比赛数据,进行严格的数据对比分析,并以 JSON 格式输出结果。\n\n【分析规则】\n1. 只能使用给定数据,禁止推断或补充\n2. 标记主队是否在以下方面占优:\n - 篮板\n - 失误(更少为占优)\n - 投篮命中率\n - 三分命中数\n3. 判断比赛走势:\n - 上半场领先方\n - 下半场净胜方\n - 是否进入加时\n4. 判断是否存在\"明确胜因\":\n - 若仅有 1 项轻微优势,标记为\"胜负接近\"\n5. 不使用任何主观或修饰性语言\n\n【输出 JSON 结构】\n{\n \"home\": \"GSW\",\n \"away\": \"LAL\",\n \"winner\": \"home/away\",\n \"halftime_leader\": \"home/away/tie\",\n \"overtime\": true/false,\n \"advantages\": {\n \"rebounds\": \"home/away/none\",\n \"rebounds_home\": number,\n \"rebounds_away\": number,\n \"turnovers\": \"home/away/none\",\n \"turnovers_home\": number,\n \"turnovers_away\": number,\n \"fg_home_made\": number,\n \"fg_away_made\": number,\n \"fg_home_attempted\": number,\n \"fg_away_attempted\": number,\n \"fg_home_percentage\": number,\n \"fg_away_percentage\": number,\n \"three_pt_home_made\": number,\n \"three_pt_away_made\": number,\n \"three_pt_home_attempted\": number,\n \"three_pt_away_attempted\": number,\n \"three_pt_home_percentage\": number,\n \"three_pt_away_percentage\": number,\n \"ft_home_made\": number,\n \"ft_away_made\": number,\n \"ft_home_attempted\": number,\n \"ft_away_attempted\": number,\n \"ft_home_percentage\": number,\n \"ft_away_percentage\": number,\n \"points_in_paint_home\": number,\n \"points_in_paint_away\": number,\n \"fast_break_points_home\": number,\n \"fast_break_points_away\": number,\n \"turnover_points_home\": number,\n \"turnover_points_away\": number,\n \"largest_lead_home\": number,\n \"largest_lead_away\": number,\n \"foul_home\": number,\n \"foul_away\": number,\n \"decisive_factors\": [decisive_factor1, decisive_factor2, ...],\n \"close_game\": true/false\n },\n \"decisive_factors\": [decisive_factor1, decisive_factor2, ...],\n \"close_game\": true/false\n}\n\n比赛数据:\n- 主队:${home_team}\n- 客队:${away_team}\n- 最终比分:${home_score} - ${away_score}\n- 半场比分:${home_half} - ${away_half}\n- 每节比分:\n Q1: ${q1}\n Q2: ${q2}\n Q3: ${q3}\n Q4: ${q4}\n${overtimeSection}\n- 投篮命中数(主):${fg_home_made} 投篮出手数:${fg_home_attempted}\n- 投篮命中率(主):${fg_home_percent}%\n- 投篮命中数(客):${fg_away_made} 投篮出手数:${fg_away_attempted}\n- 投篮命中率(客):${fg_away_percent}%\n- 三分命中数(主):${three_home_made} 三分出手数:${three_home_attempted}\n- 三分命中率(主):${three_home_percent}%\n- 三分命中数(客):${three_away_made} 三分出手数:${three_away_attempted}\n- 三分命中率(客):${three_away_percent}%\n- 罚球命中数(主):${ft_home_made} 罚球出手数:${ft_home_attempted}\n- 罚球命中率(主):${ft_home_percent}%\n- 罚球命中数(客):${ft_away_made} 罚球出手数:${ft_away_attempted}\n- 罚球命中率(客):${ft_away_percent}%\n- 失误(主/客):${to_home} / ${to_away}\n- 篮板(主/客):${reb_home} / ${reb_away}\n- 篮板(主/客):${reb_home_offensive} / ${reb_away_offensive}\n- 篮板(主/客):${reb_home_defensive} / ${reb_away_defensive}\n- 最大领先(主/客):${largest_lead_home} / ${largest_lead_away}\n- 犯规(主/客):${foul_home} / ${foul_away}\n- 内线得分(主/客):${points_in_paint_home} / ${points_in_paint_away}\n- 快攻得分(主/客):${fast_break_points_home} / ${fast_break_points_away}\n- 失误得分(主/客):${turnover_points_home} / ${turnover_points_away}\n `;}\n\n /**\n * Translate NBA news article to Simplified Chinese\n * @param {Object} params - { title, content }\n * @returns {Promise<{ translated_title: string, translated_content: string }>}\n */\n async translateNewsArticle({ title = '', content }) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n if (!content || typeof content !== 'string') {\n throw new Error('Content is required for translation');\n }\n\n const noPromoLinksInstruction =\n '\\n\\nImportant: Do not include promotional or tracking short links in your translation (e.g. nba.smart.link/…, other smart.link URLs, or bare app/watch CTAs that are only URLs). Remove them entirely from translated_title and translated_content. If the source ends with symbols like ➡️ or text that only points to such a link, end the Chinese sentence naturally without any URL.';\n const userPrompt = title\n ? `Translate the following NBA news to Simplified Chinese. Keep it professional, accurate, and fluent.${noPromoLinksInstruction}\\n\\nTitle: ${title}\\n\\nContent:\\n${content}\\n\\nRespond with JSON only: { \"translated_title\": \"...\", \"translated_content\": \"...\" }`\n : `Translate the following NBA news to Simplified Chinese. Keep it professional, accurate, and fluent.${noPromoLinksInstruction}\\n\\nContent:\\n${content}\\n\\nRespond with JSON only: { \"translated_title\": \"...\", \"translated_content\": \"...\" }. Use the first sentence or a short summary as translated_title if no title is given.`;\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个专业的体育新闻翻译。将英文NBA新闻翻译成简体中文,保持专业、准确、流畅。译文标题和正文中不要保留营销推广类短链接(例如 nba.smart.link/ 等);删除这类链接及仅用于引流的符号(如 ➡️),必要时改写使语句通顺。只输出JSON,不要其他文字。'\n\n },\n {\n role: 'user',\n content: userPrompt\n }\n ],\n temperature: 0.3,\n max_tokens: 2000,\n response_format: { type: 'json_object' }\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 60000\n }\n );\n\n const contentStr = response.data?.choices?.[0]?.message?.content?.trim();\n if (!contentStr) {\n throw new Error('Empty response from OpenAI');\n }\n\n const result = JSON.parse(contentStr);\n return {\n translated_title: result.translated_title || result.translatedTitle || '',\n translated_content: result.translated_content || result.translatedContent || content\n };\n } catch (error) {\n logger.error({ component: 'openai', task: 'translation', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI translation error');\n throw error;\n }\n }\n\n /**\n * Generate Swish Insight for a top performer (Chinese, data-driven)\n * @param {Object} payload - { playerName, gameStats, seasonStats }\n * @returns {Promise<string>} 1-2 sentence insight in Chinese\n */\n async generateSwishInsight(payload) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const { playerName, gameStats = {}, seasonStats = {} } = payload;\n const seasonData = seasonStats?.stats || seasonStats || {};\n const gamePts = parseInt(gameStats.points) || 0;\n const gameReb = parseInt(gameStats.rebounds) || 0;\n const gameAst = parseInt(gameStats.assists) || 0;\n const gameStl = parseInt(gameStats.steals) || 0;\n const gameBlk = parseInt(gameStats.blocks) || 0;\n const gameTov = parseInt(gameStats.turnovers) || 0;\n\n const fg = (gameStats.fieldGoals || '0-0').split('-');\n const three = (gameStats.threePointers || '0-0').split('-');\n const ft = (gameStats.freeThrows || '0-0').split('-');\n const fgm = parseInt(fg[0]) || 0;\n const fga = parseInt(fg[1]) || 0;\n const threePM = parseInt(three[0]) || 0;\n const threePA = parseInt(three[1]) || 0;\n const ftm = parseInt(ft[0]) || 0;\n const fta = parseInt(ft[1]) || 0;\n\n const fgPct = fga > 0 ? Math.round((fgm / fga) * 100) : 0;\n const threePct = threePA > 0 ? Math.round((threePM / threePA) * 100) : 0;\n const ftPct = fta > 0 ? Math.round((ftm / fta) * 100) : 0;\n\n const seasonPts = parseFloat(seasonData.avgPoints || seasonData.points || 0) || 0;\n const seasonFg = parseFloat(seasonData.fieldGoalPct || seasonData.fieldGoalPercentage || 0) || 0;\n const seasonThree = parseFloat(seasonData.threePointFieldGoalPct || seasonData.threePointFieldGoalPercentage || 0) || 0;\n const seasonFt = parseFloat(seasonData.freeThrowPct || seasonData.freeThrowPercentage || 0) || 0;\n const seasonReb = parseFloat(seasonData.avgRebounds || seasonData.rebounds || 0) || 0;\n const seasonAst = parseFloat(seasonData.avgAssists || seasonData.assists || 0) || 0;\n const seasonStl = parseFloat(seasonData.avgSteals || seasonData.steals || 0) || 0;\n const seasonBlk = parseFloat(seasonData.avgBlocks || seasonData.blocks || 0) || 0;\n const seasonTov = parseFloat(seasonData.avgTurnovers || seasonData.turnovers || 0) || 0;\n\n const userPrompt = `球员:${playerName}\n\n本场数据:${gamePts}分 ${gameReb}篮板 ${gameAst}助攻 ${gameStl}抢断 ${gameBlk}盖帽 ${gameTov}失误,投篮${fgm}/${fga}(${fgPct}%),三分${threePM}/${threePA}(${threePct}%),罚球${ftm}/${fta}(${ftPct}%)\n\n赛季场均:${seasonPts}分 ${seasonReb}篮板 ${seasonAst}助攻 ${seasonStl}抢断 ${seasonBlk}盖帽 ${seasonTov}失误,投篮${seasonFg}%,三分${seasonThree}%,罚球${seasonFt}%\n\n这是球员${playerName}的本场数据和赛季场均数据,请生成一句非常简短的数据洞察(最多20个汉字)。要求:\n- 突出球员对比赛的影响力\n- 球员对比赛的影响力包括进攻以及防守两个方面\n- 进攻球员需要在命中率以及得分上有显著的表现\n- 防守球员需要在抢断、篮板、盖帽以上有显著的表现\n- 如果球员有三双表现需要特别强调\n- 必须包含具体数字,不要解释背景\n- 按照你的理解,突出影响比赛的具体数据以及表现\n- 像体育解说的短评\n- 只输出一句话`;\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个NBA数据分析师。根据本场数据和赛季场均数据,生成一句简短的数据洞察,用中文。不要空洞赞美,要有具体对比。只输出洞察文字,不要引号或前缀。'\n },\n {\n role: 'user',\n content: userPrompt\n }\n ],\n temperature: 0.5,\n max_tokens: 40\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 15000\n }\n );\n\n const insight = response.data?.choices?.[0]?.message?.content?.trim();\n if (!insight) {\n throw new Error('Empty response from OpenAI');\n }\n return insight;\n } catch (error) {\n logger.error({ component: 'openai', task: 'swishInsight', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI Swish Insight error');\n throw error;\n }\n }\n\n /**\n * Build Step 2 prompt: Generate summary from analysis\n * @param {Object} analysis - Analysis JSON from step 1\n * @returns {string} Formatted prompt\n */\n buildSummaryPrompt(analysis) {\n const analysisJson = JSON.stringify(analysis, null, 2);\n\n return `请严格根据以下分析结果,用中文总结比赛。\n\n【写作规则】\n1. 只能使用提供的分析结论,禁止自行推断\n2. 若 close_game 为 true,尽量分析比赛成败关键因素,如果无法分析则不用描述\n3. 只能将 decisive_factors 中的数据作为胜因\n4. 若有加时赛,只能描述加时比分差异,若没有则不用描述加时赛\n5. 禁止使用情绪化或主观词语\n6. 如果不能准确翻译球队名称,请使用球队英文名或简称。\n7. 字数 60–120 字\n8. 语言可以轻松,口语化,不要过于正式\n\n【分析结果】\n${analysisJson}`;\n }\n}\n\nmodule.exports = new OpenAIService();\n"}}}}