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:
@@ -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] 管理员密码已重置")
|
||||
}
|
||||
Reference in New Issue
Block a user