Files
vision-tool/web/js/app.js
Geliebte f9cdeb5d64 feat: 认证简化为固定进入密钥 + 页面增强(API 文档/主题切换/版权署名)
- 移除管理员初始化/登录/JWT 流程,改为固定进入密钥(19735500),
  首次启动自动初始化到数据库 settings 表(可改库更新)
- analyze 接口支持可选密钥鉴权:X-Access-Key 请求头或 access_key 字段,
  带密钥强制校验(错误 401),不带密钥保持免鉴权
- 解锁后顶栏"复制 API 文档"按钮:以当前访问 URL 为基础地址动态生成
  Markdown 文档(免鉴权/带密钥两种 curl 示例,供智能体直接调用)
- 白昼/黑夜主题切换:CSS 变量驱动,localStorage 持久化,默认跟随系统
- 修复 hidden 属性被 display:flex 覆盖导致解锁层无法隐藏的问题
- 页面去除"免费/Agnes"文案,页脚署名青梧映星软件开发工作室
- build.bat/build.sh 简化为 Linux 交叉编译(CGO_ENABLED=0)
- 依赖精简:移除 golang-jwt/bcrypt,直接依赖仅 gin/sqlite/gorm/yaml

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 01:21:18 +08:00

275 lines
8.0 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.
// ============================================================================
// Vision Tool — 在线图片识别(原生单页)
// 进入方式:固定进入密钥(存于数据库,首次启动自动初始化)
// ============================================================================
const $ = (id) => document.getElementById(id);
// ---------- 白昼/黑夜主题 ----------
const THEME_KEY = "vt_theme";
function currentTheme() {
const saved = localStorage.getItem(THEME_KEY);
if (saved === "dark" || saved === "light") return saved;
// 未设置时跟随系统偏好
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem(THEME_KEY, theme);
$("themeBtn").textContent = theme === "dark" ? "☀️" : "🌙";
}
applyTheme(currentTheme());
$("themeBtn").addEventListener("click", () => {
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
});
// ---------- API 封装 ----------
async function api(path, options = {}) {
const headers = options.headers || {};
if (!(options.body instanceof FormData)) {
headers["Content-Type"] = "application/json";
}
const resp = await fetch(path, { ...options, headers });
let data;
try {
data = await resp.json();
} catch {
throw new Error(`HTTP ${resp.status}: 响应解析失败`);
}
if (data.code !== 0) throw new Error(data.message || `HTTP ${resp.status}`);
return data.data;
}
// ---------- 进入密钥 ----------
const KEY_STORAGE = "vt_access_key";
function isUnlocked() {
return !!localStorage.getItem(KEY_STORAGE);
}
function refreshUnlockState() {
const unlocked = isUnlocked();
$("unlockLayer").hidden = unlocked;
const area = $("accountArea");
area.innerHTML = "";
if (!unlocked) return;
const chip = document.createElement("span");
chip.className = "user-chip";
chip.textContent = "🔓 已解锁";
area.appendChild(chip);
// 一键复制 API 文档(以当前 URL 为基础地址)
const docBtn = document.createElement("button");
docBtn.className = "btn btn-ghost";
docBtn.textContent = "📋 复制 API 文档";
docBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(buildApiDoc());
docBtn.textContent = "已复制 ✓";
setTimeout(() => (docBtn.textContent = "📋 复制 API 文档"), 1500);
} catch {
alert("复制失败,请手动复制");
}
};
area.appendChild(docBtn);
const logout = document.createElement("button");
logout.className = "btn btn-ghost";
logout.textContent = "退出";
logout.onclick = () => {
localStorage.removeItem(KEY_STORAGE);
refreshUnlockState();
};
area.appendChild(logout);
}
// 生成 API 文档(Markdown,供智能体/开发者直接调用)
function buildApiDoc() {
const base = window.location.origin;
const key = localStorage.getItem(KEY_STORAGE) || "19735500";
return `# Vision Tool — 在线图片识别 API
这是一个在线图片识别服务。
智能体可通过下面的 HTTP API 直接调用,识别图片内容。
## 基础信息
- 服务地址: \`${base}\`
- 进入密钥: \`${key}\`(可选,用于鉴权调用)
## 接口:图片识别
POST \`${base}/api/v1/vision/analyze\`
multipart/form-data 字段:
- \`images\`: 图片文件,可传多个(支持 png/jpg/gif/webp/bmp/tiff)
- \`prompt\`: 可选,自定义分析提问(不传则默认详细描述图片内容)
### 免鉴权调用示例
\`\`\`bash
curl -X POST ${base}/api/v1/vision/analyze \\
-F "images=@photo.png" \\
-F "prompt=这张图里有什么?"
\`\`\`
### 带密钥调用示例
\`\`\`bash
curl -X POST ${base}/api/v1/vision/analyze \\
-H "X-Access-Key: ${key}" \\
-F "images=@photo.png"
\`\`\`
## 响应格式
\`\`\`json
{
"code": 0,
"message": "ok",
"data": {
"result": "识别结果文本",
"images": ["/uploads/xxx.png"],
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }
}
}
\`\`\`
## 其他接口
- GET \`${base}/api/health\` — 健康检查
- POST \`${base}/api/v1/auth/verify\` — 校验进入密钥 \`{"key": "${key}"}\`
## 用法建议
需要识别图片内容时,将图片路径交给智能体,让它执行上述 curl 命令即可。
`;
}
$("unlockBtn").addEventListener("click", unlock);
$("unlockKey").addEventListener("keydown", (e) => { if (e.key === "Enter") unlock(); });
async function unlock() {
const key = $("unlockKey").value.trim();
if (!key) {
$("unlockError").textContent = "请输入进入密钥";
return;
}
try {
await api("/api/v1/auth/verify", {
method: "POST",
body: JSON.stringify({ key }),
});
localStorage.setItem(KEY_STORAGE, key);
$("unlockKey").value = "";
$("unlockError").textContent = "";
refreshUnlockState();
} catch (err) {
$("unlockError").textContent = err.message;
}
}
// ---------- 图片选择与预览 ----------
let selectedFiles = [];
let analyzing = false;
const dropzone = $("dropzone");
const fileInput = $("fileInput");
dropzone.addEventListener("click", () => fileInput.click());
dropzone.addEventListener("dragover", (e) => { e.preventDefault(); dropzone.classList.add("dragover"); });
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("dragover"));
dropzone.addEventListener("drop", (e) => {
e.preventDefault();
dropzone.classList.remove("dragover");
addFiles(e.dataTransfer.files);
});
fileInput.addEventListener("change", () => { addFiles(fileInput.files); fileInput.value = ""; });
function addFiles(files) {
for (const f of files) {
if (!f.type.startsWith("image/")) continue;
if (f.size > 10 * 1024 * 1024) { alert(`图片过大(>10MB): ${f.name}`); continue; }
selectedFiles.push(f);
}
renderPreviews();
}
function renderPreviews() {
const list = $("previewList");
list.innerHTML = "";
selectedFiles.forEach((f, i) => {
const item = document.createElement("div");
item.className = "preview-item";
const img = document.createElement("img");
img.src = URL.createObjectURL(f);
img.alt = f.name;
const rm = document.createElement("button");
rm.className = "remove";
rm.textContent = "×";
rm.onclick = () => {
selectedFiles.splice(i, 1);
renderPreviews();
};
item.append(img, rm);
list.appendChild(item);
});
$("analyzeBtn").disabled = selectedFiles.length === 0 || analyzing;
}
// ---------- 识别 ----------
$("analyzeBtn").addEventListener("click", analyze);
async function analyze() {
if (analyzing || selectedFiles.length === 0) return;
analyzing = true;
renderPreviews();
const btn = $("analyzeBtn");
btn.innerHTML = '<span class="loading"><span class="spinner"></span>识别中…</span>';
const resultCard = $("resultCard");
resultCard.hidden = false;
$("resultBody").textContent = "正在识别图片内容,请稍候(最长约 2 分钟)…";
$("resultMeta").textContent = "";
const fd = new FormData();
selectedFiles.forEach((f) => fd.append("images", f, f.name));
const prompt = $("promptInput").value.trim();
if (prompt) fd.append("prompt", prompt);
try {
const data = await api("/api/v1/vision/analyze", { method: "POST", body: fd });
$("resultBody").textContent = data.result;
if (data.usage) {
$("resultMeta").textContent =
`token 用量: prompt ${data.usage.prompt_tokens} / completion ${data.usage.completion_tokens} / total ${data.usage.total_tokens}`;
}
} catch (err) {
$("resultBody").textContent = "❌ 识别失败: " + err.message;
} finally {
analyzing = false;
btn.textContent = "开始识别";
renderPreviews();
}
}
// 复制结果
$("copyBtn").addEventListener("click", async () => {
const text = $("resultBody").textContent;
try {
await navigator.clipboard.writeText(text);
$("copyBtn").textContent = "已复制 ✓";
setTimeout(() => ($("copyBtn").textContent = "复制"), 1500);
} catch {
alert("复制失败,请手动选择文本");
}
});
// ---------- 启动 ----------
refreshUnlockState();