Files
vision-tool/internal/handler/vision_handler.go
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

137 lines
4.0 KiB
Go
Raw Permalink 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.
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"vision-tool/internal/config"
"vision-tool/internal/model"
"vision-tool/internal/service"
)
// VisionHandler HTTP 处理器:在线图片识别。
type VisionHandler struct {
svc *service.VisionService
access *service.AccessService
upload config.UploadConfig
}
func NewVisionHandler(svc *service.VisionService, access *service.AccessService, upload config.UploadConfig) *VisionHandler {
return &VisionHandler{svc: svc, access: access, upload: upload}
}
// Analyze POST /api/v1/vision/analyze — 上传 1-N 张图片并调用免费视觉模型识别。
//
// multipart 表单字段:
//
// images 图片文件(可多个,支持 png/jpg/gif/webp/bmp/tiff)
// prompt 可选,自定义分析提问(默认:结构化描述图片内容)
//
// 鉴权(可选):提供 X-Access-Key 请求头 或 access_key 表单字段时校验进入密钥,
// 密钥错误返回 401;不提供时保持免鉴权调用,方便 curl / Claude Code 直接使用。
func (h *VisionHandler) Analyze(c *gin.Context) {
// 可选密钥校验:带密钥必须正确
key := c.GetHeader("X-Access-Key")
if key == "" {
key = c.PostForm("access_key")
}
if key != "" && !h.access.VerifyKey(key) {
c.JSON(http.StatusUnauthorized, model.Err(model.CodeUnauthorized, "密钥错误"))
return
}
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, model.Err(model.CodeBadParam, "请使用 multipart/form-data 上传图片"))
return
}
files := form.File["images"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, model.Err(model.CodeBadParam, "请至少上传一张图片 (images)"))
return
}
// prompt 字段可选:未传时表单里没有该 key(切片为空),直接取 [0] 会越界 panic
prompt := ""
if len(form.Value["prompt"]) > 0 {
prompt = strings.TrimSpace(form.Value["prompt"][0])
}
// 保存上传图片到 uploads/,返回本地路径列表
paths := make([]string, 0, len(files))
urls := make([]string, 0, len(files))
for _, fh := range files {
path, url, err := h.saveImage(fh)
if err != nil {
c.JSON(http.StatusBadRequest, model.Err(model.CodeBadParam, err.Error()))
return
}
paths = append(paths, path)
urls = append(urls, url)
}
// 调用免费视觉模型(Web 端默认开启 thinking)
result, err := h.svc.Analyze(paths, prompt, false)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Err(model.CodeServerErr, "识别失败: "+err.Error()))
return
}
c.JSON(http.StatusOK, model.OK(gin.H{
"result": result.Content,
"images": urls,
"usage": result.Usage,
}))
}
// saveImage 校验并保存上传图片,返回磁盘路径与访问 URL。
func (h *VisionHandler) saveImage(fh *multipart.FileHeader) (string, string, error) {
ext := strings.ToLower(filepath.Ext(fh.Filename))
allowed := map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".bmp": true, ".tiff": true, ".tif": true}
if !allowed[ext] {
return "", "", fmt.Errorf("不支持的图片格式: %s(支持 png/jpg/gif/webp/bmp/tiff)", fh.Filename)
}
if fh.Size > h.upload.MaxSize {
return "", "", fmt.Errorf("图片超过大小限制: %s", fh.Filename)
}
src, err := fh.Open()
if err != nil {
return "", "", fmt.Errorf("读取上传文件: %w", err)
}
defer src.Close()
if err := os.MkdirAll(h.upload.Path, 0755); err != nil {
return "", "", fmt.Errorf("创建上传目录: %w", err)
}
name := randomName() + ext
diskPath := filepath.Join(h.upload.Path, name)
dst, err := os.Create(diskPath)
if err != nil {
return "", "", fmt.Errorf("保存文件: %w", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", "", fmt.Errorf("写入文件: %w", err)
}
return diskPath, "/uploads/" + name, nil
}
// randomName 生成 16 字节 hex 随机文件名。
func randomName() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}