refactor: 收口技能与专员目录定义

统一 skill 与 specialist 清单,移除旧的 office 执行包与下线对象,并补入 text_to_speech、customer_followup 等当前目录定义与种子数据。
This commit is contained in:
eaiadmin
2026-09-22 23:38:56 +08:00
parent 90031b75f3
commit b7238ad667
148 changed files with 1230 additions and 1710 deletions
@@ -199,7 +199,7 @@ func ExecuteOfficeSkillOCR(c *gin.Context) {
TaskID: task.ID,
SpecialistKey: task.SpecialistKey,
ActionKey: "ocr-understanding",
ActionTitle: "OCR / 图文理解",
ActionTitle: "图片/扫描件智能识别",
ActionType: "ocr",
Status: "done",
InputJSON: string(inputJSON),
@@ -0,0 +1,192 @@
package skillapi
import (
"crypto/sha1"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"eai_agentplatform/backend/internal/config"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
specialistruntime "eai_agentplatform/backend/internal/specialists/runtime"
"eai_agentplatform/backend/internal/store"
"eai_agentplatform/backend/internal/web"
)
// ttsDefaultVoice 默认中文女声(edge-tts 自带音色)。
const ttsDefaultVoice = "zh-CN-XiaoxiaoNeural"
// ttsAudioDir 存放合成 MP3 的目录,与公众号配图一致放在数据库同级的 tts_audio 下。
func ttsAudioDir() string {
cfg := config.Load()
return filepath.Join(filepath.Dir(cfg.DBPath), "tts_audio")
}
func defaultTTSText(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return "这是一个文字转语音的示例。"
}
return text
}
// ExecuteOfficeSkillTTS POST /api/skills/office/tts —— 文字转语音。
// body: { "task_id": <任务ID>, "text": "要朗读的文字", "voice": "音色(可空)", "rate": "语速(可空, 如 +0% / -10%)" }
func ExecuteOfficeSkillTTS(c *gin.Context) {
user := middleware.CurrentUser(c)
if user == nil {
web.Fail(c, web.NewAuthError("未登录"))
return
}
var req struct {
TaskID uint `json:"task_id"`
Text string `json:"text"`
Voice string `json:"voice"`
Rate string `json:"rate"`
}
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
if req.TaskID == 0 {
web.Fail(c, web.NewBadRequest("task_id 必填"))
return
}
text := defaultTTSText(req.Text)
voice := strings.TrimSpace(req.Voice)
if voice == "" {
voice = ttsDefaultVoice
}
rate := strings.TrimSpace(req.Rate)
task, ok := loadMyOwnedTask(c, user, req.TaskID)
if !ok {
return
}
now := time.Now()
dir := ttsAudioDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
web.Fail(c, web.NewBadRequest("无法创建音频目录"))
return
}
sum := sha1.Sum([]byte(text + "|" + voice))
filename := fmt.Sprintf("task_%d_%s_%x.mp3", task.ID, now.Format("20060102-150405"), sum[:6])
audioPath := filepath.Join(dir, filename)
args := []string{"-m", "edge_tts", "--text", text, "--voice", voice, "--write-media", audioPath}
if rate != "" {
args = append(args, "--rate", rate)
}
cmd := exec.Command("python3", args...)
var stderr strings.Builder
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
web.Fail(c, web.NewBadRequest("语音合成失败:"+firstNonEmpty(strings.TrimSpace(stderr.String()), err.Error())))
return
}
if _, err := os.Stat(audioPath); err != nil {
web.Fail(c, web.NewBadRequest("语音合成失败:未生成音频文件"))
return
}
audioURL := "/api/tts/audio/" + filename
inputJSON, _ := json.Marshal(gin.H{"text": text, "voice": voice, "rate": rate})
outputJSON, _ := json.Marshal(gin.H{
"summary": "已将文本合成为语音,可在线播放或下载。",
"text": text,
"voice": voice,
"audio_url": audioURL,
})
logsJSON, _ := json.Marshal([]string{
"接收文字转语音请求",
fmt.Sprintf("调用本地 TTS 引擎(voice=%s)", voice),
"已生成 MP3 音频",
})
artifactPayloadJSON, _ := json.Marshal(gin.H{
"text": text,
"voice": voice,
"audio_url": audioURL,
"format": "mp3",
})
sourceRefsJSON, _ := json.Marshal([]string{"本地 edge-tts"})
var run model.TaskRun
var artifact model.TaskArtifact
err := store.DB.Transaction(func(tx *gorm.DB) error {
run = model.TaskRun{
TaskID: task.ID,
SpecialistKey: task.SpecialistKey,
ActionKey: "text-to-speech",
ActionTitle: "文字转语音",
ActionType: "audio",
Status: "done",
InputJSON: string(inputJSON),
OutputJSON: string(outputJSON),
LogsJSON: string(logsJSON),
StartedAt: now,
FinishedAt: &now,
}
if err := tx.Create(&run).Error; err != nil {
return err
}
artifact = model.TaskArtifact{
TaskID: task.ID,
SpecialistKey: task.SpecialistKey,
Title: fmt.Sprintf("%s - 语音合成结果", task.Title),
ArtifactType: "audio",
Status: specialistruntime.ArtifactStatusDraft,
ContentText: text,
ContentJSON: string(artifactPayloadJSON),
SourceRefsJSON: string(sourceRefsJSON),
CreatedByRunID: &run.ID,
}
if err := tx.Create(&artifact).Error; err != nil {
return err
}
task.CurrentRunID = &run.ID
task.CurrentResult = "已将文本合成为语音并生成 MP3。"
task.Status = specialistruntime.TaskStatusDraft
task.LastTriggeredAt = &now
return tx.Save(&task).Error
})
if err != nil {
web.Fail(c, web.NewBadRequest("保存结果失败:"+err.Error()))
return
}
freshTask, _ := reloadTask(task.ID)
web.OK(c, gin.H{
"audio_url": audioURL,
"text": text,
"voice": voice,
"rate": rate,
"run": run,
"artifact": artifact,
"task": freshTask,
})
}
// ServeTTSAudio GET /api/tts/audio/:filename —— 提供生成的 MP3 音频文件。
func ServeTTSAudio(c *gin.Context) {
filename := filepath.Base(strings.TrimSpace(c.Param("filename")))
if filename == "." || filename == "" {
web.Fail(c, web.NewNotFoundError("音频不存在"))
return
}
fullPath := filepath.Join(ttsAudioDir(), filename)
if _, err := os.Stat(fullPath); err != nil {
web.Fail(c, web.NewNotFoundError("音频不存在"))
return
}
c.File(fullPath)
}