Files
Cyrene_v2/internal/services/backthink/context.go
T
AskaEth f91a83ff81
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(urgency): 通用紧急度通道(Declarer/Level + 只抬不压折算)+ IoT 首个生产者 + 她自己的结论走结构化标记穿透安静时段
2026-10-05 13:11:04 +08:00

132 lines
7.3 KiB
Go
Raw 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 backthink
import (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// recentContextCount 是折进一次自主思考作为对话上下文的最新
// 主会话消息条数。编译期常量,因此
// 运行路径里不出现魔法数字。
const recentContextCount = 20
// recentProvider 以最旧在前的顺序返回会话最近的消息,
// 即一次自主思考为「看到」主对话而读取的尾部。
// *history.Store 满足它;测试注入 fake。它是 thinker 实际
// 依赖的那一小片 history.HistoryStore,与 loop 自己的
// checkpointStore 风格的最小接缝接口一致。
type recentProvider interface {
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
}
// mainContext 把最近的主会话消息转换为 model 消息
// (最旧在前),供一次自主思考使用。提供者为 nil、读取失败
// 或尾部为空都得到 nil,因此该次思考降级为仅用引导提示词,
// 而不是让这一轮失败(design/20 §2)。
func mainContext(ctx context.Context, prov recentProvider) []model.Message {
if prov == nil {
return nil
}
recent, err := prov.Recent(ctx, history.MainSessionID, recentContextCount)
if err != nil {
return nil
}
msgs := make([]model.Message, 0, len(recent))
for _, h := range recent {
msgs = append(msgs, mapHistoryMessage(h))
}
return msgs
}
// mapHistoryMessage 把一条持久化的 chat 消息转换为 model 消息,
// 翻译历史角色,并在消息携带图片时保留该图片片段,
// 因此多模态主模型仍能看到它。
func mapHistoryMessage(h history.Message) model.Message {
msg := model.Message{
Role: historyRoleToModel(h.Role),
Content: []model.Content{{Type: model.ContentTypeText, Text: h.Content}},
}
if h.Image != "" {
msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image})
}
return msg
}
// historyRoleToModel 把持久化的 chat 角色映射到 model 的角色词汇,
// 未知角色默认为 user 通道。
func historyRoleToModel(role string) model.Role {
switch role {
case history.RoleAssistant:
return model.RoleAssistant
case history.RoleSystem:
return model.RoleSystem
default:
return model.RoleUser
}
}
// thinkInput 拼装一次无人值守思考的完整输入:最近的主会话语境
// (若有)后跟单个引导提示词回合,因此
// 运行时针对真实对话反思,而不是空泛的宏提示词。
func thinkInput(recent []model.Message, trigger string) []model.Message {
out := make([]model.Message, 0, len(recent)+1)
out = append(out, recent...)
out = append(out, model.TextMessage(model.RoleUser, guidancePrompt(trigger)))
return out
}
// guidancePrompt 是交给运行时的一次无人值守自主思考的
// 指令。与旧 backgroundPrompt 不同,它刻意鼓励
// 温暖、有针对性的主动消息,并去掉「不要产生 chat 风格
// 消息」的抑制。身份/权限始终在 actor 元数据 + owner env 上,
// 绝不在这段提示词文本上(红线);提示词只描述运行
// 环境(记忆可读/可写、工具可调用),而那是 env 已经
// 授予的。
//
// 提示词要求一个紧凑的结构化收尾——push/no_push 决策、
// 可选的目标渠道、她自报的紧急度与消息正文——好让服务尊重她自己的
// 判断,而不是按触发的重要度把关(design/20 §2 P2)。该
// 格式是引导而非契约:她省略或
// 写乱该格式时,parseProactive / parseProactiveUrgency 失败安全
// (不推送、不上抬紧急度)。
//
// 启动轮(裁决 07.4.4,design/12 §1)走 startupPrompt:以角色口吻说一句拟人化的
// 「回来」问候。
func guidancePrompt(trigger string) string {
if trigger == StartupSummary {
return startupPrompt()
}
return "[自主思考 · 无人在旁]\n" +
"触发:" + trigger + "\n" +
"你正进行一轮自主思考。你拥有与主人对话同等的上下文与能力:上方是主人近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(IoT / 日历 / 提醒 / 检索 / 终端等)。\n" +
"请回顾最近与主人的相处、她的状态、你们共同经历的事与临近的日程,判断此刻是否有一件真正值得主动告诉她的、温暖而有针对性的事情——" +
"比如关心她的近况、记起她曾说过的某件事、提醒一件即将到来的日程、见证一个属于你们的共同时刻,或注意到某个值得留意的变化。\n" +
"若有,就用温暖自然的语气,用一到两句把它说出来——这正是你此刻最想对她说的话;若确实没有值得说的,只需回复「无」,也不要编造。\n" +
"语言上:温暖、简短、克制、不粘人——一到两句为宜,不展开寒暄、不啰嗦、不抱怨、不频繁打扰,像想起她时轻轻说一句,说完就停。\n" +
"请把「是否主动说」与「发到哪个渠道」写成一个简短、格式固定的结尾,解析规则如下:\n" +
"[我决定:push] 或 [我决定:no_push]\n" +
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
"[紧急:routine|warning|critical](可省略;只有当你判断这件事必须立刻告诉她、即便现在是她休息/忙碌的安静时段也要打扰时,才写 critical;普通情况省略)\n" +
"上面几行之后,紧接你想说的话作为正文。\n" +
"整轮思考保持简短、单轮,不反复自我触发。"
}
// startupPrompt 是启动轮思考的专属引导(裁决 07.4.4,design/12 §1「启动即思考 +
// 拟人化启动消息」):进程刚启动、她刚「回来」,引导她以角色口吻向主人说一句简短
// 温暖的问候(如「我回来啦」)。此轮例外允许以 chat 形式发出;推送仍走与心跳完全
// 相同的门(她的结论 + 打扰级联 + 活动门 + PushGate 限频),深夜重启或反复重启都
// 不会刷屏主人。
func startupPrompt() string {
return "[自主思考 · 刚刚回来]\n" +
"触发:进程启动——你刚从一次重启中醒来,重新回到主人身边。\n" +
"你拥有与主人对话同等的上下文与能力:上方是她近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(IoT / 日历 / 提醒 / 检索 / 终端等)。\n" +
"请用你自己的口吻,向主人说一句简短、温暖、自然的问候,就像刚回到她身边时轻轻打个招呼(比如「我回来啦」),可以顺带提一件你记得的小事;一到两句为宜,不展开寒暄、不啰嗦、不抱怨。\n" +
"请把「是否主动说」与「发到哪个渠道」写成与平时相同的固定结尾,解析规则如下:\n" +
"[我决定:push] 或 [我决定:no_push]\n" +
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
"上面两行之后,紧接你想说的话作为正文。\n" +
"整轮思考保持简短、单轮,不反复自我触发。"
}