Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
5.9 KiB
JSON

{"repo": "548ujgyh/learn", "files": {"pages/api/ai.js": {"4cbfba32e700cbb48cdd111f9c2d5a48357d9f3b": {"t": 1786068264.7871604, "n": 5075, "h": "1ee8c4e549aa6988", "c": "/**\n * /pages/api/ai.js\n * ─────────────────────────────────────────────────────────────\n * 统一 AI 代理接口。所有请求从前端发到这里,再由服务器端转发到\n * 各 AI 提供商。API Key 保存在服务器环境变量中,永远不暴露给前端。\n */\n\n// Vercel 函数超时配置(最长 60 秒,Hobby plan 默认 10 秒太短)\nexport const config = { maxDuration: 60 };\n\n// ── OpenAI 兼容格式(DeepSeek / Kimi / 千问)─────────────────\nconst OAI_PROVIDERS = {\n deepseek: {\n baseUrl: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-chat\",\n envKey: \"DEEPSEEK_API_KEY\",\n },\n kimi: {\n baseUrl: \"https://api.moonshot.cn/v1/chat/completions\",\n model: \"moonshot-v1-32k\",\n envKey: \"KIMI_API_KEY\",\n },\n qwen: {\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n model: \"qwen-max\",\n envKey: \"QWEN_API_KEY\",\n },\n};\n\nasync function callClaude(apiKey, system, messages, maxTokens, withSearch) {\n const body = {\n model: \"claude-sonnet-4-20250514\",\n max_tokens: maxTokens,\n system,\n messages,\n };\n if (withSearch) {\n body.tools = [{ type: \"web_search_20250305\", name: \"web_search\" }];\n }\n const res = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify(body),\n });\n const data = await res.json();\n if (data.error) throw new Error(`Claude: ${data.error.message}`);\n return data.content?.filter(b => b.type === \"text\").map(b => b.text).join(\"\") || \"\";\n}\n\nasync function callGemini(apiKey, system, messages, maxTokens) {\n const contents = messages.map(m => ({\n role: m.role === \"user\" ? \"user\" : \"model\",\n parts: [{ text: typeof m.content === \"string\" ? m.content : JSON.stringify(m.content) }],\n }));\n\n // Gemini 中文 token 消耗约是 Claude 的 2 倍,且计数单位不同\n // 统一使用足够大的上限,避免截断\n const geminiMaxTokens = Math.max(maxTokens * 3, 8192);\n\n const body = {\n system_instruction: { parts: [{ text: system }] },\n contents,\n generationConfig: {\n maxOutputTokens: geminiMaxTokens,\n temperature: 0.7,\n },\n };\n const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-04-17:generateContent?key=${apiKey}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n const data = await res.json();\n if (data.error) throw new Error(`Gemini: ${data.error.message}`);\n\n // 检查是否因 token 截断\n const finishReason = data.candidates?.[0]?.finishReason;\n if (finishReason === \"MAX_TOKENS\") {\n throw new Error(\"Gemini: 响应被截断(MAX_TOKENS),请尝试使用 Claude 或减少请求复杂度\");\n }\n\n return data.candidates?.[0]?.content?.parts?.map(p => p.text || \"\").join(\"\") || \"\";\n}\n\nasync function callOAI(provider, apiKey, system, messages, maxTokens) {\n const cfg = OAI_PROVIDERS[provider];\n // OpenAI 兼容接口同样对中文需要更多 token\n const oaiMaxTokens = Math.max(maxTokens * 2, 4096);\n const oaiMessages = [\n { role: \"system\", content: system },\n ...messages.map(m => ({ role: m.role, content: m.content })),\n ];\n const res = await fetch(cfg.baseUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model: cfg.model,\n max_tokens: oaiMaxTokens,\n messages: oaiMessages,\n }),\n });\n const data = await res.json();\n if (data.error) throw new Error(`${provider}: ${data.error.message}`);\n return data.choices?.[0]?.message?.content || \"\";\n}\n\nexport default async function handler(req, res) {\n if (req.method !== \"POST\") {\n return res.status(405).json({ error: \"Method not allowed\" });\n }\n\n const { provider = \"claude\", apiKey: userKey, system, messages, maxTokens = 1200, withSearch = false } = req.body;\n\n if (!system || !messages) {\n return res.status(400).json({ error: \"Missing system or messages\" });\n }\n\n // Key 优先级:前端用户输入 > 服务器环境变量\n const resolveKey = (envName) => userKey || process.env[envName] || \"\";\n\n try {\n let text = \"\";\n\n if (provider === \"claude\") {\n const key = resolveKey(\"ANTHROPIC_API_KEY\");\n if (!key) return res.status(401).json({ error: \"未设置 Claude API Key\" });\n text = await callClaude(key, system, messages, maxTokens, withSearch);\n\n } else if (provider === \"gemini\") {\n const key = resolveKey(\"GEMINI_API_KEY\");\n if (!key) return res.status(401).json({ error: \"未设置 Gemini API Key\" });\n text = await callGemini(key, system, messages, maxTokens);\n\n } else if (OAI_PROVIDERS[provider]) {\n const cfg = OAI_PROVIDERS[provider];\n const key = resolveKey(cfg.envKey);\n if (!key) return res.status(401).json({ error: `未设置 ${provider} API Key` });\n text = await callOAI(provider, key, system, messages, maxTokens);\n\n } else {\n return res.status(400).json({ error: `未知 provider: ${provider}` });\n }\n\n return res.status(200).json({ text });\n } catch (err) {\n console.error(\"[AI Proxy Error]\", err.message);\n return res.status(500).json({ error: err.message || \"AI 请求失败\" });\n }\n}\n"}}}}