feat(asr): 本地语音转写接入为一级路由 + 并行工作流合并提交

按用户指示做**一包提交**,不按工作流拆分。本提交刻意混合了多条并行线:

  · 本地 ASR 接管:audio 成为与 chat/embed/image/video 同等的路由类别
    (IsLocalRoute 单一判据、audio 健康探测、default_audio_route、
    auto 占位、GET /api/ai/routes/audio、回退云端时界面明示「音频已出网」)
  · LLM 调用层:ctx 贯穿、ToolCall/ToolSchema、EmptyCompletionError /
    TransientUpstreamError(按错误类型而非文案判重试)
  · 编排 Agent:general_assistant orchestrate/persistence/spec_driver
  · 联网搜索:internal/search(playwright)
  · 网盘:backend + 前端
  · 前端 UI:导航/路由/工作台若干页
  · 交付文档:DELIVERY.md / AR04 / 部署文档的「无 Python」表述据实改写,
    新增 eai_agentplatform-asr.service、asr.env、clonezilla-cleanup 清 ~/asr-poc

不分拆的原因:dev 早期,粒度不该打断工作节奏。且实测过——这些改动
**在编译上是同一个单元**(llm.go 的 ctx 签名变更牵动 12 个调用点,
chat_message.go 的 ctx 改动又与编排重写同处一个 hunk),拆出来的中间态编不过。
详见 TOP_CODING_RULES.md G14.5 与 bugs_and_errors.md E09。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-26 22:21:39 +08:00
co-authored by Claude Code
parent e73169df50
commit c1af86c934
192 changed files with 18046 additions and 392 deletions
@@ -30,6 +30,11 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
if err := os.MkdirAll(filepath.Join(cfg.KBDataDir, "approved"), 0o755); err == nil {
r.Static("/media", filepath.Join(cfg.KBDataDir, "approved"))
}
// 网盘物理文件静态预览(只读):仅 approved 文件通过 Preview 接口取 URL,
// 未审批文件不对外暴露,落实「审批前置」。
if err := os.MkdirAll(cfg.NetdiskDataDir, 0o755); err == nil {
r.Static("/netdisk-file", cfg.NetdiskDataDir)
}
r.GET("/api/health", func(c *gin.Context) {
ver := "1.1.0"
@@ -117,6 +122,21 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
r.GET("/api/media/preview/:media_id", middleware.Auth(cfg), Preview)
r.GET("/api/media/status/:media_id", middleware.Auth(cfg), MediaStatus)
// ── 团队共享网盘(目录 / 文件 / 预览 / 下载)──
r.GET("/api/netdisk/folders", middleware.Auth(cfg), netdiskListFolders)
r.POST("/api/netdisk/folders", middleware.Auth(cfg), netdiskCreateFolder)
r.PATCH("/api/netdisk/folders/:id", middleware.Auth(cfg), netdiskPatchFolder)
r.GET("/api/netdisk/files", middleware.Auth(cfg), netdiskListFiles)
r.POST("/api/netdisk/upload", middleware.Auth(cfg), netdiskUpload)
r.GET("/api/netdisk/files/:id/download", middleware.Auth(cfg), netdiskDownload)
r.GET("/api/netdisk/files/:id/preview", middleware.Auth(cfg), netdiskPreview)
r.PUT("/api/netdisk/files/:id/rename", middleware.Auth(cfg), netdiskRenameFile)
r.PUT("/api/netdisk/files/:id/move", middleware.Auth(cfg), netdiskMoveFile)
r.DELETE("/api/netdisk/files/:id", middleware.Auth(cfg), netdiskRecycleFile)
// 回收站
r.GET("/api/netdisk/recycle", middleware.Auth(cfg), netdiskRecycleList)
r.POST("/api/netdisk/recycle/:id/restore", middleware.Auth(cfg), netdiskRestoreFile)
r.GET("/api/knowledge/spaces", middleware.Auth(cfg), ListKnowledgeSpaces)
r.GET("/api/knowledge/search", middleware.Auth(cfg), SearchKnowledge)
r.GET("/api/knowledge/status/:source_id", middleware.Auth(cfg), KnowledgeStatus)
@@ -130,6 +150,8 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
r.GET("/api/ai/usage", middleware.Auth(cfg), AIUsage)
r.GET("/api/ai/routes/chat", ListChatRoutes)
r.GET("/api/ai/routes/embed", ListEmbedRoutes)
r.GET("/api/ai/routes/audio", ListAudioRoutes)
r.GET("/api/assistant/opening", middleware.Auth(cfg), GetAssistantOpening)
// 学习进度(员工上报 + 本人查询)
r.POST("/api/learning/progress", middleware.Auth(cfg), RecordLearningProgress)
@@ -159,6 +181,19 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
r.POST("/api/skills/office/execute", middleware.Auth(cfg), skillapi.ExecuteOfficeSkill)
r.POST("/api/skills/office/ocr", middleware.Auth(cfg), skillapi.ExecuteOfficeSkillOCR)
r.POST("/api/skills/office/tts", middleware.Auth(cfg), skillapi.ExecuteOfficeSkillTTS)
// 语音转写技能的四步。**不复用 /api/skills/office/execute**:office 的运行时
// 是纯产物构造器(不发网络请求),转写必须真的调 ASR 与 LLM。
// 每步一条路由 = 每步一条 task_run,右栏「工作流 x/4」据此推进。
// 语音转写:一步一个端点、一步一条 task_run,右栏的「工作流 x/6」靠这个推进。
// speakers 与 speakers/confirm 是**两步**:前者只推断(产物落 ready、任务落待确认),
// 后者是用户拍板。中间停多久由用户决定,后端不设超时。
r.POST("/api/skills/audio/scope", middleware.Auth(cfg), skillapi.ExecuteAudioScope)
r.POST("/api/skills/audio/transcribe", middleware.Auth(cfg), skillapi.ExecuteAudioTranscribe)
r.POST("/api/skills/audio/speakers", middleware.Auth(cfg), skillapi.ExecuteAudioSpeakers)
r.POST("/api/skills/audio/speakers/confirm", middleware.Auth(cfg), skillapi.ExecuteAudioSpeakersConfirm)
r.POST("/api/skills/audio/structure", middleware.Auth(cfg), skillapi.ExecuteAudioStructure)
r.POST("/api/skills/audio/minutes", middleware.Auth(cfg), skillapi.ExecuteAudioMinutes)
r.GET("/api/tts/audio/:filename", middleware.Auth(cfg), skillapi.ServeTTSAudio)
r.POST("/api/chat/message", middleware.Auth(cfg), HandleChatMessage)
@@ -189,6 +224,12 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
admin.GET("/media/audit-list", AuditList)
admin.POST("/media/audit/:media_id", AuditMedia)
// 网盘管理:删目录、审批、回收站彻底删除
admin.DELETE("/netdisk/folders/:id", netdiskDeleteFolder)
admin.GET("/netdisk/audit-list", netdiskAuditList)
admin.POST("/netdisk/audit/:id", netdiskAuditFile)
admin.DELETE("/netdisk/recycle/:id", netdiskPurgeFile)
admin.POST("/knowledge/scan", KnowledgeScan)
admin.POST("/knowledge/index/rebuild", RebuildKnowledgeIndex)
admin.POST("/knowledge/spaces", CreateKnowledgeSpace)