const form = document.getElementById("converterForm"); const jsCode = document.getElementById("jsCode"); const scriptNotes = document.getElementById("scriptNotes"); const scriptFile = document.getElementById("scriptFile"); const selectedFile = document.getElementById("selectedFile"); const skillRoot = document.getElementById("skillRoot"); const aiStatus = document.getElementById("aiStatus"); const aiServerCfg = document.getElementById("aiServerCfg"); const aiEffectiveCfg = document.getElementById("aiEffectiveCfg"); const useAi = document.getElementById("useAi"); const statusBox = document.getElementById("status"); const output = document.getElementById("output"); const resultList = document.getElementById("resultList"); const clearBtn = document.getElementById("clear"); const API_CONFIG_KEY = "sgclaw.converter.apiConfig"; let serverCfg = null; function setStatus(text, ok = true) { statusBox.textContent = text; statusBox.style.color = ok ? "#20a76b" : "#c62828"; } function fileToText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(String(e.target.result || "")); reader.onerror = () => reject(new Error("读取文件失败")); reader.readAsText(file); }); } function readApiConfigFromStorage() { try { const raw = localStorage.getItem(API_CONFIG_KEY); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } function trimText(v) { if (typeof v === "number" && Number.isFinite(v)) { return String(v); } return typeof v === "string" ? v.trim() : ""; } function parseTimeout(v) { const n = Number.parseInt(trimText(v), 10); return Number.isFinite(n) && n > 0 ? n : ""; } function maskApiKey(v) { const value = trimText(v); if (!value) { return "未配置"; } if (value.length <= 6) { return `${value.slice(0, 1)}***${value.slice(-1)}`; } return `${value.slice(0, 3)}***${value.slice(-3)}`; } function resolveAiConfig() { const localCfg = readApiConfigFromStorage() || {}; const effective = { base_url: trimText(localCfg.base_url) || trimText(serverCfg?.base_url), model: trimText(localCfg.model) || trimText(serverCfg?.model), timeout: parseTimeout(trimText(localCfg.timeout || serverCfg?.timeout)), api_key: trimText(localCfg.api_key), api_key_masked: localCfg.api_key ? maskApiKey(localCfg.api_key) : trimText(serverCfg?.api_key_masked), key_source: localCfg.api_key ? "local" : "server_env", }; return { local: localCfg, effective }; } function formatCfgLine(cfg) { const timeout = cfg.timeout ? `${cfg.timeout}s` : "未设置"; return `base_url=${cfg.base_url || "未设置"} | model=${cfg.model || "未设置"} | timeout=${timeout} | api_key=${cfg.api_key_masked || "未配置"}`; } function updateAiStatusSummary() { const resolved = resolveAiConfig(); aiEffectiveCfg.textContent = `实际生效: ${formatCfgLine(resolved.effective)}(${resolved.effective.key_source})`; } function formatAiState(result, requestedUseAi) { if (!requestedUseAi) { return "已关闭"; } if (result.ai_used) { return "已使用模型"; } if (result.ai_attempted) { return `已回退(${result.ai_fallback_reason || "模型调用失败"})`; } return `未调用模型(${result.ai_fallback_reason || "AI配置不可用"})`; } function formatElapsed(ms) { if (!Number.isFinite(ms) || ms < 0) { return "-"; } if (ms < 1000) { return `${Math.round(ms)} ms`; } return `${(ms / 1000).toFixed(2)} s`; } function formatSourceName(fileName, index, total, isManual, note) { const tag = note ? ` (${note})` : ""; if (!isManual) { return `${fileName}${tag}`; } return total > 1 ? `manual-${index + 1}${tag}` : `manual${tag}`; } scriptFile.addEventListener("change", () => { const files = scriptFile.files || []; if (!files.length) { selectedFile.textContent = "未选择文件"; return; } selectedFile.textContent = Array.from(files).map((f) => f.name).join(","); }); function renderResults(rows) { if (!rows.length) { resultList.innerHTML = ""; return; } resultList.innerHTML = rows .map((item) => { const statusClass = item.ok ? "result-ok" : "result-err"; const openBtn = item.ok && item.target_dir ? `` : ""; return `
${item.source} ${openBtn}
技能名:${item.skill_name || "-"}
目录:${item.target_dir || "-"}
耗时:${formatElapsed(item.elapsed_ms)}
实际AI配置:${item.ai_config_line || "-"}
AI状态:${item.ai_state || "-"}
${item.ok ? "" : `
${item.error || "处理失败"}
`}
`; }) .join(""); } resultList.addEventListener("click", async (e) => { const targetBtn = e.target.closest(".open-dir-btn"); if (!targetBtn) { return; } const target = decodeURIComponent(targetBtn.dataset.target || ""); if (!target) { setStatus("没有可打开的目录", false); return; } try { const resp = await fetch(`/api/open-dir?path=${encodeURIComponent(target)}`); const result = await resp.json().catch(() => ({})); if (!resp.ok) { setStatus(`打开目录失败:${result.error || "server error"}`, false); return; } setStatus(`已打开:${result.target_dir}`); } catch (err) { setStatus(`打开目录失败:${err.message}`, false); } }); form.addEventListener("submit", async (e) => { e.preventDefault(); setStatus("正在生成..."); const totalStart = performance.now(); const files = Array.from(scriptFile.files || []); const manualCode = jsCode.value.trim(); const note = scriptNotes.value.trim(); const tasks = []; for (const f of files) { tasks.push({ source: f.name, isManual: false, name: `${f.name.replace(/\.[^/.]+$/, "")}`, promise: fileToText(f).then((code) => ({ code, error: null })).catch((err) => ({ code: "", error: err.message || "读取文件失败" })), }); } if (manualCode) { tasks.push({ source: "manual-script", isManual: true, name: "manual-script", promise: Promise.resolve({ code: manualCode, error: null }), }); } if (!tasks.length) { setStatus("请上传脚本文件或粘贴脚本内容", false); return; } const reads = await Promise.all(tasks.map((t) => t.promise)); const { effective } = resolveAiConfig(); const aiConfig = { base_url: effective.base_url, model: effective.model, timeout: effective.timeout || undefined, api_key: effective.api_key, }; const jobs = tasks.map((task, idx) => { const payload = reads[idx] || {}; const code = payload.code || ""; const readError = payload.error; if (readError || !code.trim()) { const sourceName = formatSourceName(task.source, idx, tasks.length, task.isManual, note); return Promise.resolve({ ok: false, source: sourceName, skill_name: task.name, target_dir: "", elapsed_ms: 0, ai_config_line: formatCfgLine(effective), ai_state: useAi.checked ? "未执行(输入读取失败)" : "已关闭", error: readError || "脚本内容为空", }); } const displaySource = formatSourceName(task.source, idx, tasks.length, task.isManual, note); return (async () => { const itemStart = performance.now(); const req = { js_script_code: code, write: true, use_ai: useAi.checked, skill_root: skillRoot.value, skill_name: task.name, skill_use_case: note, ai_config: aiConfig, }; try { const resp = await fetch("/api/convert", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify(req), }); const result = await resp.json(); if (!resp.ok) { return { ok: false, source: displaySource, skill_name: task.name, target_dir: "", elapsed_ms: performance.now() - itemStart, ai_config_line: formatCfgLine(effective), ai_state: useAi.checked ? "请求失败" : "已关闭", error: result.error || "server error", }; } return { ok: true, source: displaySource, skill_name: result.skill_name, target_dir: result.target_dir, elapsed_ms: performance.now() - itemStart, ai_config_line: formatCfgLine(effective), ai_state: formatAiState(result, useAi.checked), raw: result, }; } catch (err) { return { ok: false, source: displaySource, skill_name: task.name, target_dir: "", elapsed_ms: performance.now() - itemStart, ai_config_line: formatCfgLine(effective), ai_state: useAi.checked ? "请求失败" : "已关闭", error: err.message || "请求失败", }; } })(); }); const results = await Promise.all(jobs); const success = results.filter((r) => r.ok).length; const fail = results.length - success; const totalElapsed = performance.now() - totalStart; setStatus(`完成:成功 ${success} / ${results.length},失败 ${fail},总耗时 ${formatElapsed(totalElapsed)}`, fail === 0); renderResults(results); output.textContent = JSON.stringify(results, null, 2); }); clearBtn.addEventListener("click", () => { jsCode.value = ""; scriptNotes.value = ""; resultList.innerHTML = ""; output.textContent = ""; setStatus(""); scriptFile.value = ""; selectedFile.textContent = "未选择文件"; }); async function loadConfig() { try { const resp = await fetch("/api/config"); const cfg = await resp.json(); serverCfg = cfg; aiStatus.textContent = `AI: ${cfg.ai_enabled ? "已启用" : "未启用"}`; aiServerCfg.textContent = `服务端: ${formatCfgLine(cfg)}`; updateAiStatusSummary(); const root = cfg.skill_root || "skills"; const hasOption = Array.from(skillRoot.options).some((o) => o.value === root); if (!hasOption) { const option = document.createElement("option"); option.value = root; option.textContent = root; skillRoot.appendChild(option); } skillRoot.value = root; } catch { serverCfg = {}; aiStatus.textContent = "AI: 未读取到配置"; aiServerCfg.textContent = "服务端: 读取失败"; updateAiStatusSummary(); skillRoot.value = "skills"; } } loadConfig(); setInterval(updateAiStatusSummary, 2000);