✨ feat(core): 初始化 Bot 命令处理服务器
- 基于 Gin 框架搭建 HTTP 服务,接收并处理 Bot 命令请求 - 实现插件化命令系统,支持通过 Plugin 接口扩展新命令 - 内置菜单、启用/禁用、时间查询等基础命令 - 新增图片生成插件,对接 OpenAI Images API - 支持管理员权限控制、命令动态启禁用 - 提供完整配置管理(.env)与 Docker 部署方案
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.env
|
||||
.env.example
|
||||
.git
|
||||
.claude
|
||||
images/
|
||||
README.md
|
||||
command-server
|
||||
command-server.exe
|
||||
20
.env.example
Normal file
20
.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Server
|
||||
SERVER_ADDR=127.0.0.1:8000
|
||||
|
||||
# OpenAI Image Generation
|
||||
OPENAI_API_KEY=sk-xxxxxx
|
||||
OPENAI_BASE_URL=https://api.openai.com
|
||||
OPENAI_MODEL=gpt-image-2
|
||||
REQUEST_TIMEOUT=120
|
||||
IMAGE_COUNT=1
|
||||
IMAGE_OUTPUT_DIR=images
|
||||
|
||||
# Upload
|
||||
UPLOAD_URL=http://localhost:8082/upload
|
||||
UPLOAD_API_KEY=xxxx
|
||||
|
||||
# Proxy (leave empty to disable)
|
||||
PROXY=
|
||||
|
||||
# Admin (comma-separated user IDs)
|
||||
ADMIN_USERS=
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
images/
|
||||
command-server
|
||||
.claude
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w" -o /command-server .
|
||||
|
||||
FROM scratch
|
||||
|
||||
# 时区(固定 Asia/Shanghai)
|
||||
ENV TZ=Asia/Shanghai
|
||||
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
# CA 证书(HTTPS 请求必需)
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
COPY --from=builder /command-server /
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/command-server"]
|
||||
173
README.md
Normal file
173
README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# NcatBot Command Server
|
||||
|
||||
基于 Go 的 Bot 命令处理服务器,支持插件化扩展命令。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
command-server/
|
||||
├── main.go # 入口
|
||||
├── config/
|
||||
│ └── config.go # 配置加载(从 .env)
|
||||
├── command/
|
||||
│ ├── types.go # 共享类型(Req, Resp, Message)
|
||||
│ ├── plugin.go # Plugin 接口定义
|
||||
│ ├── registry.go # 命令注册表
|
||||
│ ├── admin.go # 管理员权限检查
|
||||
│ ├── init.go # 内置命令初始化
|
||||
│ └── builtin.go # 内置命令(菜单/启用/禁用/时间)
|
||||
├── plugins/
|
||||
│ ├── plugins.go # 统一导入所有插件
|
||||
│ └── image/
|
||||
│ └── image.go # 图片生成插件
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── .env # 环境变量配置
|
||||
└── .env.example # 配置模板
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 本地运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
go mod download
|
||||
|
||||
# 复制配置文件并填写
|
||||
cp .env.example .env
|
||||
|
||||
# 运行
|
||||
go run .
|
||||
```
|
||||
|
||||
### Docker 运行
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
|
||||
docker login gitea.kmux.cn
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t gitea.kmux.cn/ncatbot/command-server:0.0.1 --no-cache .
|
||||
docker push gitea.kmux.cn/ncatbot/command-server:0.0.1
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
复制 `.env.example` 为 `.env`,按需修改:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `SERVER_ADDR` | `127.0.0.1:8000` | 服务监听地址 |
|
||||
| `OPENAI_API_KEY` | - | OpenAI API 密钥 |
|
||||
| `OPENAI_BASE_URL` | `https://api.openai.com` | API 基础地址 |
|
||||
| `OPENAI_MODEL` | `gpt-image-2` | 图片生成模型 |
|
||||
| `REQUEST_TIMEOUT` | `120` | 请求超时(秒) |
|
||||
| `IMAGE_COUNT` | `1` | 单次生成图片数 |
|
||||
| `IMAGE_OUTPUT_DIR` | `images` | 图片本地暂存目录 |
|
||||
| `UPLOAD_URL` | - | 图片上传接口地址 |
|
||||
| `UPLOAD_API_KEY` | - | 上传接口 API Key |
|
||||
| `PROXY` | - | HTTP 代理地址 |
|
||||
| `ADMIN_USERS` | - | 管理员用户ID,逗号分隔 |
|
||||
|
||||
## 内置命令
|
||||
|
||||
| 命令 | 说明 | 权限 |
|
||||
|------|------|------|
|
||||
| `菜单` | 显示所有可用命令 | 所有人 |
|
||||
| `启用 <命令名>` | 启用指定命令 | 管理员 |
|
||||
| `禁用 <命令名>` | 禁用指定命令 | 管理员 |
|
||||
| `时间` | 查询当前时间 | 所有人 |
|
||||
|
||||
## 插件命令
|
||||
|
||||
| 命令 | 说明 | 权限 |
|
||||
|------|------|------|
|
||||
| `生图 <描述>` | 根据描述生成图片 | 所有人 |
|
||||
|
||||
## 添加新插件
|
||||
|
||||
### 1. 创建插件文件
|
||||
|
||||
新建 `plugins/weather/weather.go`:
|
||||
|
||||
```go
|
||||
package weather
|
||||
|
||||
import (
|
||||
"ncatbot-command-server/command"
|
||||
)
|
||||
|
||||
type Plugin struct{}
|
||||
|
||||
func init() {
|
||||
command.RegisterPlugin(&Plugin{})
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return "天气" }
|
||||
func (p *Plugin) Description() string { return "查询天气" }
|
||||
func (p *Plugin) Usage() string { return "天气 <城市>" }
|
||||
func (p *Plugin) Run(req *command.Req) command.Resp {
|
||||
return command.Resp{Reply: "今天晴天"}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册插件
|
||||
|
||||
在 `plugins/plugins.go` 中添加导入:
|
||||
|
||||
```go
|
||||
import (
|
||||
_ "ncatbot-command-server/plugins/image"
|
||||
_ "ncatbot-command-server/plugins/weather" // 新增
|
||||
)
|
||||
```
|
||||
|
||||
### Plugin 接口
|
||||
|
||||
| 接口 | 方法 | 必须实现 |
|
||||
|------|------|----------|
|
||||
| `Plugin` | `Name()`, `Description()`, `Usage()`, `Run()` | 是 |
|
||||
| `PluginWithInit` | 加 `Init()` | 需要初始化时 |
|
||||
| `PluginAdmin` | 加 `IsAdminOnly()` | 管理员专用时 |
|
||||
|
||||
## 请求格式
|
||||
|
||||
```json
|
||||
POST /
|
||||
|
||||
{
|
||||
"command": "生图",
|
||||
"content": "一只猫",
|
||||
"raw_message": "生图 一只猫",
|
||||
"user_id": "123456",
|
||||
"group_id": "789",
|
||||
"message_id": "abc"
|
||||
}
|
||||
```
|
||||
|
||||
## 响应格式
|
||||
|
||||
文本回复:
|
||||
|
||||
```json
|
||||
{
|
||||
"reply": "当前时间是: 2026-05-04 16:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
图片回复:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
20
command/admin.go
Normal file
20
command/admin.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"ncatbot-command-server/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsAdmin 检查用户是否为管理员
|
||||
func IsAdmin(userID string) bool {
|
||||
admins := config.Cfg.AdminUsers
|
||||
if admins == "" {
|
||||
return false
|
||||
}
|
||||
for _, id := range strings.Split(admins, ",") {
|
||||
if strings.TrimSpace(id) == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
92
command/builtin.go
Normal file
92
command/builtin.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RegisterBuiltinCommands 注册内置命令
|
||||
func RegisterBuiltinCommands() {
|
||||
// 菜单
|
||||
Register(Command{
|
||||
Name: "菜单",
|
||||
Description: "显示所有可用命令",
|
||||
Usage: "菜单",
|
||||
Handler: handleMenu,
|
||||
})
|
||||
|
||||
// 启用(管理员)
|
||||
Register(Command{
|
||||
Name: "启用",
|
||||
Description: "启用指定命令",
|
||||
Usage: "启用 <命令名>",
|
||||
Handler: handleEnable,
|
||||
AdminOnly: true,
|
||||
})
|
||||
|
||||
// 禁用(管理员)
|
||||
Register(Command{
|
||||
Name: "禁用",
|
||||
Description: "禁用指定命令",
|
||||
Usage: "禁用 <命令名>",
|
||||
Handler: handleDisable,
|
||||
AdminOnly: true,
|
||||
})
|
||||
|
||||
// 时间
|
||||
Register(Command{
|
||||
Name: "时间",
|
||||
Description: "查询当前时间",
|
||||
Usage: "时间",
|
||||
Handler: func(req *Req) Resp {
|
||||
return Resp{Reply: "当前时间是: " + time.Now().Format("2006-01-02 15:04:05")}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleMenu(req *Req) Resp {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("可用命令列表:\n\n")
|
||||
|
||||
for _, cmd := range AllCommands() {
|
||||
status := "✅"
|
||||
if !cmd.Enabled {
|
||||
status = "❌"
|
||||
}
|
||||
adminTag := ""
|
||||
if cmd.AdminOnly {
|
||||
adminTag = " [管理员]"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s %s%s — %s\n", status, cmd.Name, adminTag, cmd.Description))
|
||||
sb.WriteString(fmt.Sprintf(" 用法:%s\n\n", cmd.Usage))
|
||||
}
|
||||
|
||||
return Resp{Reply: strings.TrimSpace(sb.String())}
|
||||
}
|
||||
|
||||
func handleEnable(req *Req) Resp {
|
||||
name := strings.TrimSpace(req.Content)
|
||||
if name == "" {
|
||||
return Resp{Reply: "请指定要启用的命令,例如:启用 生图"}
|
||||
}
|
||||
|
||||
if err := Toggle(name, true); err != nil {
|
||||
return Resp{Reply: err.Error()}
|
||||
}
|
||||
|
||||
return Resp{Reply: fmt.Sprintf("已启用命令\"%s\"", name)}
|
||||
}
|
||||
|
||||
func handleDisable(req *Req) Resp {
|
||||
name := strings.TrimSpace(req.Content)
|
||||
if name == "" {
|
||||
return Resp{Reply: "请指定要禁用的命令,例如:禁用 问候"}
|
||||
}
|
||||
|
||||
if err := Toggle(name, false); err != nil {
|
||||
return Resp{Reply: err.Error()}
|
||||
}
|
||||
|
||||
return Resp{Reply: fmt.Sprintf("已禁用命令\"%s\"", name)}
|
||||
}
|
||||
11
command/init.go
Normal file
11
command/init.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Init 初始化命令系统(仅注册内置命令,插件由 main.go 调用)
|
||||
func Init() {
|
||||
RegisterBuiltinCommands()
|
||||
log.Printf("registered %d commands", len(AllCommands()))
|
||||
}
|
||||
20
command/plugin.go
Normal file
20
command/plugin.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package command
|
||||
|
||||
// Plugin 命令插件接口
|
||||
type Plugin interface {
|
||||
Name() string // 命令名
|
||||
Description() string // 命令说明
|
||||
Usage() string // 用法提示
|
||||
Run(req *Req) Resp // 执行命令
|
||||
}
|
||||
|
||||
// PluginWithInit 支持初始化的插件接口
|
||||
type PluginWithInit interface {
|
||||
Plugin
|
||||
Init() // 初始化函数
|
||||
}
|
||||
|
||||
// PluginAdmin 可选接口:标记是否仅管理员可用
|
||||
type PluginAdmin interface {
|
||||
IsAdminOnly() bool
|
||||
}
|
||||
167
command/registry.go
Normal file
167
command/registry.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
plugins []Plugin
|
||||
commands map[string]*commandInfo
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
type commandInfo struct {
|
||||
name string
|
||||
description string
|
||||
usage string
|
||||
handler Handler
|
||||
initFunc func()
|
||||
enabled bool
|
||||
adminOnly bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugins = make([]Plugin, 0)
|
||||
commands = make(map[string]*commandInfo)
|
||||
}
|
||||
|
||||
// RegisterPlugin 注册一个插件(推荐方式)
|
||||
func RegisterPlugin(p Plugin) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
plugins = append(plugins, p)
|
||||
|
||||
info := &commandInfo{
|
||||
name: p.Name(),
|
||||
description: p.Description(),
|
||||
usage: p.Usage(),
|
||||
handler: p.Run,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
// 检查是否实现了 PluginWithInit
|
||||
if initable, ok := p.(PluginWithInit); ok {
|
||||
info.initFunc = initable.Init
|
||||
}
|
||||
|
||||
// 检查是否实现了 PluginAdmin
|
||||
if admin, ok := p.(PluginAdmin); ok {
|
||||
info.adminOnly = admin.IsAdminOnly()
|
||||
}
|
||||
|
||||
commands[info.name] = info
|
||||
}
|
||||
|
||||
// Register 兼容旧方式:直接注册 Command
|
||||
func Register(cmd Command) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
commands[cmd.Name] = &commandInfo{
|
||||
name: cmd.Name,
|
||||
description: cmd.Description,
|
||||
usage: cmd.Usage,
|
||||
handler: cmd.Handler,
|
||||
initFunc: cmd.Init,
|
||||
enabled: true,
|
||||
adminOnly: cmd.AdminOnly,
|
||||
}
|
||||
}
|
||||
|
||||
// InitAll 初始化所有需要初始化的插件
|
||||
func InitAll() {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, info := range commands {
|
||||
if info.initFunc != nil {
|
||||
info.initFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 查找并执行命令
|
||||
func Handle(req *Req) Resp {
|
||||
mu.RLock()
|
||||
info, ok := commands[req.Command]
|
||||
mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return Resp{Reply: "无法识别的命令。发送\"菜单\"查看可用命令。"}
|
||||
}
|
||||
|
||||
if !info.enabled {
|
||||
return Resp{Reply: fmt.Sprintf("命令\"%s\"已被禁用。", info.name)}
|
||||
}
|
||||
|
||||
if info.adminOnly && !IsAdmin(req.UserID) {
|
||||
return Resp{Reply: "权限不足,该命令仅管理员可用。"}
|
||||
}
|
||||
|
||||
return info.handler(req)
|
||||
}
|
||||
|
||||
// AllPlugins 返回所有插件
|
||||
func AllPlugins() []Plugin {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return append([]Plugin(nil), plugins...)
|
||||
}
|
||||
|
||||
// AllCommands 返回所有命令信息(用于菜单)
|
||||
func AllCommands() []CommandInfo {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
result := make([]CommandInfo, 0, len(commands))
|
||||
for _, info := range commands {
|
||||
result = append(result, CommandInfo{
|
||||
Name: info.name,
|
||||
Description: info.description,
|
||||
Usage: info.usage,
|
||||
Enabled: info.enabled,
|
||||
AdminOnly: info.adminOnly,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CommandInfo 命令信息(用于菜单显示)
|
||||
type CommandInfo struct {
|
||||
Name string
|
||||
Description string
|
||||
Usage string
|
||||
Enabled bool
|
||||
AdminOnly bool
|
||||
}
|
||||
|
||||
// Toggle 启用或禁用命令
|
||||
func Toggle(name string, enable bool) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
info, ok := commands[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("命令\"%s\"不存在", name)
|
||||
}
|
||||
|
||||
if info.adminOnly {
|
||||
return fmt.Errorf("管理命令\"%s\"不可被禁用", name)
|
||||
}
|
||||
|
||||
info.enabled = enable
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler 命令处理函数(兼容旧方式)
|
||||
type Handler func(req *Req) Resp
|
||||
|
||||
// Command 命令定义(兼容旧方式)
|
||||
type Command struct {
|
||||
Name string
|
||||
Description string
|
||||
Usage string
|
||||
Handler Handler
|
||||
Init func()
|
||||
AdminOnly bool
|
||||
}
|
||||
35
command/types.go
Normal file
35
command/types.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package command
|
||||
|
||||
// Req 请求结构体
|
||||
type Req struct {
|
||||
Command string `json:"command"`
|
||||
Content string `json:"content"`
|
||||
RawMessage string `json:"raw_message"`
|
||||
UserID string `json:"user_id"`
|
||||
GroupID string `json:"group_id"`
|
||||
MessageID string `json:"message_id"`
|
||||
}
|
||||
|
||||
// Resp 响应结构体
|
||||
type Resp struct {
|
||||
Reply string `json:"reply,omitempty"`
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
// Message 消息结构体
|
||||
type Message struct {
|
||||
Type MsgType `json:"type"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// MsgType 消息类型
|
||||
type MsgType string
|
||||
|
||||
const (
|
||||
MsgTypeText MsgType = "text"
|
||||
MsgTypeImage MsgType = "image"
|
||||
MsgTypeFile MsgType = "file"
|
||||
MsgTypeVideo MsgType = "video"
|
||||
)
|
||||
87
config/config.go
Normal file
87
config/config.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var Cfg Config
|
||||
|
||||
// API 路径常量
|
||||
const (
|
||||
ImageGenerationsPath = "/v1/images/generations"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Server
|
||||
ServerAddr string
|
||||
|
||||
// OpenAI Image
|
||||
OpenAIBaseURL string // API 基础地址,如 https://api.openai.com
|
||||
OpenAIAPIKey string
|
||||
OpenAIModel string
|
||||
RequestTimeout int
|
||||
ImageCount int
|
||||
ImageOutputDir string
|
||||
|
||||
// Upload
|
||||
UploadURL string
|
||||
UploadAPIKey string
|
||||
|
||||
// Proxy
|
||||
Proxy string
|
||||
|
||||
// Admin
|
||||
AdminUsers string
|
||||
}
|
||||
|
||||
func Load(envPath string) error {
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
if err := godotenv.Load(envPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
Cfg = Config{
|
||||
ServerAddr: getEnv("SERVER_ADDR", "127.0.0.1:8000"),
|
||||
OpenAIBaseURL: getEnv("OPENAI_BASE_URL", "https://api.openai.com"),
|
||||
OpenAIAPIKey: os.Getenv("OPENAI_API_KEY"),
|
||||
OpenAIModel: getEnv("OPENAI_MODEL", "gpt-image-2"),
|
||||
RequestTimeout: getEnvInt("REQUEST_TIMEOUT", 120),
|
||||
ImageCount: getEnvInt("IMAGE_COUNT", 1),
|
||||
ImageOutputDir: getEnv("IMAGE_OUTPUT_DIR", "images"),
|
||||
UploadURL: os.Getenv("UPLOAD_URL"),
|
||||
UploadAPIKey: os.Getenv("UPLOAD_API_KEY"),
|
||||
Proxy: os.Getenv("PROXY"),
|
||||
AdminUsers: os.Getenv("ADMIN_USERS"),
|
||||
}
|
||||
|
||||
if Cfg.OpenAIAPIKey == "" {
|
||||
log.Println("warn: OPENAI_API_KEY is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getEnvInt(key string, def int) int {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
log.Printf("warn: invalid int for %s=%q, using default %d", key, v, def)
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
command-server:
|
||||
image: gitea.kmux.cn/ncatbot/command-server:0.0.1
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./images:/images
|
||||
restart: always
|
||||
39
go.mod
Normal file
39
go.mod
Normal file
@@ -0,0 +1,39 @@
|
||||
module ncatbot-command-server
|
||||
|
||||
go 1.25.6
|
||||
|
||||
require github.com/gin-gonic/gin v1.12.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.2 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
93
go.sum
Normal file
93
go.sum
Normal file
@@ -0,0 +1,93 @@
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
44
main.go
Normal file
44
main.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"ncatbot-command-server/command"
|
||||
"ncatbot-command-server/config"
|
||||
|
||||
// 统一加载所有插件
|
||||
_ "ncatbot-command-server/plugins"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := config.Load(".env"); err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
// 注册内置命令
|
||||
command.Init()
|
||||
|
||||
// 初始化所有插件
|
||||
command.InitAll()
|
||||
|
||||
r := gin.Default()
|
||||
r.POST("/", handleCommand)
|
||||
|
||||
log.Printf("server starting on %s", config.Cfg.ServerAddr)
|
||||
if err := r.Run(config.Cfg.ServerAddr); err != nil {
|
||||
log.Fatalf("failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCommand(c *gin.Context) {
|
||||
var req command.Req
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("request: command=%s user=%s group=%s", req.Command, req.UserID, req.GroupID)
|
||||
|
||||
resp := command.Handle(&req)
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
211
plugins/image/image.go
Normal file
211
plugins/image/image.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"ncatbot-command-server/command"
|
||||
"ncatbot-command-server/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// Plugin 图片生成插件
|
||||
type Plugin struct{}
|
||||
|
||||
var imgClient *resty.Client
|
||||
var uploadClient *resty.Client
|
||||
|
||||
func init() {
|
||||
command.RegisterPlugin(&Plugin{})
|
||||
}
|
||||
|
||||
// Name 实现 Plugin 接口
|
||||
func (p *Plugin) Name() string {
|
||||
return "生图"
|
||||
}
|
||||
|
||||
// Description 实现 Plugin 接口
|
||||
func (p *Plugin) Description() string {
|
||||
return "根据描述生成图片"
|
||||
}
|
||||
|
||||
// Usage 实现 Plugin 接口
|
||||
func (p *Plugin) Usage() string {
|
||||
return "生图 <描述>"
|
||||
}
|
||||
|
||||
// Init 实现 PluginWithInit 接口
|
||||
func (p *Plugin) Init() {
|
||||
cfg := config.Cfg
|
||||
imgClient = newHTTPClient(cfg, true)
|
||||
uploadClient = newHTTPClient(cfg, false)
|
||||
os.MkdirAll(cfg.ImageOutputDir, 0755)
|
||||
}
|
||||
|
||||
// Run 实现 Plugin 接口
|
||||
func (p *Plugin) Run(req *command.Req) command.Resp {
|
||||
if req.Content == "" {
|
||||
return command.Resp{Reply: "请输入生图描述,例如:生图 一只猫"}
|
||||
}
|
||||
|
||||
url, err := genImage(req.Content)
|
||||
if err != nil {
|
||||
log.Printf("gen image error: %v", err)
|
||||
return command.Resp{Reply: "生成失败: " + err.Error()}
|
||||
}
|
||||
|
||||
return command.Resp{
|
||||
Messages: []command.Message{
|
||||
{Type: command.MsgTypeImage, URL: url},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPClient(cfg config.Config, withAuth bool) *resty.Client {
|
||||
c := resty.New().
|
||||
SetTimeout(time.Duration(cfg.RequestTimeout) * time.Second)
|
||||
if withAuth {
|
||||
c.SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+cfg.OpenAIAPIKey)
|
||||
}
|
||||
if cfg.Proxy != "" {
|
||||
c.SetProxy(cfg.Proxy)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// --- API Response types ---
|
||||
|
||||
type OpenAIImageResp struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
URL string `json:"url"`
|
||||
B64JSON string `json:"b64_json"`
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Files []string `json:"files"`
|
||||
Path string `json:"path"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// --- Pipeline ---
|
||||
|
||||
func genImage(prompt string) (string, error) {
|
||||
cfg := config.Cfg
|
||||
if cfg.OpenAIAPIKey == "" {
|
||||
return "", errors.New("OPENAI_API_KEY not set")
|
||||
}
|
||||
|
||||
b64, err := requestImage(prompt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath, err := saveImage(b64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
remotePath, err := uploadImage(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload failed: %w", err)
|
||||
}
|
||||
|
||||
os.Remove(filePath)
|
||||
return remotePath, nil
|
||||
}
|
||||
|
||||
func requestImage(prompt string) (string, error) {
|
||||
cfg := config.Cfg
|
||||
|
||||
var result OpenAIImageResp
|
||||
resp, err := imgClient.R().
|
||||
SetBody(map[string]any{
|
||||
"model": cfg.OpenAIModel,
|
||||
"prompt": prompt,
|
||||
"n": cfg.ImageCount,
|
||||
"response_format": "b64_json",
|
||||
}).
|
||||
SetResult(&result).
|
||||
Post(cfg.OpenAIBaseURL + config.ImageGenerationsPath)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
var errResp struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(resp.Body(), &errResp) == nil && errResp.Error.Message != "" {
|
||||
return "", fmt.Errorf("%s", errResp.Error.Message)
|
||||
}
|
||||
return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode(), resp.Status())
|
||||
}
|
||||
|
||||
if len(result.Data) == 0 {
|
||||
return "", errors.New("empty image result")
|
||||
}
|
||||
|
||||
return result.Data[0].B64JSON, nil
|
||||
}
|
||||
|
||||
func saveImage(b64 string) (string, error) {
|
||||
cfg := config.Cfg
|
||||
|
||||
imgBytes, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode failed: %w", err)
|
||||
}
|
||||
|
||||
filename := filepath.Join(cfg.ImageOutputDir, fmt.Sprintf("%d.png", time.Now().UnixNano()))
|
||||
if err := os.WriteFile(filename, imgBytes, 0644); err != nil {
|
||||
return "", fmt.Errorf("write file failed: %w", err)
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func uploadImage(filePath string) (string, error) {
|
||||
cfg := config.Cfg
|
||||
if cfg.UploadURL == "" {
|
||||
return "", errors.New("UPLOAD_URL not set")
|
||||
}
|
||||
|
||||
filename := filepath.Base(filePath)
|
||||
var result UploadResp
|
||||
|
||||
resp, err := uploadClient.R().
|
||||
SetHeader("X-API-Key", cfg.UploadAPIKey).
|
||||
SetFile(filename, filePath).
|
||||
SetResult(&result).
|
||||
Post(cfg.UploadURL)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return "", fmt.Errorf("upload failed (status %d): %s", resp.StatusCode(), resp.Status())
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return "", fmt.Errorf("upload error: %s", result.Msg)
|
||||
}
|
||||
|
||||
return result.Data.Path, nil
|
||||
}
|
||||
7
plugins/plugins.go
Normal file
7
plugins/plugins.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// Package plugins 统一导入所有插件
|
||||
package plugins
|
||||
|
||||
import (
|
||||
// 导入所有插件(触发 init() 自动注册)
|
||||
_ "ncatbot-command-server/plugins/image"
|
||||
)
|
||||
Reference in New Issue
Block a user