\n
\n 📋 Request Payload\n \n
\n
\n
`;\n\n // 用 textContent 写入避免 XSS\n overlay.querySelector('#_payload_pre').textContent = JSON.stringify(payload, null, 2);\n\n document.body.appendChild(overlay);\n overlay.querySelector('#_payload_close').onclick = () => overlay.remove();\n overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });\n}\n\n// ─────────────────────────────────────────────\n// 辅助:执行一轮 LLM 调用并流式输出到 agentContent\n// 返回 { fullText, action }\n// ─────────────────────────────────────────────\nasync function streamAgentTurn(history, agentContent, isContinuation, signal) {\n // 续轮时插入分隔条\n if (isContinuation) {\n const sep = document.createElement('div');\n sep.style.cssText = [\n 'margin:6px 0 4px', `border-top:1px dashed ${themeVar('--border')}`,\n 'font-size:11px', `color:${themeVar('--muted-2')}`, 'padding-top:4px',\n ].join(';');\n sep.textContent = '↩ Continuing…';\n agentContent.appendChild(sep);\n scrollToBottom();\n }\n\n // ── 加载配置 ──\n const cfg = await loadConfig();\n const conf = PROVIDERS[cfg.provider];\n if (!conf) throw new Error(`Unknown provider: ${cfg.provider}`);\n\n const requestUrl = conf.needsEndpoint\n ? (cfg.endpoints && typeof cfg.endpoints[cfg.provider] === 'string' ? cfg.endpoints[cfg.provider].trim() : '')\n : conf.url;\n\n const apiKey = (cfg.apiKeys && cfg.apiKeys[cfg.provider]) || conf.defaultKey || \"\";\n if (!apiKey && cfg.provider !== \"ollama\" && cfg.provider !== \"copilot\") {\n throw new Error(`${conf.name}: API Key not configured. Please open the settings to configure it.`);\n }\n if (conf.needsEndpoint && !requestUrl) {\n throw new Error(`${conf.name}: Endpoint URL not configured. Please open the settings to configure it.`);\n }\n const model = cfg.model;\n if (!model) throw new Error(\"Model name not set. Please open the settings to configure it.\");\n\n // 更新顶栏\n modelInfoEl.textContent = conf.name + \" · \" + model;\n\n const effectivePrompt = buildEffectiveSystemPrompt(cfg);\n const allMessages = effectivePrompt\n ? [{ role: \"system\", content: effectivePrompt }, ...history]\n : [...history];\n const requestPayload = { provider: cfg.provider, model, url: requestUrl, messages: allMessages };\n\n // 本轮流式输出容器\n const stepEl = document.createElement('div');\n agentContent.appendChild(stepEl);\n\n let fullText = \"\";\n let inReasoning = false;\n\n if (conf.format === \"copilot\") {\n // ── GitHub Copilot (OpenAI 局式 + 特殊认证头) ─────────────────\n let copilotToken;\n try {\n copilotToken = await GitHubAuth.getValidCopilotToken();\n } catch (e) {\n if (e.message === 'NOT_AUTHED') {\n throw new Error('GitHub Copilot 未授权,请在设置中连接 GitHub 账号');\n }\n throw e;\n }\n\n const res = await fetch(conf.url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${copilotToken}`,\n 'Content-Type': 'application/json',\n 'Editor-Version': 'vscode/1.97.2',\n 'Editor-Plugin-Version': 'copilot-chat/0.26.1',\n 'Copilot-Integration-Id': 'vscode-chat',\n 'Openai-Intent': 'conversation-panel',\n 'User-Agent': 'GitHubCopilotChat/0.26.1',\n 'X-Github-Api-Version': '2024-12-15',\n },\n signal,\n body: JSON.stringify({ model, messages: allMessages, stream: true, max_tokens: 4096 }),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n if (res.status === 401 || res.status === 403) {\n // Copilot token 失效,清理缓存让下次重新获取\n await new Promise(r => chrome.storage.sync.remove(['copilotToken', 'copilotTokenExpiry'], r));\n throw new Error(`Copilot 认证失效 (${res.status}),请重新连接 GitHub 账号`);\n }\n throw new Error(`HTTP ${res.status}${body ? ': ' + body.slice(0, 300) : ''}`);\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop();\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed === 'data: [DONE]') continue;\n const jsonStr = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;\n try {\n const parsed = JSON.parse(jsonStr);\n const content = parsed?.choices?.[0]?.delta?.content || '';\n if (content) { fullText += content; stepEl.innerHTML = renderMarkdown(fullText); scrollToBottom(); }\n } catch { /* 非 JSON 行忽略 */ }\n }\n }\n } catch (err) {\n if (err.name !== 'AbortError') throw err;\n // AbortError:带着已累积的部分文本继续执行,下方 signal.aborted 检查会提前返回\n }\n\n } else if (conf.format === \"gemini\") {\n // ── Google Gemini ─────────────────────────────────────────\n const geminiUrl = `${conf.url}/${model}:streamGenerateContent?alt=sse&key=${apiKey}`;\n const geminiContents = history.map(m => ({\n role: m.role === \"assistant\" ? \"model\" : \"user\",\n parts: [{ text: m.content }],\n }));\n\n const res = await fetch(geminiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n signal,\n body: JSON.stringify({\n ...(effectivePrompt ? { systemInstruction: { parts: [{ text: effectivePrompt }] } } : {}),\n contents: geminiContents,\n generationConfig: { maxOutputTokens: 2000 },\n }),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`HTTP ${res.status}${body ? \": \" + body.slice(0, 300) : \"\"}`);\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop();\n for (const line of lines) {\n if (!line.trim().startsWith(\"data: \")) continue;\n try {\n const chunk = JSON.parse(line.trim().slice(6));\n const text = chunk?.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n if (text) { fullText += text; stepEl.innerHTML = renderMarkdown(fullText); scrollToBottom(); }\n } catch { /* 非 JSON 行忽略 */ }\n }\n }\n } catch (err) {\n if (err.name !== 'AbortError') throw err;\n }\n\n } else {\n // ── OpenAI 兼容格式(Ollama / OpenRouter / DeepSeek)─────\n const isDeepSeek = cfg.provider === \"deepseek\";\n const reqBody = {\n model,\n messages: allMessages,\n stream: true,\n };\n if (cfg.provider === \"ollama\") {\n reqBody.options = { num_predict: 2000 };\n } else {\n reqBody.max_tokens = 2000;\n }\n\n const res = await fetch(requestUrl, {\n method: \"POST\",\n headers: {\n \"Authorization\": \"Bearer \" + apiKey,\n \"Content-Type\": \"application/json\",\n },\n signal,\n body: JSON.stringify(reqBody),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`HTTP ${res.status}${body ? \": \" + body.slice(0, 300) : \"\"}`);\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop();\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed === \"data: [DONE]\") continue;\n const jsonStr = trimmed.startsWith(\"data: \") ? trimmed.slice(6) : trimmed;\n try {\n const parsed = JSON.parse(jsonStr);\n const delta = parsed?.choices?.[0]?.delta;\n const reasoning = isDeepSeek ? (delta?.reasoning_content || \"\") : \"\";\n const content = delta?.content || \"\";\n\n if (reasoning) {\n if (!inReasoning) { inReasoning = true; fullText += \"*思考中...*\\n\\n> \"; }\n fullText += reasoning.replace(/\\n/g, \"\\n> \");\n } else if (content) {\n if (inReasoning) { inReasoning = false; fullText += \"\\n\\n---\\n\\n\"; }\n fullText += content;\n }\n if (reasoning || content) { stepEl.innerHTML = renderMarkdown(fullText); scrollToBottom(); }\n } catch { /* 非 JSON 行忽略 */ }\n }\n }\n } catch (err) {\n if (err.name !== 'AbortError') throw err;\n }\n }\n\n // 用户中途停止:带着已累积内容提前返回,不解析 action 也不添加报文按钮\n if (signal?.aborted) {\n return { fullText, actions: [] };\n }\n\n // 解析全部