// ==UserScript== // @name Steam AI Agent 对话智能体 // @name:en Steam AI Agent Chat // @namespace https://github.com/steam-card-view // @version 1.8.2 // @description Steam AI对话智能体侧边栏,支持多模型供应商(DeepSeek/GLM/Qwen/MiMo/Kimi等),精美SVG图标,游戏评价/玩家分析/购买建议/打折分析,多对话管理,对话历史,自定义智能体。支持知识库热更新。v1.8.2 新增游戏官方动态查询工具(getGameNews):问"dota2/cs2 最近更新"等具体游戏动态时,自动解析游戏名称(dota2/cs2等常见游戏内置别名直达)并拉取官方公告/版本更新(ISteamNews),无需API Key // @description:en Steam AI agent sidebar with multi-provider support, SVG icons, game review, player analysis, purchase advice, discount analysis, conversation history, custom agents. // @icon data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3E%3Crect%20width='24'%20height='24'%20rx='6'%20fill='%236366f1'/%3E%3Cpath%20d='M7%205.5h10a1.5%201.5%200%200%201%201.5%201.5v7a1.5%201.5%200%200%201-1.5%201.5h-4.5l-4%203.2V15.5H7a1.5%201.5%200%200%201-1.5-1.5V7A1.5%201.5%200%200%201%207%205.5z'%20fill='white'/%3E%3Cpath%20d='M12%207.5l1%202.8a2%202%200%200%200%201.1%201.1L17%2012.5l-2.9%201.1a2%202%200%200%200-1.1%201.1L12%2017.5l-1-2.8a2%202%200%200%200-1.1-1.1L7%2012.5l2.9-1.1a2%202%200%200%200%201.1-1.1L12%207.5z'%20fill='%236366f1'/%3E%3C/svg%3E // @match https://store.steampowered.com/* // @match https://steamcommunity.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @grant unsafeWindow // @connect store.steampowered.com // @connect api.steampowered.com // @connect steamcommunity.com // @connect api.deepseek.com // @connect open.bigmodel.cn // @connect api.xiaomimimo.com // @connect dashscope.aliyuncs.com // @connect api.moonshot.cn // @connect api.baichuan-ai.com // @connect api.minimaxi.com // @connect ark.cn-beijing.volces.com // @connect api.openai.com // @connect raw.githubusercontent.com // @connect api.augmentedsteam.com // @connect api.isthereanydeal.com // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; const _v = typeof GM_info !== 'undefined' ? GM_info.script.version : '1.8.0'; console.log(`%c[SA-Agent] v${_v} Steam AI Agent 已启动`, 'color:#8b5cf6;font-weight:bold;font-size:13px'); // ============================================================ // 配置与常量 // ============================================================ const CONFIG = { AI_DEFAULT_URL: 'https://api.deepseek.com/v1/chat/completions', AI_DEFAULT_MODEL: 'deepseek-chat', PANEL_WIDTH: 420, MAX_ITERATIONS: 5, TOOL_TIMEOUT: 30000, AI_TIMEOUT: 120000, CACHE_TTL: 30 * 60 * 1000, TOAST_DURATION: 3000, }; // ============================================================ // 模型供应商预设 // ============================================================ const MODEL_PROVIDERS = { deepseek: { name: 'DeepSeek', icon: 'deepseek', apiUrl: 'https://api.deepseek.com/v1/chat/completions', models: [ { value: 'deepseek-v4-pro', label: 'DeepSeek V4' }, { value: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash' }, ], defaultModel: 'deepseek-v4-pro', apiKeyUrl: 'https://platform.deepseek.com/api_keys', keyHint: '在 platform.deepseek.com 创建 API Key', }, glm: { name: '智谱GLM', icon: 'glm', apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', models: [ { value: 'glm-4-flash', label: 'GLM-4-Flash (免费)' }, { value: 'glm-5', label: 'GLM-5' }, { value: 'glm-5.3', label: 'GLM-5.3' } ], defaultModel: 'glm-4-flash', apiKeyUrl: 'https://open.bigmodel.cn/usercenter/apikeys', keyHint: '在 open.bigmodel.cn 创建 API Key', }, qwen: { name: '通义千问', icon: 'qwen', apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', models: [ { value: 'qwen-plus', label: 'Qwen Plus' }, { value: 'qwen-max', label: 'Qwen Max' } ], defaultModel: 'qwen-plus', apiKeyUrl: 'https://dashscope.console.aliyun.com/apiKey', keyHint: '在 dashscope.console.aliyun.com 创建 API Key', }, mimo: { name: '小米MiMo', icon: 'mimo', apiUrl: 'https://api.xiaomimimo.com/v1/chat/completions', models: [ { value: 'mimo-v2.5-pro', label: 'MiMo v2.5 Pro' }, { value: 'mimo-v2.5', label: 'MiMo v2.5' }, ], defaultModel: 'mimo-v2.5-pro', apiKeyUrl: 'https://platform.xiaomimimo.com/#/console/api-keys', keyHint: '在 platform.xiaomimimo.com 创建 API Key', }, moonshot: { name: 'Kimi', icon: 'moonshot', apiUrl: 'https://api.moonshot.cn/v1/chat/completions', models: [ { value: 'kimi-k3', label: 'Kimi K3' } ], defaultModel: 'kimi-k3', apiKeyUrl: 'https://platform.moonshot.cn/console/api-keys', keyHint: '在 platform.moonshot.cn 创建 API Key', }, baichuan: { name: '百川', icon: 'baichuan', apiUrl: 'https://api.baichuan-ai.com/v1/chat/completions', models: [ { value: 'Baichuan4-Turbo', label: 'Baichuan4 Turbo' }, { value: 'Baichuan4', label: 'Baichuan4' }, ], defaultModel: 'Baichuan4-Turbo', apiKeyUrl: 'https://platform.baichuan-ai.com', keyHint: '在 platform.baichuan-ai.com 创建 API Key', }, minimax: { name: 'MiniMax', icon: 'minimax', apiUrl: 'https://api.minimaxi.com/v1/chat/completions', models: [ { value: 'MiniMax-M3', label: 'MiniMax M3' } ], defaultModel: 'MiniMax-M3', apiKeyUrl: 'https://platform.minimaxi.com', keyHint: '在 platform.minimaxi.com 创建 API Key', }, doubao: { name: '豆包', icon: 'doubao', apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', models: [ { value: 'doubao-seed-2-1-pro-260628', label: 'Doubao Seed 2.1 Pro' }, { value: 'doubao-pro-32k', label: 'Doubao Pro 32K' }, { value: 'doubao-pro-128k', label: 'Doubao Pro 128K' }, ], defaultModel: 'doubao-seed-2-1-pro-260628', apiKeyUrl: 'https://console.volcengine.com/ark', keyHint: '在 console.volcengine.com/ark 创建 API Key', }, openai: { name: 'OpenAI', icon: 'openai', apiUrl: 'https://api.openai.com/v1/chat/completions', models: [ { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, { value: 'gpt-4o', label: 'GPT-4o' }, { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }, ], defaultModel: 'gpt-4o-mini', apiKeyUrl: 'https://platform.openai.com/api-keys', keyHint: '在 platform.openai.com 创建 API Key', }, custom: { name: '自定义', icon: 'custom', apiUrl: '', models: [], defaultModel: '', apiKeyUrl: '', keyHint: '输入兼容 OpenAI 格式的 API 地址', }, }; // ============================================================ // SVG 图标库 // ============================================================ const SVG_ICONS = { robot: ``, gamepad: ``, gift: ``, bot: ``, sparkle: ``, stats: ``, chat: ``, refresh: ``, copy: ``, send: ``, stop: ``, plus: ``, close: ``, download: ``, edit: ``, trash: ``, play: ``, check: ``, gear: ``, history: ``, key: ``, cpu: ``, messageSquare: ``, lightbulb: ``, search: ``, news: ``, book: ``, zap: ``, globe: ``, user: ``, // 供应商图标 - 彩色几何图形 provider_deepseek: ``, provider_glm: ``, provider_qwen: ``, provider_mimo: ``, provider_moonshot: ``, provider_baichuan: ``, provider_minimax: ``, provider_doubao: ``, provider_openai: ``, provider_custom: ``, }; function getIcon(name, size = 16) { const svg = SVG_ICONS[name]; if (!svg) return ''; return svg.replace(/${escapeHtml(agent.icon)}`; return getIcon('bot', size); } function providerIconHtml(providerKey, size = 18) { return getProviderIcon(providerKey, size); } const SK = { AI_URL: 'sa_ai_api_url', AI_KEY: 'sa_ai_api_key', AI_MODEL: 'sa_ai_model', STEAM_KEY: 'sa_steam_api_key', STEAM_ID: 'sa_steam_id', AGENTS: 'sa_agents_custom', CONVERSATIONS: 'sa_conversations', CONVERSATIONS_V2: 'sa_conversations_v2', CURRENT_AGENT: 'sa_current_agent', CURRENT_CONV: 'sa_current_conv', FAB_POS: 'sa_fab_position', PANEL_SIDE: 'sa_panel_side', SETTINGS: 'sa_settings', MODELS: 'sa_model_configs', ACTIVE_MODEL: 'sa_active_model', RULES_REMOTE: 'sa_rules_remote', RULES_URL: 'sa_rules_remote_url', TOKEN_STATS: 'sa_token_stats_v1', ITAD_KEY: 'sa_itad_key', }; // ============================================================ // CSS 样式 // ============================================================ GM_addStyle(` /* ====== 浮动按钮 ====== */ #sa-fab { position: fixed !important; width: 52px; height: 52px; border-radius: 50%; background: linear-gradient(135deg, #8b5cf6, #3b82f6) !important; cursor: grab; z-index: 1000004 !important; box-shadow: 0 4px 20px rgba(139,92,246,0.45) !important; transition: transform 0.2s ease, box-shadow 0.2s ease !important; display: flex !important; align-items: center; justify-content: center; user-select: none; touch-action: none; -webkit-tap-highlight-color: transparent; border: 2px solid rgba(255,255,255,0.15) !important; } #sa-fab:hover { transform: scale(1.1) !important; box-shadow: 0 6px 28px rgba(139,92,246,0.6) !important; } #sa-fab:active { cursor: grabbing; } #sa-fab svg { width: 28px !important; height: 28px !important; pointer-events: none; max-width: 28px !important; max-height: 28px !important; flex-shrink: 0 !important; } #sa-fab .sa-fab-badge { position: absolute; top: -2px; right: -2px; width: 12px; height: 12px; border-radius: 50%; background: #4ade80; border: 2px solid #0d1223; display: none; } #sa-fab.sa-has-key .sa-fab-badge { display: block; } /* ====== 侧边栏面板 ====== */ #sa-panel { position: fixed !important; top: 0 !important; right: 0 !important; width: ${CONFIG.PANEL_WIDTH}px !important; height: 100vh !important; background: rgba(13, 18, 35, 0.92) !important; backdrop-filter: blur(24px) saturate(160%) !important; -webkit-backdrop-filter: blur(24px) saturate(160%) !important; z-index: 1000004 !important; transform: translateX(100%) !important; transition: transform 0.35s cubic-bezier(0.4,0,0.2,1) !important; display: flex !important; flex-direction: column !important; border-left: 1px solid rgba(139,92,246,0.25) !important; box-shadow: -16px 0 60px rgba(0,0,0,0.65) !important; font-family: "Motiva Sans", Arial, sans-serif !important; color: #e2e8f0 !important; overflow: hidden; } #sa-panel.sa-open { transform: translateX(0) !important; } #sa-panel.sa-left-side { right: auto !important; left: 0 !important; transform: translateX(-100%) !important; border-left: none !important; border-right: 1px solid rgba(139,92,246,0.25) !important; box-shadow: 16px 0 60px rgba(0,0,0,0.65) !important; } #sa-panel.sa-left-side.sa-open { transform: translateX(0) !important; } /* ====== 顶部栏 ====== */ .sa-header { display: flex !important; align-items: center; gap: 6px; padding: 8px 12px !important; background: rgba(20, 27, 50, 0.9) !important; border-bottom: 1px solid rgba(139,92,246,0.15) !important; flex-shrink: 0; } .sa-agent-selector { flex: 1; min-width: 0; background: rgba(30, 41, 59, 0.8) !important; border: 1px solid rgba(139,92,246,0.2) !important; color: #e2e8f0 !important; border-radius: 8px !important; padding: 5px 8px !important; font-size: 12px !important; cursor: pointer; outline: none; font-family: inherit !important; } .sa-agent-selector:hover { border-color: rgba(139,92,246,0.4) !important; } .sa-agent-selector option { background: #1e293b !important; color: #e2e8f0 !important; } .sa-icon-btn { width: 30px; height: 30px; border-radius: 7px; background: rgba(30, 41, 59, 0.6) !important; border: 1px solid rgba(139,92,246,0.15) !important; color: #94a3b8 !important; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.15s ease; font-size: 14px; } .sa-icon-btn:hover { background: rgba(139,92,246,0.2) !important; color: #e2e8f0 !important; } .sa-icon-btn svg { width: 15px; height: 15px; } /* ====== 消息列表 ====== */ .sa-messages { flex: 1; overflow-y: auto; padding: 14px !important; display: flex; flex-direction: column; gap: 12px; scrollbar-width: thin; scrollbar-color: rgba(139,92,246,0.3) transparent; } .sa-messages::-webkit-scrollbar { width: 6px; } .sa-messages::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 3px; } .sa-msg { max-width: 88%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; word-wrap: break-word; } .sa-msg-user { align-self: flex-end; background: linear-gradient(135deg, #8b5cf6, #3b82f6) !important; color: #fff !important; border-bottom-right-radius: 4px; box-shadow: 0 2px 12px rgba(139,92,246,0.25) !important; } .sa-msg-ai { align-self: flex-start; background: rgba(30, 41, 59, 0.7) !important; border: 1px solid rgba(139,92,246,0.12) !important; border-bottom-left-radius: 4px; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; } .sa-msg-system { align-self: center; max-width: 95%; background: rgba(251, 191, 36, 0.08) !important; border: 1px solid rgba(251, 191, 36, 0.2) !important; color: #fbbf24 !important; font-size: 12px; text-align: center; } /* ====== AI 消息操作栏(复制按钮) ====== */ .sa-msg-footer { display: flex; align-items: center; justify-content: flex-end; gap: 6px; margin-top: 8px; padding-top: 6px; border-top: 1px dashed rgba(139, 92, 246, 0.15); } .sa-msg-copy { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 10px; border: none; cursor: pointer; background: rgba(139, 92, 246, 0.1); color: #94a3b8; font-size: 11px; line-height: 1.5; transition: all 0.15s ease; } .sa-msg-copy:hover { background: rgba(139, 92, 246, 0.25); color: #ddd6fe; } .sa-msg-copy svg { width: 11px; height: 11px; } .sa-msg-copy.sa-copied { background: rgba(74, 222, 128, 0.15); color: #4ade80; } /* ====== Markdown 内容 ====== */ .sa-md h1, .sa-md h2, .sa-md h3 { margin: 8px 0 4px; color: #f1f5f9; font-size: 15px; } .sa-md h1 { font-size: 17px; } .sa-md p { margin: 4px 0; } .sa-md ul, .sa-md ol { margin: 4px 0; padding-left: 18px; } .sa-md li { margin: 2px 0; } .sa-md strong { color: #c4b5fd; font-weight: 600; } .sa-md em { color: #93c5fd; } .sa-md code { background: rgba(0,0,0,0.3); padding: 1px 5px; border-radius: 4px; font-family: "Cascadia Code", Consolas, monospace; font-size: 12px; color: #4ade80; } .sa-md pre { background: rgba(0,0,0,0.35); border: 1px solid rgba(139,92,246,0.15); border-radius: 8px; padding: 10px; margin: 6px 0; overflow-x: auto; } .sa-md pre code { background: none; padding: 0; color: #e2e8f0; } .sa-md a { color: #60a5fa; text-decoration: none; } .sa-md a:hover { text-decoration: underline; } .sa-md blockquote { border-left: 3px solid rgba(139,92,246,0.3); padding-left: 10px; margin: 4px 0; color: #94a3b8; } /* ====== 输入区 (重新设计) ====== */ .sa-input-area { padding: 8px 12px 10px !important; background: rgba(20, 27, 50, 0.88) !important; backdrop-filter: blur(16px) saturate(150%) !important; -webkit-backdrop-filter: blur(16px) saturate(150%) !important; border-top: 1px solid rgba(139,92,246,0.18) !important; flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; } .sa-model-bar { display: flex; align-items: center; gap: 6px; } .sa-model-selector { flex: 1; min-width: 0; background: rgba(30, 41, 59, 0.6) !important; border: 1px solid rgba(139,92,246,0.2) !important; color: #c4b5fd !important; border-radius: 8px !important; padding: 4px 8px !important; font-size: 11px !important; cursor: pointer; outline: none; font-family: inherit !important; } .sa-model-selector:hover { border-color: rgba(139,92,246,0.4) !important; } .sa-model-selector option { background: #1e293b !important; color: #e2e8f0 !important; } .sa-input-wrap { position: relative; border-radius: 12px; padding: 1.5px; background: linear-gradient(135deg, rgba(139,92,246,0.35), rgba(59,130,246,0.35)); transition: background 0.3s ease, box-shadow 0.3s ease; } .sa-input-wrap:focus-within { background: linear-gradient(135deg, rgba(139,92,246,0.7), rgba(59,130,246,0.7), rgba(139,92,246,0.7)); background-size: 200% 200%; animation: sa-border-glow 3s ease infinite; box-shadow: 0 0 16px rgba(139,92,246,0.25); } @keyframes sa-border-glow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .sa-input-inner { display: flex; gap: 6px; align-items: flex-end; background: rgba(15, 23, 42, 0.9); border-radius: 11px; padding: 5px 5px 5px 0; } .sa-input { flex: 1; min-height: 36px; max-height: 120px; resize: none; background: transparent !important; border: none !important; color: #e2e8f0 !important; padding: 8px 12px !important; font-size: 13px !important; line-height: 1.5; font-family: inherit !important; outline: none; } .sa-input::placeholder { color: #64748b; } .sa-send-btn { width: 36px; height: 36px; border-radius: 9px; background: linear-gradient(135deg, #8b5cf6, #3b82f6) !important; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: opacity 0.15s ease, transform 0.15s ease; } .sa-send-btn:hover { transform: scale(1.05); } .sa-send-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } .sa-send-btn svg { width: 18px; height: 18px; } .sa-send-btn.sa-stop { background: linear-gradient(135deg, #ef4444, #dc2626) !important; } .sa-send-btn.sa-stopping { background: linear-gradient(135deg, #64748b, #475569) !important; animation: sa-pulse 1.2s infinite; } /* ====== 工具状态 ====== */ .sa-tool-status { font-size: 11px; color: #94a3b8; padding: 5px 14px; display: none; align-items: center; gap: 7px; background: rgba(20, 27, 50, 0.75); border-bottom: 1px solid rgba(139,92,246,0.1); } .sa-tool-status.sa-active { display: flex; } .sa-tool-status .sa-tool-icon { display: flex; align-items: center; color: #a78bfa; flex-shrink: 0; animation: sa-pulse 1.2s infinite; } .sa-tool-status .sa-tool-icon svg { width: 13px; height: 13px; } .sa-tool-status .sa-tool-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @keyframes sa-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } } /* ====== 工具调用卡片(消息内嵌) ====== */ .sa-tool-calls { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; } .sa-tool-chip { display: flex; align-items: center; gap: 7px; padding: 6px 10px; border-radius: 8px; background: rgba(139,92,246,0.08) !important; border: 1px solid rgba(139,92,246,0.2) !important; font-size: 11px; color: #94a3b8; animation: sa-fade-in 0.2s ease; } @keyframes sa-fade-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } } .sa-tool-chip .sa-tc-icon { width: 20px; height: 20px; border-radius: 6px; background: rgba(139,92,246,0.15); display: flex; align-items: center; justify-content: center; color: #c4b5fd; flex-shrink: 0; } .sa-tool-chip .sa-tc-icon svg { width: 12px; height: 12px; } .sa-tool-chip .sa-tc-name { color: #c4b5fd; font-weight: 600; flex-shrink: 0; } .sa-tool-chip .sa-tc-args { color: #64748b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; } .sa-tool-chip .sa-tc-state { flex-shrink: 0; display: flex; align-items: center; gap: 4px; color: #64748b; } .sa-tool-chip .sa-tc-state svg { width: 11px; height: 11px; } .sa-tool-chip.sa-running { border-color: rgba(139,92,246,0.45) !important; background: rgba(139,92,246,0.12) !important; } .sa-tool-chip.sa-running .sa-tc-state { color: #a78bfa; } .sa-tool-chip .sa-tc-spinner { width: 10px; height: 10px; border-radius: 50%; border: 2px solid rgba(167,139,250,0.25); border-top-color: #a78bfa; animation: sa-rotate 0.7s linear infinite; flex-shrink: 0; } @keyframes sa-rotate { to { transform: rotate(360deg); } } .sa-tool-chip.sa-done .sa-tc-state { color: #4ade80; } .sa-tool-chip.sa-tool-fail { border-color: rgba(248, 113, 113, 0.35); background: rgba(248, 113, 113, 0.08); } .sa-tool-chip.sa-tool-fail .sa-tc-name { color: #f87171; } .sa-tool-chip.sa-tool-fail .sa-tc-state { color: #f87171; } /* ====== Steam Key 提权操作行(工具需要 API Key 时显示) ====== */ .sa-key-action { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; padding: 6px 10px; border-radius: 8px; background: rgba(245, 158, 11, 0.08); border: 1px dashed rgba(245, 158, 11, 0.35); } .sa-key-btn { display: inline-flex; align-items: center; gap: 5px; padding: 5px 12px; border-radius: 7px; cursor: pointer; border: 1px solid rgba(245, 158, 11, 0.55); background: rgba(245, 158, 11, 0.15); color: #fcd34d; font-size: 12px; font-weight: 600; transition: background 0.15s ease; } .sa-key-btn:hover { background: rgba(245, 158, 11, 0.28); } .sa-key-btn svg { width: 12px; height: 12px; } .sa-key-hint { font-size: 11px; color: #94a3b8; } /* ====== 智能体编辑器:标签行 + 复制按钮 ====== */ .sa-ae-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; } .sa-ae-label-row label { margin-bottom: 0; } .sa-copy-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 6px; cursor: pointer; border: 1px solid rgba(139, 92, 246, 0.45); background: rgba(139, 92, 246, 0.12); color: #c4b5fd; font-size: 11.5px; transition: background 0.15s ease, color 0.15s ease; } .sa-copy-btn:hover { background: rgba(139, 92, 246, 0.25); color: #ddd6fe; } .sa-copy-btn:disabled { opacity: 0.5; cursor: default; } .sa-copy-btn svg { width: 12px; height: 12px; } /* ====== 空状态 ====== */ .sa-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #64748b; text-align: center; padding: 40px; } .sa-empty svg { width: 48px; height: 48px; opacity: 0.3; margin-bottom: 12px; } .sa-empty h3 { color: #94a3b8; font-size: 15px; margin-bottom: 6px; } .sa-empty p { font-size: 12px; line-height: 1.6; max-width: 280px; } /* ====== 对话历史面板 ====== */ .sa-history-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(13, 18, 35, 0.98) !important; z-index: 10; display: flex; flex-direction: column; transform: translateX(-100%); transition: transform 0.3s cubic-bezier(0.4,0,0.2,1); } .sa-history-panel.sa-show { transform: translateX(0); } .sa-history-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: rgba(20, 27, 50, 0.9); border-bottom: 1px solid rgba(139,92,246,0.15); flex-shrink: 0; } .sa-history-header h3 { font-size: 14px; color: #e2e8f0; margin: 0; flex: 1; } .sa-history-list { flex: 1; overflow-y: auto; padding: 8px; scrollbar-width: thin; scrollbar-color: rgba(139,92,246,0.3) transparent; } .sa-history-list::-webkit-scrollbar { width: 5px; } .sa-history-list::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 3px; } .sa-history-item { display: flex; flex-direction: column; gap: 3px; padding: 10px 12px; border-radius: 10px; background: rgba(30, 41, 59, 0.4); border: 1px solid transparent; margin-bottom: 6px; cursor: pointer; transition: all 0.15s ease; position: relative; } .sa-history-item:hover { background: rgba(30, 41, 59, 0.7); border-color: rgba(139,92,246,0.2); } .sa-history-item.sa-active { background: rgba(139,92,246,0.12); border-color: rgba(139,92,246,0.3); } .sa-history-item-title { font-size: 13px; color: #e2e8f0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 24px; } .sa-history-item-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; } .sa-history-item-meta .sa-history-tag { padding: 1px 6px; border-radius: 4px; background: rgba(139,92,246,0.1); color: #c4b5fd; font-size: 10px; } .sa-history-del { position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 5px; background: transparent; border: none; color: #64748b; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; opacity: 0; transition: opacity 0.15s ease; } .sa-history-item:hover .sa-history-del { opacity: 1; } .sa-history-del:hover { background: rgba(239,68,68,0.2); color: #f87171; } .sa-history-empty { text-align: center; padding: 40px 20px; color: #64748b; font-size: 13px; } /* ====== 设置面板 ====== */ .sa-overlay { position: fixed; inset: 0; z-index: 1000005 !important; background: rgba(0,0,0,0.6) !important; display: none; align-items: center; justify-content: center; backdrop-filter: blur(4px); } .sa-overlay.sa-show { display: flex; } .sa-modal { background: rgba(15, 23, 42, 0.96) !important; border: 1px solid rgba(139,92,246,0.25) !important; border-radius: 16px; width: 480px; max-width: 92vw; max-height: 85vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 24px 70px rgba(0,0,0,0.6), 0 0 0 1px rgba(139,92,246,0.08) !important; backdrop-filter: blur(20px) saturate(160%) !important; -webkit-backdrop-filter: blur(20px) saturate(160%) !important; } .sa-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid rgba(139,92,246,0.15); flex-shrink: 0; background: linear-gradient(180deg, rgba(139,92,246,0.08), transparent); } .sa-modal-header h3 { font-size: 16px; color: #f1f5f9; margin: 0; display: flex; align-items: center; gap: 6px; } .sa-modal-header h3 svg { width: 18px; height: 18px; color: #c4b5fd; } .sa-modal-body { flex: 1; overflow-y: auto; padding: 16px 18px; } .sa-modal-body::-webkit-scrollbar { width: 5px; } .sa-modal-body::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 3px; } .sa-form-group { margin-bottom: 14px; } .sa-form-group label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 4px; } .sa-form-input, .sa-form-textarea, .sa-form-select { width: 100%; box-sizing: border-box; background: rgba(30, 41, 59, 0.6) !important; border: 1px solid rgba(139,92,246,0.2) !important; border-radius: 8px; color: #e2e8f0 !important; padding: 9px 12px; font-size: 13px; font-family: inherit; outline: none; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .sa-form-input:focus, .sa-form-textarea:focus, .sa-form-select:focus { border-color: rgba(139,92,246,0.6) !important; box-shadow: 0 0 0 3px rgba(139,92,246,0.15) !important; } .sa-form-textarea { resize: vertical; min-height: 60px; } .sa-form-hint { font-size: 11px; color: #64748b; margin-top: 3px; } .sa-form-hint a { color: #60a5fa; text-decoration: none; } .sa-form-hint a:hover { text-decoration: underline; } .sa-btn { padding: 9px 16px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px; font-family: inherit; transition: opacity 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; white-space: nowrap; } .sa-btn-primary { background: linear-gradient(135deg, #8b5cf6, #3b82f6) !important; color: #fff !important; box-shadow: 0 2px 12px rgba(139,92,246,0.3) !important; } .sa-btn-primary:hover { opacity: 0.9; box-shadow: 0 4px 18px rgba(139,92,246,0.45) !important; transform: translateY(-1px); } .sa-btn-secondary { background: rgba(30, 41, 59, 0.8) !important; color: #94a3b8 !important; border: 1px solid rgba(139,92,246,0.15) !important; } .sa-btn-secondary:hover { background: rgba(139,92,246,0.12) !important; color: #e2e8f0 !important; } .sa-btn:hover { opacity: 0.9; } .sa-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none !important; } .sa-agent-card { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; background: rgba(30, 41, 59, 0.4) !important; border: 1px solid rgba(139,92,246,0.1) !important; margin-bottom: 8px; transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; } .sa-agent-card:hover { border-color: rgba(139,92,246,0.25) !important; background: rgba(30, 41, 59, 0.6) !important; } .sa-agent-card .sa-agent-icon { font-size: 22px; flex-shrink: 0; } .sa-agent-card .sa-agent-info { flex: 1; min-width: 0; } .sa-agent-card .sa-agent-name { font-size: 13px; color: #e2e8f0; font-weight: 600; } .sa-agent-card .sa-agent-desc { font-size: 11px; color: #64748b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sa-agent-card .sa-agent-actions { display: flex; gap: 4px; flex-shrink: 0; } .sa-agent-card .sa-mini-btn { width: 26px; height: 26px; border-radius: 6px; background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(139,92,246,0.1); color: #94a3b8; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; } .sa-agent-card .sa-mini-btn:hover { background: rgba(139,92,246,0.2); color: #e2e8f0; } .sa-agent-card.sa-builtin .sa-agent-actions { display: none; } /* ====== 模型管理卡片 ====== */ .sa-model-card { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; background: rgba(30, 41, 59, 0.4) !important; border: 1px solid rgba(139,92,246,0.1) !important; margin-bottom: 8px; transition: border-color 0.15s ease, background 0.15s ease; } .sa-model-card:hover { border-color: rgba(139,92,246,0.25) !important; } .sa-model-card.sa-active { border-color: rgba(139,92,246,0.45) !important; background: linear-gradient(135deg, rgba(139,92,246,0.12), rgba(59,130,246,0.08)) !important; box-shadow: 0 0 0 1px rgba(139,92,246,0.15) inset !important; } .sa-model-card .sa-model-icon { font-size: 18px; flex-shrink: 0; } .sa-model-card .sa-model-info { flex: 1; min-width: 0; } .sa-model-card .sa-model-name { font-size: 13px; color: #e2e8f0; font-weight: 600; } .sa-model-card .sa-model-detail { font-size: 11px; color: #64748b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sa-model-card .sa-model-actions { display: flex; gap: 4px; flex-shrink: 0; } .sa-model-card .sa-model-badge { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: rgba(74,222,128,0.15); color: #4ade80; margin-left: 4px; } /* ====== 供应商选择标签 ====== */ .sa-provider-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; } .sa-provider-tag { display: flex; align-items: center; gap: 4px; padding: 5px 10px; border-radius: 8px; background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(139,92,246,0.15); color: #94a3b8; font-size: 12px; cursor: pointer; transition: all 0.15s ease; } .sa-provider-tag:hover { border-color: rgba(139,92,246,0.3); color: #e2e8f0; } .sa-provider-tag.sa-selected { background: rgba(139,92,246,0.2); border-color: rgba(139,92,246,0.5); color: #c4b5fd; } .sa-provider-tag .sa-provider-tag-icon { font-size: 14px; } /* ====== 设置分区标题 ====== */ .sa-settings-section { margin-bottom: 18px; } .sa-settings-section-title { font-size: 13px; color: #c4b5fd; font-weight: 600; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 1px solid rgba(139,92,246,0.1); } /* ====== 综合设置中心(多标签单浮窗) ====== */ .sa-settings-modal { width: 760px; max-width: 94vw; height: 640px; max-height: 88vh; } .sa-set-body { flex: 1; display: flex; min-height: 0; overflow: hidden; } .sa-set-nav { width: 156px; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; padding: 12px 10px; border-right: 1px solid rgba(139,92,246,0.15); background: rgba(15, 23, 42, 0.35); } .sa-set-nav-btn { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-radius: 8px; border: none; cursor: pointer; background: transparent; color: #94a3b8; font-size: 13px; font-family: inherit; text-align: left; transition: background 0.15s ease, color 0.15s ease; white-space: nowrap; } .sa-set-nav-btn svg { width: 15px; height: 15px; flex-shrink: 0; } .sa-set-nav-btn:hover { background: rgba(139,92,246,0.1); color: #e2e8f0; } .sa-set-nav-btn.sa-active { background: rgba(139,92,246,0.18); color: #c4b5fd; font-weight: 600; box-shadow: inset 2px 0 0 #8b5cf6; } .sa-set-nav-summary { margin-top: auto; padding: 8px 10px; font-size: 10px; color: #64748b; border-top: 1px solid rgba(139,92,246,0.1); line-height: 1.7; } .sa-set-panes { flex: 1; min-width: 0; overflow-y: auto; padding: 16px 18px; } .sa-set-panes::-webkit-scrollbar { width: 5px; } .sa-set-panes::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 3px; } .sa-set-pane { display: none; } .sa-set-pane.sa-active { display: block; } .sa-set-pane-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; } .sa-set-pane-title { font-size: 14px; font-weight: 600; color: #f1f5f9; } .sa-set-pane-desc { font-size: 11px; color: #64748b; margin-top: 2px; } .sa-modal-footer { flex-shrink: 0; display: flex; gap: 8px; align-items: center; padding: 12px 18px; border-top: 1px solid rgba(139,92,246,0.15); background: rgba(15, 23, 42, 0.35); } .sa-modal-footer .sa-footer-hint { flex: 1; font-size: 11px; color: #64748b; } /* 检测结果行 */ .sa-check-item { display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; margin-bottom: 6px; border-radius: 8px; background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(139,92,246,0.12); font-size: 12px; line-height: 1.5; } .sa-check-item .sa-check-state { flex-shrink: 0; font-weight: 600; } .sa-check-item.sa-ok { border-color: rgba(74, 222, 128, 0.35); } .sa-check-item.sa-ok .sa-check-state { color: #4ade80; } .sa-check-item.sa-err { border-color: rgba(239, 68, 68, 0.35); } .sa-check-item.sa-err .sa-check-state { color: #f87171; } .sa-check-item.sa-warn { border-color: rgba(251, 191, 36, 0.35); } .sa-check-item.sa-warn .sa-check-state { color: #fbbf24; } .sa-check-item .sa-check-detail { color: #94a3b8; flex: 1; min-width: 0; overflow-wrap: anywhere; } /* 模型/智能体内嵌编辑视图返回行 */ .sa-editor-backrow { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(139,92,246,0.12); } @media (max-width: 640px) { .sa-settings-modal { width: 96vw; height: 90vh; } .sa-set-body { flex-direction: column; } .sa-set-nav { width: auto; flex-direction: row; overflow-x: auto; border-right: none; border-bottom: 1px solid rgba(139,92,246,0.15); } .sa-set-nav-summary { display: none; } } /* ====== Toast ====== */ .sa-toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1000006 !important; padding: 8px 18px; border-radius: 10px; font-size: 13px; color: #fff; background: rgba(15, 23, 42, 0.95) !important; border: 1px solid rgba(139,92,246,0.3) !important; box-shadow: 0 4px 20px rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; } .sa-toast.sa-show { opacity: 1; } .sa-toast.sa-error { border-color: rgba(239, 68, 68, 0.4) !important; } .sa-toast.sa-success { border-color: rgba(74, 222, 128, 0.4) !important; } /* ====== 集成按钮 ====== */ .sa-integration-btn { display: inline-flex !important; align-items: center; gap: 4px; padding: 4px 10px !important; border-radius: 6px !important; background: rgba(139, 92, 246, 0.15) !important; border: 1px solid rgba(139, 92, 246, 0.3) !important; color: #c4b5fd !important; font-size: 12px !important; cursor: pointer; transition: all 0.15s ease; font-family: inherit; } .sa-integration-btn:hover { background: rgba(139, 92, 246, 0.25) !important; color: #c4b5fd !important; } /* ====== 欢迎屏幕 ====== */ .sa-welcome { flex: 1; display: flex; flex-direction: column; overflow-y: auto; padding: 20px 16px !important; scrollbar-width: thin; scrollbar-color: rgba(139,92,246,0.3) transparent; } .sa-welcome::-webkit-scrollbar { width: 5px; } .sa-welcome::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 3px; } .sa-welcome-greeting { margin-bottom: 18px; } .sa-welcome-greeting h2 { font-size: 20px; font-weight: 700; margin: 0 0 4px; background: linear-gradient(135deg, #c4b5fd, #a78bfa, #f0abfc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .sa-welcome-greeting p { font-size: 12px; color: #64748b; margin: 0; } .sa-welcome-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; } .sa-stat-card { background: rgba(30, 41, 59, 0.5) !important; border: 1px solid rgba(139,92,246,0.12) !important; border-radius: 12px; padding: 12px 8px; text-align: center; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; transition: border-color 0.2s ease, transform 0.15s ease, box-shadow 0.15s ease; } .sa-stat-card:hover { border-color: rgba(139,92,246,0.3) !important; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(139,92,246,0.15) !important; } .sa-stat-card .sa-stat-icon { width: 28px; height: 28px; margin: 0 auto 6px; display: flex; align-items: center; justify-content: center; color: #a78bfa; } .sa-stat-card .sa-stat-icon svg { width: 20px; height: 20px; } .sa-stat-card .sa-stat-value { font-size: 20px; font-weight: 800; color: #f1f5f9; font-variant-numeric: tabular-nums; line-height: 1.2; } .sa-stat-card .sa-stat-label { font-size: 10px; color: #64748b; margin-top: 2px; } .sa-welcome-agent { background: linear-gradient(135deg, rgba(139,92,246,0.2), rgba(59,130,246,0.2)) !important; border: 1px solid rgba(139,92,246,0.25) !important; border-radius: 12px; padding: 12px 14px; display: flex; align-items: center; gap: 10px; margin-bottom: 16px; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .sa-welcome-agent:hover { border-color: rgba(139,92,246,0.4) !important; box-shadow: 0 4px 20px rgba(139,92,246,0.2) !important; } .sa-welcome-agent .sa-wa-icon { width: 36px; height: 36px; border-radius: 10px; background: linear-gradient(135deg, #8b5cf6, #3b82f6); display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: white; box-shadow: 0 2px 10px rgba(139,92,246,0.4); } .sa-welcome-agent .sa-wa-icon svg { width: 20px; height: 20px; } .sa-welcome-agent .sa-wa-info { flex: 1; min-width: 0; } .sa-welcome-agent .sa-wa-label { font-size: 10px; color: #c4b5fd; display: flex; align-items: center; gap: 3px; } .sa-welcome-agent .sa-wa-name { font-size: 14px; color: #f1f5f9; font-weight: 600; margin-top: 1px; } .sa-welcome-agent .sa-wa-desc { font-size: 11px; color: #94a3b8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sa-welcome-suggestions { margin-bottom: 8px; } .sa-welcome-suggestions-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .sa-welcome-suggestions-header span { font-size: 12px; color: #c4b5fd; font-weight: 600; display: flex; align-items: center; gap: 4px; } .sa-welcome-suggestions-header span svg { width: 14px; height: 14px; } .sa-suggestion-refresh { background: none; border: none; color: #64748b; cursor: pointer; font-size: 11px; display: flex; align-items: center; gap: 3px; padding: 2px 6px; border-radius: 6px; transition: all 0.15s ease; font-family: inherit; } .sa-suggestion-refresh:hover { color: #c4b5fd; background: rgba(139,92,246,0.1); } .sa-suggestion-refresh svg { width: 12px; height: 12px; } .sa-suggestion-refresh.sa-spin svg { animation: sa-spin 0.6s ease; } @keyframes sa-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .sa-suggestion-list { display: flex; flex-direction: column; gap: 6px; } .sa-suggestion-item { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 10px; background: rgba(30, 41, 59, 0.4) !important; border: 1px solid rgba(139,92,246,0.08) !important; cursor: pointer; transition: all 0.15s ease; backdrop-filter: blur(6px) !important; -webkit-backdrop-filter: blur(6px) !important; } .sa-suggestion-item:hover { background: rgba(30, 41, 59, 0.7) !important; border-color: rgba(139,92,246,0.3) !important; transform: translateX(2px); box-shadow: 0 2px 12px rgba(139,92,246,0.12) !important; } .sa-suggestion-item .sa-sug-text { flex: 1; font-size: 12px; color: #cbd5e1; line-height: 1.4; } .sa-suggestion-item .sa-sug-btn { width: 26px; height: 26px; border-radius: 7px; background: rgba(139,92,246,0.15); border: none; color: #c4b5fd; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; } .sa-suggestion-item:hover .sa-sug-btn { background: linear-gradient(135deg, #8b5cf6, #3b82f6); color: white; box-shadow: 0 2px 8px rgba(139,92,246,0.4); } .sa-suggestion-item .sa-sug-btn svg { width: 13px; height: 13px; } /* ====== 设置面板增强 ====== */ .sa-settings-section-title { display: flex; align-items: center; gap: 6px; } .sa-settings-section-title svg { width: 15px; height: 15px; color: #c4b5fd; } .sa-model-card .sa-model-icon, .sa-agent-card .sa-agent-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: rgba(139,92,246,0.1); } .sa-model-card .sa-model-icon svg, .sa-agent-card .sa-agent-icon svg { width: 18px; height: 18px; } .sa-model-card .sa-mini-btn, .sa-agent-card .sa-mini-btn { width: 26px; height: 26px; border-radius: 6px; background: rgba(30,41,59,0.6) !important; border: 1px solid rgba(139,92,246,0.1) !important; color: #94a3b8 !important; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; } .sa-model-card .sa-mini-btn:hover, .sa-agent-card .sa-mini-btn:hover { background: rgba(139,92,246,0.2) !important; color: #e2e8f0 !important; } .sa-model-card .sa-mini-btn svg, .sa-agent-card .sa-mini-btn svg { width: 13px; height: 13px; } /* ====== 供应商下拉选择 ====== */ .sa-provider-select-wrap { position: relative; } .sa-provider-select-wrap::after { content: ''; position: absolute; right: 10px; top: 50%; width: 7px; height: 7px; border-right: 2px solid #94a3b8; border-bottom: 2px solid #94a3b8; transform: translateY(-70%) rotate(45deg); pointer-events: none; } .sa-provider-select { width: 100%; box-sizing: border-box; appearance: none; -webkit-appearance: none; background: rgba(30, 41, 59, 0.6) !important; border: 1px solid rgba(139,92,246,0.2) !important; border-radius: 8px; color: #e2e8f0 !important; padding: 9px 30px 9px 12px; font-size: 13px; font-family: inherit; outline: none; cursor: pointer; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .sa-provider-select:focus { border-color: rgba(139,92,246,0.6) !important; box-shadow: 0 0 0 3px rgba(139,92,246,0.15) !important; } .sa-provider-select option { background: #1e293b !important; color: #e2e8f0 !important; } /* ====== 历史面板标题SVG ====== */ .sa-history-header h3 { display: flex; align-items: center; gap: 5px; } .sa-history-header h3 svg { width: 15px; height: 15px; color: #c4b5fd; } /* ====== 模型选择器增强 ====== */ .sa-model-selector { display: flex; align-items: center; gap: 4px; } .sa-model-selector option { background: #1e293b !important; color: #e2e8f0 !important; } /* ====== 图标按钮SVG统一 ====== */ .sa-icon-btn svg { width: 15px; height: 15px; } /* ====== 响应式 ====== */ @media (max-width: 600px) { #sa-panel { width: 100vw !important; } .sa-msg { max-width: 95%; } .sa-welcome-stats { grid-template-columns: repeat(3, 1fr); gap: 6px; } .sa-stat-card { padding: 10px 4px; } .sa-stat-card .sa-stat-value { font-size: 17px; } } `); // ============================================================ // 工具函数 // ============================================================ function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function parseStored(raw, fallback) { if (raw === null || raw === undefined || raw === '') return fallback; if (typeof raw === 'object') return raw; try { const val = JSON.parse(raw); return (val === null || val === undefined) ? fallback : val; } catch { return fallback; } } function safeUrl(url) { const u = String(url || '').trim(); return /^(https?:|steam:)/i.test(u) ? u : '#'; } function sanitizeSvgIcon(svg) { try { const doc = new DOMParser().parseFromString(String(svg), 'image/svg+xml'); const root = doc.documentElement; if (!root || root.nodeName.toLowerCase() !== 'svg' || root.querySelector('parsererror')) return ''; const clean = (el) => { for (const attr of [...el.attributes]) { const name = attr.name.toLowerCase(); const val = String(attr.value || '').trim().toLowerCase(); if (name.startsWith('on') || ((name === 'href' || name === 'xlink:href') && val.startsWith('javascript:'))) { el.removeAttribute(attr.name); } } for (const child of [...el.children]) clean(child); }; clean(root); root.querySelectorAll('script, foreignObject').forEach(n => n.remove()); const out = new XMLSerializer().serializeToString(root); return out.startsWith(' `
${code.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')}
`); html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/`([^`]+)`/g, '$1'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, text, url) => `${text}`); html = html.replace(/^> (.+)$/gm, '
$1
'); html = html.replace(/^[\-\*] (.+)$/gm, '
  • $1
  • '); html = html.replace(/(
  • [\s\S]*?<\/li>)(?!\s*
  • )/g, ''); html = html.replace(/^\d+\. (.+)$/gm, '
  • $1
  • '); html = html.replace(/\n\n/g, '

    '); html = html.replace(/\n/g, '
    '); if (!html.match(/^<(h\d|pre|ul|ol|blockquote|p)/)) html = '

    ' + html + '

    '; return `
    ${html}
    `; } function downloadFile(content, filename) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); } function formatTime(ts) { const d = new Date(ts); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; } function formatRelativeTime(ts) { const now = Date.now(); const diff = now - ts; if (diff < 60000) return '刚刚'; if (diff < 3600000) return `${Math.floor(diff/60000)}分钟前`; if (diff < 86400000) return `${Math.floor(diff/3600000)}小时前`; if (diff < 604800000) return `${Math.floor(diff/86400000)}天前`; return formatTime(ts).slice(0, 10); } function toast(msg, type = '') { const el = document.createElement('div'); el.className = `sa-toast ${type ? 'sa-' + type : ''}`; el.textContent = msg; document.body.appendChild(el); requestAnimationFrame(() => el.classList.add('sa-show')); setTimeout(() => { el.classList.remove('sa-show'); setTimeout(() => el.remove(), 300); }, CONFIG.TOAST_DURATION); } function gmFetch(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url, headers: options.headers || {}, data: options.data, timeout: options.timeout || 20000, onload(resp) { if (resp.status >= 200 && resp.status < 300) { resolve(resp.responseText); } else { reject(new Error(`HTTP ${resp.status}: ${resp.responseText.slice(0, 200)}`)); } }, onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }); }); } function detectSteamId() { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow.g_steamID) return String(unsafeWindow.g_steamID); } catch {} try { const appConfig = document.getElementById('application_config'); if (appConfig) { const userInfo = JSON.parse(appConfig.getAttribute('data-userinfo') || '{}'); if (userInfo.steamid) return String(userInfo.steamid); } } catch {} try { const m = document.documentElement.innerHTML.match(/g_steamID\s*=\s*"(\d{17})"/); if (m) return m[1]; } catch {} try { // steamLoginSecure cookie: 76561198xxx%7C%7Ctoken,第一段即 SteamID64 const m = /steamLoginSecure=([0-9]+)/.exec(document.cookie || ''); if (m && /^\d{17}$/.test(m[1])) return m[1]; } catch {} return ''; } function getConfiguredSteamId() { return String(GM_getValue(SK.STEAM_ID, '') || '').trim(); } // 统一解析 SteamID:显式参数 > 设置中配置 > 自动检测当前登录用户 function resolveSteamId(explicit) { const fromParam = String(explicit || '').trim(); if (fromParam) return { steamId: fromParam, source: '参数' }; const configured = getConfiguredSteamId(); if (configured) return { steamId: configured, source: '设置' }; const detected = detectSteamId(); if (detected) return { steamId: detected, source: '自动检测' }; return { steamId: '', source: '' }; } // 自定义 ID(vanity URL 名) → SteamID64,需要 Steam API Key async function resolveVanityToId64(vanity, apiKey) { const resp = await gmFetch(`https://api.steampowered.com/ISteamUser/ResolveVanityURL/v1/?key=${encodeURIComponent(apiKey)}&vanityurl=${encodeURIComponent(vanity)}`, { timeout: 15000 }); const data = JSON.parse(resp); return (data?.response?.success === 1 && data?.response?.steamid) ? data.response.steamid : null; } function getStoreCountryCode() { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow.g_strCountryCode) return String(unsafeWindow.g_strCountryCode); } catch {} return 'CN'; } function formatCents(cents, currency) { const n = Number(cents); if (!Number.isFinite(n) || n < 0) return ''; const val = (n / 100).toFixed(2); const cur = String(currency || '').toUpperCase(); if (cur === 'CNY') return `¥${val}`; if (cur === 'USD') return `US$${val}`; if (cur === 'EUR') return `€${val}`; if (cur === 'GBP') return `£${val}`; if (cur === 'JPY') return `¥${val}`; return cur ? `${val} ${cur}` : val; } function formatDealEnds(expirationSec) { const ts = Number(expirationSec); if (!Number.isFinite(ts) || ts <= 0) return ''; const diffMs = ts * 1000 - Date.now(); if (diffMs <= 0) return '已结束'; const d = new Date(ts * 1000); const dateStr = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const hours = diffMs / 3600000; if (hours >= 48) return `${Math.round(hours / 24)}天后结束(${dateStr}截止)`; if (hours >= 1) return `${Math.round(hours)}小时后结束(${dateStr})`; return `${Math.max(1, Math.round(diffMs / 60000))}分钟后结束`; } let _featuredCache = { data: null, ts: 0 }; async function fetchFeaturedCategories() { if (_featuredCache.data && Date.now() - _featuredCache.ts < 5 * 60 * 1000) return _featuredCache.data; const cc = getStoreCountryCode(); const resp = await gmFetch(`https://store.steampowered.com/api/featuredcategories?cc=${encodeURIComponent(cc)}&l=schinese`, { timeout: 15000 }); if (typeof resp === 'string' && resp.trimStart().startsWith('<')) throw new Error('Steam 返回页面而非数据(可能被限流),请稍后重试'); const data = JSON.parse(resp); _featuredCache = { data, ts: Date.now() }; return data; } function findFeaturedCat(fc, catId) { for (const key of Object.keys(fc || {})) { const v = fc[key]; if (v && v.id === catId && Array.isArray(v.items)) return v; } return null; } // ============================================================ // ModelManager - 多模型供应商管理 // ============================================================ const ModelManager = { _models: [], _activeModelId: null, init() { try { this._models = JSON.parse(GM_getValue(SK.MODELS, '[]')); } catch { this._models = []; } this._activeModelId = GM_getValue(SK.ACTIVE_MODEL, null); if (this._models.length === 0) { const legacyUrl = GM_getValue(SK.AI_URL, null) || GM_getValue('swe_ai_api_url', ''); const legacyKey = GM_getValue(SK.AI_KEY, null) || GM_getValue('swe_ai_api_key', ''); const legacyModel = GM_getValue(SK.AI_MODEL, null) || GM_getValue('swe_ai_model', ''); if (legacyKey) { let provider = 'custom'; if (legacyUrl.includes('deepseek')) provider = 'deepseek'; else if (legacyUrl.includes('bigmodel')) provider = 'glm'; else if (legacyUrl.includes('dashscope')) provider = 'qwen'; else if (legacyUrl.includes('xiaomimimo')) provider = 'mimo'; else if (legacyUrl.includes('moonshot')) provider = 'moonshot'; else if (legacyUrl.includes('baichuan')) provider = 'baichuan'; else if (legacyUrl.includes('minimaxi')) provider = 'minimax'; else if (legacyUrl.includes('volces.com')) provider = 'doubao'; else if (legacyUrl.includes('openai.com')) provider = 'openai'; const model = this.createModel({ name: `${MODEL_PROVIDERS[provider]?.name || '默认'} 默认`, provider, apiUrl: legacyUrl || CONFIG.AI_DEFAULT_URL, apiKey: legacyKey, model: legacyModel || MODEL_PROVIDERS[provider]?.defaultModel || '', }); this._activeModelId = model.id; GM_setValue(SK.ACTIVE_MODEL, model.id); } } if (!this._activeModelId || !this.getModel(this._activeModelId)) { this._activeModelId = this._models[0]?.id || null; if (this._activeModelId) GM_setValue(SK.ACTIVE_MODEL, this._activeModelId); } }, getModels() { return [...this._models]; }, getModel(id) { return this._models.find(m => m.id === id) || null; }, getActiveModel() { return this.getModel(this._activeModelId) || this._models[0] || null; }, getActiveModelId() { return this._activeModelId; }, setActiveModel(id) { if (!this.getModel(id)) return false; this._activeModelId = id; GM_setValue(SK.ACTIVE_MODEL, id); return true; }, createModel(data) { const model = { id: `model_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, name: data.name || '未命名模型', provider: data.provider || 'custom', apiUrl: data.apiUrl || '', apiKey: data.apiKey || '', model: data.model || '', createdAt: new Date().toISOString(), }; this._models.push(model); this.save(); return model; }, updateModel(id, data) { const idx = this._models.findIndex(m => m.id === id); if (idx === -1) return false; this._models[idx] = { ...this._models[idx], ...data, id }; this.save(); return true; }, deleteModel(id) { const idx = this._models.findIndex(m => m.id === id); if (idx === -1) return false; this._models.splice(idx, 1); this.save(); if (this._activeModelId === id) { this._activeModelId = this._models[0]?.id || null; if (this._activeModelId) GM_setValue(SK.ACTIVE_MODEL, this._activeModelId); } return true; }, getConfig() { const model = this.getActiveModel(); if (!model) return null; return { apiUrl: model.apiUrl, apiKey: model.apiKey, model: model.model, modelId: model.id, modelName: model.name, provider: model.provider, }; }, save() { GM_setValue(SK.MODELS, JSON.stringify(this._models)); }, }; // ============================================================ // AI 客户端 // ============================================================ function getAiConfig() { const cfg = ModelManager.getConfig(); if (cfg && cfg.apiKey) return cfg; return { apiUrl: GM_getValue(SK.AI_URL, null) || CONFIG.AI_DEFAULT_URL, apiKey: GM_getValue(SK.AI_KEY, null) || '', model: GM_getValue(SK.AI_MODEL, null) || CONFIG.AI_DEFAULT_MODEL, }; } function getSteamApiKey() { return GM_getValue(SK.STEAM_KEY, '') || GM_getValue('sglv_steam_api_key', ''); } // ITAD Key:本脚本存储优先,未配置时复用 SGLV 的 Key(跨脚本共享) function getItadApiKey() { return GM_getValue(SK.ITAD_KEY, '') || GM_getValue('sglv_itad_api_key', ''); } function isSSEResponse(text) { return !!(text && text.trimStart().startsWith('data: ')); } function parseSSEStream(text) { let content = ''; let usage = null; const lines = (text || '').split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith('data: ')) continue; const dataStr = trimmed.slice(6); if (dataStr === '[DONE]') continue; try { const chunk = JSON.parse(dataStr); const delta = chunk.choices?.[0]?.delta?.content; if (delta) content += delta; if (chunk.usage && (chunk.usage.prompt_tokens || chunk.usage.completion_tokens || chunk.usage.total_tokens)) { usage = chunk.usage; } } catch {} } return { content, usage }; } function parseSSEResponse(text) { return parseSSEStream(text).content; } function stripThinkTags(text) { return String(text || '').replace(/[\s\S]*?<\/think>/gi, '').replace(/^[\s]*<\/?think>[\s]*$/gim, '').trim(); } // ============================================================ // Token 消耗统计 (KPI 卡片) // 优先采用 API 返回的 usage 精确值,无 usage 时按字符估算 // ============================================================ function estimateTokens(text) { if (!text) return 0; const s = String(text); let cjk = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3040 && c <= 0x30ff) || (c >= 0xac00 && c <= 0xd7af) || (c >= 0x3000 && c <= 0x303f) || (c >= 0xff00 && c <= 0xffef)) cjk++; } return Math.ceil(cjk / 1.5 + (s.length - cjk) / 4); } function formatTokenCount(n) { const v = Number(n) || 0; if (v >= 1000000) return (v / 1000000).toFixed(2) + 'M'; if (v >= 1000) return (v / 1000).toFixed(1) + 'k'; return String(v); } const TokenStatsManager = { _stats: null, init() { this._stats = parseStored(GM_getValue(SK.TOKEN_STATS, null), null) || { totalPromptTokens: 0, totalCompletionTokens: 0, requestCount: 0, updatedAt: '', }; }, record({ promptTokens = 0, completionTokens = 0 }) { if (!promptTokens && !completionTokens) return; if (!this._stats) this.init(); this._stats.totalPromptTokens += Math.max(0, Math.round(promptTokens)); this._stats.totalCompletionTokens += Math.max(0, Math.round(completionTokens)); this._stats.requestCount += 1; this._stats.updatedAt = new Date().toISOString(); try { GM_setValue(SK.TOKEN_STATS, JSON.stringify(this._stats)); } catch {} }, getStats() { if (!this._stats) this.init(); return { totalTokens: this._stats.totalPromptTokens + this._stats.totalCompletionTokens, totalPromptTokens: this._stats.totalPromptTokens, totalCompletionTokens: this._stats.totalCompletionTokens, requestCount: this._stats.requestCount, updatedAt: this._stats.updatedAt || '', }; }, }; async function callAiApi(prompt, opts = {}) { const cfg = getAiConfig(); if (!cfg.apiKey) throw new Error('AI API Key 未配置,请点击设置按钮配置模型'); const messages = []; if (opts.systemInstruction) { messages.push({ role: 'system', content: opts.systemInstruction }); } if (opts.history && opts.history.length) { for (const m of opts.history) { if (m.role === 'user' || m.role === 'assistant') { messages.push({ role: m.role, content: m.content }); } } } messages.push({ role: 'user', content: prompt }); // isolated: 独立请求(如智能体提示词生成),不注册 activeAiRequest、不受聊天停止按钮影响 const isolated = !!opts.isolated; const aborted = () => !isolated && abortFlag; const useStream = !!opts.onChunk; let streamedContent = ''; let reqRef = null; const reqBody = { model: cfg.model, messages, temperature: opts.temperature ?? 0.7, stream: useStream, }; // 仅在显式传入正数时才发送 max_tokens,否则使用模型默认输出上限(长提示词不被截断) if (opts.maxTokens && Number(opts.maxTokens) > 0) reqBody.max_tokens = Number(opts.maxTokens); // token 估算基线:所有送入模型的内容 const promptTokensEstimate = estimateTokens(messages.map(m => m.content).join('\n')); const recordTokenUsage = (usage, outputText) => { try { const pTok = (usage && Number(usage.prompt_tokens) > 0) ? Number(usage.prompt_tokens) : promptTokensEstimate; const cTok = (usage && Number(usage.completion_tokens) > 0) ? Number(usage.completion_tokens) : estimateTokens(outputText); TokenStatsManager.record({ promptTokens: pTok, completionTokens: cTok }); } catch {} }; const p = new Promise((resolve, reject) => { reqRef = GM_xmlhttpRequest({ method: 'POST', url: cfg.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}`, }, data: JSON.stringify(reqBody), timeout: opts.timeout || CONFIG.AI_TIMEOUT, onprogress(resp) { if (!useStream || !opts.onChunk) return; if (aborted()) return; const text = resp.responseText || ''; if (!isSSEResponse(text)) return; const newContent = parseSSEResponse(text); if (newContent && newContent !== streamedContent) { streamedContent = newContent; opts.onChunk(newContent); } }, onload(resp) { try { const text = resp.responseText || ''; if (resp.status < 200 || resp.status >= 300) { let errMsg; if (isSSEResponse(text)) { errMsg = parseSSEResponse(text) || text.slice(0, 150); } else { try { const errData = JSON.parse(text); errMsg = errData?.error?.message || text.slice(0, 150); } catch { errMsg = text.slice(0, 150); } } reject(new Error(`HTTP ${resp.status}: ${errMsg}`)); return; } if (isSSEResponse(text)) { const { content, usage } = parseSSEStream(text); const cleaned = stripThinkTags(content); if (!cleaned) { reject(new Error('AI 返回为空')); return; } if (useStream && opts.onChunk && content !== streamedContent) { opts.onChunk(content); } recordTokenUsage(usage, cleaned); resolve(cleaned); return; } const data = JSON.parse(text); const choice = data.choices?.[0]; const msg = choice?.message; const content = stripThinkTags(msg?.content || ''); if (!content) { if (msg?.reasoning_content) { reject(new Error('模型仅返回了推理内容(reasoning)未产出正文,请更换为非推理模型或重试')); return; } if (choice?.finish_reason === 'length') { reject(new Error('AI 输出被 max_tokens 截断,请重试或调大 token 上限')); return; } reject(new Error('AI 返回为空')); return; } recordTokenUsage(data.usage, content); resolve(content); } catch (e) { reject(new Error('AI 响应解析失败: ' + e.message)); } }, onerror: () => { if (aborted()) { if (useStream && streamedContent) resolve(stripThinkTags(streamedContent)); else reject(new Error('__SA_ABORTED__')); return; } reject(new Error('AI API 网络错误')); }, ontimeout: () => { if (aborted()) { reject(new Error('__SA_ABORTED__')); return; } reject(new Error('AI API 请求超时')); }, }); if (!isolated) activeAiRequest = reqRef; }); return p.finally(() => { if (!isolated && activeAiRequest === reqRef) activeAiRequest = null; }); } async function callAiApiWithTyping(prompt, opts, onChunk) { let gotStream = false; const wrappedOnChunk = (text) => { gotStream = true; onChunk(text); }; const fullResponse = await callAiApi(prompt, { ...opts, onChunk: wrappedOnChunk }); if (!gotStream && onChunk) { const chunkSize = Math.max(1, Math.ceil(fullResponse.length / 60)); for (let i = 0; i < fullResponse.length; i += chunkSize) { if (abortFlag) break; onChunk(fullResponse.slice(0, i + chunkSize)); await new Promise(r => setTimeout(r, 12)); } } return fullResponse; } // ============================================================ // Steam 规则知识库(远程 JSON 热更新 + 内置降级快照) // 事实来源:steam-game-docs/0-steam-rules.md // 远程文件:script_libs/saa-rules-knowledge.json(纯数据,不执行任何远程代码) // 解析优先级:远程JSON缓存 > 内置快照 // ============================================================ const RULES_REMOTE_URL_DEFAULT = 'https://raw.githubusercontent.com/SmallRob/steam-namespace/refs/heads/main/data/saa-rules-knowledge.json'; const RULES_SNAPSHOT = { version: '1.1.0-snapshot', updatedAt: '2026-08-16', source: 'steam-game-docs/0-steam-rules.md (内置快照)', sections: { regionTransfer: { id: 'regionTransfer', label: '转区购买规则', bullets: [ '冷却期:商店国家/地区每 3 个月只能变更一次,系统强制执行', '转区前提:需在目标区服用当地支付方式成功完成一次购买', '合法变更条件:仅真实居住地变更、能提供护照或当地居住证明的用户,可联系客服申请提前解除限制', 'IP 校验:登录 IP 归属地需与目标区服一致,不符会驳回,多次尝试可能触发红信', '支付方式匹配:2023 年起强制要求支付方式与地区货币一致(如土耳其信用卡、俄罗斯 MIR 卡)', '高风险行为:虚假地址证明、频繁切换地区、黑市充值卡可能导致账号受限或永久封禁', '转区影响:游戏库可保留下载,但部分区域限定 DLC/版本可能在新区域不可见或无法激活', '账户余额无法跨区转移,原区域余额将被冻结', ], }, gifting: { id: 'gifting', label: '送礼规则', bullets: [ '好友时长要求:送礼需双方成为 Steam 好友满 30 天,未满 30 天无法发起礼物赠送', '跨区送礼限制:发送者与接收者必须在"允许跨区交易"的国家列表中,且价格差异不能过大', '高价区→低价区:通常允许,但需满足价格差异限制(差价需在 15% 以内)', '低价区→高价区:系统通常会拒绝,防止套利', '锁区游戏:标注"仅限特定地区激活"的游戏无法跨区赠送', '结账时若礼物存在地区限制,系统会明确标注可兑换的国家/地区', '接收者需在对应区域内激活,VPN 伪造地区接收礼物触发风控概率极高', '数字礼物卡可跨区赠送并自动换算当地货币,但不能用 Steam 钱包余额购买', '替代方案:家庭共享 — 小号合规转区后主号共享游戏库(被锁区游戏无法在被锁地区账号共享)', ], }, giftingQuota: { id: 'giftingQuota', label: '送礼额度规则', bullets: [ '计算公式:送礼额度 = 商店直购金额 − 退款金额;剩余额度 = 送礼额度 − 已送礼金额', '30 天滚动窗口:已送礼金额按最近 30 天内的送礼记录统计,超过 30 天的送礼不再占用额度', '计入项:仅商店直购(游戏/软件/DLC 购买)计入额度;内购、市场交易、钱包充值等不计入送礼额度', '多币种:额度按账户主货币统计,其他币种按实时汇率折算汇总', '超额后果:剩余额度为负(已超额)时无法继续送礼,需等待 30 天窗口滑出旧送礼记录或增加新的直购', '查询工具:Steam-Spending-History-Classifier(消费历史分类器)可在账户消费历史页自动分类并计算送礼额度', ], }, regionLock: { id: 'regionLock', label: '游戏锁区规则', bullets: [ '激活锁:CDKEY 只能在特定地区激活,国区专属 KEY 与海外区 KEY 互不通用', '运行锁:部分游戏限定只能在特定地区账户下下载游玩(如《失落的方舟》欧美服)', '商店锁:游戏在特定区域商店未上架,该地区用户无法直接购买', '判断方法:购买前查看 Steam 商店页面"区域限制"标签,或"查看购物车中的区域限制"', '锁区由发行商根据区域定价策略和版权保护要求设定,并非所有游戏都锁区', ], }, pricing: { id: 'pricing', label: '区域汇率结算规则', bullets: [ '低价区价格可能仅为国区一半甚至三分之一:阿根廷(4-6 折,ARS)、土耳其(3.5-5.5 折,TRY)、俄罗斯(3-5 折,RUB)、印度(2.5-4 折,INR)、巴西(2-3.5 折,BRL)', '货币匹配:结算币种必须为目标区服币种,Steam 按实时汇率转换', '案例:《霍格沃茨之遗》阿根廷约 ¥160、土耳其约 ¥180、国区约 ¥298、美区约 ¥430,选对地区最高省 63%', ], }, risks: { id: 'risks', label: '风险警示', bullets: [ '红信警告:频繁跨区、支付方式不匹配等异常行为会收到账户警告,可能导致功能受限', '秋后算账:Valve 对违规转区可能延迟处理,且一封到底不给申诉', '礼物失效:跨区赠送失败时礼物可能被自动退款;已购锁区 KEY 无法退换', '合规建议:优先使用当地支付方式、保持稳定当地 IP,避免短期频繁操作', ], }, }, faqs: [ { q: 'Steam 转区有什么限制', a: '商店地区每 3 个月只能变更一次;需先在目标区服用当地支付方式完成一次购买;IP 归属地需与目标区服一致。' }, { q: '可以低价区买游戏高价区玩吗', a: '不可以。低价区→高价区送礼通常被拒绝;高价区→低价区一般允许但需价格差异在 15% 以内。' }, { q: '送礼需要什么前置条件', a: '双方需成为 Steam 好友满 30 天;游戏需在收礼人所在区可购买;锁区游戏无法赠送。' }, { q: '送礼额度是怎么算的', a: '送礼额度 = 商店直购金额 − 退款金额;剩余额度 = 送礼额度 − 已送礼金额(按最近 30 天滚动窗口统计)。内购、市场交易、钱包充值不计入额度。' }, { q: '送礼额度用完了怎么办', a: '剩余额度为负(超额)时无法继续送礼。需等待 30 天窗口滑出旧的送礼记录释放额度,或通过新的商店直购增加额度。' }, { q: '怎么判断一个游戏是否锁区', a: '购买前在商店页面查看"区域限制"标签,或点击"查看购物车中的区域限制"。锁区分激活锁/运行锁/商店锁,由发行商设定。' }, { q: '钱包余额可以跨区用吗', a: '不能。转区后原区域余额被冻结,仅限原区域消费;数字礼物卡可跨区赠送但不能用钱包余额购买。' }, { q: '违规转区会被封号吗', a: '可能。虚假地址、频繁切换地区、黑市充值卡可能导致账号受限或永久封禁,且 Valve 可能延迟处理不给申诉。' }, ], agentSectionMap: { agent_steam_general: ['regionTransfer', 'gifting', 'giftingQuota', 'regionLock', 'pricing', 'risks'], agent_steam_gifting: ['gifting', 'giftingQuota', 'regionLock', 'regionTransfer', 'pricing', 'risks'], }, }; const RulesKnowledgeProvider = { _active: null, getRemoteUrl() { return GM_getValue(SK.RULES_URL, '') || RULES_REMOTE_URL_DEFAULT; }, setRemoteUrl(url) { GM_setValue(SK.RULES_URL, String(url || '').trim()); }, _loadRemoteCache() { const raw = GM_getValue(SK.RULES_REMOTE, ''); if (!raw) return null; return this._validate(parseStored(raw, null)); }, _validate(data) { if (!data || typeof data !== 'object') return null; if (!data.sections || typeof data.sections !== 'object' || !Array.isArray(data.faqs)) return null; const sections = {}; for (const [id, sec] of Object.entries(data.sections)) { if (!sec || typeof sec !== 'object' || typeof sec.label !== 'string' || !Array.isArray(sec.bullets)) continue; sections[id] = { id: typeof sec.id === 'string' ? sec.id : id, label: sec.label, bullets: sec.bullets.filter(b => typeof b === 'string'), }; } if (!Object.keys(sections).length) return null; return { version: String(data.version || 'unknown'), updatedAt: String(data.updatedAt || ''), source: String(data.source || '远程规则知识库'), sections, faqs: data.faqs .filter(f => f && typeof f.q === 'string' && typeof f.a === 'string') .map(f => ({ q: f.q, a: f.a })), agentSectionMap: (data.agentSectionMap && typeof data.agentSectionMap === 'object') ? data.agentSectionMap : {}, }; }, resolve() { if (this._active) return this._active; const remote = this._loadRemoteCache(); if (remote) { this._active = { ...remote, origin: 'remote' }; } else { this._active = { ...RULES_SNAPSHOT, origin: 'snapshot' }; } return this._active; }, formatAsContext(agentId, opts = {}) { const k = this.resolve(); const sectionIds = (k.agentSectionMap && k.agentSectionMap[agentId]) || Object.keys(k.sections); const charLimit = typeof opts.charLimit === 'number' && opts.charLimit > 0 ? opts.charLimit : 2200; const maxFaqs = typeof opts.maxFaqs === 'number' ? opts.maxFaqs : 4; const parts = []; let used = 0; for (const id of sectionIds) { const sec = k.sections[id]; if (!sec) continue; const block = `## ${sec.label}\n${sec.bullets.map(b => `- ${b}`).join('\n')}`; if (used + block.length > charLimit) break; parts.push(block); used += block.length + 1; } if (!parts.length) return ''; let out = `【Steam 平台规则知识】(v${k.version})\n${parts.join('\n')}`; const topFaqs = k.faqs.slice(0, maxFaqs); if (topFaqs.length && out.length + 400 < charLimit + 600) { out += `\n【规则 FAQ 参考】\n${topFaqs.map(f => `Q: ${f.q}\nA: ${f.a}`).join('\n')}`; } return out; }, getStatus() { const k = this.resolve(); return { origin: k.origin, version: k.version, updatedAt: k.updatedAt, source: k.source, sectionCount: Object.keys(k.sections).length, faqCount: k.faqs.length, remoteUrl: this.getRemoteUrl(), }; }, refreshFromRemote() { return new Promise((resolve, reject) => { const url = this.getRemoteUrl(); if (!url) { reject(new Error('未配置规则库远程地址')); return; } GM_xmlhttpRequest({ method: 'GET', url, timeout: 15000, onload: (res) => { if (res.status < 200 || res.status >= 300) { reject(new Error(`HTTP ${res.status}`)); return; } try { let data; try { data = this._validate(JSON.parse(res.responseText)); } catch { data = null; } if (!data) throw new Error('远程内容不是有效的规则知识库 JSON'); GM_setValue(SK.RULES_REMOTE, JSON.stringify(data)); this._active = { ...data, origin: 'remote' }; resolve(this.getStatus()); } catch (e) { reject(e); } }, onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }); }); }, }; // ============================================================ // 内置智能体定义 // ============================================================ const BUILTIN_AGENTS = [ { id: 'agent_steam_general', name: 'Steam 通用助手', description: '游戏评价、玩家数据分析、购买建议、打折分析', icon: 'gamepad', keywords: ['steam', '游戏', '评价', '推荐', '打折', '分析', '购买'], builtin: true, systemPrompt: `你是 Steam 游戏助手,精通 Steam 平台的游戏评价、玩家数据分析和购买建议。 你的能力: 1. 游戏评价:基于 Steam Store 数据(好评率、价格、类型)给出客观评价 2. 玩家数据分析:分析玩家的游戏库、游玩时长、消费习惯 3. 购买建议:根据用户偏好和预算推荐游戏 4. 打折分析:判断当前折扣是否值得入手 回答规范: - 使用 Markdown 格式,清晰分段 - 数据引用时标注来源(如"根据 Steam Store 数据") - 购买建议需考虑性价比、个人喜好匹配度 - 打折分析需对比历史价格趋势 - 保持客观中立,不吹捧不贬低 - 涉及转区、送礼、锁区、区域定价等平台规则问题时,优先依据【Steam 平台规则知识】回答,并主动提示相关风险 - 回答使用中文`, }, { id: 'agent_steam_gifting', name: '跨区送礼助手', description: '跨区送礼计算、最佳送礼推荐、差价分析', icon: 'gift', keywords: ['送礼', '跨区', 'gift', '差价', '推荐'], builtin: true, systemPrompt: `你是 Steam 跨区送礼顾问,熟悉 Steam 礼物系统各区价格差异和送礼规则。 你的能力: 1. 跨区价格对比:对比不同地区的游戏价格(优先使用 getMultiRegionPrices 工具获取多地区实时价格) 2. 送礼推荐:根据预算推荐性价比最高的游戏 3. 差价计算:计算送礼 vs 自买的价格差异(基于工具返回的 amountUsd 美元折算价计算跨区差价) 4. 区域推荐:推荐最划算的送礼区域 注意规则: - 送礼规则、锁区限制、转区风控、送礼额度等平台规则已通过【Steam 平台规则知识】注入,回答时严格遵循 - 送礼需双方成为 Steam 好友满 30 天;高价区→低价区送礼需价格差异在 15% 以内,低价区→高价区通常被拒绝 - 送礼额度 = 商店直购 − 退款;剩余额度按 30 天滚动窗口计算,涉及额度问题参考规则知识中的送礼额度章节 - 涉及多地区价格对比时必须调用 getMultiRegionPrices 工具获取实时数据,禁止凭记忆报各区价格 - 若 getMultiRegionPrices 返回 degraded=true(降级模式),必须在回答中明确告知用户:当前价格数据准确度降低,并引导用户在设置面板配置 ITAD API Key(isthereanydeal.com/apps 免费申请)以启用增强模式 - 价格以 Steam Store 实时数据为准 - 回答使用中文`, }, ]; const BUILTIN_KNOWLEDGE = { 'agent_steam_general': { constraints: [ '不提供盗版或破解建议', '价格信息以 Steam Store 实时数据为准,可能存在延迟', '游戏评价基于 Steam 好评率数据,不代表绝对质量', ], faq: [ { question: '如何获取打折/特惠/愿望单折扣等实时信息', answer: '必须先调用对应工具获取实时数据再回答:getOfficialDeals(官方特惠动态聚合:促销活动/每日优惠/当前特惠/官方新闻,用户问"最近有什么打折/值得入手"时优先用)、getSpecialDeals(按分类查特惠/热销榜/促销活动)、getWishlistDeals(愿望单打折)、getPriceHistory(历史低价/史低判断)、searchDiscountedGames(高级折扣检索,需 Steam Key,未配置时引导用户配置)、getGameDetail(单个游戏详情与现价)。禁止在未调用工具的情况下声称"无法获取实时折扣数据"。' }, { question: '用户问某个游戏(dota2/cs2等)最近有什么更新/动态/公告时怎么办', answer: '必须调用 getGameNews 工具(传 game=游戏名或简称,也支持 appid),返回该游戏官方公告/版本更新/平衡性补丁/活动新闻(数据来自 Steam ISteamNews 官方动态,同游戏库插件信息面板)。常见游戏(dota2/cs2/csgo/吃鸡/老头环等)内置简称直达;未知名称会自动经商店搜索解析。禁止凭训练记忆编造版本号或更新内容。若用户问的是平台整体动态(最近有什么打折)则用 getOfficialDeals 而非本工具。' }, { question: '怎么判断一个游戏值不值得买', answer: '综合看好评率(>80%为好评)、价格与内容量比、个人喜好匹配度、打折力度。可组合使用 getGameDetail 获取详情与好评率、getSpecialDeals 查特惠、getPriceHistory 查史低。' }, { question: '史低是什么意思', answer: '史低(Historical Low)指游戏历史最低售价。使用 getPriceHistory 工具查询历史最低价并判断当前折扣是否为 Steam 史低。' }, { question: '好评率怎么看', answer: 'Steam评价分为:好评如潮(>95%)、特别好评(80-95%)、多半好评(70-80%)、褒贬不一(40-70%)、多半差评(20-40%)、差评(<20%)。' }, ], domainKnowledge: 'Steam 评价系统:好评/差评/特别好评/好评如潮/褒贬不一/多半差评/差评如潮。折扣类型:常规折扣/每日优惠/特卖活动/发行商促销。Steam 区域定价:不同地区价格不同,跨区购买违反用户协议。Steam 退款政策:购买后14天内且游玩不超过2小时可退款。', }, 'agent_steam_gifting': { constraints: [ '送礼需双方成为 Steam 好友满 30 天', '跨区送礼需价格差异在 15% 以内,超出可能被系统拒绝', '部分游戏有区域限制,送礼前需确认收礼人所在区可激活', '剩余送礼额度不足(超额)时无法送礼,额度按 30 天滚动窗口计算', ], faq: [ { question: '怎么查一个游戏各区的价格', answer: '调用 getMultiRegionPrices 工具(传 appid),返回各地区 Steam 原币售价与 amountUsd 美元折算价。已配置 ITAD API Key 时覆盖 16 个地区且数据准确;未配置时为 AugmentedSteam 降级模式(约 7 个地区,返回 degraded=true),此时必须告知用户价格准确度降低,并引导在设置中配置 ITAD Key。' }, { question: '送礼和自买哪个划算', answer: '当两区价格差异在 15% 以内时送礼通常可行;高价区→低价区送礼通常更划算,低价区→高价区通常被系统拒绝。价格差异用 getMultiRegionPrices 返回的 amountUsd 折算价计算。' }, { question: '送礼额度怎么算', answer: '送礼额度 = 商店直购 − 退款;剩余额度 = 送礼额度 − 已送礼(最近 30 天)。内购、市场交易、钱包充值不计入。可用 Steam-Spending-History-Classifier 脚本查询。' }, { question: '礼物可以退款吗', answer: '礼物在接收方接受前可申请退款,退款退回原支付方式;不能退为 Steam 钱包余额。' }, ], domainKnowledge: '平台规则(转区/锁区/送礼限制/送礼额度/汇率结算/风控)已通过外部规则知识库注入系统指令,回答时优先遵循规则知识内容。补充:礼物价格以送礼人所在区域定价;数字礼物卡不能用 Steam 钱包余额购买。', }, }; // ============================================================ // Agent 知识管理器 // ============================================================ const AgentKnowledgeManager = { getKnowledge(agentId) { const builtin = BUILTIN_KNOWLEDGE[agentId]; const custom = this.getUserKnowledge(agentId); if (!builtin && !custom) return null; return { agentId, constraints: [...(builtin?.constraints || []), ...(custom?.constraints || [])], faq: [...(builtin?.faq || []), ...(custom?.faq || [])], domainKnowledge: [builtin?.domainKnowledge || '', custom?.domainKnowledge || ''].filter(Boolean).join('\n'), }; }, formatAsContext(agentId) { const k = this.getKnowledge(agentId); if (!k) return ''; const parts = []; if (k.constraints.length) parts.push(`【行为约束】\n${k.constraints.map(c => `- ${c}`).join('\n')}`); if (k.faq.length) { const top = k.faq.slice(0, 3); parts.push(`【常见问题参考】\n${top.map(f => `Q: ${f.question}\nA: ${f.answer}`).join('\n\n')}`); } if (k.domainKnowledge) { const summary = k.domainKnowledge.length > 300 ? k.domainKnowledge.slice(0, 300) + '...' : k.domainKnowledge; parts.push(`【专业知识摘要】\n${summary}`); } return parts.join('\n\n'); }, getUserKnowledge(agentId) { try { const raw = GM_getValue(SK.AGENTS, '[]'); const agents = JSON.parse(raw); const agent = agents.find(a => a.id === agentId); return agent?.knowledge || null; } catch { return null; } }, }; // ============================================================ // Agent 管理器 // ============================================================ const AgentManager = { _currentAgentId: null, init() { this._currentAgentId = GM_getValue(SK.CURRENT_AGENT, 'agent_steam_general'); }, getAgents() { const builtin = BUILTIN_AGENTS.map(a => ({ ...a, builtin: true })); const custom = this.getCustomAgents(); return [...builtin, ...custom]; }, getCustomAgents() { try { const raw = GM_getValue(SK.AGENTS, '[]'); return JSON.parse(raw).map(a => ({ ...a, builtin: false })); } catch { return []; } }, getAgent(id) { const builtin = BUILTIN_AGENTS.find(a => a.id === id); if (builtin) return { ...builtin, builtin: true }; const custom = this.getCustomAgents().find(a => a.id === id); return custom || null; }, getCurrentAgent() { return this.getAgent(this._currentAgentId) || this.getAgent('agent_steam_general'); }, switchAgent(id) { if (!this.getAgent(id)) return false; this._currentAgentId = id; GM_setValue(SK.CURRENT_AGENT, id); return true; }, createAgent(data) { const agents = this.getCustomAgents(); const id = `agent_custom_${Date.now()}`; const agent = { id, name: data.name || '自定义助手', description: data.description || '', icon: data.icon || 'bot', keywords: data.keywords || [], systemPrompt: data.systemPrompt || '你是一个 Steam 助手。', builtin: false, knowledge: data.knowledge || null, createdAt: new Date().toISOString(), }; agents.push(agent); GM_setValue(SK.AGENTS, JSON.stringify(agents)); return agent; }, updateAgent(id, data) { const agents = this.getCustomAgents(); const idx = agents.findIndex(a => a.id === id); if (idx === -1) return false; agents[idx] = { ...agents[idx], ...data, id, builtin: false }; GM_setValue(SK.AGENTS, JSON.stringify(agents)); return true; }, deleteAgent(id) { const agents = this.getCustomAgents(); const filtered = agents.filter(a => a.id !== id); if (filtered.length === agents.length) return false; GM_setValue(SK.AGENTS, JSON.stringify(filtered)); if (this._currentAgentId === id) this.switchAgent('agent_steam_general'); return true; }, exportAgents() { return JSON.stringify(this.getCustomAgents(), null, 2); }, importAgents(jsonStr) { try { const imported = JSON.parse(jsonStr); if (!Array.isArray(imported)) throw new Error('格式错误'); const existing = this.getCustomAgents(); const existingIds = new Set(existing.map(a => a.id)); let added = 0; for (const a of imported) { if (!a.id || !a.name || !a.systemPrompt) continue; let id = a.id; while (existingIds.has(id)) id = `${a.id}_${Date.now()}`; existing.push({ ...a, id, builtin: false }); existingIds.add(id); added++; } GM_setValue(SK.AGENTS, JSON.stringify(existing)); return added; } catch (e) { throw new Error('导入失败: ' + e.message); } }, }; // ============================================================ // 工具注册表 // ============================================================ const ToolRegistry = { _tools: new Map(), register(tool) { this._tools.set(tool.name, tool); }, get(name) { return this._tools.get(name); }, getAll() { return Array.from(this._tools.values()); }, getToolsDescription() { const tools = this.getAll(); if (!tools.length) return '(无可用工具)'; return tools.map(t => `【${t.name}】${t.description}\n 参数: ${JSON.stringify(t.parameters.properties)}` ).join('\n\n'); }, async execute(name, args) { const tool = this.get(name); if (!tool) return { success: false, error: `工具 "${name}" 不存在` }; try { return await Promise.race([ tool.execute(args), new Promise((_, reject) => setTimeout(() => reject(new Error('工具执行超时')), CONFIG.TOOL_TIMEOUT) ), ]); } catch (e) { return { success: false, error: `工具执行错误: ${e.message}` }; } }, }; ToolRegistry.register({ name: 'getGameDetail', description: '获取 Steam 游戏详细信息(名称、价格、好评率、类型、发行日期等)', parameters: { type: 'object', properties: { appid: { type: 'string', description: 'Steam 游戏 AppID' } }, required: ['appid'] }, category: 'knowledge', async execute(params) { const appid = params.appid; if (!appid) return { success: false, error: '缺少 appid 参数' }; try { const resp = await gmFetch(`https://store.steampowered.com/api/appdetails?appids=${appid}&l=schinese&filters=basic,price_overview`); const data = JSON.parse(resp); const game = data[appid]?.data; if (!game) return { success: false, error: `未找到 AppID ${appid} 的游戏信息` }; const result = { name: game.name, type: game.type, developers: game.developers, publishers: game.publishers, genres: game.genres?.map(g => g.description) || [], release_date: game.release_date?.date, price: game.price_overview?.final_formatted || '免费', discount: game.price_overview?.discount_percent || 0, initial_price: game.price_overview?.initial_formatted, platforms: game.platforms, }; return { success: true, data: result, summary: `${game.name} - ${result.price}${result.discount ? ` (-${result.discount}%)` : ''}` }; } catch (e) { return { success: false, error: e.message }; } }, }); ToolRegistry.register({ name: 'searchGames', description: '搜索 Steam 游戏(支持中英文关键词)', parameters: { type: 'object', properties: { query: { type: 'string', description: '搜索关键词' } }, required: ['query'] }, category: 'knowledge', async execute(params) { const query = encodeURIComponent(params.query || ''); if (!query) return { success: false, error: '缺少搜索关键词' }; try { const resp = await gmFetch(`https://store.steampowered.com/api/storesearch?term=${query}&l=schinese&cc=cn`); const data = JSON.parse(resp); const items = (data.items || []).slice(0, 5).map(item => ({ appid: item.id, name: item.name, price: item.price?.final / 100 || 0, discount: item.price?.discount_percent || 0, })); return { success: true, data: items, summary: `找到 ${items.length} 个结果` }; } catch (e) { return { success: false, error: e.message }; } }, }); ToolRegistry.register({ name: 'getPlayerSummary', description: '获取 Steam 玩家个人资料摘要(昵称、等级、游戏数等)。steamId 参数可选(SteamID64 或自定义ID),不传时自动使用设置中配置的 SteamID 或当前登录用户', parameters: { type: 'object', properties: { steamId: { type: 'string', description: 'SteamID64(17位数字)或自定义ID,可选,默认当前用户' } }, required: [] }, category: 'user', async execute(params) { const { steamId, source } = resolveSteamId(params.steamId ?? params.steamid); if (!steamId) return { success: false, error: '未提供也未检测到 SteamID:请在设置中心「接口服务」中配置 SteamID(留空自动检测当前登录用户),或在参数中提供 steamId' }; try { const isId64 = /^\d{17}$/.test(steamId); const url = isId64 ? `https://steamcommunity.com/profiles/${steamId}/?xml=1` : `https://steamcommunity.com/id/${encodeURIComponent(steamId)}/?xml=1`; const resp = await gmFetch(url, { timeout: 15000 }); const profile = parseSteamProfileXml(resp); if (!profile) return { success: false, error: '无法解析玩家资料(可能资料不公开或 ID 无效)' }; return { success: true, data: { steamID: profile.steamID, steamId64: profile.steamID64, idSource: source, visibility: profile.visibility, avatar: profile.avatarIcon, memberSince: profile.memberSince, summary: profile.summary, }, summary: `${profile.steamID || steamid}`, }; } catch (e) { return { success: false, error: e.message }; } }, }); ToolRegistry.register({ name: 'getOwnedGames', description: '获取玩家拥有的游戏列表和游玩时长,适合分析游戏库、"吃灰"游戏等。steamId 参数可选(SteamID64 或自定义ID),不传时自动使用设置中配置的 SteamID 或当前登录用户。需要 Steam API Key:未配置时会返回配置引导', parameters: { type: 'object', properties: { steamId: { type: 'string', description: 'SteamID64(17位数字)或自定义ID,可选,默认当前用户' } }, required: [] }, category: 'user', async execute(params) { const apiKey = getSteamApiKey(); if (!apiKey) { return { success: false, needsApiKey: true, error: '此工具需要 Steam Web API Key。请引导用户点击对话中的「配置 Steam API Key」按钮,或打开设置面板填写 Steam Web API Key(可在 steamcommunity.com/dev 免费申请),配置完成后用户回复"已配置",再重新调用本工具', }; } const { steamId, source } = resolveSteamId(params.steamId ?? params.steamid); if (!steamId) return { success: false, error: '未提供也未检测到 SteamID:请在设置中心「接口服务」中配置 SteamID(留空自动检测当前登录用户),或在参数中提供 steamId' }; let id64 = steamId; if (!/^\d{17}$/.test(steamId)) { try { id64 = await resolveVanityToId64(steamId, apiKey); } catch (e) { return { success: false, error: `自定义 ID 解析失败: ${e.message}` }; } if (!id64) return { success: false, error: `自定义 ID "${steamId}" 未解析到 SteamID64(可能不存在)` }; } try { const resp = await gmFetch(`https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${apiKey}&steamid=${id64}&include_appinfo=1&format=json`); const data = JSON.parse(resp); const games = (data.response.games || []).map(g => ({ appid: g.appid, name: g.name, playtime: Math.round((g.playtime_forever || 0) / 60 * 10) / 10, })).sort((a, b) => b.playtime - a.playtime); if (data.response.game_count === undefined && !games.length) { return { success: false, error: '游戏库不公开或为空:请确认 Steam 资料中的「游戏详情」隐私设置为公开' }; } const zeroPlay = games.filter(g => g.playtime === 0).length; return { success: true, data: { steamId: id64, idSource: source, total: data.response.game_count ?? games.length, neverPlayed: zeroPlay, topGames: games.slice(0, 20), }, summary: `共 ${data.response.game_count ?? games.length} 个游戏(${zeroPlay} 个从未游玩),按时长降序返回前 ${Math.min(games.length, 20)}`, }; } catch (e) { const msg = /HTTP 403/.test(e.message) ? 'Steam Web API Key 无效或无权限,请检查设置中的 Key' : e.message; return { success: false, error: `游戏库查询失败: ${msg}` }; } }, }); ToolRegistry.register({ name: 'getSpecialDeals', description: '获取 Steam 商店实时打折/特惠数据。category 可选:specials(当前特惠列表,默认)、dailydeal(今日特惠)、topsellers(热销榜)、new_releases(新品)、coming_soon(即将推出)、saleevents(大型促销活动页,如夏促/冬促)。返回游戏名、AppID、折扣百分比、原价、现价、折扣截止时间、商店链接', parameters: { type: 'object', properties: { category: { type: 'string', description: '分类:specials|dailydeal|topsellers|new_releases|coming_soon|saleevents,默认 specials' }, minDiscount: { type: 'number', description: '最低折扣百分比过滤,如 50 表示只看 5 折及以上,默认 0 不过滤' }, limit: { type: 'number', description: '返回条数 1-20,默认 10' }, }, required: [] }, category: 'knowledge', async execute(params) { const category = String(params.category || 'specials').toLowerCase(); const minDiscount = Math.max(0, Number(params.minDiscount) || 0); const limit = Math.min(20, Math.max(1, Number(params.limit) || 10)); try { const fc = await fetchFeaturedCategories(); if (!fc || typeof fc !== 'object') return { success: false, error: '特惠数据返回为空' }; if (category === 'saleevents') { const events = []; for (const key of Object.keys(fc)) { const v = fc[key]; if (!v || !Array.isArray(v.items)) continue; if (v.id === 'cat_spotlight' || (v.id || '').startsWith('cat_') && /促销|特卖|sale/i.test(String(v.name || ''))) { v.items.forEach(it => { if (!it) return; events.push({ name: it.name || '', description: (it.body || '').slice(0, 120), url: it.url || '', endsAt: formatDealEnds(it.discount_expiration), headerImage: it.header_image || '', }); }); } } if (!events.length) return { success: true, data: { events: [], note: '当前没有进行中的大型促销活动' }, summary: '当前无大型促销活动' }; return { success: true, data: { events: events.slice(0, limit) }, summary: `当前有 ${Math.min(events.length, limit)} 个促销活动(如 ${events[0].name})` }; } let cat; let note = ''; if (category === 'dailydeal') { cat = findFeaturedCat(fc, 'cat_dailydeal'); if (!cat && fc.specials && Array.isArray(fc.specials.items)) { // Steam 现行 API 已无独立"今日特惠"分类,回退为特惠中折扣即将结束(≤48h)的优惠 const now = Date.now() / 1000; const discounted = fc.specials.items.filter(it => it && it.id && it.discounted && Number(it.discount_percent) > 0 && Number(it.discount_expiration) > 0); const soon = discounted.filter(it => Number(it.discount_expiration) - now <= 48 * 3600); const items = soon.length ? soon : discounted.sort((a, b) => a.discount_expiration - b.discount_expiration).slice(0, 5); cat = { id: 'cat_dailydeal', name: '今日特惠(即将结束)', items }; note = soon.length ? 'Steam 未提供独立今日特惠分类,已返回 48 小时内结束的优惠' : 'Steam 未提供独立今日特惠分类,已返回折扣最接近结束的优惠'; } } else if (category === 'specials') cat = fc.specials; else if (category === 'topsellers') cat = fc.top_sellers || fc.topsellers; else if (category === 'new_releases') cat = fc.new_releases; else if (category === 'coming_soon') cat = fc.coming_soon; else return { success: false, error: `未知的 category: ${category},可选 specials|dailydeal|topsellers|new_releases|coming_soon|saleevents` }; if (!cat || !Array.isArray(cat.items) || !cat.items.length) { return { success: true, data: { games: [], note: `「${cat?.name || category}」分类暂无数据` }, summary: '该分类暂无数据' }; } const needDiscounted = category === 'specials' || category === 'dailydeal'; const games = cat.items .filter(it => it && it.id) .filter(it => needDiscounted ? (it.discounted && Number(it.discount_percent) > 0) : true) .filter(it => (Number(it.discount_percent) || 0) >= minDiscount) .sort((a, b) => (Number(b.discount_percent) || 0) - (Number(a.discount_percent) || 0)) .slice(0, limit) .map(it => ({ appid: it.id, name: it.name || '', discount: Number(it.discount_percent) || 0, originalPrice: formatCents(it.original_price, it.currency), finalPrice: formatCents(it.final_price, it.currency), endsAt: formatDealEnds(it.discount_expiration), url: `https://store.steampowered.com/app/${it.id}`, })); if (!games.length) return { success: true, data: { games: [], note: minDiscount ? `没有折扣 ≥ ${minDiscount}% 的游戏` : '该分类暂无条目' }, summary: '无符合条件的游戏' }; const discounts = games.map(g => g.discount); return { success: true, data: Object.assign({ category: cat.name || category, count: games.length, games }, note ? { note } : {}), summary: `「${cat.name || category}」返回 ${games.length} 款(折扣 ${Math.min(...discounts)}%-${Math.max(...discounts)}%)`, }; } catch (e) { return { success: false, error: `特惠查询失败: ${e.message}` }; } }, }); ToolRegistry.register({ name: 'getOfficialDeals', description: '获取 Steam 官方特惠动态聚合数据(对应游戏库插件的"官方动态"面板):促销活动(夏促/冬促/系列特卖/发行商特卖等活动页及截止时间)、每日优惠、当前特惠(含折扣倒计时)、Steam 官方新闻。用户询问"最近有什么打折/特惠/促销/值得入手的游戏"时优先调用本工具,一次返回全部实时数据,无需参数', parameters: { type: 'object', properties: { sections: { type: 'string', description: '需要的分区,逗号分隔:specials(当前特惠)|dailydeals(每日优惠)|events(促销活动)|news(官方新闻)。默认全部' }, limit: { type: 'number', description: '每个分区最大返回条数 1-15,默认 8' }, }, required: [] }, category: 'knowledge', async execute(params) { const wanted = String(params.sections || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean); const has = s => !wanted.length || wanted.includes(s); const limit = Math.min(15, Math.max(1, Number(params.limit) || 8)); try { const fc = await fetchFeaturedCategories(); if (!fc || typeof fc !== 'object') return { success: false, error: '官方动态数据返回为空' }; const mapGame = it => ({ appid: it.id, name: it.name || '', discount: Number(it.discount_percent) || 0, originalPrice: formatCents(it.original_price, it.currency), finalPrice: formatCents(it.final_price, it.currency), endsAt: formatDealEnds(it.discount_expiration), url: `https://store.steampowered.com/app/${it.id}`, }); let specials = []; if (has('specials') && fc.specials && Array.isArray(fc.specials.items)) { specials = fc.specials.items .filter(it => it && it.id && it.discounted && Number(it.discount_percent) > 0) .sort((a, b) => (Number(b.discount_percent) || 0) - (Number(a.discount_percent) || 0)) .slice(0, limit) .map(mapGame); } const notes = []; let dailyDeals = []; if (has('dailydeals')) { const dd = findFeaturedCat(fc, 'cat_dailydeal'); let items = dd && Array.isArray(dd.items) ? dd.items.filter(it => it && it.id && it.discounted) : []; if (!items.length && fc.specials && Array.isArray(fc.specials.items)) { const now = Date.now() / 1000; const pool = fc.specials.items.filter(it => it && it.id && it.discounted && Number(it.discount_expiration) > 0); const soon = pool.filter(it => Number(it.discount_expiration) - now <= 48 * 3600); items = soon.length ? soon : pool.sort((a, b) => a.discount_expiration - b.discount_expiration).slice(0, 3); notes.push(soon.length ? '官方未单列今日特惠,已取 48 小时内结束的优惠' : '官方未单列今日特惠,已取折扣最接近结束的优惠'); } dailyDeals = items.slice(0, 5).map(mapGame); } let events = []; if (has('events')) { const seen = new Set(); for (const key of Object.keys(fc)) { const v = fc[key]; if (!v || v.id !== 'cat_spotlight' || !Array.isArray(v.items)) continue; for (const sp of v.items) { if (!sp || !sp.url || !sp.url.includes('/sale/')) continue; const m = sp.url.match(/\/sale\/([^\/\?]+)/); const saleId = m ? m[1] : sp.url; if (seen.has(saleId)) continue; seen.add(saleId); events.push({ name: sp.name || '促销活动', url: sp.url, description: (sp.body || '').slice(0, 120), endsAt: formatDealEnds(sp.discount_expiration), }); } } events = events.slice(0, limit); } let news = []; if (has('news')) { try { // 官方 Steam 商店新闻 RSS(旧 ISteamNews 593050 实为 Dead Rising 4 DLC,返回恒空) const raw = await gmFetch('https://store.steampowered.com/feeds/news.xml', { timeout: 12000 }); const blocks = String(raw).match(/][\s\S]*?<\/item>/g) || []; const decode = s => String(s || '') .replace(//g, '') .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'") .trim(); const pickTag = (b, tag) => decode((b.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)`)) || [])[1]); news = blocks.slice(0, limit).map(b => { const date = (b.match(/]*>([\s\S]*?)<\/dc:date>/) || [])[1] || ''; return { title: pickTag(b, 'title'), url: pickTag(b, 'link'), date: date ? date.slice(0, 10) : '', summary: pickTag(b, 'description').replace(/<[^>]+>/g, '').slice(0, 100), }; }).filter(n => n.title); } catch { notes.push('官方新闻获取失败,不影响特惠数据'); } } const data = { specials, dailyDeals, events, news }; if (notes.length) data.notes = notes; const parts = []; if (has('specials')) parts.push(`当前特惠 ${specials.length} 款`); if (has('dailydeals')) parts.push(`每日优惠 ${dailyDeals.length} 款`); if (has('events')) parts.push(`促销活动 ${events.length} 个`); if (has('news')) parts.push(`官方新闻 ${news.length} 条`); const top = specials[0] || dailyDeals[0]; return { success: true, data, summary: `官方动态:${parts.join('、')}${top ? `(折扣最高 ${top.name} -${top.discount}%)` : ''}`, }; } catch (e) { return { success: false, error: `官方动态获取失败: ${e.message}` }; } }, }); // ============================================================ // 游戏官方动态查询 (v1.8.2) — 数据源同游戏库插件信息面板的 ISteamNews // ============================================================ const GAME_NEWS_ALIASES = { 'dota2': { appid: '570', name: 'Dota 2' }, 'dota': { appid: '570', name: 'Dota 2' }, '刀塔': { appid: '570', name: 'Dota 2' }, '刀塔2': { appid: '570', name: 'Dota 2' }, 'cs2': { appid: '730', name: 'Counter-Strike 2' }, 'csgo': { appid: '730', name: 'Counter-Strike 2' }, 'cs': { appid: '730', name: 'Counter-Strike 2' }, '反恐精英': { appid: '730', name: 'Counter-Strike 2' }, '反恐精英2': { appid: '730', name: 'Counter-Strike 2' }, 'pubg': { appid: '578080', name: 'PUBG: BATTLEGROUNDS' }, '绝地求生': { appid: '578080', name: 'PUBG: BATTLEGROUNDS' }, '吃鸡': { appid: '578080', name: 'PUBG: BATTLEGROUNDS' }, 'gta': { appid: '271590', name: 'Grand Theft Auto V' }, 'gta5': { appid: '271590', name: 'Grand Theft Auto V' }, 'gtav': { appid: '271590', name: 'Grand Theft Auto V' }, 'gtaol': { appid: '271590', name: 'Grand Theft Auto V' }, '艾尔登法环': { appid: '1245620', name: 'ELDEN RING' }, '老头环': { appid: '1245620', name: 'ELDEN RING' }, '法环': { appid: '1245620', name: 'ELDEN RING' }, 'elden ring': { appid: '1245620', name: 'ELDEN RING' }, '黑神话': { appid: '2358720', name: 'Black Myth: Wukong' }, '黑神话悟空': { appid: '2358720', name: 'Black Myth: Wukong' }, '悟空': { appid: '2358720', name: 'Black Myth: Wukong' }, 'wukong': { appid: '2358720', name: 'Black Myth: Wukong' }, 'rust': { appid: '252490', name: 'Rust' }, '腐蚀': { appid: '252490', name: 'Rust' }, '赛博朋克': { appid: '1091500', name: 'Cyberpunk 2077' }, '赛博朋克2077': { appid: '1091500', name: 'Cyberpunk 2077' }, 'cyberpunk': { appid: '1091500', name: 'Cyberpunk 2077' }, 'cyberpunk 2077': { appid: '1091500', name: 'Cyberpunk 2077' }, '幻兽帕鲁': { appid: '1623730', name: 'Palworld' }, '帕鲁': { appid: '1623730', name: 'Palworld' }, 'palworld': { appid: '1623730', name: 'Palworld' }, '泰拉瑞亚': { appid: '105600', name: 'Terraria' }, 'terraria': { appid: '105600', name: 'Terraria' }, '饥荒': { appid: '322330', name: "Don't Starve Together" }, "don't starve": { appid: '322330', name: "Don't Starve Together" }, 'dst': { appid: '322330', name: "Don't Starve Together" }, '流放之路': { appid: '269449', name: 'Path of Exile' }, 'poe': { appid: '269449', name: 'Path of Exile' }, '流放之路2': { appid: '238960', name: 'Path of Exile 2' }, 'poe2': { appid: '238960', name: 'Path of Exile 2' }, 'warframe': { appid: '230410', name: 'Warframe' }, '星际战甲': { appid: '230410', name: 'Warframe' }, '命运2': { appid: '1085660', name: 'Destiny 2' }, 'destiny2': { appid: '1085660', name: 'Destiny 2' }, 'destiny 2': { appid: '1085660', name: 'Destiny 2' }, '荒野大镖客': { appid: '1174180', name: 'Red Dead Redemption 2' }, '荒野大镖客2': { appid: '1174180', name: 'Red Dead Redemption 2' }, 'rdr2': { appid: '1174180', name: 'Red Dead Redemption 2' }, '大表哥2': { appid: '1174180', name: 'Red Dead Redemption 2' }, 'apex': { appid: '1172470', name: 'Apex Legends' }, 'apex英雄': { appid: '1172470', name: 'Apex Legends' }, 'apex legends': { appid: '1172470', name: 'Apex Legends' }, '彩虹六号': { appid: '359550', name: 'Rainbow Six Siege' }, 'r6': { appid: '359550', name: 'Rainbow Six Siege' }, 'r6s': { appid: '359550', name: 'Rainbow Six Siege' }, '围攻': { appid: '359550', name: 'Rainbow Six Siege' }, '军团要塞': { appid: '440', name: 'Team Fortress 2' }, 'tf2': { appid: '440', name: 'Team Fortress 2' }, '求生之路': { appid: '550', name: 'Left 4 Dead 2' }, 'l4d2': { appid: '550', name: 'Left 4 Dead 2' }, '雨中冒险': { appid: '632360', name: 'Risk of Rain 2' }, 'ror2': { appid: '632360', name: 'Risk of Rain 2' }, '无人深空': { appid: '275850', name: 'No Man\'s Sky' }, "no man's sky": { appid: '275850', name: 'No Man\'s Sky' }, }; const _appidResolveCache = new Map(); async function resolveGameToAppid(query) { const raw = String(query || '').trim(); if (!raw) return null; if (/^\d{1,7}$/.test(raw)) return { appid: raw, name: '', source: 'direct' }; const key = raw.toLowerCase(); if (GAME_NEWS_ALIASES[key]) return Object.assign({}, GAME_NEWS_ALIASES[key], { source: 'alias' }); if (_appidResolveCache.has(key)) return _appidResolveCache.get(key); try { const resp = await gmFetch(`https://store.steampowered.com/api/storesearch?term=${encodeURIComponent(raw)}&l=schinese&cc=cn`, { timeout: 12000 }); const data = JSON.parse(resp); const item = (data.items || []).filter(it => it && it.id)[0]; if (item) { const hit = { appid: String(item.id), name: item.name || raw, source: 'storesearch' }; _appidResolveCache.set(key, hit); return hit; } } catch { /* fallthrough */ } return null; } function cleanNewsContent(text) { return String(text || '') .replace(/\{STEAM_CLAN(?:_LOC)?_(?:IMAGE|VIDEO)\}\S*/gi, '') .replace(/\[img\][\s\S]*?\[\/img\]/gi, '') .replace(/\[url=[^\]]*\]([\s\S]*?)\[\/url\]/gi, '$1') .replace(/\[\/?(?:b|i|u|s|quote|h[1-6]|list|olist|li|code|hr|url|img|spoiler|previewyoutube)[^\]]*\]/gi, '') .replace(/\[\/?[a-z0-9_=#.\s:%{}|\/\\-]*\]/gi, '') .replace(/https?:\/\/\S+/g, m => (m.length > 60 ? '' : m)) .replace(/\s{2,}/g, ' ') .trim(); } function classifyNewsType(title) { const t = String(title || '').toLowerCase(); if (/patch|update|hotfix|balance|nerf|buff|fix|maintenance|更新|补丁|平衡|修复|维护|版本/.test(t)) return 'update'; if (/sale|discount|deal|off\b|特惠|促销|打折|优惠|折扣/.test(t)) return 'sale'; if (/event|tournament|major|invitational|championship|international|cup\b|league|活动|赛事|锦标赛|比赛|联赛|大赛|邀请赛/.test(t)) return 'event'; return 'announcement'; } const _gameNewsCache = new Map(); const GAME_NEWS_TTL = 30 * 60 * 1000; async function fetchGameNewsItems(appid) { const cached = _gameNewsCache.get(appid); if (cached && Date.now() - cached.ts < GAME_NEWS_TTL) return cached; const base = `https://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=${encodeURIComponent(appid)}&count=20&maxlength=800&l=schinese`; let feedsMode = 'official'; let items = []; try { const resp = await gmFetch(`${base}&feeds=steam_community_announcements`, { timeout: 15000 }); items = JSON.parse(resp)?.appnews?.newsitems || []; } catch { items = []; } if (!items.length) { // 官方公告源为空 → 全源重试(部分游戏经 steam_news 等其它 feed 发布) try { const resp = await gmFetch(base, { timeout: 15000 }); items = JSON.parse(resp)?.appnews?.newsitems || []; if (items.length) feedsMode = 'all'; } catch { /* keep empty */ } } const entry = { items, ts: Date.now(), feedsMode }; _gameNewsCache.set(appid, entry); return entry; } ToolRegistry.register({ name: 'getGameNews', description: '查询某个具体游戏的官方动态/最新消息(同游戏库插件信息面板的"官方动态"):版本更新公告、平衡性补丁、运营活动、官方新闻等。用户问及"dota2/cs2/PUBG 等某个游戏的最近更新/最新动态/官方公告/补丁内容"时必须调用本工具,禁止凭训练记忆编造版本更新内容。支持游戏名称(中英文及常用简称:dota2、cs2、csgo、吃鸡、老头环等内置直达)或 AppID,无需 API Key', parameters: { type: 'object', properties: { game: { type: 'string', description: '游戏名称或简称(如 dota2 / cs2 / 艾尔登法环),与 appid 二选一' }, appid: { type: 'string', description: 'Steam 游戏 AppID,优先于 game' }, count: { type: 'number', description: '返回条数 1-10,默认 5' }, type: { type: 'string', description: '内容类型过滤:update(版本更新/补丁)|event(活动/赛事)|sale(促销)|all(不过滤,默认)' }, }, required: [] }, category: 'knowledge', async execute(params) { const appidParam = String(params.appid || '').trim(); const gameName = String(params.game || params.gameName || params.title || '').trim(); if (!appidParam && !gameName) return { success: false, error: '缺少参数:请提供 game(游戏名称,如 dota2/cs2)或 appid' }; const resolved = appidParam ? await resolveGameToAppid(appidParam) : await resolveGameToAppid(gameName); if (!resolved || !resolved.appid) { return { success: false, error: `未能识别游戏「${gameName || appidParam}」:建议先用 searchGames 确认 AppID 后以 appid 参数重试,或让用户提供更准确的名称` }; } const { appid } = resolved; // direct appid 无名称时补查游戏名(失败不阻塞) let gameLabel = resolved.name || ''; if (!gameLabel) { try { const resp = await gmFetch(`https://store.steampowered.com/api/appdetails?appids=${appid}&l=schinese&filters=basic`, { timeout: 10000 }); gameLabel = JSON.parse(resp)?.[appid]?.data?.name || ''; } catch { /* ignore */ } } const count = Math.min(10, Math.max(1, Number(params.count) || 5)); const typeFilter = String(params.type || 'all').trim().toLowerCase(); const entry = await fetchGameNewsItems(appid); const mapped = (entry.items || []) .map(n => ({ title: String(n.title || '').trim(), url: n.url || '', date: n.date ? new Date(n.date * 1000).toISOString().slice(0, 10) : '', type: classifyNewsType(n.title), author: n.author || '', feed: n.feedlabel || n.feedname || '', contents: cleanNewsContent(n.contents).slice(0, 400), })) .filter(n => n.title); if (!mapped.length) { return { success: true, data: { appid, game: gameLabel || `AppID ${appid}`, resolvedBy: resolved.source, totalAvailable: 0, returnedType: typeFilter === 'all' || !typeFilter ? 'all' : typeFilter, news: [], latestUpdate: null, source: entry.feedsMode === 'all' ? 'Steam 全部动态源(官方公告源无数据,已自动回退)' : 'Steam 官方公告源(steam_community_announcements)', }, summary: `《${gameLabel || 'AppID ' + appid}》近期无官方动态记录`, }; } let news = typeFilter && typeFilter !== 'all' ? mapped.filter(n => n.type === typeFilter) : mapped; news = news.slice(0, count); const data = { appid, game: gameLabel || `AppID ${appid}`, resolvedBy: resolved.source, totalAvailable: mapped.length, returnedType: typeFilter === 'all' || !typeFilter ? 'all' : typeFilter, news, latestUpdate: mapped.find(n => n.type === 'update') || null, source: entry.feedsMode === 'all' ? 'Steam 全部动态源(官方公告源无数据,已自动回退)' : 'Steam 官方公告源(steam_community_announcements)', }; if (typeFilter !== 'all' && typeFilter && !news.length) { data.note = `该游戏最近 ${mapped.length} 条动态中没有 ${typeFilter} 类型内容,建议以 type=all 重新调用查看全部动态`; } const latest = news[0] || mapped[0]; return { success: true, data, summary: `《${data.game}》动态 ${news.length}/${mapped.length} 条,最新:${latest.title.slice(0, 40)}(${latest.date})`, }; }, }); ToolRegistry.register({ name: 'searchDiscountedGames', description: '高级打折检索:通过 Steam 官方 Web API(IStoreQueryService) 按好评率/最新发行等排序批量扫描商店游戏并筛选当前正在打折的,适合"找折扣大的好评游戏"类深度检索。需要 Steam Web API Key:未配置时会返回配置引导,用户在设置中配置并回复"已配置"后重新调用即可', parameters: { type: 'object', properties: { minDiscount: { type: 'number', description: '最低折扣百分比过滤,如 50 表示 5 折起,默认 0 不过滤' }, sort: { type: 'string', description: '扫描排序:topRated(好评率优先,默认)|newest(最新发行)|alpha(字母序)' }, count: { type: 'number', description: '扫描条数 10-500,默认 100,越大越全但越慢' }, limit: { type: 'number', description: '返回条数 1-20,默认 10' }, }, required: [] }, category: 'knowledge', async execute(params) { const apiKey = getSteamApiKey(); if (!apiKey) { return { success: false, needsApiKey: true, error: '此工具需要 Steam Web API Key。请引导用户点击对话中的「配置 Steam API Key」按钮,或打开设置面板填写 Steam Web API Key(可在 steamcommunity.com/dev 免费申请),配置完成后用户回复"已配置",再重新调用本工具', }; } const minDiscount = Math.max(0, Number(params.minDiscount) || 0); const limit = Math.min(20, Math.max(1, Number(params.limit) || 10)); const count = Math.min(500, Math.max(10, Number(params.count) || 100)); const sortMap = { toprated: 21, newest: 20, alpha: 1 }; const sort = sortMap[String(params.sort || 'toprated').toLowerCase()] ?? 21; const inputJson = JSON.stringify({ query: { start: 0, count, sort, filter: { released_only: true, type_filters: { include_games: true } } }, context: { language: 6, country_code: getStoreCountryCode() }, data_request: { include_basic_info: true, include_all_purchase_options: true, include_ratings: true }, }); try { const raw = await gmFetch(`https://api.steampowered.com/IStoreQueryService/Query/v1/?key=${encodeURIComponent(apiKey)}&input_json=${encodeURIComponent(inputJson)}`, { timeout: 25000 }); const data = JSON.parse(raw); const items = (data && data.response && (data.response.store_items || data.response.items)) || []; const games = []; for (const it of items) { if (!it || !it.appid) continue; const opts = it.all_purchase_options || it.purchase_options || []; let best = null; for (const o of opts) { if (!o) continue; const disc = Number(o.discount_percent ?? o.price_discount_percent ?? o.discount_pct) || 0; if (disc > (best ? best.discount : -1)) { best = { discount: disc, price: o.formatted_final_price || o.final_formatted_price || formatCents(o.final_price_in_cents ?? o.price_final_in_cents, o.currency), originalPrice: o.formatted_original_price || formatCents(o.original_price_in_cents ?? o.price_original_price_in_cents, o.currency), }; } } if (!best || best.discount < Math.max(1, minDiscount)) continue; const rating = it.review_summary || it.ratings || null; games.push({ appid: it.appid, name: it.name || '', discount: best.discount, price: best.price, originalPrice: best.originalPrice, reviewScore: rating ? (rating.review_score_desc || rating.score_desc || '') : '', url: `https://store.steampowered.com/app/${it.appid}`, }); } games.sort((a, b) => b.discount - a.discount); if (!games.length) { return { success: true, data: { games: [], scanned: items.length, note: `扫描 ${items.length} 款,无折扣 ≥ ${Math.max(1, minDiscount)}% 的游戏` }, summary: '无符合条件的打折游戏' }; } return { success: true, data: { scanned: items.length, count: Math.min(games.length, limit), games: games.slice(0, limit) }, summary: `已通过 Steam Web API 扫描 ${items.length} 款,${games.length} 款在打折(最高 -${games[0].discount}%),返回前 ${Math.min(games.length, limit)} 款`, }; } catch (e) { const msg = /HTTP 403/.test(e.message) ? 'Steam Web API Key 无效或无权限,请检查设置中的 Key' : /HTTP 400|HTTP 429/.test(e.message) ? '请求被拒绝(参数/频率限制),请减小 count 后重试' : e.message; return { success: false, error: `折扣检索失败: ${msg}` }; } }, }); ToolRegistry.register({ name: 'getWishlistDeals', description: '获取 Steam 愿望单游戏及打折情况(默认只返回正在打折的)。不传 steamId 时自动使用当前登录用户;也可查询他人的公开愿望单。返回游戏名、折扣百分比、原价、现价、愿望单优先级', parameters: { type: 'object', properties: { steamId: { type: 'string', description: 'SteamID64(17位数字)或自定义ID(vanity URL名),可选,默认当前登录用户' }, onlyDiscounted: { type: 'boolean', description: '是否只返回正在打折的游戏,默认 true' }, limit: { type: 'number', description: '返回条数 1-20,默认 10' }, }, required: [] }, category: 'user', async execute(params) { const { steamId, source } = resolveSteamId(params.steamId); if (!steamId) return { success: false, error: '未提供也未检测到 SteamID:请在设置中心「接口服务」中配置 SteamID(留空自动检测当前登录用户),或在参数中提供 steamId' }; const onlyDiscounted = params.onlyDiscounted !== false; const limit = Math.min(20, Math.max(1, Number(params.limit) || 10)); const isId64 = /^\d{17}$/.test(steamId); const base = isId64 ? `https://store.steampowered.com/wishlist/profiles/${steamId}/wishlistdata/` : `https://store.steampowered.com/wishlist/id/${encodeURIComponent(steamId)}/wishlistdata/`; try { const all = []; const MAX_PAGES = 12; let truncated = false; for (let p = 0; p < MAX_PAGES; p++) { let text; try { text = await gmFetch(`${base}?p=${p}`, { timeout: 20000 }); } catch (e) { if (p === 0) throw new Error(`愿望单请求失败: ${e.message}(可能愿望单不公开或 ID 无效)`); break; } if (typeof text === 'string' && text.trimStart().startsWith('<')) { throw new Error('愿望单返回页面而非数据(可能未公开或需登录)'); } let entries; try { const data = JSON.parse(text); entries = Array.isArray(data) ? data : Object.entries(data).map(([k, v]) => ({ appid: Number(k), ...(v || {}) })); } catch { if (p === 0) throw new Error('愿望单数据解析失败(可能不公开)'); break; } if (!entries.length) break; all.push(...entries.filter(e => e && (e.appid || e.name))); if (p === MAX_PAGES - 1 && entries.length > 0) truncated = all.length >= MAX_PAGES * 50; await new Promise(r => setTimeout(r, 200)); } if (!all.length) return { success: true, data: { games: [], note: '愿望单为空' }, summary: '愿望单为空' }; const normalized = all.map(e => { const bpo = e.best_purchase_option || {}; const sub = (Array.isArray(e.subs) && e.subs[0]) || {}; const discount = Number(bpo.discount_pct ?? sub.discount_pct ?? 0) || 0; const finalCents = Number(bpo.final_price_in_cents ?? sub.price ?? -1); const originalCents = Number(bpo.original_price_in_cents ?? -1); return { appid: e.appid, name: e.name || '', discount, finalPrice: bpo.formatted_final_price || (finalCents >= 0 ? formatCents(finalCents, 'CNY') : (e.is_free ? '免费' : '')), originalPrice: originalCents >= 0 ? formatCents(originalCents, 'CNY') : (discount > 0 && finalCents >= 0 ? formatCents(Math.round(finalCents / (1 - discount / 100)), 'CNY') : ''), priority: Number(e.priority) || 0, comingSoon: !!e.is_coming_soon, url: `https://store.steampowered.com/app/${e.appid}`, }; }); const discounted = normalized.filter(g => g.discount > 0).sort((a, b) => b.discount - a.discount); const result = (onlyDiscounted ? discounted : normalized).slice(0, limit); const data = { owner: isId64 ? `SteamID ${steamId}` : `自定义ID ${steamId}`, totalInWishlist: normalized.length, discountedCount: discounted.length, games: result, }; if (truncated) data.truncated = true; if (onlyDiscounted && !discounted.length) { return { success: true, data: { ...data, games: [], note: '愿望单中当前没有正在打折的游戏' }, summary: `愿望单共 ${normalized.length} 款,暂无打折` }; } const maxD = discounted.length ? discounted[0].discount : 0; return { success: true, data, summary: `愿望单共 ${normalized.length} 款,${discounted.length} 款打折(最高 -${maxD}%),返回 ${result.length} 款${truncated ? '(数据可能不全)' : ''}` }; } catch (e) { return { success: false, error: e.message }; } }, }); ToolRegistry.register({ name: 'getPriceHistory', description: '查询游戏历史价格与历史最低价(史低),判断当前折扣是否值得入手。返回 Steam 商店现价、折扣百分比、全网历史最低价、是否为 Steam 史低。数据来自 AugmentedSteam', parameters: { type: 'object', properties: { appid: { type: 'string', description: 'Steam 游戏 AppID' }, }, required: ['appid'] }, category: 'knowledge', async execute(params) { const appid = String(params.appid || '').trim(); if (!appid) return { success: false, error: '缺少 appid 参数' }; try { const body = JSON.stringify({ country: getStoreCountryCode(), apps: [Number(appid)] }); const resp = await gmFetch('https://api.augmentedsteam.com/prices/v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: body, timeout: 15000, }); const data = JSON.parse(resp); const entry = data?.prices?.[`app/${appid}`] || data?.prices?.[appid]; if (!entry) return { success: true, data: { appid, note: '该游戏暂无历史价格记录(可能为新作/免费游戏/未收录)' }, summary: '无历史价格记录' }; const fmt = (p) => { if (!p || !p.price) return ''; const amt = Number(p.price.amount); if (!Number.isFinite(amt)) return ''; const cur = String(p.price.currency || '').toUpperCase(); const sym = cur === 'CNY' ? '¥' : cur === 'USD' ? 'US$' : cur === 'EUR' ? '€' : (cur ? cur + ' ' : ''); return sym + amt.toFixed(2); }; const current = entry.current || null; const lowest = entry.lowest || null; const curAmt = current ? Number(current.price?.amount) : null; const lowAmt = lowest ? Number(lowest.price?.amount) : null; const result = { appid, steamCurrent: current ? { price: fmt(current), originalPrice: fmt(current.regular) || fmt({ price: current.regular }), cut: Number(current.cut) || 0, url: current.url || '' } : null, historicalLow: lowest ? { price: fmt(lowest), shop: lowest.shop?.name || '', cut: Number(lowest.cut) || 0, date: lowest.timestamp ? new Date(lowest.timestamp).toISOString().slice(0, 10) : '' } : null, lowestNote: 'historicalLow 为全网各商店(含第三方 key 店)历史最低价记录;isSteamHistoricalLow=true 表示当前 Steam 售价不高于该全网最低价,即 Steam 史低', isSteamHistoricalLow: (curAmt !== null && lowAmt !== null) ? curAmt <= lowAmt : null, }; const cur = result.steamCurrent ? `Steam 现价 ${result.steamCurrent.price}${result.steamCurrent.cut ? ` (-${result.steamCurrent.cut}%)` : ''}` : 'Steam 无在售价'; const low = result.historicalLow ? `历史最低 ${result.historicalLow.price}(${result.historicalLow.shop})` : '无历史低价记录'; const flag = result.isSteamHistoricalLow === true ? ',【Steam 史低】' : ''; return { success: true, data: result, summary: `${cur},${low}${flag}` }; } catch (e) { return { success: false, error: `价格历史查询失败: ${e.message}` }; } }, }); // ============================================================ // ITAD (IsThereAnyDeal) 多地区比价客户端 // 参考 steam-game-library-viewer 的 sglv-itad.lib.js 实现内联精简版 // 注意坑位(来自 SGLV v1.0.2):不发 vouchers 参数;deals 仅 true 时发送; // country 必须是有效 ISO 3166-1 alpha-2(EU 无效会 400) // ============================================================ const ITAD_BASE_URL = 'https://api.isthereanydeal.com'; const ITAD_SHOP_STEAM = 61; const ITAD_REGIONS_ENHANCED = ['CN', 'US', 'TR', 'AR', 'RU', 'IN', 'BR', 'UA', 'KZ', 'CL', 'CO', 'PL', 'MX', 'JP', 'KR', 'GB']; const ITAD_REGIONS_FALLBACK = ['CN', 'US', 'TR', 'AR', 'RU', 'IN', 'BR']; const ITAD_CACHE_TTL = 12 * 60 * 60 * 1000; const REGION_NAMES = { CN: '中国', US: '美国', TR: '土耳其', AR: '阿根廷', RU: '俄罗斯', IN: '印度', BR: '巴西', UA: '乌克兰', KZ: '哈萨克斯坦', CL: '智利', CO: '哥伦比亚', PL: '波兰', MX: '墨西哥', JP: '日本', KR: '韩国', GB: '英国', }; let _itadCache = null; function itadCacheGet(key) { try { if (!_itadCache) _itadCache = parseStored(GM_getValue('sa_itad_cache_v1', null), {}) || {}; const hit = _itadCache[key]; if (hit && hit.expires > Date.now()) return hit.data; } catch {} return null; } function itadCacheSet(key, data) { try { if (!_itadCache) _itadCache = parseStored(GM_getValue('sa_itad_cache_v1', null), {}) || {}; _itadCache[key] = { data, expires: Date.now() + ITAD_CACHE_TTL }; const entries = Object.entries(_itadCache); if (entries.length > 120) { for (const [k, v] of entries.sort((a, b) => a[1].expires - b[1].expires).slice(0, entries.length - 120)) { delete _itadCache[k]; } } GM_setValue('sa_itad_cache_v1', JSON.stringify(_itadCache)); } catch {} } async function itadTestConnection(key) { if (!key) return { success: false, error: '未填写 Key' }; try { // shops/v1 对无效 key 也返回 200 无法校验;lookup 端点无效 key 返回 403 "Invalid or expired api key" const resp = await gmFetch(`${ITAD_BASE_URL}/lookup/id/shop/${ITAD_SHOP_STEAM}/v1?key=${encodeURIComponent(key)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(['app/220']), timeout: 10000, }); const data = JSON.parse(resp); return { success: true, sampleUuid: data?.['app/220'] || null }; } catch (e) { const m = String(e.message || '').match(/HTTP (\d+)/); if (m && (m[1] === '401' || m[1] === '403')) return { success: false, error: `Key 无效 (HTTP ${m[1]})` }; return { success: false, error: e.message }; } } // AI 模型连通性检测:发送最小 chat 请求(max_tokens=1),校验地址/Key/模型名 async function testModelConnection(model) { if (!model?.apiUrl) throw new Error('未填写 API 地址'); if (!model?.apiKey) throw new Error('未填写 API Key'); if (!model?.model) throw new Error('未填写模型名称'); const started = Date.now(); const resp = await gmFetch(model.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${model.apiKey}` }, data: JSON.stringify({ model: model.model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false }), timeout: 20000, }); const data = JSON.parse(resp); if (data?.error) { const msg = typeof data.error === 'string' ? data.error : (data.error.message || `API 错误码 ${data.error.code ?? ''}`.trim()); throw new Error(msg || 'API 返回错误'); } if (!Array.isArray(data?.choices) || !data.choices.length) { throw new Error('响应缺少 choices,请检查 API 地址是否指向 /chat/completions 端点'); } return { latencyMs: Date.now() - started, modelEcho: data.model || model.model, usage: data.usage || null }; } // 将 AI 请求异常翻译为用户可读的诊断文案 function describeModelError(e) { const msg = String(e?.message || ''); const m = msg.match(/HTTP (\d+)/); if (m) { if (m[1] === '401' || m[1] === '403') return 'API Key 无效或无权限 (鉴权失败)'; if (m[1] === '404') return 'API 地址不存在 (404),请检查端点路径'; if (m[1] === '429') return '请求频率超限 (429),请稍后重试'; if (m[1] === '400' || m[1] === '422') return '请求被拒绝 (多为模型名称不正确)'; return `HTTP ${m[1]}: ${msg.slice(0, 120)}`; } if (msg.includes('超时')) return '连接超时:网络不可达,或目标域名未在脚本 @connect 中声明'; if (msg.includes('网络错误')) return '网络错误:域名无法访问或跨域未授权'; return msg || '未知错误'; } // Steam Web API Key 检测:查询公开玩家资料验证 Key 有效性 async function testSteamApiKey(key) { if (!key) return { success: false, error: '未填写 Key(可选配置)' }; try { const resp = await gmFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${encodeURIComponent(key)}&steamids=76561197960265728`, { timeout: 15000 }); const data = JSON.parse(resp); const players = data?.response?.players; if (!Array.isArray(players)) return { success: false, error: '响应格式异常,请确认 Key 来自 steamcommunity.com/dev' }; if (!players.length) return { success: true, samplePlayer: null, note: 'Key 有效(示例查询未返回玩家资料)' }; return { success: true, samplePlayer: players[0]?.personaname || null }; } catch (e) { const msg = String(e.message || ''); const m = msg.match(/HTTP (\d+)/); if (m) { if (m[1] === '403') return { success: false, error: 'Key 无效 (HTTP 403),请到 steamcommunity.com/dev 重新生成' }; if (m[1] === '400') return { success: false, error: 'Key 格式错误 (HTTP 400)' }; return { success: false, error: `HTTP ${m[1]}` }; } return { success: false, error: msg }; } } // Steam AppID → ITAD Game UUID async function itadLookupId(appid) { const cacheKey = `lookup_${appid}`; const cached = itadCacheGet(cacheKey); if (cached !== null) return cached; const resp = await gmFetch(`${ITAD_BASE_URL}/lookup/id/shop/${ITAD_SHOP_STEAM}/v1?key=${encodeURIComponent(getItadApiKey())}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify([`app/${appid}`]), timeout: 15000, }); const data = JSON.parse(resp); const uuid = data?.[`app/${appid}`] || null; if (uuid) itadCacheSet(cacheKey, uuid); return uuid; } // 单地区 Steam 价格(ITAD prices/v3,仅 Steam 商店) async function itadRegionPrice(uuid, country) { const cacheKey = `price_${uuid}_${country}`; const cached = itadCacheGet(cacheKey); if (cached !== null) return cached; const resp = await gmFetch(`${ITAD_BASE_URL}/games/prices/v3?key=${encodeURIComponent(getItadApiKey())}&country=${country}&shops=${ITAD_SHOP_STEAM}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify([uuid]), timeout: 15000, }); const data = JSON.parse(resp); const item = Array.isArray(data) ? data[0] : null; const deals = item?.deals || []; const steamDeal = deals.find(d => d?.shop?.id === ITAD_SHOP_STEAM) || deals[0] || null; const result = steamDeal ? { price: steamDeal.price?.amount ?? null, currency: steamDeal.price?.currency || '', cut: Number(steamDeal.cut) || 0, shop: steamDeal.shop?.name || 'Steam', } : null; if (result) itadCacheSet(cacheKey, result); return result; } // 降级模式:AugmentedSteam prices/v2 单地区查询 async function augRegionPrice(appid, country) { try { const resp = await gmFetch('https://api.augmentedsteam.com/prices/v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ country, apps: [Number(appid)] }), timeout: 15000, }); const data = JSON.parse(resp); const entry = data?.prices?.[`app/${appid}`] || data?.prices?.[appid]; const current = entry?.current; if (!current?.price) return null; return { price: Number(current.price.amount), currency: String(current.price.currency || '').toUpperCase(), cut: Number(current.cut) || 0, shop: 'Steam', }; } catch { return null; } } // 简单并发池(mapLimit 模式) async function mapLimit(items, limit, fn) { const results = new Array(items.length); let idx = 0; const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { while (idx < items.length) { const i = idx++; results[i] = await fn(items[i], i).catch(() => null); } }); await Promise.all(workers); return results; } // 汇率表(AugmentedSteam rates/v1?to=USD,响应为顶层 { 币种: { USD: 1单位该币=X美元 } },须带 to 参数否则 400),缓存 2 小时 let _usdRates = null; async function fetchUsdRates() { if (_usdRates && _usdRates.expires > Date.now()) return _usdRates.data; try { const resp = await gmFetch('https://api.augmentedsteam.com/rates/v1?to=USD', { timeout: 10000 }); const parsed = JSON.parse(resp); const top = parsed?.data || parsed || {}; const flat = {}; for (const [cur, val] of Object.entries(top)) { const usd = val && typeof val === 'object' ? val.USD : (typeof val === 'number' ? val : null); if (typeof usd === 'number' && usd > 0) flat[cur] = usd; } if (flat.USD === undefined) flat.USD = 1; if (Object.keys(flat).length > 5) { _usdRates = { data: flat, expires: Date.now() + 2 * 60 * 60 * 1000 }; return flat; } } catch {} return null; } // rates 语义:1 单位 currency = rates[currency] USD,即 usd = amount * rate function toUsd(amount, currency, rates) { if (!rates || amount === null || amount === undefined) return null; const cur = String(currency || '').toUpperCase(); if (cur === 'USD') return Math.round(amount * 100) / 100; const rate = rates[cur]; if (typeof rate !== 'number' || rate <= 0) return null; return Math.round(amount * rate * 100) / 100; } function formatRegionPrice(p) { if (!p || p.price === null || p.price === undefined) return '无在售价'; const cur = String(p.currency || '').toUpperCase(); const sym = { CNY: '¥', USD: 'US$', EUR: '€', GBP: '£', TRY: '₺', ARS: 'AR$', RUB: '₽', INR: '₹', BRL: 'R$', UAH: '₴', KZT: '〒', CLP: 'CLP$', COP: 'COL$', PLN: 'zł', MXN: 'MX$', JPY: '¥', KRW: '₩' }[cur]; const amt = (sym === '₩' || sym === '¥' && cur === 'JPY') ? Math.round(p.price) : p.price.toFixed(2); return `${sym || cur + ' '}${amt}${p.cut ? ` (-${p.cut}%)` : ''}`; } ToolRegistry.register({ name: 'getMultiRegionPrices', description: '查询游戏在多个 Steam 地区的当前售价并对比(跨区送礼/差价分析必备)。已配置 ITAD API Key 时为增强模式(16 个地区,数据准确);未配置时自动降级为 AugmentedSteam 基础模式(约 7 个地区,价格准确度降低,返回 degraded=true 需向用户说明)。返回各地区原币价格与最低价地区', parameters: { type: 'object', properties: { appid: { type: 'string', description: 'Steam 游戏 AppID' }, regions: { type: 'array', items: { type: 'string' }, description: '可选,ISO 3166-1 alpha-2 地区代码列表(如 ["CN","US","TR"]),不传则查询全部支持地区' }, }, required: ['appid'] }, category: 'knowledge', async execute(params) { const appid = String(params.appid || '').trim(); if (!appid) return { success: false, error: '缺少 appid 参数' }; const itadKey = getItadApiKey(); const userRegions = Array.isArray(params.regions) && params.regions.length ? params.regions.map(r => String(r).trim().toUpperCase()).filter(r => /^[A-Z]{2}$/.test(r)) : null; let source = 'augmentedsteam'; let regions = userRegions || ITAD_REGIONS_FALLBACK; let regionPrices = {}; if (itadKey) { source = 'itad'; regions = userRegions || ITAD_REGIONS_ENHANCED; try { const uuid = await itadLookupId(appid); if (!uuid) throw new Error('ITAD 未收录该游戏'); const results = await mapLimit(regions, 3, cc => itadRegionPrice(uuid, cc)); regions.forEach((cc, i) => { if (results[i]) regionPrices[cc] = results[i]; else regionPrices[cc] = null; }); } catch (e) { // ITAD 增强失败 → 降级 AugmentedSteam source = 'augmentedsteam'; regions = userRegions || ITAD_REGIONS_FALLBACK; regionPrices = {}; } } if (source === 'augmentedsteam') { const results = await mapLimit(regions, 3, cc => augRegionPrice(appid, cc)); regions.forEach((cc, i) => { regionPrices[cc] = results[i]; }); } // 汇率换算(用于跨币种比较最低价区),失败时保留原币并提示 const rates = await fetchUsdRates(); const rows = Object.entries(regionPrices) .map(([cc, p]) => { const usd = p ? toUsd(p.price, p.currency, rates) : null; return { region: `${cc}(${REGION_NAMES[cc] || cc})`, available: !!p, price: p ? formatRegionPrice(p) : null, currency: p?.currency || null, amount: p?.price ?? null, amountUsd: usd, cut: p?.cut || 0, }; }); // 有换算数据时按 USD 升序(最低价在前),否则保持地区顺序 if (rates) { rows.sort((a, b) => (a.amountUsd === null) - (b.amountUsd === null) || (a.amountUsd ?? 0) - (b.amountUsd ?? 0)); } const validRows = rows.filter(r => r.available); if (!validRows.length) { return { success: true, data: { appid, regions: rows, source, note: '所有查询地区均无在售价记录(可能为新作/免费游戏/未收录)' }, summary: '多地区均无在售价' }; } const cheapest = rates && validRows[0]?.amountUsd !== null && validRows[0]?.amountUsd !== undefined ? { region: validRows[0].region, price: validRows[0].price, currency: validRows[0].currency, amountUsd: validRows[0].amountUsd } : null; const data = { appid, source, sourceLabel: source === 'itad' ? 'ITAD 增强模式(16 地区,价格准确)' : 'AugmentedSteam 降级模式(地区较少,价格准确度降低)', regions: rows, cheapest, ratesAvailable: !!rates, currentStoreRegion: getStoreCountryCode(), note: source === 'itad' ? `价格为各区域 Steam 商店原币售价${rates ? ',amountUsd 为按当前汇率折算的美元参考价(用于跨区比价,15% 送礼规则计算请以此为准)' : ',未能获取汇率,暂无美元折算,跨区差价请用户自行换算'}` : '未配置 ITAD API Key,当前为降级模式:仅覆盖少数地区且部分区域可能查询失败,价格准确度降低。建议在设置面板配置 ITAD API Key(isthereanydeal.com/apps 免费申请)启用 16 地区增强模式', }; if (source !== 'itad') { data.degraded = true; data.needsItadKey = true; } const cheapText = cheapest ? `,USD 折算最低为 ${cheapest.region}(约 $${cheapest.amountUsd})` : ''; const summary = `${source === 'itad' ? 'ITAD 增强' : '降级'}模式查询 ${regions.length} 地区,${validRows.length} 区有价${cheapText}${source !== 'itad' ? ';未配置 ITAD Key,价格准确度降低,建议配置后重试' : ''}`; return { success: true, data, summary }; }, }); function parseSteamProfileXml(xmlText) { try { const get = (tag) => { const m = xmlText.match(new RegExp(`<${tag}>`)); if (m) return m[1]; const m2 = xmlText.match(new RegExp(`<${tag}>([^<]*)`)); return m2 ? m2[1] : ''; }; return { steamID: get('steamID'), steamID64: get('steamID64'), visibility: get('visibilityState'), avatarIcon: get('avatarIcon'), memberSince: get('memberSince'), summary: get('summary'), }; } catch { return null; } } // ============================================================ // ReAct 循环引擎 // ============================================================ function parseToolCalls(response) { const calls = []; const text = String(response || ''); // 宽松标签匹配:兼容 /、大小写、带属性、未闭合截断 const tagRegex = /]*>([\s\S]*?)(?:<\/tool_calls?>|$)/gi; let match; while ((match = tagRegex.exec(text)) !== null) { const call = parseToolCallJson(match[1]); if (call) calls.push(call); } // 兜底:整段响应就是一个裸 JSON(模型直接输出函数调用对象) if (!calls.length) { const stripped = text.replace(/```(?:json)?/gi, '').trim(); if (/^\{[\s\S]*\}$/.test(stripped)) { const call = parseToolCallJson(stripped); if (call) calls.push(call); } } return calls; } // 修复模型常见的 JSON 瑕疵:代码围栏、尾逗号 function repairToolCallJson(raw) { return String(raw || '') .replace(/^[\s\S]*?```(?:json)?\s*/i, m => (m.includes('```') ? '' : m)) .replace(/\s*```[\s\S]*$/, m => (m.includes('```') ? '' : m)) .replace(/,\s*([}\]])/g, '$1') .trim(); } function tryJson(s) { try { return JSON.parse(s); } catch { return null; } } // 生成中途被截断时,按括号计数补齐未闭合的 { [ 和字符串引号 function salvageTruncatedJson(s) { const stack = []; let inStr = false, esc = false; for (const ch of s) { if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '"') { inStr = !inStr; continue; } if (inStr) continue; if (ch === '{' || ch === '[') stack.push(ch); else if (ch === '}' || ch === ']') stack.pop(); } let out = inStr ? s + '"' : s; out = out.replace(/,\s*$/, ''); while (stack.length) out += stack.pop() === '{' ? '}' : ']'; return out; } function parseToolCallJson(raw) { const repaired = repairToolCallJson(raw); let parsed = tryJson(repaired) || tryJson(salvageTruncatedJson(repaired)); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; const name = parsed.name || parsed.tool || (typeof parsed.function === 'object' ? parsed.function?.name : null); if (!name || typeof name !== 'string') return null; let args = parsed.arguments ?? parsed.parameters ?? parsed.args ?? parsed.input ?? {}; if (typeof args === 'string') { const argsRepaired = repairToolCallJson(args); args = tryJson(argsRepaired) || tryJson(salvageTruncatedJson(argsRepaired)) || {}; } if (!args || typeof args !== 'object' || Array.isArray(args)) args = {}; return { name: name.trim(), arguments: args }; } function cleanToolCallMarkers(text) { return String(text || '') .replace(/]*>[\s\S]*?(?:<\/tool_calls?>|$)/gi, '') .replace(/[\s\S]*?<\/tool_result>/g, '') .trim(); } const TOOL_DISPLAY = { getGameDetail: { label: '游戏详情', icon: 'gamepad' }, searchGames: { label: '游戏搜索', icon: 'search' }, getPlayerSummary: { label: '玩家资料', icon: 'stats' }, getOwnedGames: { label: '游戏库查询', icon: 'gamepad' }, getSpecialDeals: { label: '特惠查询', icon: 'search' }, getOfficialDeals: { label: '官方特惠动态', icon: 'search' }, getGameNews: { label: '游戏动态', icon: 'news' }, searchDiscountedGames: { label: '折扣检索', icon: 'search' }, getWishlistDeals: { label: '愿望单折扣', icon: 'gamepad' }, getPriceHistory: { label: '价格历史', icon: 'stats' }, getMultiRegionPrices: { label: '跨区比价', icon: 'globe' }, }; function getToolDisplay(name) { return TOOL_DISPLAY[name] || { label: name, icon: 'cpu' }; } function formatToolArgs(args = {}) { const values = Object.values(args).filter(v => v !== undefined && v !== null && v !== ''); if (!values.length) return ''; return values.slice(0, 2).map(v => String(v).slice(0, 24)).join(' · '); } function renderToolChip(name, args, running, failed) { const d = getToolDisplay(name); const argText = formatToolArgs(args); return `
    ${getIcon(d.icon, 12)} ${escapeHtml(d.label)} ${argText ? `${escapeHtml(argText)}` : ''} ${running ? '' : failed ? getIcon('close', 11) : getIcon('check', 11)}
    `; } function buildToolCallsHtml(toolCalls, runningIndex = -1) { if (!toolCalls || !toolCalls.length) return ''; const needsKey = toolCalls.some(tc => tc && tc.result && tc.result.needsApiKey); const keyAction = needsKey ? `
    steamcommunity.com/dev 免费申请 · 配置后回复「已配置」继续
    ` : ''; const needsItad = toolCalls.some(tc => tc && tc.result && tc.result.needsItadKey); const itadAction = needsItad ? `
    isthereanydeal.com/apps 免费申请 · 配置后跨区比价升级为增强模式
    ` : ''; return `
    ${toolCalls.map((tc, i) => renderToolChip(tc.name, tc.arguments, i === runningIndex, !!(tc.result && tc.result.success === false)) ).join('')}${keyAction}${itadAction}
    `; } function buildSystemInstruction(agent) { const parts = [agent.systemPrompt]; const rulesCtx = RulesKnowledgeProvider.formatAsContext(agent.id); if (rulesCtx) parts.push(rulesCtx); const kContext = AgentKnowledgeManager.formatAsContext(agent.id); if (kContext) parts.push(kContext); return parts.join('\n\n'); } // ============================================================ // 多轮对话上下文管理(智能窗口 + 完整性验证) // 目标:长会话/大工具结果不丢失关键上下文,也不撑爆模型输入 // ============================================================ const CONTEXT_HISTORY_BUDGET = 9000; // 历史区域字符预算 const CONTEXT_MSG_MAX_CHARS = 1600; // 单条普通消息上限 const CONTEXT_TOOL_RESULT_MAX_CHARS = 2600; // 单条工具结果上限 const CONTEXT_ANCHOR_MAX_CHARS = 300; // 首条用户消息(主题锚点)上限 function truncateMiddle(text, maxLen) { const s = String(text || ''); if (s.length <= maxLen) return s; const head = Math.ceil(maxLen * 0.6); const tail = Math.floor(maxLen * 0.3); return s.slice(0, head) + `\n…(中间省略约 ${s.length - head - tail} 字符)…\n` + s.slice(-tail); } function dedupeHistory(history) { const out = []; for (const m of history) { const prev = out[out.length - 1]; if (prev && prev.role === m.role && (prev.content || '') === (m.content || '')) continue; out.push(m); } return out; } // 构建上下文窗口:预算内保留尽可能多的最近消息 + 首条用户消息主题锚点 function buildContextWindow(history) { const cleaned = dedupeHistory(history || []); if (!cleaned.length) return { messages: [], anchor: null, omittedCount: 0 }; const items = cleaned.map(m => { const isToolResult = m.role === 'system' && String(m.content || '').includes('tool_result'); const limit = isToolResult ? CONTEXT_TOOL_RESULT_MAX_CHARS : CONTEXT_MSG_MAX_CHARS; return { role: m.role, content: truncateMiddle(String(m.content || ''), limit), rawRef: m }; }); const picked = []; let used = 0; for (let i = items.length - 1; i >= 0; i--) { const len = (items[i].content || '').length + 8; if (picked.length && used + len > CONTEXT_HISTORY_BUDGET) break; picked.unshift(items[i]); used += len; } // 边界对齐:窗口不以 assistant 开头,避免回答缺失对应问题 while (picked.length && picked[0].role === 'assistant') picked.shift(); // 工具结果收敛:仅保留窗口内最后 2 条完整工具结果,更早的替换为占位符 let toolKept = 0; for (let i = picked.length - 1; i >= 0; i--) { const isTool = picked[i].role === 'system' && picked[i].content.includes('tool_result'); if (!isTool) continue; toolKept++; if (toolKept > 2) picked[i].content = '(更早的工具结果已省略以节省上下文)'; } // 主题锚点:首条用户消息未进入窗口时单独注入(截断) const firstUser = items.find(m => m.role === 'user'); let anchor = null; if (firstUser && !picked.includes(firstUser)) { anchor = truncateMiddle(firstUser.content, CONTEXT_ANCHOR_MAX_CHARS); } return { messages: picked, anchor, omittedCount: cleaned.length - picked.length }; } // 上下文完整性验证:当前用户消息必须在 prompt 中,历史窗口结构与预期一致 function verifyContextIntegrity(prompt, userMessage, ctxWindow) { const issues = []; if (!prompt || typeof prompt !== 'string') issues.push('prompt 为空'); if (!String(userMessage || '').trim() || !prompt.includes(userMessage)) issues.push('当前用户消息未注入 prompt'); if (!prompt.includes('## 当前任务')) issues.push('缺少当前任务区块'); if (ctxWindow && ctxWindow.messages.length && !prompt.includes('## 对话历史')) issues.push('历史窗口未渲染'); return issues; } function buildReActPrompt(userMessage, history, toolsDesc, currentIter, maxIter) { const ctx = buildContextWindow(history); const historyText = ctx.messages .filter(m => m.role !== 'system' || !m.content.includes('tool_result')) .map(m => { const label = m.role === 'user' ? '用户' : m.role === 'assistant' ? '助手' : '系统'; let content = m.content; if (m.role === 'assistant') content = cleanToolCallMarkers(content); return `${label}: ${content}`; }).join('\n'); const toolHistory = ctx.messages .filter(m => m.role === 'system' && m.content.includes('tool_result')) .map(m => m.content).join('\n'); const contextNotes = []; if (ctx.anchor) contextNotes.push(`本轮会话最初的话题:「${ctx.anchor}」(较早的中间历史已省略,回答时以此为背景延续话题,如需中间细节可请用户补充)`); if (ctx.omittedCount > 0) contextNotes.push(`已省略 ${ctx.omittedCount} 条较早的历史消息以控制上下文长度,最近的消息均完整保留`); contextNotes.push(`本窗口共包含 ${ctx.messages.length} 条历史消息${history.length > ctx.messages.length ? '' : '(完整历史,无裁剪)'}`); let prompt = `(提示:你的角色设定已在系统指令中给出,请严格遵循。) ## 可用工具 ${toolsDesc || '(无可用工具)'} ## 对话历史 ${historyText || '(无历史记录)'} ${toolHistory ? '## 工具调用结果\n' + toolHistory + '\n' : ''}${contextNotes.length ? `## 上下文说明\n${contextNotes.map(t => '- ' + t).join('\n')}\n\n` : ''}## 当前任务 用户消息: ${userMessage} ## 执行指令 你正在执行第 ${currentIter}/${maxIter} 次迭代。 在回答之前,请先思考是否需要使用工具: - 如果需要调用工具,必须严格使用以下格式(一段完整合法的 JSON,必须以
    闭合): {"name":"工具名称","arguments":{"字符串参数":"值","数值参数":123}} - 如果不需要工具,直接回答用户问题 工具调用格式要求: 1. JSON 必须合法:键与字符串值一律用双引号,无注释、无尾逗号,不要用 markdown 代码围栏(\`\`\`)包裹 2. 参数对象的键名固定为 "arguments",不要写成 parameters/args/input 3. 每次迭代最多调用一个工具;决定调用时只输出一段 ...,不要附加任何其它文本 4. 工具调用完成后,系统会把结果加入对话历史,你再继续下一轮 5. 只有获得所有必要信息后才给出最终回答 6. 若收到 formatError 的工具结果,说明你上一次的调用格式有误,请严格按上述格式重新输出一次 其他规则: 7. 涉及打折/特惠/促销/价格/"值得买"等平台级问题时,必须优先调用实时数据工具(getOfficialDeals/getSpecialDeals/searchDiscountedGames/getWishlistDeals/getPriceHistory/getMultiRegionPrices),禁止凭训练记忆虚构当前价格或声称"无法获取实时折扣数据" 8. 用户问及某个具体游戏(如 dota2、cs2、绝地求生、艾尔登法环等)的最新动态/版本更新/平衡补丁/官方公告/运营活动时,必须调用 getGameNews(传 game=游戏名或简称,未知 AppID 时可先用 searchGames 确认),禁止凭训练记忆编造版本号或更新内容;若结果为空则如实告知该游戏近期无官方动态 9. 若工具结果包含 "needsApiKey": true,表示需要 Steam Web API Key:请在回答中引导用户点击对话中的「配置 Steam API Key」按钮(或在设置面板填写 Key,steamcommunity.com/dev 可免费申请),并告知配置完成后回复"已配置",届时重新调用该工具即可。注意:若用户已回复"已配置",直接重新调用工具,不要重复引导配置 10. 查询玩家自身数据("我的游戏库/我的资料/我的愿望单"等)时不要向用户索要 SteamID:直接调用工具即可,工具会自动使用设置中配置或当前登录用户的身份;仅当工具明确返回"未提供也未检测到 SteamID"的错误时,才引导用户在设置中心「接口服务」配置 SteamID 或直接提供 11. 跨区送礼/多地区比价问题优先调用 getMultiRegionPrices 获取多地区实时价格;若其结果标记 degraded=true(未配置 ITAD Key 的降级模式),须在回答中提示用户价格准确度降低,并引导在设置中配置 ITAD API Key 以启用增强模式 12. 工具返回 success:false 时(Key 未配置、超时、参数错误等),不要机械重复同一调用:向用户简述失败原因并给出下一步建议(配置 Key、缩小检索范围、稍后再试),或改用其他可用工具完成任务 13. 多轮对话中若上下文说明提示"中间历史已省略",不要臆测被省略的内容,可结合话题锚点回答或请用户确认 14. 最终回答使用中文,使用 Markdown 格式`; const issues = verifyContextIntegrity(prompt, userMessage, ctx); if (issues.length) { console.warn('[SA-Agent] 上下文完整性验证未通过,降级为仅当前消息:', issues); prompt = prompt.replace(/## 对话历史\n[\s\S]*?(?=\n## 工具调用结果|\n## 上下文说明|\n## 当前任务)/, '## 对话历史\n(上下文异常,已重置,请基于当前消息回答)'); const finalCheck = prompt.includes(userMessage); if (!finalCheck) { prompt = `${prompt}\n(系统注:用户消息即为本轮唯一上下文)`; } } return prompt; } async function runAgentLoop(userMessage, history, agentId, onChunk, onToolCall) { const agent = AgentManager.getAgent(agentId); if (!agent) return { response: '未找到智能体', completed: false, iterations: 0 }; const toolsDesc = ToolRegistry.getToolsDescription(); const systemInstruction = buildSystemInstruction(agent); const parsedSettings = parseStored(GM_getValue(SK.SETTINGS, null), {}); const maxIter = parsedSettings.maxIterations || CONFIG.MAX_ITERATIONS; let currentHistory = [...history]; const toolCallLog = []; for (let i = 0; i < maxIter; i++) { if (abortFlag) { const lastAi = [...currentHistory].reverse().find(m => m.role === 'assistant'); return { response: lastAi ? cleanToolCallMarkers(lastAi.content) : '', completed: false, aborted: true, iterations: i, toolCalls: toolCallLog }; } const prompt = buildReActPrompt(userMessage, currentHistory, toolsDesc, i + 1, maxIter); let aiResponse; try { if (i === 0 && onChunk) { aiResponse = await callAiApiWithTyping(prompt, { systemInstruction, history: [], temperature: 0.7 }, onChunk); } else { aiResponse = await callAiApi(prompt, { systemInstruction, history: [], temperature: 0.7 }); } } catch (e) { if (abortFlag) { const lastAi = [...currentHistory].reverse().find(m => m.role === 'assistant'); return { response: lastAi ? cleanToolCallMarkers(lastAi.content) : '', completed: false, aborted: true, iterations: i + 1, toolCalls: toolCallLog }; } return { response: `AI 调用失败: ${e.message}`, completed: false, iterations: i + 1, error: e.message }; } if (abortFlag) { const partial = cleanToolCallMarkers(aiResponse || ''); return { response: partial, completed: false, aborted: true, iterations: i + 1, toolCalls: toolCallLog }; } currentHistory.push({ id: `ai_${Date.now()}`, role: 'assistant', content: aiResponse, timestamp: Date.now() }); const toolCalls = parseToolCalls(aiResponse); if (toolCalls.length === 0) { const cleanResponse = cleanToolCallMarkers(aiResponse); const attemptedToolCall = /${JSON.stringify({ success: false, formatError: true, error: '上一次输出包含工具调用标签,但内容不是合法的工具调用(JSON 解析失败、缺少 name 或标签未闭合)。请重新输出一段合法调用:仅输出 {"name":"工具名称","arguments":{"参数名":"值"}},JSON 用双引号、键名固定为 arguments、以 闭合、不要附加其它文本;若已无需调用工具,请直接给出最终回答', })}`, timestamp: Date.now(), }); console.warn('[SA-Agent] 工具调用格式解析失败,已回灌纠错提示:', String(aiResponse).slice(0, 200)); continue; } const finalResponse = cleanResponse.trim() || '(本轮未能生成有效回复:工具调用格式异常,请重试或换个问法)'; return { response: finalResponse, completed: true, iterations: i + 1, toolCalls: toolCallLog }; } const toolCall = toolCalls[0]; if (onChunk) onChunk(''); if (onToolCall) onToolCall(toolCall.name, toolCall.arguments, 'start'); const toolStatusEl = document.querySelector('.sa-tool-status'); if (toolStatusEl) { toolStatusEl.querySelector('.sa-tool-text').textContent = `正在调用智能体工具 · ${getToolDisplay(toolCall.name).label}…`; toolStatusEl.classList.add('sa-active'); } const result = await ToolRegistry.execute(toolCall.name, toolCall.arguments); toolCallLog.push({ name: toolCall.name, arguments: toolCall.arguments, result }); if (onToolCall) onToolCall(toolCall.name, toolCall.arguments, 'done'); if (toolStatusEl) toolStatusEl.classList.remove('sa-active'); currentHistory.push({ id: `tool_${Date.now()}`, role: 'system', content: `${JSON.stringify(result)}`, timestamp: Date.now(), }); } return { response: '抱歉,处理该问题需要更多步骤,请尝试缩小问题范围。', completed: false, iterations: maxIter, toolCalls: toolCallLog }; } // ============================================================ // 对话管理器 (多对话历史支持) // ============================================================ const ConversationManager = { _conversations: {}, _currentConvId: null, init() { try { this._conversations = JSON.parse(GM_getValue(SK.CONVERSATIONS_V2, '{}')); } catch { this._conversations = {}; } this._currentConvId = GM_getValue(SK.CURRENT_CONV, null); this._migrateOldConversations(); if (!this._currentConvId || !this._conversations[this._currentConvId]) { this._currentConvId = null; } }, _migrateOldConversations() { try { const oldData = JSON.parse(GM_getValue(SK.CONVERSATIONS, '{}')); if (Object.keys(oldData).length === 0) return; let migrated = false; for (const [agentId, session] of Object.entries(oldData)) { if (session.messages && session.messages.length > 0) { const convId = `conv_migrated_${agentId}`; if (!this._conversations[convId]) { const firstMsg = session.messages.find(m => m.role === 'user'); this._conversations[convId] = { id: convId, title: firstMsg?.content?.slice(0, 30) || '导入的对话', agentId, modelId: null, messages: session.messages, createdAt: session.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), }; migrated = true; } } } if (migrated) this.save(); } catch {} }, createConversation(agentId) { const convId = `conv_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; this._conversations[convId] = { id: convId, title: '新对话', agentId: agentId || AgentManager._currentAgentId, modelId: ModelManager.getActiveModelId(), messages: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; this._currentConvId = convId; GM_setValue(SK.CURRENT_CONV, convId); this.save(); return this._conversations[convId]; }, getCurrentConversation() { if (!this._currentConvId || !this._conversations[this._currentConvId]) { return this.createConversation(AgentManager._currentAgentId); } return this._conversations[this._currentConvId]; }, getCurrentId() { return this._currentConvId; }, getConversation(id) { return this._conversations[id] || null; }, getAllConversations() { return Object.values(this._conversations).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) ); }, switchTo(id) { if (!this._conversations[id]) return false; this._currentConvId = id; GM_setValue(SK.CURRENT_CONV, id); const conv = this._conversations[id]; if (conv.agentId) { AgentManager.switchAgent(conv.agentId); } if (conv.modelId && ModelManager.getModel(conv.modelId)) { ModelManager.setActiveModel(conv.modelId); } return true; }, deleteConversation(id) { delete this._conversations[id]; if (this._currentConvId === id) { this._currentConvId = null; GM_setValue(SK.CURRENT_CONV, ''); } this.save(); }, renameConversation(id, title) { if (!this._conversations[id]) return false; this._conversations[id].title = title; this._conversations[id].updatedAt = new Date().toISOString(); this.save(); return true; }, addMessage(role, content, extra = {}) { const conv = this.getCurrentConversation(); const msg = { id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, role, content, timestamp: Date.now(), ...extra, }; conv.messages.push(msg); conv.updatedAt = new Date().toISOString(); if (conv.title === '新对话' && role === 'user') { conv.title = content.slice(0, 30) + (content.length > 30 ? '...' : ''); } this.save(); return msg; }, getCurrentMessages() { const conv = this.getCurrentConversation(); return conv.messages; }, clearCurrent() { const conv = this.getCurrentConversation(); conv.messages = []; conv.title = '新对话'; conv.updatedAt = new Date().toISOString(); this.save(); }, updateCurrentModel(modelId) { const conv = this.getCurrentConversation(); conv.modelId = modelId; this.save(); }, save() { try { GM_setValue(SK.CONVERSATIONS_V2, JSON.stringify(this._conversations)); } catch (e) { console.error('[SA-Agent] 对话保存失败:', e); toast('对话保存失败,可能超出存储配额,请导出并清理旧对话', 'error'); } }, exportAsJson(convId) { const conv = this.getConversation(convId) || this.getCurrentConversation(); const agent = AgentManager.getAgent(conv.agentId); const model = ModelManager.getModel(conv.modelId); const data = { conversation: conv.title, agent: agent?.name || conv.agentId, agentId: conv.agentId, model: model?.name || conv.modelId, modelId: conv.modelId, createdAt: conv.createdAt, exportedAt: new Date().toISOString(), messages: conv.messages.map(m => ({ role: m.role, content: m.content, timestamp: new Date(m.timestamp).toISOString(), toolCalls: m.toolCalls || [], })), }; downloadFile(JSON.stringify(data, null, 2), `steam-chat-${conv.id}-${Date.now()}.json`); }, exportAsMarkdown(convId) { const conv = this.getConversation(convId) || this.getCurrentConversation(); const agent = AgentManager.getAgent(conv.agentId); const lines = [ `# Steam AI 对话记录`, ``, `**智能体:** ${agent?.name || conv.agentId}`, `**创建时间:** ${conv.createdAt}`, `**导出时间:** ${new Date().toISOString()}`, `---`, ``, ]; for (const m of conv.messages) { if (m.role === 'user') { lines.push(`## 用户`, ``, m.content, ``); } else if (m.role === 'assistant') { lines.push(`## ${agent?.name || 'AI'}`, ``, cleanToolCallMarkers(m.content), ``); } else if (m.role === 'system' && m.content.includes('tool_result')) { lines.push(`> 工具调用结果`, ``, m.content, ``); } } downloadFile(lines.join('\n'), `steam-chat-${conv.id}-${Date.now()}.md`); }, }; // ============================================================ // 事件总线 // ============================================================ const EventBus = { _handlers: {}, on(event, cb) { if (!this._handlers[event]) this._handlers[event] = []; this._handlers[event].push(cb); }, off(event, cb) { if (!this._handlers[event]) return; this._handlers[event] = this._handlers[event].filter(h => h !== cb); }, emit(event, data) { if (!this._handlers[event]) return; for (const cb of this._handlers[event]) { try { cb(data); } catch (e) { console.warn('[SA-Agent] Event handler error:', e); } } }, }; // ============================================================ // UI 状态 // ============================================================ let panelEl = null; let fabEl = null; let messagesEl = null; let inputEl = null; let sendBtn = null; let modelSelectEl = null; let historyPanelEl = null; let isProcessing = false; let abortFlag = false; let activeAiRequest = null; let prefillMessage = null; let panelSide = 'right'; // ============================================================ // UI: 浮动按钮 // ============================================================ const FAB_ICON = ``; function createFloatingButton() { if (fabEl) { if (!document.contains(fabEl) && document.body) document.body.appendChild(fabEl); return; } if (!document.body) return; fabEl = document.createElement('div'); fabEl.id = 'sa-fab'; fabEl.innerHTML = FAB_ICON + '
    '; document.body.appendChild(fabEl); const pos = parseStored(GM_getValue(SK.FAB_POS, null), null); if (pos && pos.side) { panelSide = pos.side === 'left' ? 'left' : 'right'; if (pos.side === 'left') { fabEl.style.left = '0px'; fabEl.style.top = (pos.y || 200) + 'px'; fabEl.style.right = 'auto'; fabEl.style.bottom = 'auto'; } else { fabEl.style.right = '0px'; fabEl.style.top = (pos.y || 200) + 'px'; fabEl.style.left = 'auto'; fabEl.style.bottom = 'auto'; } } else { fabEl.style.bottom = '80px'; fabEl.style.right = '24px'; } const cfg = getAiConfig(); if (cfg.apiKey) fabEl.classList.add('sa-has-key'); makeFabDraggable(fabEl); } function makeFabDraggable(fab) { let isDragging = false, hasMoved = false, startX, startY, origX, origY; const onDown = (e) => { isDragging = true; hasMoved = false; const pt = e.touches ? e.touches[0] : e; startX = pt.clientX; startY = pt.clientY; const rect = fab.getBoundingClientRect(); origX = rect.left; origY = rect.top; e.preventDefault(); e.stopPropagation(); }; const onMove = (e) => { if (!isDragging) return; const pt = e.touches ? e.touches[0] : e; const dx = pt.clientX - startX, dy = pt.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true; fab.style.left = (origX + dx) + 'px'; fab.style.top = Math.max(0, Math.min(window.innerHeight - 52, origY + dy)) + 'px'; fab.style.right = 'auto'; fab.style.bottom = 'auto'; }; const onUp = () => { if (!isDragging) return; isDragging = false; if (hasMoved) { const rect = fab.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const winCenterX = window.innerWidth / 2; const newY = Math.max(0, Math.min(window.innerHeight - 52, rect.top)); if (centerX < winCenterX) { fab.style.left = '0px'; fab.style.right = 'auto'; panelSide = 'left'; } else { fab.style.right = '0px'; fab.style.left = 'auto'; panelSide = 'right'; } fab.style.top = newY + 'px'; GM_setValue(SK.FAB_POS, JSON.stringify({ side: panelSide, y: newY })); GM_setValue(SK.PANEL_SIDE, panelSide); if (panelEl) { if (panelSide === 'left') panelEl.classList.add('sa-left-side'); else panelEl.classList.remove('sa-left-side'); } } else { toggleSidebar(); } }; fab.addEventListener('mousedown', onDown); fab.addEventListener('touchstart', onDown, { passive: false }); document.addEventListener('mousemove', onMove); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('mouseup', onUp); document.addEventListener('touchend', onUp); } // ============================================================ // UI: 侧边栏 // ============================================================ function createSidebar() { if (panelEl) return; panelEl = document.createElement('div'); panelEl.id = 'sa-panel'; if (panelSide === 'left') panelEl.classList.add('sa-left-side'); panelEl.innerHTML = `
    ${getIcon('sparkle', 13)} 正在思考…
    ${getIcon('sparkle', 13)}
    `; document.body.appendChild(panelEl); messagesEl = panelEl.querySelector('#sa-messages'); inputEl = panelEl.querySelector('#sa-input'); sendBtn = panelEl.querySelector('#sa-send-btn'); modelSelectEl = panelEl.querySelector('#sa-model-select'); historyPanelEl = panelEl.querySelector('#sa-history-panel'); const select = panelEl.querySelector('#sa-agent-select'); function refreshAgentSelect() { const agents = AgentManager.getAgents(); select.innerHTML = agents.map(a => `` ).join(''); } refreshAgentSelect(); select.addEventListener('change', () => { AgentManager.switchAgent(select.value); ConversationManager.getCurrentConversation().agentId = select.value; ConversationManager.save(); renderMessages(); }); panelEl.querySelector('#sa-btn-history').addEventListener('click', () => toggleHistoryPanel()); panelEl.querySelector('#sa-btn-new').addEventListener('click', () => { ConversationManager.createConversation(AgentManager._currentAgentId); renderMessages(); toast('已新建会话', 'success'); }); panelEl.querySelector('#sa-btn-export').addEventListener('click', () => { const conv = ConversationManager.getCurrentConversation(); if (!conv.messages.length) { toast('暂无对话记录', 'error'); return; } openSettings({ tab: 'data' }); }); panelEl.querySelector('#sa-btn-settings').addEventListener('click', () => openSettings()); panelEl.querySelector('#sa-btn-close').addEventListener('click', () => closeSidebar()); inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); inputEl.addEventListener('input', () => { inputEl.style.height = 'auto'; inputEl.style.height = Math.min(120, inputEl.scrollHeight) + 'px'; }); sendBtn.addEventListener('click', handleSend); panelEl.addEventListener('click', (e) => { const keyBtn = e.target.closest('.sa-key-btn'); if (keyBtn) { openSettings({ tab: 'api', focus: '#sa-cfg-steamkey' }); return; } const itadBtn = e.target.closest('.sa-itadkey-btn'); if (itadBtn) { openSettings({ tab: 'api', focus: '#sa-cfg-itadkey' }); return; } const copyBtn = e.target.closest('.sa-msg-copy'); if (copyBtn) { const content = msgCopyRegistry[Number(copyBtn.dataset.copyId)]; if (!content) { toast('未找到可复制的消息内容', 'error'); return; } copyTextToClipboard(content).then(ok => { if (ok) { copyBtn.classList.add('sa-copied'); copyBtn.innerHTML = `${getIcon('check', 11)} 已复制`; toast('回复已复制到剪贴板', 'success'); setTimeout(() => { copyBtn.classList.remove('sa-copied'); copyBtn.innerHTML = `${getIcon('copy', 11)} 复制`; }, 1500); } else { toast('复制失败,请手动选择复制', 'error'); } }); } }); refreshModelSelector(); modelSelectEl.addEventListener('change', () => { const modelId = modelSelectEl.value; if (modelId) { ModelManager.setActiveModel(modelId); ConversationManager.updateCurrentModel(modelId); toast(`已切换到 ${ModelManager.getModel(modelId)?.name}`, 'success'); } }); panelEl._refreshAgentSelect = refreshAgentSelect; panelEl._refreshModelSelector = refreshModelSelector; renderMessages(); } function refreshModelSelector() { if (!modelSelectEl) return; const models = ModelManager.getModels(); const activeId = ModelManager.getActiveModelId(); if (models.length === 0) { modelSelectEl.innerHTML = ``; modelSelectEl.style.color = '#f87171'; } else { modelSelectEl.style.color = ''; modelSelectEl.innerHTML = models.map(m => { const provider = MODEL_PROVIDERS[m.provider]; const providerName = provider?.name || '自定义'; return ``; }).join(''); } } // ============================================================ // UI: 对话历史面板 // ============================================================ function toggleHistoryPanel() { if (!historyPanelEl) return; if (historyPanelEl.classList.contains('sa-show')) { historyPanelEl.classList.remove('sa-show'); } else { renderHistoryPanel(); historyPanelEl.classList.add('sa-show'); } } function renderHistoryPanel() { if (!historyPanelEl) return; const conversations = ConversationManager.getAllConversations(); const currentId = ConversationManager.getCurrentId(); historyPanelEl.innerHTML = `

    ${getIcon('history', 15)} 对话历史

    ${conversations.length === 0 ? `
    暂无对话记录
    点击 + 开始新对话
    ` : conversations.map(conv => { const agent = AgentManager.getAgent(conv.agentId); const model = ModelManager.getModel(conv.modelId); const provider = model ? MODEL_PROVIDERS[model.provider] : null; const msgCount = conv.messages.filter(m => m.role === 'user' || m.role === 'assistant').length; return `
    ${escapeHtml(conv.title)}
    ${agent ? `${agentIconHtml(agent, 11)} ${escapeHtml(agent.name)}` : ''} ${provider ? `${providerIconHtml(model.provider, 11)} ${escapeHtml(model.name)}` : ''} ${msgCount} 条 ${formatRelativeTime(new Date(conv.updatedAt).getTime())}
    `; }).join('')}
    `; historyPanelEl.querySelector('#sa-history-new').addEventListener('click', () => { ConversationManager.createConversation(AgentManager._currentAgentId); renderMessages(); renderHistoryPanel(); historyPanelEl.classList.remove('sa-show'); toast('已新建会话', 'success'); }); historyPanelEl.querySelector('#sa-history-close').addEventListener('click', () => { historyPanelEl.classList.remove('sa-show'); }); historyPanelEl.querySelectorAll('.sa-history-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.closest('.sa-history-del')) return; const id = item.dataset.id; if (ConversationManager.switchTo(id)) { renderMessages(); refreshModelSelector(); if (panelEl._refreshAgentSelect) panelEl._refreshAgentSelect(); historyPanelEl.classList.remove('sa-show'); toast('已切换对话', 'success'); } }); }); historyPanelEl.querySelectorAll('.sa-history-del').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const id = btn.dataset.id; const conv = ConversationManager.getConversation(id); if (confirm(`确认删除对话 "${conv?.title || ''}"?`)) { const wasCurrent = id === ConversationManager.getCurrentId(); ConversationManager.deleteConversation(id); if (wasCurrent) { renderMessages(); refreshModelSelector(); } renderHistoryPanel(); toast('已删除', 'success'); } }); }); } // ============================================================ // UI: 侧边栏控制 // ============================================================ function openSidebar(options = {}) { if (!panelEl) createSidebar(); panelEl.classList.add('sa-open'); if (options.agentId) { AgentManager.switchAgent(options.agentId); if (panelEl._refreshAgentSelect) panelEl._refreshAgentSelect(); } renderMessages(); if (options.message) { prefillMessage = options.message; inputEl.value = options.message; inputEl.focus(); } else { setTimeout(() => inputEl?.focus(), 100); } EventBus.emit('open', { agentId: options.agentId, message: options.message }); } function closeSidebar() { if (panelEl) panelEl.classList.remove('sa-open'); if (historyPanelEl) historyPanelEl.classList.remove('sa-show'); EventBus.emit('close', {}); } function toggleSidebar() { if (panelEl?.classList.contains('sa-open')) closeSidebar(); else openSidebar(); } // ============================================================ // UI: 欢迎屏幕数据 // ============================================================ const SUGGESTED_QUESTIONS = [ '帮我分析一下 Steam 上最近有什么值得入手的打折游戏', '如何评价《赛博朋克2077》现在的状态,值得买吗?', '我预算200元,推荐几款性价比高的 Steam 游戏', 'Steam 好评如潮的游戏有哪些?帮我筛选一下', '帮我查一下 AppID 570 (Dota 2) 的游戏信息', 'Steam 打折什么时候力度最大?特卖时间表是什么?', '怎么判断一个游戏是不是史低价?', '推荐一些适合和朋友联机的 Steam 游戏', 'Steam 退款政策是什么?怎么申请退款?', '帮我分析一下我的 Steam 游戏库,哪些游戏吃灰了', '跨区送礼有什么注意事项?哪个区最划算?', 'Steam 上的独立游戏有什么值得推荐的?', ]; function getGreeting() { const hour = new Date().getHours(); if (hour < 6) return '夜深了'; if (hour < 9) return '早上好'; if (hour < 12) return '上午好'; if (hour < 14) return '中午好'; if (hour < 18) return '下午好'; if (hour < 22) return '晚上好'; return '夜深了'; } function getChatStats() { const conversations = ConversationManager.getAllConversations(); let totalMessages = 0; for (const conv of conversations) { totalMessages += conv.messages.filter(m => m.role === 'user' || m.role === 'assistant').length; } const tokenStats = TokenStatsManager.getStats(); return { totalConversations: conversations.length, totalMessages, tokenStats, }; } function getSuggestedQuestions(count = 5) { const shuffled = [...SUGGESTED_QUESTIONS].sort(() => Math.random() - 0.5); return shuffled.slice(0, count); } function renderWelcomeScreen() { const agent = AgentManager.getCurrentAgent(); const stats = getChatStats(); const questions = getSuggestedQuestions(5); const ts = stats.tokenStats; const tokenTitle = `Token 消耗统计\n总计: ${ts.totalTokens}\n输入: ${ts.totalPromptTokens} / 输出: ${ts.totalCompletionTokens}\nAI 请求数: ${ts.requestCount}\n注: 优先采用 API 返回的精确 usage,无 usage 时按字符估算`; messagesEl.innerHTML = `

    ${getGreeting()}${getIcon('sparkle', 18)}

    我是 ${escapeHtml(agent?.name || 'AI 助手')},${escapeHtml(agent?.description || '有什么可以帮你的吗?')}

    ${getIcon('chat', 20)}
    ${stats.totalConversations}
    累计会话
    ${getIcon('messageSquare', 20)}
    ${stats.totalMessages}
    消息总数
    ${getIcon('zap', 20)}
    ${escapeHtml(formatTokenCount(stats.tokenStats.totalTokens))}
    Token消耗
    ${agentIconHtml(agent, 20)}
    ${getIcon('sparkle', 12)} 当前智能体
    ${escapeHtml(agent?.name || 'AI 助手')}
    ${escapeHtml(agent?.description || '')}
    ${getIcon('lightbulb', 14)} 建议问题
    ${questions.map(q => `
    ${escapeHtml(q)}
    `).join('')}
    `; const refreshBtn = messagesEl.querySelector('#sa-sug-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { refreshBtn.classList.add('sa-spin'); setTimeout(() => refreshBtn.classList.remove('sa-spin'), 600); const newQuestions = getSuggestedQuestions(5); const listEl = messagesEl.querySelector('#sa-sug-list'); if (listEl) { listEl.innerHTML = newQuestions.map(q => `
    ${escapeHtml(q)}
    `).join(''); bindSuggestionClicks(); } }); } function bindSuggestionClicks() { messagesEl.querySelectorAll('.sa-suggestion-item').forEach(item => { item.addEventListener('click', () => { const q = item.dataset.question; if (q && inputEl) { if (isProcessing) { toast('AI 正在回复中,请等待完成或点击停止按钮', 'error'); return; } inputEl.value = q; inputEl.focus(); handleSend(); } }); }); } bindSuggestionClicks(); } // ============================================================ // UI: 消息渲染 // ============================================================ // 复制源注册表:渲染/追加 AI 消息时登记 Markdown 原文,按钮通过 data-copy-id 取回 let msgCopyRegistry = []; function buildMsgFooterHtml(content) { if (!content) return ''; const id = msgCopyRegistry.push(content) - 1; return ``; } function appendMsgFooterEl(msgEl, content) { if (!msgEl || !content) return; const wrap = document.createElement('div'); wrap.innerHTML = buildMsgFooterHtml(content).trim(); const footer = wrap.firstElementChild; if (footer) msgEl.appendChild(footer); } async function copyTextToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch { try { const tmp = document.createElement('textarea'); tmp.value = text; tmp.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;'; document.body.appendChild(tmp); tmp.select(); const ok = document.execCommand('copy'); tmp.remove(); return ok; } catch { return false; } } } function renderMessages() { if (!messagesEl) return; const conv = ConversationManager.getCurrentConversation(); if (!conv.messages.length) { renderWelcomeScreen(); return; } msgCopyRegistry = []; messagesEl.innerHTML = conv.messages.map(m => { if (m.role === 'user') { return `
    ${escapeHtml(m.content)}
    `; } else if (m.role === 'assistant') { const clean = cleanToolCallMarkers(m.content); const toolsHtml = buildToolCallsHtml(m.toolCalls); const bodyHtml = clean ? renderMarkdown(clean) : ''; return `
    ${toolsHtml}${bodyHtml}${buildMsgFooterHtml(clean)}
    `; } else if (m.role === 'system') { return ''; } return ''; }).join(''); messagesEl.scrollTop = messagesEl.scrollHeight; } function appendMessageEl(role, content) { if (!messagesEl) return; const welcome = messagesEl.querySelector('.sa-welcome'); if (welcome) welcome.remove(); const empty = messagesEl.querySelector('.sa-empty'); if (empty) empty.remove(); const msgEl = document.createElement('div'); if (role === 'user') { msgEl.className = 'sa-msg sa-msg-user'; msgEl.textContent = content; } else { msgEl.className = 'sa-msg sa-msg-ai'; msgEl.innerHTML = renderMarkdown(cleanToolCallMarkers(content)); } messagesEl.appendChild(msgEl); messagesEl.scrollTop = messagesEl.scrollHeight; return msgEl; } function setSendState(state) { if (!sendBtn) return; sendBtn.classList.toggle('sa-stop', state === 'generating'); sendBtn.classList.toggle('sa-stopping', state === 'stopping'); sendBtn.disabled = state === 'stopping'; if (state === 'idle') { sendBtn.title = '发送'; sendBtn.setAttribute('aria-label', '发送消息'); sendBtn.innerHTML = getIcon('send', 18); } else if (state === 'stopping') { sendBtn.title = '停止中…'; sendBtn.setAttribute('aria-label', '正在停止生成'); sendBtn.innerHTML = getIcon('stop', 18); } else { sendBtn.title = '停止生成'; sendBtn.setAttribute('aria-label', '停止生成'); sendBtn.innerHTML = getIcon('stop', 18); } if (inputEl) { inputEl.placeholder = state === 'idle' ? '输入消息… (Enter 发送, Shift+Enter 换行)' : 'AI 正在回复中… 点击红色按钮可停止'; } } async function handleSend() { if (isProcessing) { if (abortFlag) return; abortFlag = true; setSendState('stopping'); try { activeAiRequest && activeAiRequest.abort(); } catch {} const st = panelEl?.querySelector('.sa-tool-status'); if (st) { st.querySelector('.sa-tool-text').textContent = '正在停止…'; st.classList.add('sa-active'); } return; } const text = inputEl.value.trim(); if (!text) return; const cfg = getAiConfig(); if (!cfg.apiKey) { toast('请先配置模型和 API Key', 'error'); openSettings({ tab: 'ai' }); return; } ConversationManager.addMessage('user', text); appendMessageEl('user', text); inputEl.value = ''; inputEl.style.height = 'auto'; isProcessing = true; abortFlag = false; activeAiRequest = null; setSendState('generating'); inputEl.disabled = true; const aiMsgEl = appendMessageEl('assistant', ''); let displayedText = ''; let liveToolCalls = []; const updateAiBubble = (runningIndex = -1) => { const cleanText = cleanToolCallMarkers(displayedText); const toolsHtml = buildToolCallsHtml(liveToolCalls, runningIndex); const bodyHtml = cleanText ? renderMarkdown(cleanText) : ''; aiMsgEl.innerHTML = toolsHtml + bodyHtml; messagesEl.scrollTop = messagesEl.scrollHeight; }; const toolStatusEl = panelEl?.querySelector('.sa-tool-status'); if (toolStatusEl) { toolStatusEl.querySelector('.sa-tool-text').textContent = '正在思考…'; toolStatusEl.classList.add('sa-active'); } try { const messages = ConversationManager.getCurrentMessages(); const history = messages.slice(0, -1); const result = await runAgentLoop(text, history, AgentManager._currentAgentId, (partial) => { if (abortFlag) return; if (partial) { displayedText = partial; updateAiBubble(); } }, (name, args, phase) => { if (abortFlag) return; if (phase === 'start') { liveToolCalls.push({ name, arguments: args }); updateAiBubble(liveToolCalls.length - 1); } else { updateAiBubble(); } }); if (toolStatusEl) toolStatusEl.classList.remove('sa-active'); liveToolCalls = result.toolCalls || liveToolCalls; if (result.aborted) { const partial = result.response || cleanToolCallMarkers(displayedText); aiMsgEl.innerHTML = buildToolCallsHtml(liveToolCalls) + (partial ? renderMarkdown(partial) : '已停止生成'); appendMsgFooterEl(aiMsgEl, partial); messagesEl.scrollTop = messagesEl.scrollHeight; if (partial) ConversationManager.addMessage('assistant', partial, { toolCalls: liveToolCalls }); } else { displayedText = result.response; aiMsgEl.innerHTML = buildToolCallsHtml(liveToolCalls) + renderMarkdown(result.response); appendMsgFooterEl(aiMsgEl, result.response); messagesEl.scrollTop = messagesEl.scrollHeight; ConversationManager.addMessage('assistant', result.response, { toolCalls: result.toolCalls || [], }); } } catch (e) { if (toolStatusEl) toolStatusEl.classList.remove('sa-active'); aiMsgEl.innerHTML = `错误: ${escapeHtml(e.message)}`; ConversationManager.addMessage('assistant', `错误: ${e.message}`); } finally { isProcessing = false; abortFlag = false; setSendState('idle'); inputEl.disabled = false; inputEl.focus(); } } // ============================================================ // UI: 综合设置中心(单浮窗多标签,消除二级/三级弹窗) // ============================================================ const SETTINGS_TABS = [ { key: 'global', label: '全局设置', icon: 'gear' }, { key: 'ai', label: 'AI 设置', icon: 'cpu' }, { key: 'agents', label: '智能体', icon: 'bot' }, { key: 'api', label: '接口服务', icon: 'globe' }, { key: 'data', label: '数据管理', icon: 'download' }, ]; function renderCheckItem(state, title, detail) { const cls = state === 'ok' ? 'sa-ok' : state === 'err' ? 'sa-err' : 'sa-warn'; const mark = state === 'ok' ? `${getIcon('check', 11)} 正常` : state === 'err' ? `${getIcon('close', 11)} 失败` : `${getIcon('zap', 11)} 提示`; return `
    ${mark}${escapeHtml(title)}${detail ? '
    ' + escapeHtml(detail) : ''}
    `; } function prependCheckResult(container, html, cap = 8) { if (!container) return; container.insertAdjacentHTML('afterbegin', html); while (container.children.length > cap) container.lastElementChild.remove(); } function openSettings(options = {}) { const initialTab = SETTINGS_TABS.some(t => t.key === options.tab) ? options.tab : 'global'; // 单例:已打开则仅切换标签/聚焦,不重复创建 const existingModal = document.querySelector('.sa-overlay .sa-settings-modal'); if (existingModal) { switchSettingsTab(existingModal, initialTab); applySettingsFocus(existingModal, options.focus); return; } const steamKey = getSteamApiKey(); const itadKey = getItadApiKey(); const settings = parseStored(GM_getValue(SK.SETTINGS, null), {}); const overlay = document.createElement('div'); overlay.className = 'sa-overlay sa-show'; overlay.innerHTML = `

    ${getIcon('gear', 18)} 智能体设置中心 v${_v}

    ${SETTINGS_TABS.map(t => ``).join('')}
    `; document.body.appendChild(overlay); const closeSettings = () => { document.removeEventListener('keydown', onEsc); overlay.remove(); }; const onEsc = (e) => { if (e.key === 'Escape') closeSettings(); }; document.addEventListener('keydown', onEsc); overlay.querySelector('#sa-settings-close').addEventListener('click', closeSettings); overlay.querySelector('#sa-settings-cancel').addEventListener('click', closeSettings); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(); }); function switchSettingsTab(modalRoot, key) { modalRoot.querySelectorAll('.sa-set-nav-btn').forEach(b => b.classList.toggle('sa-active', b.dataset.tab === key)); modalRoot.querySelectorAll('.sa-set-pane').forEach(p => p.classList.toggle('sa-active', p.dataset.pane === key)); } function applySettingsFocus(modalRoot, selector) { if (!selector) return; const inp = modalRoot.querySelector(selector); if (inp) { inp.focus(); inp.scrollIntoView({ block: 'center' }); } } function renderNavSummary() { const el = overlay.querySelector('#sa-set-nav-summary'); if (!el) return; const models = ModelManager.getModels(); const agents = AgentManager.getAgents(); const ts = TokenStatsManager.getStats(); el.innerHTML = `模型 ${models.length} 个 · 智能体 ${agents.length} 个
    累计 Token ${formatTokenCount(ts.totalTokens)}`; } overlay.querySelectorAll('.sa-set-nav-btn').forEach(btn => { btn.addEventListener('click', () => switchSettingsTab(overlay, btn.dataset.tab)); }); // ---------- Tab: 全局设置 ---------- function buildGlobalPane() { const pane = overlay.querySelector('#sa-pane-global'); pane.innerHTML = `
    ${getIcon('cpu', 15)} 生成参数
    值越低回答越稳定,越高越发散;推荐 0.5-0.8
    ReAct 循环最大工具调用次数
    ${getIcon('book', 15)} 规则知识库
    指向 saa-rules-knowledge.json 的原始链接(纯数据 JSON,不执行代码);更新知识只需改 JSON 文件并推送,无需更新主脚本
    `; const ORIGIN_LABEL = { remote: { text: '远程最新', color: '#4ade80' }, snapshot: { text: '内置快照', color: '#fbbf24' }, }; function renderRulesStatus() { const el = pane.querySelector('#sa-rules-status'); if (!el) return; const s = RulesKnowledgeProvider.getStatus(); const o = ORIGIN_LABEL[s.origin] || ORIGIN_LABEL.snapshot; el.innerHTML = `
    v${escapeHtml(s.version)} ${o.text} ${s.sectionCount} 章节 · ${s.faqCount} FAQ 更新于 ${escapeHtml(s.updatedAt)}
    来源: ${escapeHtml(s.source)}
    `; } renderRulesStatus(); pane.querySelector('#sa-rules-refresh').addEventListener('click', async (e) => { const btn = e.currentTarget; const urlInput = pane.querySelector('#sa-rules-url'); RulesKnowledgeProvider.setRemoteUrl(urlInput.value.trim()); btn.disabled = true; btn.style.opacity = '0.6'; try { const s = await RulesKnowledgeProvider.refreshFromRemote(); toast(`规则知识库已更新到 v${s.version}`, 'success'); } catch (err) { toast(`规则库更新失败: ${err.message}(继续使用本地知识)`, 'error'); } finally { btn.disabled = false; btn.style.opacity = ''; renderRulesStatus(); } }); pane.querySelector('#sa-rules-reset').addEventListener('click', () => { GM_setValue(SK.RULES_REMOTE, ''); RulesKnowledgeProvider._active = null; renderRulesStatus(); toast('已清除远程缓存,恢复本地知识', 'success'); }); } // ---------- Tab: AI 设置 ---------- function buildAiPane() { const pane = overlay.querySelector('#sa-pane-ai'); pane.innerHTML = `
    AI 模型配置
    选择供应商预设可自动填充 API 地址和模型列表,只需填入 API Key 即可使用。支持配置多个模型。
    `; const listView = pane.querySelector('#sa-model-list-view'); const editorView = pane.querySelector('#sa-model-editor-view'); function renderModelList() { const listEl = pane.querySelector('#sa-model-list'); const models = ModelManager.getModels(); const activeId = ModelManager.getActiveModelId(); if (models.length === 0) { listEl.innerHTML = `
    尚未配置模型,点击下方按钮添加
    `; return; } listEl.innerHTML = models.map(m => { const provider = MODEL_PROVIDERS[m.provider]; const providerName = provider?.name || '自定义'; return `
    ${providerIconHtml(m.provider, 18)}
    ${escapeHtml(m.name)} ${m.id === activeId ? '当前' : ''}
    ${escapeHtml(providerName)} · ${escapeHtml(m.model)} · ${m.apiKey ? '' + getIcon('check', 10) + ' 已配置' : '' + getIcon('close', 10) + ' 未配置'}
    ${m.id !== activeId ? `` : ''}
    `; }).join(''); listEl.querySelectorAll('.sa-model-card').forEach(card => { const id = card.dataset.id; card.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', async () => { const action = btn.dataset.action; if (action === 'use') { ModelManager.setActiveModel(id); renderModelList(); renderNavSummary(); toast(`已切换到 ${ModelManager.getModel(id)?.name}`, 'success'); } else if (action === 'edit') { renderModelEditor(id); } else if (action === 'del') { if (confirm(`确认删除模型 "${ModelManager.getModel(id)?.name}"?`)) { ModelManager.deleteModel(id); renderModelList(); renderNavSummary(); toast('已删除', 'success'); } } else if (action === 'check') { const model = ModelManager.getModel(id); if (!model) return; btn.disabled = true; const resultHtml = await (async () => { try { const r = await testModelConnection(model); return renderCheckItem('ok', `模型「${model.name}」连接正常`, `延迟 ${r.latencyMs}ms · 服务端返回模型 ${r.modelEcho}`); } catch (e) { return renderCheckItem('err', `模型「${model.name}」连接失败`, describeModelError(e)); } })(); prependCheckResult(pane.querySelector('#sa-model-check-results'), resultHtml); btn.disabled = false; } }); }); }); } function showListView() { listView.style.display = ''; editorView.style.display = 'none'; editorView.innerHTML = ''; renderModelList(); } // 模型编辑:标签内嵌视图(不再弹出二级窗口) function renderModelEditor(existingId) { const model = existingId ? ModelManager.getModel(existingId) : null; editorView.innerHTML = `
    ${model ? '编辑模型(保存后立即生效)' : '添加模型(保存后立即生效)'}
    `; listView.style.display = 'none'; editorView.style.display = ''; let selectedProvider = model?.provider || 'deepseek'; function collectFormModel() { return { name: editorView.querySelector('#sa-me-name').value.trim(), provider: selectedProvider, apiUrl: editorView.querySelector('#sa-me-url').value.trim(), apiKey: editorView.querySelector('#sa-me-key').value.trim(), model: editorView.querySelector('#sa-me-model').value.trim(), }; } function applyProvider(providerKey) { selectedProvider = providerKey; const p = MODEL_PROVIDERS[providerKey]; const providerSelect = editorView.querySelector('#sa-me-provider'); if (providerSelect) providerSelect.value = providerKey; const urlInput = editorView.querySelector('#sa-me-url'); if (!model || model.provider !== providerKey) { urlInput.value = p.apiUrl; } const urlHint = editorView.querySelector('#sa-me-url-hint'); urlHint.textContent = p.apiUrl ? p.keyHint : `${p.keyHint};自定义地址首次请求时需在浏览器扩展弹窗中允许跨域,或在脚本头部 @connect 中声明目标域名`; const keyHint = editorView.querySelector('#sa-me-key-hint'); if (p.apiKeyUrl) { keyHint.innerHTML = `在 ${p.apiKeyUrl} 获取 API Key`; } else { keyHint.textContent = ''; } const modelWrap = editorView.querySelector('#sa-me-model-input-wrap'); const modelHint = editorView.querySelector('#sa-me-model-hint'); if (p.models.length > 0) { const currentModel = (model && model.provider === providerKey) ? model.model : p.defaultModel; modelWrap.innerHTML = ` ${p.models.map(m => ``).join('')} `; modelHint.textContent = `可下拉选择或手动输入,共 ${p.models.length} 个预设模型`; } else { modelWrap.innerHTML = ``; modelHint.textContent = '请手动输入模型名称'; } const nameInput = editorView.querySelector('#sa-me-name'); if (!nameInput.value.trim()) { nameInput.value = `${p.name} ${p.defaultModel || '自定义'}`; } } editorView.querySelector('#sa-me-provider').addEventListener('change', (e) => { applyProvider(e.target.value); }); applyProvider(selectedProvider); editorView.querySelector('#sa-me-back').addEventListener('click', showListView); editorView.querySelector('#sa-me-test').addEventListener('click', async (e) => { const btn = e.currentTarget; const statusEl = editorView.querySelector('#sa-me-check-status'); btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 14)} 检测中…`; statusEl.style.color = '#94a3b8'; statusEl.textContent = '正在发送最小测试请求 (max_tokens=1),请稍候…'; try { const form = collectFormModel(); const r = await testModelConnection(form); statusEl.style.color = '#4ade80'; statusEl.textContent = `连接正常:延迟 ${r.latencyMs}ms,服务端返回模型 ${r.modelEcho}`; prependCheckResult(pane.querySelector('#sa-model-check-results'), renderCheckItem('ok', `模型「${form.name || form.model}」连接正常`, `编辑表单实测 · 延迟 ${r.latencyMs}ms · 服务端模型 ${r.modelEcho}`)); } catch (err) { statusEl.style.color = '#f87171'; statusEl.textContent = `连接失败:${describeModelError(err)}`; } finally { btn.disabled = false; btn.innerHTML = originalHtml; } }); editorView.querySelector('#sa-me-save').addEventListener('click', () => { const form = collectFormModel(); if (!form.apiUrl) { toast('请输入 API 地址', 'error'); return; } if (!form.apiKey) { toast('请输入 API Key', 'error'); return; } if (!form.model) { toast('请选择或输入模型名称', 'error'); return; } const data = { ...form, name: form.name || `${MODEL_PROVIDERS[selectedProvider]?.name || '自定义'} ${form.model}`, }; if (existingId) { ModelManager.updateModel(existingId, data); toast('模型已更新', 'success'); } else { const created = ModelManager.createModel(data); if (!ModelManager.getActiveModelId()) { ModelManager.setActiveModel(created.id); } toast(`已添加模型 ${created.name}`, 'success'); } refreshModelSelector(); renderNavSummary(); showListView(); }); } pane.querySelector('#sa-cfg-add-model').addEventListener('click', () => renderModelEditor(null)); pane.querySelector('#sa-btn-check-all-models').addEventListener('click', async (e) => { const btn = e.currentTarget; const models = ModelManager.getModels(); const container = pane.querySelector('#sa-model-check-results'); if (!models.length) { prependCheckResult(container, renderCheckItem('warn', '尚未配置模型', '点击「添加模型」先完成配置,再执行检测')); return; } btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 13)} 检测中 (0/${models.length})…`; let okCount = 0; for (let i = 0; i < models.length; i++) { const m = models[i]; let item; try { const r = await testModelConnection(m); okCount++; item = renderCheckItem('ok', `模型「${m.name}」连接正常`, `延迟 ${r.latencyMs}ms · 服务端返回模型 ${r.modelEcho}`); } catch (err) { item = renderCheckItem('err', `模型「${m.name}」连接失败`, describeModelError(err)); } prependCheckResult(container, item); btn.innerHTML = `${getIcon('refresh', 13)} 检测中 (${i + 1}/${models.length})…`; } toast(`模型检测完成:${okCount}/${models.length} 个可用`, okCount === models.length ? 'success' : 'error'); btn.disabled = false; btn.innerHTML = originalHtml; }); renderModelList(); } // ---------- Tab: 智能体设置 ---------- function buildAgentsPane() { const pane = overlay.querySelector('#sa-pane-agents'); pane.innerHTML = `
    智能体管理
    新建、编辑、切换自定义智能体;支持 AI 辅助生成系统提示词与配置导入导出。
    `; const listView = pane.querySelector('#sa-agent-list-view'); const editorView = pane.querySelector('#sa-agent-editor-view'); function renderAgentList() { const listEl = pane.querySelector('#sa-am-list'); const agents = AgentManager.getAgents(); listEl.innerHTML = agents.map(a => `
    ${agentIconHtml(a, 18)}
    ${escapeHtml(a.name)} ${a.builtin ? '内置' : ''}
    ${escapeHtml(a.description)}
    ${!a.builtin ? ` ` : ''}
    `).join(''); listEl.querySelectorAll('.sa-agent-card').forEach(card => { const id = card.dataset.id; card.querySelectorAll('.sa-mini-btn').forEach(btn => { btn.addEventListener('click', () => { const action = btn.dataset.action; if (action === 'use') { AgentManager.switchAgent(id); if (panelEl?._refreshAgentSelect) panelEl._refreshAgentSelect(); renderMessages(); toast(`已切换到 ${AgentManager.getAgent(id)?.name}`, 'success'); closeSettings(); } else if (action === 'edit') { renderAgentEditor(id); } else if (action === 'del') { if (confirm(`确认删除智能体 "${AgentManager.getAgent(id)?.name}"?`)) { AgentManager.deleteAgent(id); toast('已删除', 'success'); renderAgentList(); renderNavSummary(); if (panelEl?._refreshAgentSelect) panelEl._refreshAgentSelect(); renderMessages(); } } }); }); }); } function showListView() { listView.style.display = ''; editorView.style.display = 'none'; editorView.innerHTML = ''; renderAgentList(); } // 智能体编辑:标签内嵌视图(不再弹出三级窗口) function renderAgentEditor(existingId) { const agent = existingId ? AgentManager.getAgent(existingId) : null; editorView.innerHTML = `
    ${agent ? '编辑智能体(保存后立即生效)' : '新建智能体(保存后立即生效)'}
    可用: bot, gamepad, gift, sparkle, stats, chat 或输入 emoji
    `; listView.style.display = 'none'; editorView.style.display = ''; editorView.querySelector('#sa-ae-back').addEventListener('click', showListView); editorView.querySelector('#sa-ae-cancel').addEventListener('click', showListView); editorView.querySelector('#sa-ae-copy').addEventListener('click', async () => { const text = editorView.querySelector('#sa-ae-prompt').value.trim(); if (!text) { toast('提示词为空,暂无可复制内容', 'error'); return; } const copyBtn = editorView.querySelector('#sa-ae-copy'); const ok = await copyTextToClipboard(text); if (ok) { copyBtn.innerHTML = `${getIcon('check', 13)} 已复制`; setTimeout(() => { copyBtn.innerHTML = `${getIcon('copy', 13)} 复制`; }, 1500); toast('系统提示词已复制到剪贴板', 'success'); } else { toast('复制失败,请手动全选复制', 'error'); } }); editorView.querySelector('#sa-ae-gen').addEventListener('click', async () => { const desc = editorView.querySelector('#sa-ae-need').value.trim(); if (!desc) { toast('请输入需求描述', 'error'); return; } const btn = editorView.querySelector('#sa-ae-gen'); const promptTa = editorView.querySelector('#sa-ae-prompt'); const cfg = getAiConfig(); if (!cfg.apiKey) { toast('请先配置模型和 API Key', 'error'); switchSettingsTab(overlay, 'ai'); return; } btn.disabled = true; btn.innerHTML = `${getIcon('refresh', 14)} 生成中…`; promptTa.value = ''; promptTa.placeholder = 'AI 正在生成提示词,请稍候…'; try { const prompt = `请根据以下需求,为一个 Steam AI 智能体编写 systemPrompt(系统提示词)。 用户需求:${desc} 要求: 1. 明确智能体的角色定位和核心能力 2. 列出回答规范和输出格式要求 3. 包含相关领域知识约束 4. 使用中文,专业且完整;内容要详尽,可分章节展开(角色定位/核心能力/回答规范/输出格式/知识约束/注意事项等),不要省略 5. 必须输出完整内容直到自然结束,不要中途截断 请直接输出 systemPrompt 内容,不要包含其他说明。`; // 流式生成 + 实时预览到提示词输入框;isolated 隔离聊天中止状态;不限 max_tokens、5 分钟超时 const result = await callAiApiWithTyping(prompt, { temperature: 0.7, timeout: 300000, isolated: true, }, (partial) => { promptTa.value = cleanToolCallMarkers(partial || ''); promptTa.scrollTop = promptTa.scrollHeight; }); promptTa.value = cleanToolCallMarkers(result).trim(); toast('提示词已生成', 'success'); } catch (e) { const msg = e.message === '__SA_ABORTED__' ? '请求已中断' : e.message; toast('生成失败: ' + msg, 'error'); } finally { btn.disabled = false; btn.innerHTML = `${getIcon('sparkle', 14)} AI 生成提示词`; promptTa.placeholder = '定义智能体的角色、能力、回答规范...'; } }); editorView.querySelector('#sa-ae-save').addEventListener('click', () => { const name = editorView.querySelector('#sa-ae-name').value.trim(); const prompt = editorView.querySelector('#sa-ae-prompt').value.trim(); if (!name) { toast('请输入名称', 'error'); return; } if (!prompt) { toast('请输入系统提示词', 'error'); return; } const data = { name, description: editorView.querySelector('#sa-ae-desc').value.trim(), icon: editorView.querySelector('#sa-ae-icon').value.trim() || 'bot', keywords: editorView.querySelector('#sa-ae-keywords').value.split(',').map(s => s.trim()).filter(Boolean), systemPrompt: prompt, }; if (existingId) { AgentManager.updateAgent(existingId, data); toast('已更新', 'success'); } else { const created = AgentManager.createAgent(data); toast(`已创建 ${created.name}`, 'success'); } if (panelEl?._refreshAgentSelect) panelEl._refreshAgentSelect(); renderNavSummary(); showListView(); }); } pane.querySelector('#sa-am-new').addEventListener('click', () => renderAgentEditor(null)); pane.querySelector('#sa-am-export').addEventListener('click', () => { const json = AgentManager.exportAgents(); downloadFile(json, `steam-agents-${Date.now()}.json`); toast('已导出智能体配置', 'success'); }); pane.querySelector('#sa-am-import').addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = () => { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const count = AgentManager.importAgents(reader.result); toast(`成功导入 ${count} 个智能体`, 'success'); renderAgentList(); renderNavSummary(); } catch (e) { toast(e.message, 'error'); } }; reader.readAsText(file); }; input.click(); }); renderAgentList(); } // ---------- Tab: 接口服务 ---------- function buildApiPane() { const pane = overlay.querySelector('#sa-pane-api'); const steamIdCfg = getConfiguredSteamId(); const detectedId = detectSteamId(); const idStatusText = steamIdCfg ? `已配置:玩家数据查询将使用 ${escapeHtml(steamIdCfg)}` : (detectedId ? `未配置:将自动使用当前登录用户(${escapeHtml(detectedId)})` : '未配置:未检测到登录用户,玩家数据查询需在对话中提供 SteamID'); pane.innerHTML = `
    接口服务
    配置并检测 Steam / ITAD 第三方接口;AI 模型接口在「AI 设置」标签中检测。
    ${getIcon('key', 15)} Steam Web API Key
    ${steamKey ? '已配置:点击「检测」验证 Key 有效性' : '未配置:游戏库查询(getOwnedGames)、高级折扣检索(searchDiscountedGames) 将不可用'}
    在 steamcommunity.com/dev 申请,用于 getOwnedGames / searchDiscountedGames 等工具
    ${getIcon('user', 15)} SteamID(玩家数据查询身份,可选)
    ${idStatusText}
    用于「我的游戏库 / 资料 / 愿望单」类查询;留空时自动识别当前登录用户(依据页面登录态),无需手动填写
    ${getIcon('globe', 15)} ITAD API Key(跨区比价增强,可选)
    ${itadKey ? '已配置:getMultiRegionPrices 工具将以 ITAD 增强模式查询 16 个地区的实时价格' : '未配置:跨区比价工具将使用降级数据源(AugmentedSteam,约 7 个地区),价格准确度降低'}
    申请步骤:① isthereanydeal.com 注册并验证邮箱 ② 进入 isthereanydeal.com/apps 点击 Register App ③ 填写应用名提交 ④ 复制 32 位密钥粘贴到上方 ⑤ 点击「测试」验证后保存。已安装 SGLV 2.9.79+ 时可自动复用其配置
    `; const resultsEl = pane.querySelector('#sa-api-check-results'); async function runSteamCheck(key, { silent = false } = {}) { const statusEl = pane.querySelector('#sa-steam-status'); const r = await testSteamApiKey(key); if (r.success) { statusEl.style.color = '#4ade80'; statusEl.textContent = `Key 有效${r.samplePlayer ? `(示例查询玩家:${r.samplePlayer})` : ''}`; prependCheckResult(resultsEl, renderCheckItem('ok', 'Steam Web API Key 有效', r.samplePlayer ? `示例查询返回玩家「${r.samplePlayer}」,接口调用正常` : (r.note || '接口调用正常'))); } else { statusEl.style.color = '#f87171'; statusEl.textContent = `检测失败:${r.error}`; prependCheckResult(resultsEl, renderCheckItem('err', 'Steam Web API Key 检测失败', r.error)); } if (!silent) toast(r.success ? 'Steam API Key 检测通过' : `Steam API Key 检测失败:${r.error}`, r.success ? 'success' : 'error'); return r; } pane.querySelector('#sa-steamkey-test').addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 13)} 检测中…`; try { await runSteamCheck(pane.querySelector('#sa-cfg-steamkey').value.trim()); } finally { btn.disabled = false; btn.innerHTML = originalHtml; } }); pane.querySelector('#sa-steamid-detect').addEventListener('click', async (e) => { const btn = e.currentTarget; const input = pane.querySelector('#sa-cfg-steamid'); const statusEl = pane.querySelector('#sa-steamid-status'); const val = input.value.trim(); btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 13)} 检测中…`; try { if (!val) { const detected = detectSteamId(); if (detected) { statusEl.style.color = '#4ade80'; statusEl.textContent = `自动检测成功:当前登录用户 ${detected},留空保存即可自动使用`; prependCheckResult(resultsEl, renderCheckItem('ok', 'SteamID 自动检测成功', `当前登录用户:${detected},「我的游戏库 / 资料 / 愿望单」查询将自动使用该身份`)); } else { statusEl.style.color = '#fbbf24'; statusEl.textContent = '未检测到登录用户:请手动填写 SteamID64(17位数字)或自定义 ID'; prependCheckResult(resultsEl, renderCheckItem('warn', 'SteamID 自动检测失败', '当前页面未检测到登录用户,请手动填写 SteamID64 或自定义 ID 后保存')); } } else if (/^\d{17}$/.test(val)) { statusEl.style.color = '#4ade80'; statusEl.textContent = `格式正确:SteamID64(${val})`; prependCheckResult(resultsEl, renderCheckItem('ok', 'SteamID 格式正确', `${val} 是标准的 17 位 SteamID64,保存后玩家数据查询将使用该身份`)); } else { const apiKey = pane.querySelector('#sa-cfg-steamkey').value.trim() || getSteamApiKey(); if (!apiKey) { statusEl.style.color = '#fbbf24'; statusEl.textContent = '自定义 ID 校验需 Steam Web API Key:可先保存,工具查询时会自动解析'; } else { const id64 = await resolveVanityToId64(val, apiKey).catch(() => null); if (id64) { statusEl.style.color = '#4ade80'; statusEl.textContent = `自定义 ID 有效:已解析为 SteamID64 ${id64}`; prependCheckResult(resultsEl, renderCheckItem('ok', 'SteamID 校验通过', `自定义 ID「${escapeHtml(val)}」解析为 ${id64}`)); } else { statusEl.style.color = '#f87171'; statusEl.textContent = '自定义 ID 未解析到 SteamID64(可能不存在),可保存后在对话中验证'; } } } } finally { btn.disabled = false; btn.innerHTML = originalHtml; } }); pane.querySelector('#sa-cfg-itadtest').addEventListener('click', async (e) => { const btn = e.currentTarget; const keyInput = pane.querySelector('#sa-cfg-itadkey'); const statusEl = pane.querySelector('#sa-cfg-itad-status'); const key = keyInput.value.trim(); if (!key) { statusEl.style.color = '#fbbf24'; statusEl.textContent = '未填写 Key:跨区比价将使用降级数据源,价格准确度降低'; return; } btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 13)} 测试中…`; try { const result = await itadTestConnection(key); if (result.success) { statusEl.style.color = '#4ade80'; statusEl.textContent = '连接成功:ITAD Key 有效,跨区比价已启用 16 地区增强模式'; prependCheckResult(resultsEl, renderCheckItem('ok', 'ITAD API Key 有效', '跨区比价将使用 ITAD 16 地区增强模式')); toast('ITAD 连接成功', 'success'); } else { statusEl.style.color = '#f87171'; statusEl.textContent = `连接失败:${result.error}`; prependCheckResult(resultsEl, renderCheckItem('err', 'ITAD API Key 检测失败', result.error)); toast(`ITAD 连接失败:${result.error}`, 'error'); } } finally { btn.disabled = false; btn.innerHTML = originalHtml; } }); pane.querySelector('#sa-btn-check-all-api').addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; const originalHtml = btn.innerHTML; btn.innerHTML = `${getIcon('refresh', 13)} 检测中…`; const activeModel = ModelManager.getActiveModel(); if (activeModel) { try { const r = await testModelConnection(activeModel); prependCheckResult(resultsEl, renderCheckItem('ok', `当前模型「${activeModel.name}」连接正常`, `延迟 ${r.latencyMs}ms · 服务端返回模型 ${r.modelEcho}`)); } catch (err) { prependCheckResult(resultsEl, renderCheckItem('err', `当前模型「${activeModel.name}」连接失败`, describeModelError(err))); } } else { prependCheckResult(resultsEl, renderCheckItem('warn', '未配置 AI 模型', '前往「AI 设置」标签添加模型')); } const steamInput = pane.querySelector('#sa-cfg-steamkey').value.trim(); if (steamInput) await runSteamCheck(steamInput, { silent: true }); else prependCheckResult(resultsEl, renderCheckItem('warn', 'Steam Web API Key 未配置', '可选配置;未填写将跳过检测')); const itadInput = pane.querySelector('#sa-cfg-itadkey').value.trim(); if (itadInput) { const r = await itadTestConnection(itadInput); prependCheckResult(resultsEl, r.success ? renderCheckItem('ok', 'ITAD API Key 有效', '跨区比价将使用 16 地区增强模式') : renderCheckItem('err', 'ITAD API Key 检测失败', r.error)); } else { prependCheckResult(resultsEl, renderCheckItem('warn', 'ITAD API Key 未配置', '跨区比价使用降级数据源(约 7 地区),价格准确度降低')); } toast('接口检测完成,结果见上方列表', 'success'); btn.disabled = false; btn.innerHTML = originalHtml; }); } // ---------- Tab: 数据管理 ---------- function buildDataPane() { const pane = overlay.querySelector('#sa-pane-data'); const conv = ConversationManager.getCurrentConversation(); pane.innerHTML = `
    会话数据管理
    当前会话:「${escapeHtml(conv?.title || '新会话')}」 · ${conv?.messages?.length || 0} 条消息
    ${getIcon('download', 15)} 导出当前会话
    导出文件可直接分享或备份;JSON 可再次导入恢复
    ${getIcon('edit', 15)} 导入会话
    导入后会作为新会话追加到历史列表,不影响现有对话
    `; const convId = ConversationManager.getCurrentId(); pane.querySelector('#sa-export-json').addEventListener('click', () => { ConversationManager.exportAsJson(convId); toast('已导出 JSON', 'success'); }); pane.querySelector('#sa-export-md').addEventListener('click', () => { ConversationManager.exportAsMarkdown(convId); toast('已导出 Markdown', 'success'); }); pane.querySelector('#sa-import-json').addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = () => { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const data = JSON.parse(reader.result); const newConvId = `conv_imported_${Date.now()}`; ConversationManager._conversations[newConvId] = { id: newConvId, title: data.conversation || data.messages?.[0]?.content?.slice(0, 30) || '导入的对话', agentId: data.agentId || AgentManager._currentAgentId, modelId: data.modelId || null, messages: (data.messages || []).map(m => ({ id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, role: m.role, content: m.content, timestamp: typeof m.timestamp === 'string' ? new Date(m.timestamp).getTime() : (m.timestamp || Date.now()), toolCalls: m.toolCalls || [], })), createdAt: data.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), }; ConversationManager.save(); ConversationManager.switchTo(newConvId); renderMessages(); toast(`导入成功,共 ${data.messages?.length || 0} 条消息`, 'success'); buildDataPane(); } catch (e) { toast('导入失败: ' + e.message, 'error'); } }; reader.readAsText(file); }; input.click(); }); } // ---------- 底部保存 ---------- overlay.querySelector('#sa-cfg-save').addEventListener('click', () => { GM_setValue(SK.STEAM_KEY, overlay.querySelector('#sa-cfg-steamkey').value.trim()); GM_setValue(SK.STEAM_ID, overlay.querySelector('#sa-cfg-steamid').value.trim()); GM_setValue(SK.ITAD_KEY, overlay.querySelector('#sa-cfg-itadkey').value.trim()); const rulesUrlEl = overlay.querySelector('#sa-rules-url'); if (rulesUrlEl) RulesKnowledgeProvider.setRemoteUrl(rulesUrlEl.value.trim()); GM_setValue(SK.SETTINGS, JSON.stringify({ ...settings, temperature: parseFloat(overlay.querySelector('#sa-cfg-temp').value) || 0.7, maxIterations: parseInt(overlay.querySelector('#sa-cfg-iter').value) || CONFIG.MAX_ITERATIONS, })); toast('设置已保存', 'success'); if (fabEl) { const newCfg = getAiConfig(); if (newCfg.apiKey) fabEl.classList.add('sa-has-key'); else fabEl.classList.remove('sa-has-key'); } refreshModelSelector(); closeSettings(); }); buildGlobalPane(); buildAiPane(); buildAgentsPane(); buildApiPane(); buildDataPane(); renderNavSummary(); switchSettingsTab(overlay, initialTab); applySettingsFocus(overlay, options.focus); } // 兼容旧入口:智能体管理按钮 → 设置中心「智能体」标签 function openAgentManager() { openSettings({ tab: 'agents' }); } // ============================================================ // 暴露 window.SteamAgent API // ============================================================ function exposeAPI() { const api = { isInstalled: () => true, version: _v, open: (options = {}) => openSidebar(options), close: () => closeSidebar(), toggle: () => toggleSidebar(), isOpen: () => panelEl?.classList.contains('sa-open') || false, send: (message) => { if (!panelEl?.classList.contains('sa-open')) openSidebar(); if (isProcessing) { toast('AI 正在回复中,请等待完成或点击停止按钮', 'error'); return; } inputEl.value = message; handleSend(); }, getAgents: () => AgentManager.getAgents(), getCurrentAgent: () => AgentManager.getCurrentAgent(), switchAgent: (agentId) => { const result = AgentManager.switchAgent(agentId); if (result && panelEl?._refreshAgentSelect) panelEl._refreshAgentSelect(); renderMessages(); return result; }, getModels: () => ModelManager.getModels(), getActiveModel: () => ModelManager.getActiveModel(), switchModel: (modelId) => { const result = ModelManager.setActiveModel(modelId); if (result) { refreshModelSelector(); ConversationManager.updateCurrentModel(modelId); } return result; }, getConversations: () => ConversationManager.getAllConversations(), getCurrentConversation: () => ConversationManager.getCurrentConversation(), createConversation: () => { const conv = ConversationManager.createConversation(AgentManager._currentAgentId); renderMessages(); return conv; }, switchConversation: (convId) => { const result = ConversationManager.switchTo(convId); if (result) { renderMessages(); refreshModelSelector(); if (panelEl?._refreshAgentSelect) panelEl._refreshAgentSelect(); } return result; }, exportCurrentChat: () => ConversationManager.exportAsJson(ConversationManager.getCurrentId()), on: (event, cb) => EventBus.on(event, cb), off: (event, cb) => EventBus.off(event, cb), }; try { if (typeof unsafeWindow !== 'undefined') { unsafeWindow.SteamAgent = api; } else { window.SteamAgent = api; } } catch (e) { console.warn('[SA-Agent] Failed to expose API:', e); } } // ============================================================ // 初始化 // ============================================================ function safeStep(name, fn) { try { fn(); } catch (e) { console.error('[SA-Agent] 初始化步骤失败: ' + name, e); } } // Steam 新版 React 页面在水合/路由切换时可能移除脚本注入的 body 子元素,定时自愈 function startSelfHealing() { const heal = () => { try { if (fabEl && !document.contains(fabEl) && document.body) { document.body.appendChild(fabEl); console.warn('[SA-Agent] 浮动按钮被页面移除,已自动恢复'); } if (panelEl && !document.contains(panelEl) && document.body) { document.body.appendChild(panelEl); } } catch {} }; setInterval(heal, 5000); try { new MutationObserver(heal).observe(document.body, { childList: true }); } catch {} } function init() { try { if (window !== window.top) return; } catch { return; } safeStep('createFloatingButton', createFloatingButton); safeStep('ModelManager.init', () => ModelManager.init()); safeStep('AgentManager.init', () => AgentManager.init()); safeStep('ConversationManager.init', () => ConversationManager.init()); safeStep('TokenStatsManager.init', () => TokenStatsManager.init()); const storedSide = GM_getValue(SK.PANEL_SIDE, null); if (storedSide === 'left' || storedSide === 'right') panelSide = storedSide; safeStep('createSidebar', createSidebar); safeStep('exposeAPI', exposeAPI); safeStep('startSelfHealing', startSelfHealing); safeStep('EventBus.ready', () => EventBus.emit('ready', { version: _v })); console.log('[SA-Agent] 初始化完成,SteamAgent API 已暴露'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 100); } })();