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>
This commit is contained in:
2026-08-13 01:21:18 +08:00
parent a3bbf33358
commit f9cdeb5d64
20 changed files with 341 additions and 465 deletions

View File

@@ -1,18 +1,37 @@
// ============================================================================
// Vision Tool — 在线免费图片识别(原生单页)
// 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 token = localStorage.getItem("vt_token");
if (token) headers["Authorization"] = `Bearer ${token}`;
const resp = await fetch(path, { ...options, headers });
let data;
try {
@@ -24,119 +43,141 @@ async function api(path, options = {}) {
return data.data;
}
// ---------- 状态 ----------
let selectedFiles = []; // 待识别的图片文件
let analyzing = false;
// ---------- 进入密钥 ----------
const KEY_STORAGE = "vt_access_key";
// ---------- 顶栏账号区 ----------
async function refreshAccount() {
function isUnlocked() {
return !!localStorage.getItem(KEY_STORAGE);
}
function refreshUnlockState() {
const unlocked = isUnlocked();
$("unlockLayer").hidden = unlocked;
const area = $("accountArea");
area.innerHTML = "";
if (!unlocked) return;
const { initialized } = await api("/api/v1/admin/check");
const chip = document.createElement("span");
chip.className = "user-chip";
chip.textContent = "🔓 已解锁";
area.appendChild(chip);
if (!initialized) {
const btn = document.createElement("button");
btn.className = "btn btn-primary";
btn.textContent = "初始化管理员";
btn.onclick = () => showModal("initModal");
area.appendChild(btn);
return;
}
// 一键复制 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 token = localStorage.getItem("vt_token");
if (!token) {
const btn = document.createElement("button");
btn.className = "btn btn-ghost";
btn.textContent = "登录";
btn.onclick = () => showModal("loginModal");
area.appendChild(btn);
return;
}
// 校验 token 有效性
try {
const user = await api("/api/v1/user/profile");
const chip = document.createElement("span");
chip.className = "user-chip";
chip.textContent = `👤 ${user.username}`;
area.appendChild(chip);
const logout = document.createElement("button");
logout.className = "btn btn-ghost";
logout.textContent = "退出";
logout.onclick = () => {
localStorage.removeItem("vt_token");
refreshAccount();
};
area.appendChild(logout);
} catch {
localStorage.removeItem("vt_token");
refreshAccount();
}
const logout = document.createElement("button");
logout.className = "btn btn-ghost";
logout.textContent = "退出";
logout.onclick = () => {
localStorage.removeItem(KEY_STORAGE);
refreshUnlockState();
};
area.appendChild(logout);
}
// ---------- 模态框 ----------
function showModal(id) {
$(id).hidden = false;
$("initError").textContent = "";
$("loginError").textContent = "";
}
function hideModal(id) { $(id).hidden = true; }
$("initModal").addEventListener("click", (e) => { if (e.target === e.currentTarget) hideModal("initModal"); });
$("loginModal").addEventListener("click", (e) => { if (e.target === e.currentTarget) hideModal("loginModal"); });
// 生成 API 文档(Markdown,供智能体/开发者直接调用)
function buildApiDoc() {
const base = window.location.origin;
const key = localStorage.getItem(KEY_STORAGE) || "19735500";
return `# Vision Tool — 在线图片识别 API
// 初始化管理员
$("initBtn").addEventListener("click", async () => {
const username = $("initUsername").value.trim();
const password = $("initPassword").value;
const password2 = $("initPassword2").value;
if (!username || password.length < 6) {
$("initError").textContent = "用户名不能为空,密码至少 6 位";
return;
这是一个在线图片识别服务。
智能体可通过下面的 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 }
}
if (password !== password2) {
$("initError").textContent = "两次输入的密码不一致";
}
\`\`\`
## 其他接口
- 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/admin/init", {
await api("/api/v1/auth/verify", {
method: "POST",
body: JSON.stringify({ username, password }),
body: JSON.stringify({ key }),
});
hideModal("initModal");
await login(username, password);
refreshAccount();
localStorage.setItem(KEY_STORAGE, key);
$("unlockKey").value = "";
$("unlockError").textContent = "";
refreshUnlockState();
} catch (err) {
$("initError").textContent = err.message;
$("unlockError").textContent = err.message;
}
});
// 登录
$("loginBtn").addEventListener("click", async () => {
const username = $("loginUsername").value.trim();
const password = $("loginPassword").value;
if (!username || !password) {
$("loginError").textContent = "请输入用户名和密码";
return;
}
try {
await login(username, password);
hideModal("loginModal");
refreshAccount();
} catch (err) {
$("loginError").textContent = err.message;
}
});
async function login(username, password) {
const data = await api("/api/v1/auth/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
localStorage.setItem("vt_token", data.token);
}
// ---------- 图片选择与预览 ----------
let selectedFiles = [];
let analyzing = false;
const dropzone = $("dropzone");
const fileInput = $("fileInput");
@@ -193,7 +234,7 @@ async function analyze() {
btn.innerHTML = '<span class="loading"><span class="spinner"></span>识别中…</span>';
const resultCard = $("resultCard");
resultCard.hidden = false;
$("resultBody").textContent = "正在调用免费视觉模型,请稍候(最长约 2 分钟)…";
$("resultBody").textContent = "正在识别图片内容,请稍候(最长约 2 分钟)…";
$("resultMeta").textContent = "";
const fd = new FormData();
@@ -230,7 +271,4 @@ $("copyBtn").addEventListener("click", async () => {
});
// ---------- 启动 ----------
refreshAccount().catch((err) => {
// check 失败(服务未启动等),提示
document.querySelector(".brand p").textContent = "⚠️ 服务连接失败: " + err.message;
});
refreshUnlockState();