Files
eaiadminandClaude Code c1af86c934 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>
2026-09-26 22:21:39 +08:00

105 lines
2.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]) + "..."
}