插件配置已完成

This commit is contained in:
GeekMaster
2025-08-29 14:49:51 +08:00
parent 696ef20a80
commit 539e91c12e
22 changed files with 610 additions and 580 deletions

View File

@@ -25,55 +25,6 @@ import (
"gorm.io/gorm"
)
// AuthConfig 定义授权配置
type AuthConfig struct {
ExactPaths map[string]bool // 精确匹配的路径
PrefixPaths map[string]bool // 前缀匹配的路径
}
var authConfig = &AuthConfig{
ExactPaths: map[string]bool{
"/api/user/login": false,
"/api/user/logout": false,
"/api/user/resetPass": false,
"/api/user/register": false,
"/api/user/clogin": false,
"/api/user/clogin/callback": false,
"/api/user/signin": false,
"/api/admin/login": false,
"/api/admin/logout": false,
"/api/admin/login/captcha": false,
"/api/app/list": false,
"/api/app/type/list": false,
"/api/app/list/user": false,
"/api/model/list": false,
"/api/mj/imgWall": false,
"/api/mj/notify": false,
"/api/invite/hits": false,
"/api/sd/imgWall": false,
"/api/dall/imgWall": false,
"/api/product/list": false,
"/api/menu/list": false,
"/api/markMap/client": false,
"/api/payment/doPay": false,
"/api/payment/payWays": false,
"/api/download": false,
"/api/dall/models": false,
"/api/chat/message": false, // 聊天接口需要特殊处理
"/api/realtime": false, // 实时通信接口需要特殊处理
"/api/realtime/voice": false, // 语音聊天接口需要特殊处理
},
PrefixPaths: map[string]bool{
"/api/test/": false,
"/api/payment/notify/": false,
"/api/config/": false,
"/api/function/": false,
"/api/sms/": false,
"/api/captcha/": false,
"/static/": false,
},
}
type AppServer struct {
Config *types.AppConfig
Engine *gin.Engine
@@ -94,10 +45,10 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
func (s *AppServer) Init(client *redis.Client) {
s.Engine.Use(middleware.ParameterHandlerMiddleware())
s.Engine.Use(middleware.StaticMiddleware())
s.Engine.Use(errorHandler)
// 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir)
s.Engine.Use(middleware.StaticMiddleware())
}
func (s *AppServer) Run(db *gorm.DB) error {

View File

@@ -20,7 +20,6 @@ type MiniOssConfig struct {
AccessKey string `json:"access_key"`
AccessSecret string `json:"access_secret"`
Bucket string `json:"bucket"`
SubDir string `json:"sub_dir"`
UseSSL bool `json:"use_ssl"`
Domain string `json:"domain"`
}
@@ -30,7 +29,6 @@ type QiNiuOssConfig struct {
AccessKey string `json:"access_key"`
AccessSecret string `json:"access_secret"`
Bucket string `json:"bucket"`
SubDir string `json:"sub_dir"`
Domain string `json:"domain"`
}

View File

@@ -8,9 +8,9 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SMSConfig struct {
Active string
Ali SmsConfigAli
Bao SmsConfigBao
Active string `json:"active"`
Ali SmsConfigAli `json:"aliyun"`
Bao SmsConfigBao `json:"bao"`
}
// SmsConfigAli 阿里云短信平台配置

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/dalle"
@@ -60,12 +59,9 @@ func (h *FunctionHandler) RegisterRoutes() {
group.GET("list", h.List)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
}
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
}
type resVo struct {

View File

@@ -1,333 +0,0 @@
package crawler
import (
"context"
"errors"
"fmt"
"geekai/logger"
"net/url"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
)
// Service 网络爬虫服务
type Service struct {
browser *rod.Browser
}
// NewService 创建一个新的爬虫服务
func NewService() (*Service, error) {
// 启动浏览器
path, _ := launcher.LookPath()
u := launcher.New().Bin(path).
Headless(true). // 无头模式
Set("disable-web-security", ""). // 禁用网络安全限制
Set("disable-gpu", ""). // 禁用 GPU 加速
Set("no-sandbox", ""). // 禁用沙箱模式
Set("disable-setuid-sandbox", ""). // 禁用 setuid 沙箱
MustLaunch()
browser := rod.New().ControlURL(u).MustConnect()
return &Service{
browser: browser,
}, nil
}
// SearchResult 搜索结果
type SearchResult struct {
Title string `json:"title"` // 标题
URL string `json:"url"` // 链接
Content string `json:"content"` // 内容摘要
}
// WebSearch 网络搜索
func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error) {
if keyword == "" {
return nil, errors.New("搜索关键词不能为空")
}
if maxPages <= 0 {
maxPages = 1
}
if maxPages > 10 {
maxPages = 10 // 最多搜索 10 页
}
results := make([]SearchResult, 0)
// 使用百度搜索
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
// 设置页面超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建页面
page := s.browser.MustPage()
defer page.MustClose()
// 设置视口大小
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: 1280,
Height: 800,
})
if err != nil {
return nil, fmt.Errorf("设置视口失败: %v", err)
}
// 导航到搜索页面
err = page.Context(ctx).Navigate(searchURL)
if err != nil {
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
}
// 等待搜索结果加载完成
err = page.WaitLoad()
if err != nil {
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
}
// 分析当前页面的搜索结果
for i := 0; i < maxPages; i++ {
if i > 0 {
// 点击下一页按钮
nextPage, err := page.Element("a.n")
if err != nil || nextPage == nil {
break // 没有下一页
}
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
if err != nil {
break // 点击下一页失败
}
// 等待新页面加载
err = page.WaitLoad()
if err != nil {
break
}
}
// 提取搜索结果
resultElements, err := page.Elements(".result, .c-container")
if err != nil || resultElements == nil {
continue
}
for _, result := range resultElements {
// 获取标题
titleElement, err := result.Element("h3, .t")
if err != nil || titleElement == nil {
continue
}
title, err := titleElement.Text()
if err != nil {
continue
}
// 获取 URL
linkElement, err := titleElement.Element("a")
if err != nil || linkElement == nil {
continue
}
href, err := linkElement.Attribute("href")
if err != nil || href == nil {
continue
}
// 获取内容摘要 - 尝试多个可能的选择器
var contentElement *rod.Element
var content string
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
for _, selector := range selectors {
contentElement, err = result.Element(selector)
if err == nil && contentElement != nil {
content, _ = contentElement.Text()
if content != "" {
break
}
}
}
// 如果所有选择器都失败,尝试直接从结果块中提取文本
if content == "" {
// 获取结果元素的所有文本
fullText, err := result.Text()
if err == nil && fullText != "" {
// 简单处理:从全文中移除标题,剩下的可能是摘要
fullText = strings.Replace(fullText, title, "", 1)
// 清理文本
content = strings.TrimSpace(fullText)
// 限制内容长度
if len(content) > 200 {
content = content[:200] + "..."
}
}
}
// 添加到结果集
results = append(results, SearchResult{
Title: title,
URL: *href,
Content: content,
})
// 限制结果数量,每页最多 10 条
if len(results) >= 10*maxPages {
break
}
}
}
// 获取真实 URL百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL
for i, result := range results {
realURL, err := s.getRedirectURL(result.URL)
if err == nil && realURL != "" {
results[i].URL = realURL
}
}
return results, nil
}
// 获取真实 URL
func (s *Service) getRedirectURL(shortURL string) (string, error) {
// 创建页面
page, err := s.browser.Page(proto.TargetCreateTarget{URL: ""})
if err != nil {
return shortURL, err // 返回原始URL
}
defer func() {
_ = page.Close()
}()
// 导航到短链接
err = page.Navigate(shortURL)
if err != nil {
return shortURL, err // 返回原始URL
}
// 等待重定向完成
time.Sleep(2 * time.Second)
// 获取当前 URL
info, err := page.Info()
if err != nil {
return shortURL, err // 返回原始URL
}
return info.URL, nil
}
// Close 关闭浏览器
func (s *Service) Close() error {
if s.browser != nil {
err := s.browser.Close()
s.browser = nil
return err
}
return nil
}
// SearchWeb 封装的搜索方法
func SearchWeb(keyword string, maxPages int) (string, error) {
// 添加panic恢复机制
defer func() {
if r := recover(); r != nil {
log := logger.GetLogger()
log.Errorf("爬虫服务崩溃: %v", r)
}
}()
service, err := NewService()
if err != nil {
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
}
defer service.Close()
// 设置超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// 使用goroutine和通道来处理超时
resultChan := make(chan []SearchResult, 1)
errChan := make(chan error, 1)
go func() {
results, err := service.WebSearch(keyword, maxPages)
if err != nil {
errChan <- err
return
}
resultChan <- results
}()
// 等待结果或超时
select {
case <-ctx.Done():
return "", fmt.Errorf("搜索超时: %v", ctx.Err())
case err := <-errChan:
return "", fmt.Errorf("搜索失败: %v", err)
case results := <-resultChan:
if len(results) == 0 {
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
}
// 格式化结果
var builder strings.Builder
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
for i, result := range results {
// // 尝试打开链接获取实际内容
// page := service.browser.MustPage()
// defer page.MustClose()
// // 设置页面超时
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
// defer pageCancel()
// // 导航到目标页面
// err := page.Context(pageCtx).Navigate(result.URL)
// if err == nil {
// // 等待页面加载
// _ = page.WaitLoad()
// // 获取页面标题
// title, err := page.Eval("() => document.title")
// if err == nil && title.Value.String() != "" {
// result.Title = title.Value.String()
// }
// // 获取页面主要内容
// if content, err := page.Element("body"); err == nil {
// if text, err := content.Text(); err == nil {
// // 清理并截取内容
// text = strings.TrimSpace(text)
// if len(text) > 200 {
// text = text[:200] + "..."
// }
// result.Prompt = text
// }
// }
// }
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
if result.Content != "" {
builder.WriteString(fmt.Sprintf(" 摘要: %s\n", result.Content))
}
builder.WriteString("\n")
}
return builder.String(), nil
}
}

View File

@@ -122,15 +122,6 @@ type ErrRes struct {
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
logger.Debugf("绘画参数:%+v", task)
prompt := task.Prompt
// translate prompt
if utils.HasChinese(prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId)
if err == nil {
prompt = content
logger.Debugf("重写后提示词:%s", prompt)
}
}
var chatModel model.ChatModel
if task.ModelId > 0 {
@@ -160,7 +151,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
reqBody := imgReq{
Model: chatModel.Value,
Prompt: prompt,
Prompt: task.Prompt,
N: 1,
Size: task.Size,
Style: task.Style,
@@ -188,7 +179,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
var imgURL string
var data = map[string]interface{}{
"progress": 100,
"prompt": prompt,
"prompt": task.Prompt,
}
// 如果返回的是base64则需要上传到oss
if res.Data[0].B64Json != "" {
@@ -214,7 +205,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片\n\n![](%s)\n", prompt, imgURL)
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片\n\n![](%s)\n", task.Prompt, imgURL)
}
return content, nil

View File

@@ -13,6 +13,7 @@ import (
"geekai/core/types"
"geekai/store"
"geekai/store/model"
"strings"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
@@ -191,8 +192,8 @@ func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) e
// 短信配置
smsConfig := map[string]any{
"active": config.SMS.Active,
"ali": map[string]any{
"active": strings.ToLower(config.SMS.Active),
"aliyun": map[string]any{
"access_key": config.SMS.Ali.AccessKey,
"access_secret": config.SMS.Ali.AccessSecret,
"sign": config.SMS.Ali.Sign,

View File

@@ -67,25 +67,6 @@ func (s *Service) Run() {
continue
}
// translate prompt
if utils.HasChinese(task.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
if err == nil {
task.Prompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
// translate negative prompt
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), task.TranslateModelId)
if err == nil {
task.NegPrompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
// use fast mode as default
if task.Mode == "" {
task.Mode = "fast"

View File

@@ -72,7 +72,7 @@ func (s MiniOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string,
if ext == "" {
ext = filepath.Ext(parse.Path)
}
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext)
info, err := s.client.PutObject(
context.Background(),
s.config.Bucket,
@@ -99,7 +99,7 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
defer fileReader.Close()
fileExt := filepath.Ext(file.Filename)
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
ContentType: file.Header.Get("Body-Type"),
})
@@ -121,7 +121,7 @@ func (s MiniOss) PutBase64(base64Img string) (string, error) {
if err != nil {
return "", fmt.Errorf("error decoding base64:%v", err)
}
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro())
info, err := s.client.PutObject(
context.Background(),
s.config.Bucket,
@@ -138,8 +138,7 @@ func (s MiniOss) PutBase64(base64Img string) (string, error) {
func (s MiniOss) Delete(fileURL string) error {
var objectKey string
if strings.HasPrefix(fileURL, "http") {
filename := filepath.Base(fileURL)
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
objectKey = filepath.Base(fileURL)
} else {
objectKey = fileURL
}

View File

@@ -75,7 +75,7 @@ func (s QiNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
defer src.Close()
fileExt := filepath.Ext(file.Filename)
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
key := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
// 上传文件
ret := storage.PutRet{}
extra := storage.PutExtra{}
@@ -112,7 +112,7 @@ func (s QiNiuOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string,
if ext == "" {
ext = filepath.Ext(parse.Path)
}
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
key := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext)
ret := storage.PutRet{}
extra := storage.PutExtra{}
// 上传文件字节数据
@@ -128,7 +128,7 @@ func (s QiNiuOss) PutBase64(base64Img string) (string, error) {
if err != nil {
return "", fmt.Errorf("error decoding base64:%v", err)
}
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro())
ret := storage.PutRet{}
extra := storage.PutExtra{}
// 上传文件字节数据
@@ -142,8 +142,7 @@ func (s QiNiuOss) PutBase64(base64Img string) (string, error) {
func (s QiNiuOss) Delete(fileURL string) error {
var objectKey string
if strings.HasPrefix(fileURL, "http") {
filename := filepath.Base(fileURL)
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
objectKey = filepath.Base(fileURL)
} else {
objectKey = fileURL
}

View File

@@ -9,8 +9,9 @@ package resp
import (
"geekai/core/types"
"github.com/gin-gonic/gin"
"net/http"
"github.com/gin-gonic/gin"
)
func SUCCESS(c *gin.Context, values ...interface{}) {

View File

@@ -223,14 +223,19 @@ const items = [
title: '存储配置',
},
{
icon: 'log',
index: '/admin/config/communication',
title: '信配置',
icon: 'sms',
index: '/admin/config/sms',
title: '信配置',
},
{
icon: 'api-key',
index: '/admin/config/api',
title: 'API配置',
icon: 'email',
index: '/admin/config/smtp',
title: '邮件配置',
},
{
icon: 'plugin',
index: '/admin/config/plugin',
title: '插件配置',
},
],
},

View File

@@ -0,0 +1,47 @@
<template>
<div class="rounded-md p-3 border-2 text-base mb-4 flex items-center" :class="typeClass">
<div :class="textColor">
<slot name="icon">
<i class="iconfont !text-xl" :class="typeIcon"></i>
</slot>
</div>
<div class="ml-3 text-sm leading-relaxed">
<slot>{{ message }}</slot>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'info',
},
})
const typeClass = computed(() => {
return {
info: 'bg-blue-100 text-blue-500 border-blue-500',
success: 'bg-green-100 text-green-500 border-green-500',
warning: 'bg-yellow-100 text-yellow-500 border-yellow-500',
}[props.type]
})
const typeIcon = computed(() => {
return {
info: 'icon-info',
success: 'icon-success',
warning: 'icon-warning',
}[props.type]
})
const textColor = computed(() => {
return {
info: 'text-blue-500',
success: 'text-green-500',
warning: 'text-yellow-500',
}[props.type]
})
</script>
<style scoped lang="scss"></style>

View File

@@ -192,16 +192,22 @@ const routes = [
component: () => import('@/views/admin/settings/StorageConfig.vue'),
},
{
path: '/admin/config/communication',
name: 'admin-config-communication',
meta: { title: '信配置' },
component: () => import('@/views/admin/settings/CommunicationConfig.vue'),
path: '/admin/config/sms',
name: 'admin-config-sms',
meta: { title: '信配置' },
component: () => import('@/views/admin/settings/SmsConfig.vue'),
},
{
path: '/admin/config/api',
name: 'admin-config-api',
meta: { title: 'API配置' },
component: () => import('@/views/admin/settings/ApiConfig.vue'),
path: '/admin/config/smtp',
name: 'admin-config-smtp',
meta: { title: '邮件配置' },
component: () => import('@/views/admin/settings/SmtpConfig.vue'),
},
{
path: '/admin/config/plugin',
name: 'admin-config-plugin',
meta: { title: '插件配置' },
component: () => import('@/views/admin/settings/PluginConfig.vue'),
},
{
path: '/admin/config/markmap',

View File

@@ -5,8 +5,32 @@
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import axios from 'axios'
import { getAdminToken, getUserToken, removeAdminToken, removeUserToken } from '@/store/session'
import axios from 'axios'
// Blob 数据读取和解析的辅助函数
export async function parseBlobResponse(blob) {
try {
// 检查 Blob 的类型
if (blob.type && blob.type.includes('application/json')) {
// 如果是 JSON 类型,直接解析
const text = await blob.text()
return JSON.parse(text)
} else {
// 如果不是 JSON 类型,尝试解析为文本
const text = await blob.text()
try {
return JSON.parse(text)
} catch (e) {
// 如果解析 JSON 失败,返回文本内容
return { message: text, rawData: text }
}
}
} catch (error) {
console.error('解析 Blob 响应失败:', error)
return { message: '解析响应数据失败', error: error.message }
}
}
axios.defaults.timeout = 180000
// axios.defaults.baseURL = process.env.VUE_APP_API_HOST
@@ -29,7 +53,7 @@ axios.interceptors.response.use(
(response) => {
return response
},
(error) => {
async (error) => {
if (error.response.status === 401) {
if (error.response.request.responseURL.indexOf('/api/admin') !== -1) {
removeAdminToken()
@@ -39,8 +63,14 @@ axios.interceptors.response.use(
error.response.data.message = '请先登录'
return Promise.reject(error.response.data)
}
if (error.response.status === 400) {
return Promise.reject(new Error(error.response.data.message))
let errorMessage = error.response.data.message
if (!errorMessage) {
const parsedData = await parseBlobResponse(error.response.data)
errorMessage = parsedData.message
}
return Promise.reject(new Error(errorMessage))
} else {
return Promise.reject(error)
}

View File

@@ -1,5 +1,4 @@
<template>
<div class="reset-pass"></div>
<div class="flex-center loginPage">
<div class="left">
<div class="login-box">

View File

@@ -1,110 +0,0 @@
<template>
<div class="form" v-loading="loading">
<el-tabs v-model="active">
<el-tab-pane label="SMTP 邮件" name="smtp">
<el-form :model="smtp" label-width="140px">
<el-form-item label="启用TLS"><el-switch v-model="smtp.use_tls" /></el-form-item>
<el-form-item label="SMTP服务器"><el-input v-model="smtp.host" /></el-form-item>
<el-form-item label="端口"><el-input-number v-model="smtp.port" :min="1" /></el-form-item>
<el-form-item label="应用名称"><el-input v-model="smtp.app_name" /></el-form-item>
<el-form-item label="发件人地址"><el-input v-model="smtp.from" /></el-form-item>
<el-form-item label="发件人密码"
><el-input v-model="smtp.password" type="password"
/></el-form-item>
<el-form-item>
<el-button type="primary" @click="save('smtp')">保存</el-button>
<el-button @click="test('smtp')">测试</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="短信服务" name="sms">
<el-form :model="sms" label-width="140px">
<el-form-item label="服务商">
<el-select v-model="sms.Active" style="width: 200px">
<el-option label="阿里云" value="Ali" />
<el-option label="短信宝" value="Bao" />
</el-select>
</el-form-item>
<template v-if="sms.Active === 'Ali'">
<el-form-item label="AccessKey"><el-input v-model="sms.Ali.AccessKey" /></el-form-item>
<el-form-item label="AccessSecret"
><el-input v-model="sms.Ali.AccessSecret"
/></el-form-item>
<el-form-item label="签名"><el-input v-model="sms.Ali.Sign" /></el-form-item>
<el-form-item label="模板ID"><el-input v-model="sms.Ali.CodeTempId" /></el-form-item>
</template>
<template v-else>
<el-form-item label="用户名"><el-input v-model="sms.Bao.Username" /></el-form-item>
<el-form-item label="密码"
><el-input v-model="sms.Bao.Password" type="password"
/></el-form-item>
<el-form-item label="签名"><el-input v-model="sms.Bao.Sign" /></el-form-item>
<el-form-item label="模板"
><el-input v-model="sms.Bao.CodeTemplate" type="textarea" :rows="2"
/></el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="save('sms')">保存</el-button>
<el-button @click="test('sms')">测试</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const loading = ref(true)
const active = ref('smtp')
const smtp = ref({ use_tls: false, host: '', port: 25, app_name: '', from: '', password: '' })
const sms = ref({
Active: 'Ali',
Ali: { AccessKey: '', AccessSecret: '', Sign: '', CodeTempId: '' },
Bao: { Username: '', Password: '', Sign: '', CodeTemplate: '' },
})
onMounted(() => {
Promise.all([httpGet('/api/admin/config/get?key=smtp'), httpGet('/api/admin/config/get?key=sms')])
.then(([s1, s2]) => {
const smtpData = s1?.data || {}
smtp.value = { ...smtp.value, ...smtpData }
const smsData = s2?.data || {}
sms.value = {
...sms.value,
...smsData,
Ali: { ...sms.value.Ali, ...(smsData.Ali || smsData.ali || {}) },
Bao: { ...sms.value.Bao, ...(smsData.Bao || smsData.bao || {}) },
}
})
.catch(() => {})
.finally(() => (loading.value = false))
})
const save = (key) => {
if (key === 'smtp') {
httpPost('/api/admin/config/update/smtp', smtp.value)
.then(() => ElMessage.success('保存成功'))
.catch((e) => ElMessage.error(e.message))
} else if (key === 'sms') {
httpPost('/api/admin/config/update/sms', sms.value)
.then(() => ElMessage.success('保存成功'))
.catch((e) => ElMessage.error(e.message))
}
}
const test = (key) => {
ElMessage.info('请在对应服务侧进行验证')
}
</script>
<style scoped>
.form {
padding: 10px 20px 40px 20px;
}
</style>

View File

@@ -8,14 +8,14 @@
<span class="ms-2">支付宝</span>
</div>
</template>
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/payment.html#%E6%94%AF%E4%BB%98%E5%AE%9D%E9%85%8D%E7%BD%AE"
target="_blank"
>支付宝配置</a
>
</div>
</Alert>
<el-form :model="alipay" class="mt-4" label-position="top">
<el-form-item label="商户ID"><el-input v-model="alipay.app_id" /></el-form-item>
@@ -52,14 +52,14 @@
</div>
</template>
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/payment.html#%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E9%85%8D%E7%BD%AE"
target="_blank"
>微信支付配置</a
>
</div>
</Alert>
<el-form :model="wxpay" class="mt-4" label-position="top">
<el-form-item label="AppID"><el-input v-model="wxpay.app_id" /></el-form-item>
@@ -89,14 +89,14 @@
<span class="ms-2">易支付</span>
</div>
</template>
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/payment.html#%E6%98%93%E6%94%AF%E4%BB%98%E5%BC%80%E9%80%9A"
target="_blank"
>易支付配置</a
>
</div>
</Alert>
<el-form :model="epay" class="mt-4" label-position="top">
<el-form-item label="商户ID"><el-input v-model="epay.app_id" /></el-form-item>
@@ -114,6 +114,7 @@
</template>
<script setup>
import Alert from '@/components/ui/Alert.vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'

View File

@@ -0,0 +1,159 @@
<template>
<div class="settings container p-5">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane name="captcha">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-yanzm"></i>
<span class="ml-2">行为验证码</span>
</div>
</template>
<Alert type="info">
行为验证码服务开启后用户登录的时候需要进行行为验证可以有效防止恶意登录<br />
请联系作者获取令牌填入下面输入框开通验证服务
</Alert>
<el-form :model="captchaConfig" label-position="top">
<el-form-item label="服务令牌">
<el-input v-model="captchaConfig.api_key" placeholder="请输入服务令牌" />
</el-form-item>
<el-form-item label="验证码类型">
<el-radio-group v-model="captchaConfig.type" size="large">
<el-radio value="dot" border>点选验证码</el-radio>
<el-radio value="slide" border>滑动验证码</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="启用验证码">
<el-switch size="large" v-model="captchaConfig.enabled" />
</el-form-item>
</el-form>
<div class="flex justify-center mt-6">
<el-button type="primary" @click="saveCaptchaConfig" :loading="loading"
>提交保存</el-button
>
</div>
</el-tab-pane>
<el-tab-pane name="wxlogin">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-wechat"></i>
<span class="ml-2">微信登录</span>
</div>
</template>
<Alert type="info">
微信登录服务开启后用户可以使用微信扫码登录<br />
请联系作者获取令牌填入下面输入框开通验证服务
</Alert>
<el-form :model="wxLoginConfig" label-position="top">
<el-form-item label="服务令牌">
<el-input v-model="wxLoginConfig.api_key" placeholder="请输入服务令牌" />
</el-form-item>
<el-form-item label="登录成功回调URL">
<el-input v-model="wxLoginConfig.notify_url" placeholder="请输入回调URL" />
</el-form-item>
<el-form-item label="启用微信登录">
<el-switch size="large" v-model="wxLoginConfig.enabled" />
</el-form-item>
</el-form>
<div class="flex justify-center mt-6">
<el-button type="primary" @click="saveWxloginConfig" :loading="loading"
>提交保存</el-button
>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import Alert from '@/components/ui/Alert.vue'
import { httpGet, httpPost } from '@/utils/http.js'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const activeTab = ref('captcha')
const loading = ref(false)
// 行为验证码配置
const captchaConfig = ref({
api_key: '',
type: 'slide',
enabled: false,
})
// 微信登录配置
const wxLoginConfig = ref({
api_key: '',
notify_url: '',
enabled: false,
})
onMounted(() => {
fetchCaptchaConfig()
fetchWxloginConfig()
})
const fetchCaptchaConfig = async () => {
try {
const res = await httpGet('/api/admin/config/get?key=captcha')
captchaConfig.value = Object.assign(captchaConfig.value, res.data)
} catch (e) {
// 使用默认值
}
}
const fetchWxloginConfig = async () => {
try {
const res = await httpGet('/api/admin/config/get?key=wx_login')
wxLoginConfig.value = Object.assign(wxLoginConfig.value, res.data)
wxLoginConfig.value.notify_url =
wxLoginConfig.value.notify_url || window.location.origin + '/api/user/login/callback'
} catch (e) {
// 使用默认值
}
}
// 保存行为验证码配置
const saveCaptchaConfig = async () => {
loading.value = true
try {
await httpPost('/api/admin/config/update/captcha', captchaConfig.value)
ElMessage.success('保存成功')
} catch (e) {
ElMessage.error('保存失败:' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
// 保存微信登录配置
const saveWxloginConfig = async () => {
loading.value = true
try {
await httpPost('/api/admin/config/update/wx_login', wxLoginConfig.value)
ElMessage.success('保存成功')
} catch (e) {
ElMessage.error('保存失败:' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
</script>
<style lang="scss">
.settings {
.el-form-item__label {
font-weight: 700;
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="settings container p-5">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="阿里云短信" name="aliyun">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-aliyun"></i>
<span class="ml-2">阿里云短信</span>
</div>
</template>
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/sms.html#%E9%98%BF%E9%87%8C%E4%BA%91"
target="_blank"
>阿里云短信配置</a
>
</Alert>
<el-form :model="configs.aliyun" label-position="top">
<el-form-item label="AccessKey">
<el-input v-model="configs.aliyun.access_key" placeholder="请输入AccessKey" />
</el-form-item>
<el-form-item label="AccessSecret">
<el-input v-model="configs.aliyun.access_secret" placeholder="请输入AccessSecret" />
</el-form-item>
<el-form-item label="短信签名">
<el-input v-model="configs.aliyun.sign" placeholder="请输入短信签名" />
</el-form-item>
<el-form-item label="验证码模板ID">
<el-input v-model="configs.aliyun.code_temp_id" placeholder="请输入验证码模板ID" />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="短信宝" name="bao">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-sms"></i>
<span class="ml-2">短信宝</span>
</div>
</template>
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/sms.html#%E7%9F%AD%E4%BF%A1%E5%AE%9D"
target="_blank"
>短信宝配置</a
>
</Alert>
<el-form :model="configs.bao" label-position="top">
<el-form-item label="用户名">
<el-input v-model="configs.bao.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="configs.bao.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="短信签名">
<el-input v-model="configs.bao.sign" placeholder="请输入短信签名" />
</el-form-item>
<el-form-item label="验证码模板">
<el-input v-model="configs.bao.code_template" placeholder="请输入验证码模板" />
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div class="mt-3">
<label class="form-label mr-2">默认使用</label>
<el-radio-group v-model="configs.active" size="large">
<el-radio value="aliyun" border>阿里云</el-radio>
<el-radio value="bao" border>短信宝</el-radio>
</el-radio-group>
</div>
<div class="flex justify-center mt-6">
<el-button type="primary" @click="saveSmsConfig" :loading="loading">提交保存</el-button>
</div>
</div>
</template>
<script setup>
import Alert from '@/components/ui/Alert.vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const loading = ref(false)
const activeTab = ref('aliyun')
const configs = ref({
active: 'aliyun',
aliyun: {
access_key: '',
access_secret: '',
sign: '',
code_temp_id: '',
},
bao: {
username: '',
password: '',
sign: '',
code_template: '',
},
})
onMounted(async () => {
try {
const res = await httpGet('/api/admin/config/get?key=sms')
configs.value = Object.assign(configs.value, res.data)
} catch (e) {
// 使用默认值
}
})
const saveSmsConfig = async () => {
loading.value = true
try {
await httpPost('/api/admin/config/update/sms', configs.value)
ElMessage.success('保存成功')
} catch (e) {
ElMessage.error('保存失败:' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
</script>
<style lang="scss">
.settings {
a {
color: #409eff;
&:hover {
text-decoration: underline;
}
}
.el-form-item__label {
font-weight: 700;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<div class="settings container p-5">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="邮件服务设置" name="smtp">
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/sms.html#%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E9%85%8D%E7%BD%AE"
target="_blank"
>邮件服务配置</a
>
</Alert>
<el-form :model="smtpConfig" label-position="top">
<el-form-item>
<label class="form-label"
>邮件服务器地址
<el-tooltip placement="top">
<template #content>
推荐使用网易邮箱<br />
国外邮箱推荐使用 Google 邮箱
</template>
<i class="iconfont icon-info"></i>
</el-tooltip>
</label>
<el-input
v-model="smtpConfig.host"
placeholder="请输入邮件服务器地址,推荐使用网易邮箱"
/>
</el-form-item>
<el-form-item>
<label class="form-label"
>邮件服务器端口
<el-tooltip placement="top">
<template #content> 线上推荐使用465端口<br />本地测试推荐使用25端口 </template>
<i class="iconfont icon-info"></i>
</el-tooltip>
</label>
<el-input v-model="smtpConfig.port" type="number" placeholder="请输入端口号" />
</el-form-item>
<el-form-item label="是否使用TLS"
><el-switch v-model="smtpConfig.use_tls"
/></el-form-item>
<el-form-item>
<label class="form-label"
>应用名称
<el-tooltip placement="top">
<template #content> 应用名称会显示在邮件的抬头 </template>
<i class="iconfont icon-info"></i>
</el-tooltip>
</label>
<el-input v-model="smtpConfig.app_name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item>
<label class="form-label">发件人邮箱地址</label>
<el-input v-model="smtpConfig.from" type="email" placeholder="请输入发件人邮箱地址" />
</el-form-item>
<el-form-item>
<label class="form-label"
>发件人邮箱密码
<el-tooltip placement="top">
<template #content> 如果使用授权码请输入授权码 </template>
<i class="iconfont icon-info"></i>
</el-tooltip>
</label>
<el-input
v-model="smtpConfig.password"
type="password"
placeholder="请输入邮箱密码或授权码"
show-password
/>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div class="flex justify-center mt-6">
<el-button type="primary" @click="saveConfig" :loading="loading">提交保存</el-button>
</div>
</div>
</template>
<script setup>
import Alert from '@/components/ui/Alert.vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const loading = ref(false)
const activeTab = ref('smtp')
const smtpConfig = ref({
use_tls: false,
host: 'smtp.163.com',
port: 465,
app_name: 'GeekAI',
from: '',
password: '',
})
onMounted(async () => {
try {
const res = await httpGet('/api/admin/config/get?key=smtp')
smtpConfig.value = Object.assign(smtpConfig.value, res.data || {})
} catch (e) {
// 使用默认值
}
})
// 保存配置
const saveConfig = async () => {
loading.value = true
try {
await httpPost('/api/admin/config/update/smtp', smtpConfig.value)
ElMessage.success('保存成功')
} catch (e) {
ElMessage.error('保存失败:' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
</script>
<style lang="scss">
.settings {
a {
color: #409eff;
&:hover {
text-decoration: underline;
}
}
.el-form-item__label {
font-weight: 700;
}
}
</style>

View File

@@ -59,14 +59,39 @@
</el-tab-pane>
<el-tab-pane label="七牛云" name="qiniu">
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
如果你不知道怎么获取这些配置信息请参考文档
<a
href="https://docs.geekai.me/plus/config/oss.html#%E4%B8%83%E7%89%9B%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
target="_blank"
>七牛云配置</a
>
</div>
<el-form :model="qiniu" class="mt-4" label-position="top">
<el-form-item label="Zone"><el-input v-model="qiniu.zone" /></el-form-item>
<el-form-item label="Zone">
<template #label>
<label
>区域Zone
<el-tooltip
placement="right"
content="华南z2华东z0华北z1北美na0新加坡as0"
>
<i class="iconfont icon-info"></i>
</el-tooltip>
</label>
</template>
<el-input
v-model="qiniu.zone"
placeholder="华南z2华东z0华北z1北美na0新加坡as0"
/></el-form-item>
<el-form-item label="AccessKey"><el-input v-model="qiniu.access_key" /></el-form-item>
<el-form-item label="AccessSecret"
><el-input v-model="qiniu.access_secret"
/></el-form-item>
<el-form-item label="Bucket"><el-input v-model="qiniu.bucket" /></el-form-item>
<el-form-item label="Domain"><el-input v-model="qiniu.domain" /></el-form-item>
<el-form-item label="Domain"
><el-input v-model="qiniu.domain" placeholder="请输入七牛云Bucket绑定的域名"
/></el-form-item>
</el-form>
</el-tab-pane>
@@ -95,7 +120,6 @@
<div class="flex justify-center mt-6">
<el-button type="primary" @click="save" :loading="loading">提交保存</el-button>
<el-button class="ml-3" @click="test">连接测试</el-button>
</div>
</div>
</template>
@@ -161,14 +185,16 @@ const save = () => {
.then(() => ElMessage.success('保存成功'))
.catch((e) => ElMessage.error(e.message))
}
const test = () => {
ElMessage.info('请在对象存储端验证配置')
}
</script>
<style lang="scss">
.settings {
a {
color: #409eff;
&:hover {
text-decoration: underline;
}
}
.el-form-item__label {
font-weight: 700;
}