- 四组并行完成: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 全过)。
168 lines
6.2 KiB
Go
168 lines
6.2 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// 以下段头对应 design/01 §2.2 中此前未被消费的人格字段。它们作为独立的 system
|
||
// 提示段追加在身份块之后,使每一段都能单独调优而不必改动身份正文。
|
||
const (
|
||
personalityHeader = "【性格】"
|
||
valuesHeader = "【价值观】"
|
||
speechStyleHeader = "【说话风格】"
|
||
catchphraseHeader = "【口头禅】"
|
||
backgroundHeader = "【背景】"
|
||
)
|
||
|
||
// personaSection 是一个追加块:方括号段头 + 正文。
|
||
type personaSection struct {
|
||
header string
|
||
body string
|
||
}
|
||
|
||
// defaultPersona 返回默认角色的 persona;没有注册角色/persona 时返回 false。
|
||
func (a *app) defaultPersona() (*loader.Persona, bool) {
|
||
c, ok := a.personas.DefaultCharacter()
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
p, ok := a.personas.Persona(c.PersonaID)
|
||
if !ok || p == nil {
|
||
return nil, false
|
||
}
|
||
return p, true
|
||
}
|
||
|
||
// identityInstructionFor 返回 persona 的身份块 —— design/01 §2.2 之前唯一注入的
|
||
// 部分:优先 identity.systemPrompt,其次 identity.role,再退回最小兜底文本。
|
||
func identityInstructionFor(p *loader.Persona) string {
|
||
if p.Identity.SystemPrompt != "" {
|
||
return p.Identity.SystemPrompt
|
||
}
|
||
if p.Identity.Role != "" {
|
||
return p.Identity.Role
|
||
}
|
||
return fallbackInstruction
|
||
}
|
||
|
||
// instructionsFromPersona 构建 persona 的 system 指令:身份块,后接此前无人消费的
|
||
// personality/values/speechStyle/catchphrase/background 各段(design/01 §2.2)。
|
||
// 空白字段会被跳过,因此不会产生空段;这些字段全空的 persona 产出与身份块完全一致
|
||
// —— 与既有输出逐字节相同。
|
||
func instructionsFromPersona(p *loader.Persona) string {
|
||
base := identityInstructionFor(p)
|
||
sections := personaSections(p)
|
||
if len(sections) == 0 {
|
||
return base
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString(base)
|
||
for _, s := range sections {
|
||
b.WriteString("\n\n")
|
||
b.WriteString(s.header)
|
||
b.WriteString("\n")
|
||
b.WriteString(s.body)
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// personaSections 按固定顺序渲染五个可选段(personality → values → speechStyle
|
||
// → catchphrase → background),跳过其中为空的。
|
||
func personaSections(p *loader.Persona) []personaSection {
|
||
out := make([]personaSection, 0, 5)
|
||
appendSection := func(header, body string) {
|
||
if body = strings.TrimSpace(body); body == "" {
|
||
return
|
||
}
|
||
out = append(out, personaSection{header: header, body: body})
|
||
}
|
||
appendSection(personalityHeader, p.Personality)
|
||
appendSection(valuesHeader, bulletList(p.Values))
|
||
appendSection(speechStyleHeader, p.SpeechStyle)
|
||
appendSection(catchphraseHeader, p.Catchphrase)
|
||
appendSection(backgroundHeader, p.Background)
|
||
return out
|
||
}
|
||
|
||
// bulletList 把字符串项渲染成 markdown 无序列表,跳过空白项;全空的列表产出 ""
|
||
// 使对应段彻底消失。
|
||
func bulletList(items []string) string {
|
||
lines := make([]string, 0, len(items))
|
||
for _, item := range items {
|
||
if item = strings.TrimSpace(item); item != "" {
|
||
lines = append(lines, "- "+item)
|
||
}
|
||
}
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
// defaultThinkingOptions 返回循环应为默认 persona 注入的推理开关:thinking 指令、
|
||
// reasoning_effort 取值与 thinking 广播开关。它是循环每轮所做的按角色解析的默认
|
||
// 一半(Options.PersonaKnobs,见下方 personaKnobs),因此配置的默认值与每轮查询
|
||
// 永远不会漂移。
|
||
//
|
||
// 只有当 persona 配置了会影响请求的内容(预算、effort,或显式的开关模式)时
|
||
// thinking 指令才非 nil。showToUser 刻意不产生指令:它只是呈现开关,而一个没有
|
||
// 开关的非 nil 指令会顺带打开供应商侧 thinking。未注册 persona 时三者均为零值,
|
||
// 循环既有行为不受影响。
|
||
func (a *app) defaultThinkingOptions() (*model.Thinking, string, *bool) {
|
||
knobs, ok := a.personaKnobs(a.defaultPersonaID())
|
||
if !ok {
|
||
return nil, "", nil
|
||
}
|
||
return knobs.Thinking, knobs.ReasoningEffort, knobs.ThinkingShowToUser
|
||
}
|
||
|
||
// personaKnobs 为循环解析某个角色的推理开关(loop.Options.PersonaKnobs,
|
||
// design/22 §4.2):循环会为即将构建请求的那一轮询问其角色,因此绑定角色 Y 的
|
||
// 会话运行时用的是 Y 的 thinking 预算 / effort / 广播开关,而不是默认角色的。
|
||
// 从角色 id 解析,而不是把取值推入每个调用方的 SessionEnv,意味所有路径 —— Web
|
||
// REST/WS/语音、IM、backthink、子代理 —— 都由同一个输入拿到正确的开关。
|
||
// ok=false 表示「角色未知」,循环沿用其配置的默认值。
|
||
func (a *app) personaKnobs(personaID string) (loop.LoopKnobs, bool) {
|
||
if personaID == "" {
|
||
return loop.LoopKnobs{}, false
|
||
}
|
||
p, ok := a.personas.Persona(personaID)
|
||
if !ok || p == nil {
|
||
return loop.LoopKnobs{}, false
|
||
}
|
||
thinking, effort := personaThinking(p)
|
||
return loop.LoopKnobs{
|
||
Thinking: thinking,
|
||
ReasoningEffort: effort,
|
||
ThinkingShowToUser: p.Thinking.ShowToUser,
|
||
}, true
|
||
}
|
||
|
||
// personaName 把角色 id 解析成其展示名,供模型上下文的说话人归属使用
|
||
// (loop.Options.PersonaName,design/22 §7-5)。循环把发话角色写成
|
||
// 「「昔涟」(xilian)」而不是裸 id,使模型在会话切换角色后仍能跟上谁说了什么。
|
||
// id 为空或未知时解析为 "",循环退回只写 id —— 归属不会因为名字解析不出就消失,
|
||
// 且这里没有任何身份或权限输入。
|
||
func (a *app) personaName(personaID string) string {
|
||
if personaID == "" {
|
||
return ""
|
||
}
|
||
p, ok := a.personas.Persona(personaID)
|
||
if !ok || p == nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(p.Name)
|
||
}
|
||
|
||
// personaThinking 把 persona 的 `thinking` 段映射到模型层开关(design/01 §2.1 /
|
||
// design/04 §3.2)。没有任何内容影响请求时返回 (nil, "")。
|
||
func personaThinking(p *loader.Persona) (*model.Thinking, string) {
|
||
effort := strings.TrimSpace(p.Thinking.Effort)
|
||
enable := p.Thinking.Enable()
|
||
if p.Thinking.BudgetTokens == 0 && effort == "" && enable == nil {
|
||
return nil, ""
|
||
}
|
||
return &model.Thinking{BudgetTokens: p.Thinking.BudgetTokens, Enable: enable}, effort
|
||
}
|