Files
skill-convertor/web/app.js
2026-04-15 01:17:01 +08:00

353 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);