🎨 优化扩展模块,完成ai接入和对话功能

This commit is contained in:
2026-02-12 23:12:28 +08:00
parent 4e611d3a5e
commit 572f3aa15b
779 changed files with 194400 additions and 3136 deletions

View File

@@ -21,15 +21,70 @@ import (
"gorm.io/gorm"
)
// extensionDataDir 扩展本地存储根目录
// 与原版 SillyTavern 完全一致的路径结构scripts/extensions/third-party/{name}/
// 扩展 JS 中的相对路径 import如 ../../../../../script.js依赖此目录层级来正确解析
// 所有 SillyTavern 核心脚本和扩展文件统一存储在 data/st-core-scripts/ 下,独立于 web-app/
// 扩展代码是公共的(不按用户隔离),用户间差异仅在于数据库中的配置和启用状态
const extensionDataDir = "data/st-core-scripts/scripts/extensions/third-party"
// getExtensionStorePath 获取扩展的本地存储路径: {extensionDataDir}/{extensionName}/
func getExtensionStorePath(extensionName string) string {
return filepath.Join(extensionDataDir, extensionName)
}
// GetExtensionAssetLocalPath 获取扩展资源文件的本地绝对路径
func (es *ExtensionService) GetExtensionAssetLocalPath(extensionName string, assetPath string) (string, error) {
storePath := getExtensionStorePath(extensionName)
fullPath := filepath.Join(storePath, assetPath)
// 安全检查:防止路径遍历攻击
absStore, _ := filepath.Abs(storePath)
absFile, _ := filepath.Abs(fullPath)
if !strings.HasPrefix(absFile, absStore) {
return "", errors.New("非法的资源路径")
}
// 检查文件是否存在
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return "", fmt.Errorf("资源文件不存在: %s", assetPath)
}
return fullPath, nil
}
// ensureExtensionDir 确保扩展存储目录存在
func ensureExtensionDir(extensionName string) (string, error) {
storePath := getExtensionStorePath(extensionName)
if err := os.MkdirAll(storePath, 0755); err != nil {
return "", fmt.Errorf("创建扩展存储目录失败: %w", err)
}
return storePath, nil
}
// removeExtensionDir 删除扩展的本地存储目录
func removeExtensionDir(extensionName string) error {
storePath := getExtensionStorePath(extensionName)
if _, err := os.Stat(storePath); os.IsNotExist(err) {
return nil // 目录不存在,无需删除
}
return os.RemoveAll(storePath)
}
type ExtensionService struct{}
// CreateExtension 创建/安装扩展
func (es *ExtensionService) CreateExtension(userID uint, req *request.CreateExtensionRequest) (*app.AIExtension, error) {
// 校验名称
if req.Name == "" {
return nil, errors.New("扩展名称不能为空")
}
// 检查扩展是否已存在
var existing app.AIExtension
err := global.GVA_DB.Where("user_id = ? AND name = ?", userID, req.Name).First(&existing).Error
if err == nil {
return nil, errors.New("扩展已存在")
return nil, fmt.Errorf("扩展 %s 已存在", req.Name)
}
if err != gorm.ErrRecordNotFound {
return nil, err
@@ -136,15 +191,18 @@ func (es *ExtensionService) DeleteExtension(userID, extensionID uint, deleteFile
return errors.New("系统内置扩展不允许删除")
}
// TODO: 如果 deleteFiles=true删除扩展文件
// 这需要文件系统支持
// 删除本地扩展文件(与原版 SillyTavern 一致:卸载扩展时清理本地文件
if err := removeExtensionDir(extension.Name); err != nil {
global.GVA_LOG.Warn("删除扩展本地文件失败", zap.Error(err), zap.String("name", extension.Name))
// 不阻断删除流程
}
// 删除扩展(配置已经在扩展记录的 Settings 字段中,无需单独删除)
// 删除数据库记录
if err := global.GVA_DB.Delete(&extension).Error; err != nil {
return err
}
global.GVA_LOG.Info("扩展卸载成功", zap.Uint("extensionID", extensionID))
global.GVA_LOG.Info("扩展卸载成功", zap.Uint("extensionID", extensionID), zap.String("name", extension.Name))
return nil
}
@@ -157,6 +215,15 @@ func (es *ExtensionService) GetExtension(userID, extensionID uint) (*app.AIExten
return &extension, nil
}
// GetExtensionByID 通过扩展 ID 获取扩展信息(不限制用户,用于公开资源路由)
func (es *ExtensionService) GetExtensionByID(extensionID uint) (*app.AIExtension, error) {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ?", extensionID).First(&extension).Error; err != nil {
return nil, errors.New("扩展不存在")
}
return &extension, nil
}
// GetExtensionList 获取扩展列表
func (es *ExtensionService) GetExtensionList(userID uint, req *request.ExtensionListRequest) (*response.ExtensionListResponse, error) {
var extensions []app.AIExtension
@@ -550,9 +617,37 @@ func isGitURL(url string) bool {
return false
}
// downloadAndInstallFromManifestURL 从 Manifest URL 下载并安装
// GetExtensionAssetURL 根据扩展的安装来源构建资源文件的远程 URL
func (es *ExtensionService) GetExtensionAssetURL(extension *app.AIExtension, assetPath string) (string, error) {
if extension.SourceURL == "" {
return "", errors.New("扩展没有源地址")
}
sourceURL := strings.TrimSuffix(strings.TrimSuffix(extension.SourceURL, "/"), ".git")
branch := extension.Branch
if branch == "" {
branch = "main"
}
// GitLab: repo/-/raw/branch/path
if strings.Contains(sourceURL, "gitlab.com") {
return fmt.Sprintf("%s/-/raw/%s/%s", sourceURL, branch, assetPath), nil
}
// GitHub: raw.githubusercontent.com/user/repo/branch/path
if strings.Contains(sourceURL, "github.com") {
rawURL := strings.Replace(sourceURL, "github.com", "raw.githubusercontent.com", 1)
return fmt.Sprintf("%s/%s/%s", rawURL, branch, assetPath), nil
}
// Gitee: repo/raw/branch/path
if strings.Contains(sourceURL, "gitee.com") {
return fmt.Sprintf("%s/raw/%s/%s", sourceURL, branch, assetPath), nil
}
return fmt.Sprintf("%s/%s", sourceURL, assetPath), nil
}
// downloadAndInstallFromManifestURL 从 Manifest URL 下载并安装(同时下载资源文件到本地)
func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manifestURL string) (*app.AIExtension, error) {
// 创建 HTTP 客户端
client := &http.Client{
Timeout: 30 * time.Second,
}
@@ -568,7 +663,6 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
return nil, fmt.Errorf("下载 manifest.json 失败: HTTP %d", resp.StatusCode)
}
// 读取响应内容
manifestData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取 manifest.json 失败: %w", err)
@@ -580,21 +674,63 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
}
// 验证必填字段
if manifest.Name == "" {
return nil, errors.New("manifest.json 缺少 name 字段")
// 获取有效名称
effectiveName := manifest.GetEffectiveName()
if effectiveName == "" {
return nil, errors.New("manifest.json 缺少 name 或 display_name 字段")
}
// 检查扩展是否已存在
var existing app.AIExtension
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, effectiveName).First(&existing).Error
if err == nil {
return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name)
return nil, fmt.Errorf("扩展 %s 已安装", effectiveName)
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// 创建本地存储目录并保存 manifest.json
storePath, err := ensureExtensionDir(effectiveName)
if err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(storePath, "manifest.json"), manifestData, 0644); err != nil {
_ = removeExtensionDir(effectiveName)
return nil, fmt.Errorf("保存 manifest.json 失败: %w", err)
}
// 获取 manifest URL 的基础目录(用于下载关联资源)
baseURL := manifestURL[:strings.LastIndex(manifestURL, "/")+1]
// 下载 JS/CSS 等资源文件到本地
filesToDownload := []string{}
if entry := manifest.GetEffectiveEntry(); entry != "" {
filesToDownload = append(filesToDownload, entry)
}
if style := manifest.GetEffectiveStyle(); style != "" {
filesToDownload = append(filesToDownload, style)
}
filesToDownload = append(filesToDownload, manifest.Assets...)
for _, file := range filesToDownload {
if file == "" {
continue
}
fileURL := baseURL + file
if err := downloadFileToLocal(client, fileURL, filepath.Join(storePath, file)); err != nil {
global.GVA_LOG.Warn("下载扩展资源文件失败(非致命)",
zap.String("file", file),
zap.String("url", fileURL),
zap.Error(err))
}
}
global.GVA_LOG.Info("扩展文件已保存到本地",
zap.String("name", effectiveName),
zap.String("path", storePath))
// 将 manifest 转换为 map[string]interface{}
var manifestMap map[string]interface{}
if err := json.Unmarshal(manifestData, &manifestMap); err != nil {
@@ -603,13 +739,13 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
// 构建创建请求
createReq := &request.CreateExtensionRequest{
Name: manifest.Name,
Name: effectiveName,
DisplayName: manifest.DisplayName,
Version: manifest.Version,
Author: manifest.Author,
Description: manifest.Description,
Homepage: manifest.Homepage,
Repository: manifest.Repository, // 使用 manifest 中的 repository
Homepage: manifest.GetEffectiveHomepage(),
Repository: manifest.Repository,
License: manifest.License,
Tags: manifest.Tags,
ExtensionType: manifest.Type,
@@ -617,13 +753,13 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
Dependencies: manifest.Dependencies,
Conflicts: manifest.Conflicts,
ManifestData: manifestMap,
ScriptPath: manifest.Entry,
StylePath: manifest.Style,
ScriptPath: manifest.GetEffectiveEntry(),
StylePath: manifest.GetEffectiveStyle(),
AssetsPaths: manifest.Assets,
Settings: manifest.Settings,
Options: manifest.Options,
InstallSource: "url",
SourceURL: manifestURL, // 记录原始 URL 用于更新
SourceURL: manifestURL,
AutoUpdate: manifest.AutoUpdate,
Metadata: nil,
}
@@ -636,6 +772,7 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
// 创建扩展
extension, err := es.CreateExtension(userID, createReq)
if err != nil {
_ = removeExtensionDir(effectiveName)
return nil, fmt.Errorf("创建扩展失败: %w", err)
}
@@ -647,6 +784,31 @@ func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manif
return extension, nil
}
// downloadFileToLocal 下载远程文件到本地路径
func downloadFileToLocal(client *http.Client, url string, localPath string) error {
// 确保目标文件的父目录存在
if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
return err
}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return os.WriteFile(localPath, data, 0644)
}
// UpgradeExtension 升级扩展版本(根据安装来源自动选择更新方式)
func (es *ExtensionService) UpgradeExtension(userID, extensionID uint, force bool) (*app.AIExtension, error) {
// 获取扩展信息
@@ -672,7 +834,7 @@ func (es *ExtensionService) UpgradeExtension(userID, extensionID uint, force boo
}
}
// updateExtensionFromGit 从 Git 仓库更新扩展
// updateExtensionFromGit 从 Git 仓库更新扩展(先删除旧记录和文件,再重新安装)
func (es *ExtensionService) updateExtensionFromGit(userID uint, extension *app.AIExtension, force bool) (*app.AIExtension, error) {
if extension.SourceURL == "" {
return nil, errors.New("缺少 Git 仓库 URL")
@@ -683,11 +845,17 @@ func (es *ExtensionService) updateExtensionFromGit(userID uint, extension *app.A
zap.String("sourceUrl", extension.SourceURL),
zap.String("branch", extension.Branch))
// 重新克隆(简单方式,避免处理本地修改)
// 先删除旧的数据库记录和本地文件
if err := global.GVA_DB.Unscoped().Delete(extension).Error; err != nil {
return nil, fmt.Errorf("删除旧扩展记录失败: %w", err)
}
_ = removeExtensionDir(extension.Name)
// 重新克隆安装
return es.InstallExtensionFromGit(userID, extension.SourceURL, extension.Branch)
}
// updateExtensionFromURL 从 URL 更新扩展(重新下载 manifest.json
// updateExtensionFromURL 从 URL 更新扩展(先删除旧记录和文件,再重新下载安装
func (es *ExtensionService) updateExtensionFromURL(userID uint, extension *app.AIExtension) (*app.AIExtension, error) {
if extension.SourceURL == "" {
return nil, errors.New("缺少 Manifest URL")
@@ -697,18 +865,24 @@ func (es *ExtensionService) updateExtensionFromURL(userID uint, extension *app.A
zap.String("name", extension.Name),
zap.String("sourceUrl", extension.SourceURL))
// 重新下载并安装
// 先删除旧的数据库记录和本地文件
if err := global.GVA_DB.Unscoped().Delete(extension).Error; err != nil {
return nil, fmt.Errorf("删除旧扩展记录失败: %w", err)
}
_ = removeExtensionDir(extension.Name)
// 重新下载安装
return es.downloadAndInstallFromManifestURL(userID, extension.SourceURL)
}
// InstallExtensionFromGit 从 Git URL 安装扩展
// InstallExtensionFromGit 从 Git URL 安装扩展(与原版 SillyTavern 一致:将源码下载到本地)
func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch string) (*app.AIExtension, error) {
// 验证 Git URL
if !strings.Contains(gitUrl, "://") && !strings.HasSuffix(gitUrl, ".git") {
return nil, errors.New("无效的 Git URL")
}
// 创建临时目录
// 先 clone 到临时目录读取 manifest获取扩展名后再移动到正式目录
tempDir, err := os.MkdirTemp("", "extension-*")
if err != nil {
return nil, fmt.Errorf("创建临时目录失败: %w", err)
@@ -717,10 +891,9 @@ func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch
global.GVA_LOG.Info("开始从 Git 克隆扩展",
zap.String("gitUrl", gitUrl),
zap.String("branch", branch),
zap.String("tempDir", tempDir))
zap.String("branch", branch))
// 执行 git clone
// 执行 git clone(浅克隆)
cmd := exec.Command("git", "clone", "--depth=1", "--branch="+branch, gitUrl, tempDir)
output, err := cmd.CombinedOutput()
if err != nil {
@@ -744,31 +917,53 @@ func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
}
// 获取有效名称(兼容 SillyTavern manifest 没有 name 字段的情况)
effectiveName := manifest.GetEffectiveName()
if effectiveName == "" {
return nil, errors.New("manifest.json 缺少 name 或 display_name 字段")
}
// 检查扩展是否已存在
var existing app.AIExtension
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, effectiveName).First(&existing).Error
if err == nil {
return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name)
return nil, fmt.Errorf("扩展 %s 已安装", effectiveName)
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// 将扩展文件保存到公共目录: web-app/public/scripts/extensions/third-party/{extensionName}/
storePath, err := ensureExtensionDir(effectiveName)
if err != nil {
return nil, err
}
// 清空目标目录(如果有残留文件)后复制 clone 内容
_ = os.RemoveAll(storePath)
if err := copyDir(tempDir, storePath); err != nil {
return nil, fmt.Errorf("保存扩展文件失败: %w", err)
}
global.GVA_LOG.Info("扩展文件已保存到本地",
zap.String("name", effectiveName),
zap.String("path", storePath))
// 将 manifest 转换为 map[string]interface{}
var manifestMap map[string]interface{}
if err := json.Unmarshal(manifestData, &manifestMap); err != nil {
return nil, fmt.Errorf("转换 manifest 失败: %w", err)
}
// 构建创建请求
// 构建创建请求(使用兼容方法获取字段值)
createReq := &request.CreateExtensionRequest{
Name: manifest.Name,
Name: effectiveName,
DisplayName: manifest.DisplayName,
Version: manifest.Version,
Author: manifest.Author,
Description: manifest.Description,
Homepage: manifest.Homepage,
Repository: manifest.Repository, // 使用 manifest 中的 repository
Homepage: manifest.GetEffectiveHomepage(),
Repository: manifest.Repository,
License: manifest.License,
Tags: manifest.Tags,
ExtensionType: manifest.Type,
@@ -776,28 +971,69 @@ func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch
Dependencies: manifest.Dependencies,
Conflicts: manifest.Conflicts,
ManifestData: manifestMap,
ScriptPath: manifest.Entry,
StylePath: manifest.Style,
ScriptPath: manifest.GetEffectiveEntry(),
StylePath: manifest.GetEffectiveStyle(),
AssetsPaths: manifest.Assets,
Settings: manifest.Settings,
Options: manifest.Options,
InstallSource: "git",
SourceURL: gitUrl, // 记录 Git URL 用于更新
Branch: branch, // 记录分支
SourceURL: gitUrl,
Branch: branch,
AutoUpdate: manifest.AutoUpdate,
Metadata: manifest.Metadata,
}
// 确保扩展类型有效
if createReq.ExtensionType == "" {
createReq.ExtensionType = "ui"
}
// 创建扩展记录
extension, err := es.CreateExtension(userID, createReq)
if err != nil {
// 创建失败则清理本地文件
_ = removeExtensionDir(effectiveName)
return nil, fmt.Errorf("创建扩展记录失败: %w", err)
}
global.GVA_LOG.Info("从 Git 安装扩展成功",
zap.Uint("extensionID", extension.ID),
zap.String("name", extension.Name),
zap.String("version", extension.Version))
zap.String("version", extension.Version),
zap.String("localPath", storePath))
return extension, nil
}
// copyDir 递归复制目录(排除 .git 目录以节省空间)
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对路径
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
// 排除 .git 目录
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
// 复制文件
srcFile, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(dstPath, srcFile, info.Mode())
})
}