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:
@@ -0,0 +1,105 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Result 单个搜索结果。
|
||||
type Result struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
Engine string `json:"engine"`
|
||||
}
|
||||
|
||||
// Response SearXNG /search?format=json 的标准响应(兼容旧响应结构)。
|
||||
type Response struct {
|
||||
Query string `json:"query"`
|
||||
Results []Result `json:"results"`
|
||||
}
|
||||
|
||||
// Client 搜索引擎聚合编排器。
|
||||
//
|
||||
// 持有一组按优先级排序的 Engine,Search 按顺序逐个调用,命中(成功且有结果)即返回;
|
||||
// 单个引擎失败/无结果时降级到下一引擎,全部失败再汇总根因。启用的引擎与顺序由
|
||||
// search_config.json 的 active 列表声明,替代此前写死代码里的硬编码回退链。
|
||||
type Client struct {
|
||||
engines []Engine
|
||||
}
|
||||
|
||||
// NewConfiguredClient 按配置构建聚合 Client。
|
||||
//
|
||||
// - cfg 为 nil(无配置文件)时使用默认硬编码链:SearXNG → 搜狗 HTTP → 真实浏览器,
|
||||
// 保证旧行为不因缺配置而改变。
|
||||
// - cfg 非 nil 时按 active 列表顺序构建引擎,跳过 disabled 或未知类型者。
|
||||
func NewConfiguredClient(cfg *SearchConfig) *Client {
|
||||
if cfg == nil {
|
||||
return &Client{engines: defaultEngines()}
|
||||
}
|
||||
c := &Client{}
|
||||
for _, name := range cfg.Active {
|
||||
ec, ok := cfg.Engines[name]
|
||||
if !ok || !ec.Enabled {
|
||||
continue
|
||||
}
|
||||
if e, err := engineFromConfig(name, ec); err != nil {
|
||||
// 未知类型直接跳过,不阻塞整链。
|
||||
continue
|
||||
} else {
|
||||
c.engines = append(c.engines, e)
|
||||
}
|
||||
}
|
||||
// 配置全被跳过时回退默认链,避免空组织。
|
||||
if len(c.engines) == 0 {
|
||||
c.engines = defaultEngines()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// defaultEngines 无配置时的默认硬编码链(保持旧行为)。
|
||||
func defaultEngines() []Engine {
|
||||
return []Engine{
|
||||
&searxngEngine{client: NewSearXNGClient("")},
|
||||
&sogouEngine{client: NewSogouClient()},
|
||||
&browserEngine{searcher: GetBrowserSearcher()},
|
||||
}
|
||||
}
|
||||
|
||||
// EngineNames 返回当前生效的引擎名(有序),供诊断/展示。
|
||||
func (c *Client) EngineNames() []string {
|
||||
names := make([]string, 0, len(c.engines))
|
||||
for _, e := range c.engines {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Search 按配置顺序逐个调用引擎,命中即返回首个有结果的引擎。
|
||||
// 全部引擎失败时聚合各引擎错误。
|
||||
func (c *Client) Search(query string) ([]Result, error) {
|
||||
var errParts []string
|
||||
for _, e := range c.engines {
|
||||
res, err := e.Search(query)
|
||||
if err == nil && len(res) > 0 {
|
||||
return res, nil
|
||||
}
|
||||
errParts = append(errParts, fmt.Sprintf("%s:%v", e.Name(), orNil(err)))
|
||||
}
|
||||
return nil, fmt.Errorf("全部搜索引擎无结果:[%s]", strings.Join(errParts, ";"))
|
||||
}
|
||||
|
||||
func orNil(err error) any {
|
||||
if err == nil {
|
||||
return "无结果"
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n]) + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user