353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
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
|
||
? `<button type="button" class="open-dir-btn" data-target="${encodeURIComponent(item.target_dir)}">打开目录</button>`
|
||
: "";
|
||
return `
|
||
<div class="result-item ${statusClass}">
|
||
<div class="result-head">
|
||
<strong>${item.source}</strong>
|
||
${openBtn}
|
||
</div>
|
||
<div class="result-meta">技能名:${item.skill_name || "-"}</div>
|
||
<div class="result-meta">目录:${item.target_dir || "-"}</div>
|
||
<div class="result-meta">耗时:${formatElapsed(item.elapsed_ms)}</div>
|
||
<div class="result-meta">实际AI配置:${item.ai_config_line || "-"}</div>
|
||
<div class="result-meta">AI状态:${item.ai_state || "-"}</div>
|
||
${item.ok ? "" : `<div class="result-meta err">${item.error || "处理失败"}</div>`}
|
||
</div>
|
||
`;
|
||
})
|
||
.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);
|