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