- 移除管理员初始化/登录/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>
275 lines
8.0 KiB
JavaScript
275 lines
8.0 KiB
JavaScript
// ============================================================================
|
||
// 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();
|