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:
242
web/js/app.js
242
web/js/app.js
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user