feat: 新增语音转文字(ASR)功能

- 后端:新增 /api/audio/transcribe 接口,调用 Ollama whisper 进行语音识别
- 前端:新增 AudioTranscribePage.vue 页面,支持 MP3/WAV/M4A/OGG/FLAC 等格式
- 注册路由、工具卡片、智能助手欢迎语更新

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-14 00:53:36 +08:00
co-authored by Claude Code
parent 752c7837ef
commit 0455f064ac
299 changed files with 1588 additions and 432 deletions
@@ -0,0 +1,214 @@
package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"strings"
"time"
_ "modernc.org/sqlite"
)
type taskRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
UpdatedAt string `json:"updated_at"`
}
type articleRow struct {
TaskID int64 `json:"task_id"`
Keyword string `json:"keyword"`
SelectedTopic string `json:"selected_topic"`
TopicHeat string `json:"topic_heat"`
OutlineStyle string `json:"outline_style"`
OutlineWords int `json:"outline_words"`
ContentTargetWords int `json:"content_target_words"`
HotspotsJSON string `json:"hotspots_json"`
TopicCandidatesJSON string `json:"topic_candidates_json"`
}
type runRow struct {
ID int64 `json:"id"`
TaskID int64 `json:"task_id"`
ActionKey string `json:"action_key"`
ActionTitle string `json:"action_title"`
OutputJSON string `json:"output_json"`
LogsJSON string `json:"logs_json"`
StartedAt string `json:"started_at"`
}
func main() {
taskID := flag.Int64("task", 0, "official account task id")
resetStuck := flag.Bool("reset-stuck", false, "reset stuck running state for the given task")
flag.Parse()
dbPath := `d:\traecode\pj0235-eai_agentplatform\eai_ap_app\backend-go\data\eai_agentplatform.db`
db, err := sql.Open("sqlite", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if *resetStuck {
if *taskID <= 0 {
log.Fatal("please pass -task when using -reset-stuck")
}
if err := resetOfficialAccountTaskState(db, *taskID); err != nil {
log.Fatal(err)
}
fmt.Printf("reset stuck running state for task %d\n", *taskID)
return
}
tasks := make([]taskRow, 0, 5)
taskRows, err := db.Query(`
select id, title, status, updated_at
from worker_task
where specialist_key = 'wechat-official-account'
order by id desc
limit 5
`)
if err != nil {
log.Fatal(err)
}
for taskRows.Next() {
var row taskRow
if err := taskRows.Scan(&row.ID, &row.Title, &row.Status, &row.UpdatedAt); err != nil {
log.Fatal(err)
}
tasks = append(tasks, row)
}
_ = taskRows.Close()
var article articleRow
_ = db.QueryRow(`
select task_id, keyword, selected_topic, topic_heat, outline_style, outline_words, content_target_words, hotspots_json, topic_candidates_json
from official_account_article
order by id desc
limit 1
`).Scan(
&article.TaskID,
&article.Keyword,
&article.SelectedTopic,
&article.TopicHeat,
&article.OutlineStyle,
&article.OutlineWords,
&article.ContentTargetWords,
&article.HotspotsJSON,
&article.TopicCandidatesJSON,
)
runs := make([]runRow, 0, 5)
runRows, err := db.Query(`
select id, task_id, action_key, action_title, output_json, logs_json, started_at
from worker_run
where task_id = ?
order by id desc
limit 5
`, article.TaskID)
if err != nil {
log.Fatal(err)
}
for runRows.Next() {
var row runRow
if err := runRows.Scan(&row.ID, &row.TaskID, &row.ActionKey, &row.ActionTitle, &row.OutputJSON, &row.LogsJSON, &row.StartedAt); err != nil {
log.Fatal(err)
}
runs = append(runs, row)
}
_ = runRows.Close()
var hotspotCount int
_ = db.QueryRow(`select count(1) from official_account_hotspot`).Scan(&hotspotCount)
result := map[string]any{
"tasks": tasks,
"latest_article": article,
"latest_runs": runs,
"hotspot_count": hotspotCount,
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(result); err != nil {
fmt.Println(err)
}
}
func resetOfficialAccountTaskState(db *sql.DB, taskID int64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
var contextJSON string
if err = tx.QueryRow(`select context_json from worker_task where id = ?`, taskID).Scan(&contextJSON); err != nil {
return err
}
type stepState struct {
Status string `json:"status"`
Progress int `json:"progress"`
ProgressText string `json:"progress_text"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
Logs []string `json:"logs"`
Output map[string]interface{} `json:"output"`
}
type workflowState struct {
CurrentStep string `json:"current_step"`
Steps map[string]*stepState `json:"steps"`
}
var workflow workflowState
if strings.TrimSpace(contextJSON) != "" {
if err = json.Unmarshal([]byte(contextJSON), &workflow); err != nil {
return err
}
}
if workflow.Steps == nil {
workflow.Steps = map[string]*stepState{}
}
for _, key := range []string{"topic_recommendation", "title_generation", "outline_generation", "content_creation"} {
step := workflow.Steps[key]
if step == nil || step.Status != "running" {
continue
}
step.Status = "error"
step.ProgressText = "上次执行已中断,请重新执行"
step.FinishedAt = time.Now().Format(time.RFC3339)
step.Logs = append(step.Logs, time.Now().Format("15:04:05")+" 执行已中断,已自动停止")
}
workflow.CurrentStep = "topic_recommendation"
newContext, err := json.Marshal(workflow)
if err != nil {
return err
}
if _, err = tx.Exec(`
update worker_task
set status = ?, current_result = ?, context_json = ?, updated_at = CURRENT_TIMESTAMP
where id = ?
`, "待处理", "", string(newContext), taskID); err != nil {
return err
}
if _, err = tx.Exec(`
update worker_run
set status = ?, output_json = ?, logs_json = ?, finished_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
where task_id = ? and status = ?
`, "error", `{"summary":"上次执行已中断,请重新执行","error":"interrupted"}`, `["执行已中断,已自动停止"]`, taskID, "running"); err != nil {
return err
}
return tx.Commit()
}
@@ -0,0 +1,62 @@
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/api"
"eai_agentplatform/backend/internal/auth"
"eai_agentplatform/backend/internal/config"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/store"
)
func main() {
// 交付维护子命令(不进服务主流程)
if len(os.Args) > 1 {
switch os.Args[1] {
case "-reset-admin":
resetAdmin(os.Args[2:])
return
}
}
cfg := config.Load()
if _, err := store.Init(cfg.DBPath); err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
if err := store.SeedDefaults(); err != nil {
log.Fatalf("种子数据初始化失败: %v", err)
}
r := gin.Default()
api.RegisterRoutes(r, cfg)
log.Printf("eai_agentplatform 后端启动,端口 :%s", cfg.Port)
if err := r.Run(":" + cfg.Port); err != nil {
log.Fatalf("启动失败: %v", err)
}
}
// resetAdmin 交付前重置管理员密码:eai_agentplatform-server -reset-admin <新密码>
func resetAdmin(args []string) {
if len(args) < 1 || args[0] == "" {
log.Fatal("用法: eai_agentplatform-server -reset-admin <新密码>")
}
cfg := config.Load()
if _, err := store.Init(cfg.DBPath); err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
hash, err := auth.HashPassword(args[0])
if err != nil {
log.Fatalf("密码加密失败: %v", err)
}
if err := store.DB.Model(&model.User{}).Where("username = ?", "admin").Update("password_hash", hash).Error; err != nil {
log.Fatalf("重置密码失败: %v", err)
}
log.Println("[OK] 管理员密码已重置")
}