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:
@@ -1,6 +1,9 @@
|
||||
/* ===== 基础 ===== */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* 保证 hidden 属性始终生效(防止被 display:flex 等样式覆盖) */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
:root {
|
||||
--primary: #4f6ef7;
|
||||
--primary-dark: #3a55d8;
|
||||
@@ -11,6 +14,32 @@
|
||||
--border: #e5e9f2;
|
||||
--danger: #e5484d;
|
||||
--radius: 12px;
|
||||
/* 组件级颜色 */
|
||||
--chip-bg: #eef1fe;
|
||||
--dropzone-border: #c7d2fe;
|
||||
--dropzone-bg: #fafbff;
|
||||
--dropzone-hover: #eef1fe;
|
||||
--result-bg: #f8f9fc;
|
||||
--unlock-bg-1: #eef1fe;
|
||||
--unlock-bg-2: #f4f6fb;
|
||||
}
|
||||
|
||||
/* 黑夜模式 */
|
||||
[data-theme="dark"] {
|
||||
--primary: #6d8bff;
|
||||
--primary-dark: #8aa4ff;
|
||||
--bg: #0f1524;
|
||||
--card: #1a2234;
|
||||
--text: #e6e9f0;
|
||||
--text-light: #9aa3b5;
|
||||
--border: #2b3548;
|
||||
--chip-bg: rgba(109, 139, 255, .18);
|
||||
--dropzone-border: #3a4760;
|
||||
--dropzone-bg: #141c2e;
|
||||
--dropzone-hover: #1e2a42;
|
||||
--result-bg: #121a2b;
|
||||
--unlock-bg-1: #0f1524;
|
||||
--unlock-bg-2: #1a2234;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -39,13 +68,16 @@ body {
|
||||
|
||||
.account { display: flex; align-items: center; gap: 10px; font-size: 14px; }
|
||||
.account .user-chip {
|
||||
background: #eef1fe;
|
||||
color: var(--primary-dark);
|
||||
background: var(--chip-bg);
|
||||
color: var(--primary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 顶栏常驻于解锁层之上,主题切换随时可用 */
|
||||
.topbar { position: relative; z-index: 150; }
|
||||
|
||||
/* ===== 布局 ===== */
|
||||
.container {
|
||||
flex: 1;
|
||||
@@ -68,15 +100,15 @@ body {
|
||||
|
||||
/* ===== 上传区 ===== */
|
||||
.dropzone {
|
||||
border: 2px dashed #c7d2fe;
|
||||
border: 2px dashed var(--dropzone-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all .15s ease;
|
||||
background: #fafbff;
|
||||
background: var(--dropzone-bg);
|
||||
}
|
||||
.dropzone:hover, .dropzone.dragover { border-color: var(--primary); background: #eef1fe; }
|
||||
.dropzone:hover, .dropzone.dragover { border-color: var(--primary); background: var(--dropzone-hover); }
|
||||
.dropzone-icon { font-size: 40px; }
|
||||
.dropzone-title { margin-top: 10px; font-weight: 600; font-size: 15px; }
|
||||
.dropzone-hint { margin-top: 6px; font-size: 12px; color: var(--text-light); }
|
||||
@@ -147,7 +179,7 @@ body {
|
||||
.result-head h2 { font-size: 16px; }
|
||||
.result-body {
|
||||
margin-top: 14px;
|
||||
background: #f8f9fc;
|
||||
background: var(--result-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
@@ -171,35 +203,40 @@ body {
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ===== 模态框 ===== */
|
||||
.modal-mask {
|
||||
/* ===== 进入密钥解锁层 ===== */
|
||||
.unlock-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 20, 35, .45);
|
||||
background: linear-gradient(135deg, var(--unlock-bg-1), var(--unlock-bg-2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.modal {
|
||||
.unlock-card {
|
||||
background: var(--card);
|
||||
border-radius: 14px;
|
||||
padding: 28px;
|
||||
width: 380px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, .2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 36px 32px;
|
||||
width: 360px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(16, 24, 40, .12);
|
||||
}
|
||||
.modal h2 { font-size: 18px; }
|
||||
.modal-desc { font-size: 13px; color: var(--text-light); margin: 6px 0 16px; }
|
||||
.modal-input {
|
||||
.unlock-icon { font-size: 42px; }
|
||||
.unlock-card h2 { font-size: 20px; margin-top: 10px; }
|
||||
.unlock-desc { font-size: 13px; color: var(--text-light); margin: 6px 0 18px; }
|
||||
.unlock-input {
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
.modal-input:focus { border-color: var(--primary); }
|
||||
.unlock-input:focus { border-color: var(--primary); }
|
||||
.modal-error { color: var(--danger); font-size: 13px; min-height: 18px; margin-bottom: 4px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vision Tool — 免费图片识别</title>
|
||||
<title>Vision Tool — 在线图片识别</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -12,11 +12,12 @@
|
||||
<span class="logo">👁️</span>
|
||||
<div>
|
||||
<h1>Vision Tool</h1>
|
||||
<p>免费图片识别 · Agnes-2.0-Flash</p>
|
||||
<p>在线图片识别</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account" id="accountArea">
|
||||
<!-- 登录状态由 JS 渲染 -->
|
||||
<button class="btn btn-ghost" id="themeBtn" title="切换白昼/黑夜">🌙</button>
|
||||
<!-- 解锁状态由 JS 渲染 -->
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -49,31 +50,19 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 初始化管理员模态框 -->
|
||||
<div class="modal-mask" id="initModal" hidden>
|
||||
<div class="modal">
|
||||
<h2>初始化管理员</h2>
|
||||
<p class="modal-desc">首次使用,请创建管理员账号</p>
|
||||
<input type="text" id="initUsername" placeholder="用户名" class="modal-input">
|
||||
<input type="password" id="initPassword" placeholder="密码(至少 6 位)" class="modal-input">
|
||||
<input type="password" id="initPassword2" placeholder="确认密码" class="modal-input">
|
||||
<div class="modal-error" id="initError"></div>
|
||||
<button class="btn btn-primary btn-block" id="initBtn">创建管理员</button>
|
||||
<!-- 进入密钥解锁层 -->
|
||||
<div class="unlock-mask" id="unlockLayer">
|
||||
<div class="unlock-card">
|
||||
<div class="unlock-icon">🔐</div>
|
||||
<h2>Vision Tool</h2>
|
||||
<p class="unlock-desc">请输入进入密钥</p>
|
||||
<input type="password" id="unlockKey" class="unlock-input" placeholder="进入密钥" autofocus>
|
||||
<div class="modal-error" id="unlockError"></div>
|
||||
<button class="btn btn-primary btn-block" id="unlockBtn">进入</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录模态框 -->
|
||||
<div class="modal-mask" id="loginModal" hidden>
|
||||
<div class="modal">
|
||||
<h2>登录</h2>
|
||||
<input type="text" id="loginUsername" placeholder="用户名" class="modal-input">
|
||||
<input type="password" id="loginPassword" placeholder="密码" class="modal-input">
|
||||
<div class="modal-error" id="loginError"></div>
|
||||
<button class="btn btn-primary btn-block" id="loginBtn">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">推理模型专心推理,图片理解交给免费小模型 · 识别接口支持 API 直接调用</footer>
|
||||
<footer class="footer">在线图片识别 · 识别接口支持 API 直接调用<br>© 青梧映星软件开发工作室 版权所有</footer>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
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