- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、 voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。 - 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀 「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。 - 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、 linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。 - 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
78 lines
2.8 KiB
Go
78 lines
2.8 KiB
Go
package main
|
||
|
||
import (
|
||
"net"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// memoryOwnerRoleID 是组合根写入提取出的记忆时所用的角色 id;主人是唯一会写入
|
||
// 私有记忆的 actor(群聊/陌生人会话根本不提取,design/11 §2)。
|
||
const memoryOwnerRoleID = "owner"
|
||
|
||
// buildMemoryExtractor 选择循环在每轮落定后运行的记忆提取器(design/03 §1
|
||
// LLM 异步自动提取):
|
||
//
|
||
// - provider 栈存在可用的活动组时用 LLM 提取器,由模型去噪并去重真正值得记的内容;
|
||
// - 否则用确定性的 ManualExtractor,使无 key 的部署仍能记录主人显式的「记住 X」
|
||
// 指令,而不是每轮都让模型调用失败。
|
||
//
|
||
// 该提取器刻意不持有任何人设:一个进程现在会以多个角色作答(design/22 §4.2),
|
||
// 若在此固化某个人设,就会把所有提取出的记忆都归到那一个角色下。循环在持久化时
|
||
// 用产生该轮的角色逐条打标(loop.persistMemories)。
|
||
//
|
||
// 循环会把 LLM 提取器分离到后台 goroutine(ExtractAsync);见
|
||
// internal/runtime/loop/memory.go。
|
||
func (a *app) buildMemoryExtractor() memory.Extractor {
|
||
if llmGroupUsable(a.llm) {
|
||
a.log.Info("memory extractor: llm", "model", a.llm.Model(), "provider", a.llm.Provider())
|
||
return memory.NewLLMExtractor(a.llm, "", memoryOwnerRoleID)
|
||
}
|
||
a.log.Warn("memory extractor: manual (no usable llm model group)")
|
||
return memory.NewManualExtractor("", memoryOwnerRoleID)
|
||
}
|
||
|
||
// llmGroupUsable 报告运行时栈的活动模型组是否真的可调用:模型与 base URL 是必需
|
||
// 项,且要么配置了 API key,要么端点是本地/内网地址 —— 后者无 key 调用属正常情况
|
||
// (自托管的 OpenAI 兼容服务)。
|
||
//
|
||
// 它刻意只做廉价的静态检查,绝不发探测请求:启动不该花掉一次模型调用。看起来可用
|
||
// 但调用时才失败的组,只会记一条提取错误日志(提取失败从不致命)。
|
||
func llmGroupUsable(llm *model.RuntimeLLM) bool {
|
||
if llm == nil {
|
||
return false
|
||
}
|
||
group := llm.Group()
|
||
if strings.TrimSpace(group.Model) == "" || strings.TrimSpace(group.BaseURL) == "" {
|
||
return false
|
||
}
|
||
if strings.TrimSpace(group.APIKey) != "" {
|
||
return true
|
||
}
|
||
return keylessEndpoint(group.BaseURL)
|
||
}
|
||
|
||
// keylessEndpoint 报告 baseURL 是否指向回环或内网主机 —— 这类地址上缺少 API key
|
||
// 是预期情况,而非配置错误。
|
||
func keylessEndpoint(baseURL string) bool {
|
||
parsed, err := url.Parse(strings.TrimSpace(baseURL))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
host := parsed.Hostname()
|
||
if host == "" {
|
||
return false
|
||
}
|
||
if strings.EqualFold(host, "localhost") {
|
||
return true
|
||
}
|
||
ip := net.ParseIP(host)
|
||
if ip == nil {
|
||
return false
|
||
}
|
||
return ip.IsLoopback() || ip.IsPrivate()
|
||
}
|