AI助手,一键生成应用
script dog · v1.0.4 · 编辑器扩展 · 15 次安装
简介
快速生成脚本,不用再折腾代码了!,注意仅限于文本编程!新增保存上一次提示词
匹配网址
*://*/*
脚本源码
(function () {
const editor = window.dogEditor;
if (editor && editor.programmingType !== 'text') {
return;
}
if (document.getElementById('ai-box')) {
return;
}
const DOG_DOC_URL = 'https://doc.scriptbox.space/reference/dog/';
const CONFIG_KEY = 'ai-provider-config';
const OLD_TOKEN_KEY = 'ai-token';
const DOC_CACHE_KEY = 'ai-dog-api-docs';
const DOC_MAX_LENGTH = 24000;
const REQUEST_TIMEOUT = 300000;
let dogDocMemory = null;
const root = document.createElement('div');
root.id = 'ai-box';
document.body.appendChild(root);
const shadow = root.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
* { box-sizing: border-box; }
.ai-container { position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 99999; font: 14px Arial, sans-serif; }
.ai-btn { width: 40px; height: 40px; border: 0; border-radius: 50%; color: #fff; background: #292929; cursor: pointer; font-weight: 700; }
.ai-btn::before { content: ''; position: absolute; inset: 0; border: 2px solid transparent; border-radius: 50%; background: linear-gradient(90deg,#ff00cc,#3333ff) border-box; -webkit-mask: linear-gradient(#fff 0 0) padding-box,linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
.ai-content { display: none; width: min(520px, calc(100vw - 2rem)); max-height: min(760px, calc(100vh - 2rem)); overflow: auto; padding: 16px; color: #fff; background: #191919; border: 1px solid #424242; border-radius: 10px; box-shadow: 0 12px 36px #0009; }
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.title { margin: 0; font-size: 17px; }
.subtitle { margin: 3px 0 0; color: #888; font-size: 11px; }
.env-label { color: #8ab4ff; }
.close { border: 0; color: #aaa; background: transparent; cursor: pointer; font-size: 20px; line-height: 1; }
.section { margin-top: 14px; }
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; margin: 0 0 6px; padding: 0; border: 0; color: inherit; background: transparent; cursor: pointer; font: inherit; text-align: left; }
.section-head:hover .section-title { color: #fff; }
.section-title { color: #bbb; font-size: 12px; font-weight: 600; }
.section-meta { display: flex; align-items: center; gap: 8px; }
.counter { color: #777; font-size: 11px; }
.caret { color: #888; font-size: 10px; transition: transform .15s ease; }
.section.collapsed .caret { transform: rotate(-90deg); }
.field { display: block; margin: 10px 0; color: #aaa; font-size: 12px; }
.field span { display: block; margin-bottom: 5px; }
input, select, textarea { width: 100%; border: 1px solid #444; border-radius: 5px; padding: 9px; outline: none; color: #fff; background: #141414; font: inherit; }
textarea { resize: vertical; min-height: 92px; }
.stream-output { min-height: 76px; max-height: 180px; overflow: auto; padding: 10px; color: #d7dee8; background: #101214; border: 1px solid #333; border-radius: 5px; font: 12px/1.6 Consolas, Monaco, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.stream-output:empty::before { content: 'AI 输出会实时显示在这里'; color: #666; }
.section.collapsed .stream-output { display: none; }
.stream-seg { margin: 0 0 8px; white-space: pre-wrap; overflow-wrap: anywhere; }
.stream-seg:last-child { margin-bottom: 0; }
.stream-seg.reasoning { color: #8b93a7; font-style: italic; }
.stream-seg.reasoning::before { content: '思考 '; color: #6b7280; font-style: normal; }
input:focus, select:focus, textarea:focus { border-color: #668cff; }
.btns { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; margin-top: 12px; }
button.btn { border: 0; border-radius: 5px; padding: 8px 11px; color: #fff; background: #303030; cursor: pointer; }
button.btn.primary { background: #405cc4; }
button.btn.danger { color: #ff9b9b; }
button.btn.stop { color: #ffb4b4; background: #542b2b; }
button:disabled { opacity: .55; cursor: wait; }
.setting-pannel { display: none; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 10px; }
.grid-full { grid-column: 1 / -1; }
.model-row { display: flex; gap: 7px; }
.model-row + .model-row { margin-top: 8px; }
.model-row .model, .model-row .model-select { flex: 1; }
.status { min-height: 18px; margin-top: 12px; color: #e8c45b; font-size: 12px; white-space: pre-wrap; }
.hint { margin: 8px 0 0; color: #777; font-size: 11px; line-height: 1.45; }
.loading { display: none; align-items: center; gap: 4px; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: #6ea8fe; animation: bounce 1s infinite ease-in-out; }
.dot:nth-child(2) { animation-delay: .15s; } .dot:nth-child(3) { animation-delay: .3s; }
@keyframes bounce { 50% { transform: translateY(-5px); } }
@media (max-width: 480px) { .ai-container { right: 1rem; bottom: 1rem; } .ai-content { width: calc(100vw - 2rem); padding: 12px; } .settings-grid { grid-template-columns: 1fr; } }
`;
shadow.appendChild(style);
const box = document.createElement('div');
box.innerHTML = `
<div class="ai-container">
<div class="ai-content">
<div class="topbar">
<div><h3 class="title">脚本狗 AI</h3><p class="subtitle">在当前文本编辑器中生成或修改脚本 · <span class="env-label"></span></p></div>
<button class="close" aria-label="关闭">×</button>
</div>
<div class="generate-panel">
<label class="field"><span>需求描述</span><textarea class="prompt" rows="4" placeholder="描述要生成、修改或修复的脚本"></textarea></label>
<div class="section">
<button class="section-head" type="button" aria-expanded="true">
<span class="section-title">AI 实时输出</span>
<span class="section-meta"><span class="counter">等待输出</span><span class="caret">▼</span></span>
</button>
<div class="stream-output" role="log" aria-live="polite"></div>
</div>
<div class="btns">
<button class="btn collapse">关闭</button>
<button class="btn settings">配置 AI</button>
<button class="btn stop">停止</button>
<button class="btn primary generate">生成代码</button>
<span class="loading btn" title="生成中,请稍候"><i class="dot"></i><i class="dot"></i><i class="dot"></i></span>
</div>
</div>
<div class="setting-pannel">
<label class="field"><span>当前配置</span><select class="profile"></select></label>
<div class="settings-grid">
<label class="field"><span>名称</span><input class="profile-name" placeholder="例如 DeepSeek"></label>
<label class="field"><span>Base URL</span><input class="base-url" placeholder="https://api.deepseek.com"></label>
<label class="field grid-full"><span>API Key</span><input class="api-key" type="password" autocomplete="off" placeholder="sk-..."></label>
</div>
<label class="field"><span>模型</span>
<div class="model-row">
<select class="model-select"></select>
<button class="btn danger remove-model" type="button">删除</button>
</div>
<div class="model-row">
<input class="model" placeholder="输入模型名,如 deepseek-chat" />
<button class="btn add-model" type="button">添加</button>
</div>
</label>
<div class="btns">
<button class="btn back">返回</button>
<button class="btn new">新增</button>
<button class="btn danger remove">删除</button>
<button class="btn primary save">保存</button>
</div>
</div>
<p class="status"></p>
<p class="hint">生成时会读取当前源码,并获取最新的脚本盒子 Dog API 文档。生成完成后会更新当前编辑器内容。</p>
</div>
<button class="ai-btn" aria-label="打开脚本狗 AI">AI</button>
</div>`;
shadow.appendChild(box);
const $ = (selector) => box.querySelector(selector);
const modal = $('.ai-content');
const aiButton = $('.ai-btn');
const prompt = $('.prompt');
const profileSelect = $('.profile');
const nameInput = $('.profile-name');
const baseUrlInput = $('.base-url');
const keyInput = $('.api-key');
const modelInput = $('.model');
const modelSelect = $('.model-select');
const status = $('.status');
const streamOutput = $('.stream-output');
const streamSection = $('.section');
const streamToggle = $('.section-head');
const counter = $('.counter');
const generateButton = $('.generate');
const loading = $('.loading');
const stopButton = $('.stop');
const generatePanel = $('.generate-panel');
const settingsPanel = $('.setting-pannel');
const envLabel = $('.env-label');
const SCRIPT_TYPE_LABELS = {
content: '内容脚本(注入当前网页)',
sidePanel: '侧边栏(独立沙盒)',
window: '独立窗口(独立沙盒)',
editorPlugin: '编辑器插件'
};
const defaultProvider = () => ({
id: 'deepseek-chat', name: 'DeepSeek Chat',
baseUrl: 'https://api.deepseek.com', apiKey: '',
models: ['deepseek-chat'], activeModel: 'deepseek-chat'
});
let config = { activeId: '', profiles: [] };
let loadingState = false;
let activeController = null;
function setStatus(message, error) {
status.textContent = message || '';
status.style.color = error ? '#ff9b9b' : '#e8c45b';
}
async function cacheGet(key) {
if (editor && typeof editor.getCache === 'function') return editor.getCache(key);
return null;
}
function cacheSet(key, value) {
if (editor && typeof editor.setCache === 'function') return editor.setCache(key, value);
return null;
}
function parseCacheValue(value) {
if (value && typeof value === 'object') return value;
if (typeof value !== 'string' || !value.trim()) return value;
try { return JSON.parse(value); } catch (_) { return value; }
}
function normalizeProvider(value) {
const parsed = parseCacheValue(value);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
if (!parsed.apiKey && !parsed.baseUrl && !parsed.model && !parsed.models) return null;
const nestedKey = parseCacheValue(parsed.apiKey);
const nestedProvider = findProvider(nestedKey);
const apiKey = nestedProvider
? nestedProvider.apiKey
: nestedKey && typeof nestedKey === 'object'
? String(nestedKey.apiKey || '')
: String(parsed.apiKey || '');
let models = Array.isArray(parsed.models) ? parsed.models.map(item => String(item).trim()).filter(Boolean) : [];
if (!models.length && parsed.model) models = [String(parsed.model).trim()].filter(Boolean);
const activeModel = models.includes(parsed.activeModel) ? String(parsed.activeModel) : (models[0] || '');
return {
id: String(parsed.id || `provider-${Date.now()}`),
name: String(parsed.name || parsed.model || activeModel || '未命名配置'),
baseUrl: String(parsed.baseUrl || ''),
apiKey,
models,
activeModel
};
}
function findProvider(value) {
const parsed = parseCacheValue(value);
const direct = normalizeProvider(parsed);
if (direct) return direct;
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.profiles)) {
return parsed.profiles.map(normalizeProvider).find(Boolean) || null;
}
return null;
}
function normalizeLegacyToken(value) {
const provider = findProvider(value);
if (provider) return provider.apiKey;
return typeof value === 'string' ? value.trim() : '';
}
async function loadConfig() {
let raw = await cacheGet(CONFIG_KEY).catch(() => null);
const parsedConfig = parseCacheValue(raw);
config = parsedConfig && typeof parsedConfig === 'object' && !Array.isArray(parsedConfig)
? parsedConfig
: { activeId: '', profiles: [] };
if (!Array.isArray(config.profiles)) config.profiles = [];
if (!config.profiles.length) {
const oldToken = await cacheGet(OLD_TOKEN_KEY).catch(() => '');
const provider = defaultProvider();
const legacyProvider = findProvider(oldToken);
if (legacyProvider) {
provider.name = legacyProvider.name;
provider.baseUrl = legacyProvider.baseUrl || provider.baseUrl;
if (legacyProvider.models && legacyProvider.models.length) {
provider.models = legacyProvider.models;
provider.activeModel = legacyProvider.activeModel || legacyProvider.models[0];
}
provider.apiKey = legacyProvider.apiKey;
} else {
provider.apiKey = normalizeLegacyToken(oldToken);
}
config.profiles = [provider];
config.activeId = provider.id;
await persistConfig();
}
config.profiles = config.profiles.map(item => normalizeProvider(item) || defaultProvider());
if (!config.profiles.some(item => item.id === config.activeId)) config.activeId = config.profiles[0].id;
await persistConfig();
renderProfiles();
}
async function persistConfig() {
return cacheSet(CONFIG_KEY, JSON.stringify(config));
}
function activeProvider() {
return config.profiles.find(item => item.id === config.activeId) || config.profiles[0];
}
function renderProfiles() {
profileSelect.innerHTML = '';
config.profiles.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name || item.activeModel || item.id;
profileSelect.appendChild(option);
});
profileSelect.value = config.activeId;
showProvider();
}
function showProvider() {
const provider = activeProvider();
if (!provider) return;
nameInput.value = provider.name || '';
baseUrlInput.value = provider.baseUrl || '';
keyInput.value = provider.apiKey || '';
modelInput.value = '';
renderModelSelect();
}
function renderModelSelect() {
const provider = activeProvider();
modelSelect.innerHTML = '';
const models = provider?.models || [];
if (!models.length) {
const option = document.createElement('option');
option.value = '';
option.textContent = '尚未添加模型';
modelSelect.appendChild(option);
modelSelect.disabled = true;
return;
}
modelSelect.disabled = false;
models.forEach(item => {
const option = document.createElement('option');
option.value = item;
option.textContent = item;
modelSelect.appendChild(option);
});
modelSelect.value = provider.activeModel || models[0];
}
function readProvider() {
const current = activeProvider();
const enteredKey = keyInput.value.trim();
const fallbackModel = current?.activeModel || (current?.models && current.models[0]) || '';
return {
id: current?.id || `provider-${Date.now()}`,
name: nameInput.value.trim() || fallbackModel || '未命名配置',
baseUrl: baseUrlInput.value.trim(),
apiKey: normalizeLegacyToken(enteredKey),
models: current?.models ? [...current.models] : [],
activeModel: current?.activeModel || ''
};
}
function addModel() {
const provider = activeProvider();
if (!provider) return;
const model = modelInput.value.trim();
if (!model) { setStatus('请输入模型名称。', true); return; }
if (!Array.isArray(provider.models)) provider.models = [];
if (!provider.models.includes(model)) provider.models.push(model);
provider.activeModel = model;
modelInput.value = '';
persistConfig();
renderModelSelect();
setStatus(`已添加模型:${model}(点击保存后持久生效)。`);
}
function removeModel() {
const provider = activeProvider();
if (!provider || !provider.models || !provider.models.length) {
setStatus('当前没有可删除的模型。', true);
return;
}
const target = provider.activeModel || provider.models[0];
provider.models = provider.models.filter(item => item !== target);
provider.activeModel = provider.models[0] || '';
persistConfig();
renderModelSelect();
setStatus(`已删除模型:${target}(点击保存后持久生效)。`);
}
function normalizeEndpoint(baseUrl) {
let url = baseUrl.trim().replace(/\/+$/, '');
if (!url) return '';
return /\/chat\/completions$/i.test(url) ? url : `${url}/chat/completions`;
}
function normalizeApiKey(value) {
return String(value || '')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.trim();
}
function validateHeaderValue(value, name) {
if (/[\u0000-\u0008\u000A-\u001F\u007F-\u00FF]/.test(value)) {
throw new Error(`${name} 包含无效字符,请重新粘贴 API Key。`);
}
for (const char of value) {
if (char.codePointAt(0) > 255) {
throw new Error(`${name} 包含非 ASCII 字符,请检查是否粘贴了中文提示或特殊空格。`);
}
}
}
async function fetchDogDocs(signal) {
const now = Date.now();
if (dogDocMemory && now - dogDocMemory.fetchedAt < 10 * 60 * 1000) return dogDocMemory.text;
try {
const response = await fetch(DOG_DOC_URL, { credentials: 'omit', signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const documentNode = new DOMParser().parseFromString(html, 'text/html');
const main = documentNode.querySelector('main') || documentNode.body;
main.querySelectorAll('script,style,nav,header,footer,aside,svg').forEach(node => node.remove());
const text = (main.textContent || '').replace(/[ \t]+/g, ' ').replace(/\n\s*\n\s*\n/g, '\n\n').trim().slice(0, DOC_MAX_LENGTH);
if (!text) throw new Error('文档内容为空');
dogDocMemory = { text, fetchedAt: now };
await cacheSet(DOC_CACHE_KEY, JSON.stringify(dogDocMemory));
return text;
} catch (error) {
if (error.name === 'AbortError') throw error;
let cached = await cacheGet(DOC_CACHE_KEY).catch(() => null);
try { cached = cached ? JSON.parse(cached) : null; } catch (_) { cached = null; }
if (cached?.text) {
dogDocMemory = cached;
return cached.text;
}
throw new Error(`无法获取官方 Dog API 文档:${error.message || error}`);
}
}
function currentSource() {
if (editor && typeof editor.getEditorDoc === 'function') return editor.getEditorDoc() || '';
return '';
}
function scriptTypeOf() {
const type = editor && editor.scriptType;
return SCRIPT_TYPE_LABELS[type] ? type : 'content';
}
function updateEnvironmentLabel() {
envLabel.textContent = SCRIPT_TYPE_LABELS[scriptTypeOf()];
}
function buildEnvironmentPrompt(scriptType) {
const base = '你是脚本盒子 JavaScript 编程助手。生成的代码必须兼容脚本盒子脚本环境。';
if (scriptType === 'sidePanel') {
return `${base}\n当前脚本类型:侧边栏脚本,运行在独立沙盒中。\n- 不能访问当前网页的 DOM,也不要假设存在网页环境。\n- 所有 DOM 操作请直接使用浏览器原生 DOM API(document、element 等),不要通过 Dog.xx 进行 DOM 操作。\n- 不要使用内容脚本专属的网页注入能力。\n- 常用于生成小工具、小游戏和面板交互。`;
}
if (scriptType === 'window') {
return `${base}\n当前脚本类型:独立窗口脚本,运行在独立沙盒中。\n- 不能访问当前网页的 DOM,也不要假设存在网页环境。\n- 所有 DOM 操作请直接使用浏览器原生 DOM API(document、element 等),不要通过 Dog.xx 进行 DOM 操作。\n- 不要使用内容脚本专属的网页注入能力。\n- 常用于生成小工具、小游戏和窗口交互界面。`;
}
if (scriptType === 'editorPlugin') {
return `${base}\n当前脚本类型:编辑器插件,运行在编辑器沙盒内,不是网页注入脚本,也不是普通沙盒脚本。\n- 通过全局对象 window.dogEditor 与编辑器交互,可用接口:\n - usePlugin(plugin):注册 Blockly 插件,plugin = { name, colour, blocks, grenrators }\n - getValue(block, generator, name)\n - getLines(block, generator, name)\n - getEditorDoc(): string\n - setEditorDoc(doc: string)\n - getCache(key): Promise<string>\n - setCache(key: string, value: string)\n - openLink(url: string)\n - programmingType: 'both' | 'text' | 'blockly'\n - scriptType\n- 插件代码会以 IIFE 形式注入并立即执行,请直接使用上述接口。\n- 不要使用内容脚本专属的网页 DOM 能力,也不要假设存在沙盒外的 Dog 能力。`;
}
return `${base}\n当前脚本类型:内容脚本,会注入并运行在当前网页环境。\n- 可以访问当前网页 DOM 和网页上下文。\n- 可以使用内容脚本可用的完整 Dog.xx API。\n- 优先考虑网页注入场景。`;
}
function setLoading(value) {
loadingState = value;
generateButton.style.display = value ? 'none' : '';
loading.style.display = value ? 'inline-flex' : 'none';
stopButton.style.display = value ? '' : 'none';
$('.settings').disabled = value;
$('.profile').disabled = value;
}
function openModal() { updateEnvironmentLabel(); modal.style.display = 'block'; aiButton.style.display = 'none'; }
function closeModal() { modal.style.display = 'none'; aiButton.style.display = 'block'; }
function showSettings() { generatePanel.style.display = 'none'; settingsPanel.style.display = 'block'; }
function showGenerate() { generatePanel.style.display = 'block'; settingsPanel.style.display = 'none'; }
let streamSegments = {};
function resetStreamOutput() {
streamOutput.textContent = '';
streamSegments = {};
counter.textContent = '等待输出';
}
function appendStreamText(kind, text) {
if (!text) return;
let segment = streamSegments[kind];
if (!segment) {
segment = document.createElement('div');
segment.className = `stream-seg ${kind}`;
streamOutput.appendChild(segment);
streamSegments[kind] = segment;
}
segment.appendChild(document.createTextNode(text));
streamOutput.scrollTop = streamOutput.scrollHeight;
}
function stopGeneration() {
if (!activeController) return;
activeController.abort();
setStatus('已停止生成,预览内容未写入编辑器。', true);
}
async function generate() {
if (loadingState) return;
const provider = activeProvider();
const model = provider?.activeModel || (provider?.models && provider.models[0]) || '';
if (!provider || !provider.apiKey || !provider.baseUrl || !model) {
showSettings();
setStatus('请先完整填写当前 AI 配置(含至少一个模型)。', true);
return;
}
const requestText = prompt.value.trim();
if (!requestText) { setStatus('请输入需求。', true); return; }
const endpoint = normalizeEndpoint(provider.baseUrl);
if (!endpoint) { setStatus('Base URL 无效。', true); return; }
const apiKey = normalizeApiKey(provider.apiKey);
try {
validateHeaderValue(`Bearer ${apiKey}`, 'API Key');
} catch (error) {
setStatus(error.message, true);
return;
}
setLoading(true);
resetStreamOutput();
const scriptType = scriptTypeOf();
const needDogDocs = scriptType === 'content';
setStatus(needDogDocs ? '正在获取 Dog API 文档并生成代码...' : '正在生成代码...');
activeController = new AbortController();
try {
const dogDocs = needDogDocs ? await fetchDogDocs(activeController.signal) : '';
const source = currentSource();
const sourceContext = source
? `当前编辑器中的完整 JavaScript 源码如下。修改、修复或重构时必须基于此源码,并返回修改后的完整源码。\n<current_code>\n${source}\n</current_code>`
: '当前编辑器没有可读取的文本源码,请生成新的脚本。';
const messages = [
{ role: 'system', content: buildEnvironmentPrompt(scriptType) },
{ role: 'system', content: sourceContext },
{ role: 'system', content: '只返回完整的 JavaScript 源码,不要 Markdown 代码块,不要前言、解释或结尾说明。' },
{ role: 'user', content: requestText }
];
if (needDogDocs) {
messages.splice(1, 0, { role: 'system', content: `以下是从脚本盒子官方文档实时获取的最新 Dog API 文档。只能使用文档中存在的 Dog.xx API,不要猜测不存在的接口:\n\n${dogDocs}` });
}
const response = await fetchWithTimeout(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({
model,
stream: true,
messages
})
}, activeController);
if (!response.ok) {
const detail = await response.text();
throw new Error(`AI 请求失败 (${response.status})${detail ? `:${detail.slice(0, 300)}` : ''}`);
}
const code = await readStreamingCode(response);
const cleanCode = code.replace(/^\s*```(?:javascript|js)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
if (!cleanCode) throw new Error('AI 没有返回有效代码。');
editor.setEditorDoc(cleanCode);
setStatus('生成完成。');
} catch (error) {
if (error.name === 'AbortError') return;
setStatus(error.message || '生成失败。', true);
} finally {
activeController = null;
setLoading(false);
}
}
async function fetchWithTimeout(url, options, controller = new AbortController()) {
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
try { return await fetch(url, { ...options, signal: controller.signal }); }
catch (error) {
if (error.name === 'AbortError') {
if (controller.signal.aborted) throw error;
throw new Error('请求超时。');
}
throw error;
}
finally { clearTimeout(timer); }
}
async function readStreamingCode(response) {
if (!response.body || typeof response.body.getReader !== 'function') {
throw new Error('AI 服务未返回可读取的流式响应。');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let code = '';
let finished = false;
const processLine = (line) => {
const data = line.trim();
if (!data || !data.startsWith('data:')) return;
const payload = data.slice(5).trim();
if (payload === '[DONE]') {
finished = true;
return;
}
let parsed;
try {
parsed = JSON.parse(payload);
} catch (_) {
return;
}
const choice = parsed?.choices?.[0];
const reasoning = choice?.delta?.reasoning_content;
const content = choice?.delta?.content ?? choice?.message?.content;
if (typeof reasoning === 'string' && reasoning) {
appendStreamText('reasoning', reasoning);
}
if (typeof content === 'string' && content) {
code += content;
appendStreamText('content', content);
counter.textContent = `${code.length.toLocaleString()} 字符`;
setStatus(`正在生成代码... 已接收 ${code.length} 个字符`);
}
};
while (!finished) {
const { value, done } = await reader.read();
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() || '';
lines.forEach(processLine);
if (done) break;
}
if (buffer) processLine(buffer);
return code;
}
async function saveProvider() {
const provider = readProvider();
if (!provider.baseUrl || !provider.apiKey || !provider.models.length) {
setStatus('Base URL、API Key 和至少一个模型不能为空。', true);
return;
}
const index = config.profiles.findIndex(item => item.id === config.activeId);
if (index >= 0) config.profiles[index] = provider;
else config.profiles.push(provider);
config.activeId = provider.id;
await persistConfig();
renderProfiles();
setStatus('配置已保存。');
}
function addProvider() {
const provider = { id: `provider-${Date.now()}`, name: '新配置', baseUrl: 'https://api.openai.com/v1', apiKey: '', models: [], activeModel: '' };
config.profiles.push(provider);
config.activeId = provider.id;
renderProfiles();
setStatus('已创建新配置,请填写并保存。');
}
async function removeProvider() {
if (config.profiles.length <= 1) { setStatus('至少保留一个配置。', true); return; }
config.profiles = config.profiles.filter(item => item.id !== config.activeId);
config.activeId = config.profiles[0].id;
await persistConfig();
renderProfiles();
setStatus('配置已删除。');
}
aiButton.addEventListener('click', openModal);
$('.collapse').addEventListener('click', closeModal);
$('.close').addEventListener('click', closeModal);
$('.settings').addEventListener('click', showSettings);
$('.back').addEventListener('click', showGenerate);
stopButton.addEventListener('click', stopGeneration);
generateButton.addEventListener('click', generate);
$('.save').addEventListener('click', saveProvider);
$('.new').addEventListener('click', addProvider);
$('.remove').addEventListener('click', removeProvider);
$('.add-model').addEventListener('click', addModel);
$('.remove-model').addEventListener('click', removeModel);
modelSelect.addEventListener('change', () => {
const provider = activeProvider();
if (!provider) return;
provider.activeModel = modelSelect.value;
persistConfig();
});
profileSelect.addEventListener('change', () => { config.activeId = profileSelect.value; persistConfig(); showProvider(); });
streamToggle.addEventListener('click', () => {
const collapsed = streamSection.classList.toggle('collapsed');
streamToggle.setAttribute('aria-expanded', String(!collapsed));
});
stopButton.style.display = 'none';
resetStreamOutput();
updateEnvironmentLabel();
loadConfig().catch(error => setStatus(`配置加载失败:${error.message || error}`, true));
})();