feat: 角色视觉体系(头像按角色取)+ 时间注入(loop 每轮当前时间)
【角色视觉体系(红线:每个角色自己的头像/背景,绝不串用昔涟)】
- persona.go:Persona 加 Visual{avatar, chatBackground{landscape,portrait}} + DefaultAvatar;每个角色各自配。
- cyrene.yaml:昔涟配 avatar(3F-Q-Happy-1) + chatBackground(横/竖屏)。
- PersonaStore 加 Persona(id);PersonaSummary 加 avatar/chatBackground;collectPersonas 按角色取
Persona(id) 视觉(无视觉→空,前端回退默认占位,绝不串角色)。
- 前端:types PersonaRecord 加 avatar/chatBackground;persona.ts personaAvatar(p)=p?.avatar||默认占位
(殊无昔涟 fallback);store 加 currentPersona;App 挂载 fetchPersonas 取默认角色;
TopBar/chat 头像改 personaAvatar(currentPersona)(写死昔涟头像已清除)。
【时间注入(她自然知道现在几点,深夜催睡交给人格/capacity)】
- options.go Options 加 Timezone;loop.go Loop 加 timeLocation/timeZoneName + New 解析 + loadTimezone;
time.go currentTimeBlock()(当前时间+周几+时区,每轮刷新);memory.go assembleContext 在「工作记忆」
前注入时间 system(keyed 在 timeLocation!=nil,无时区不注入不破坏测试);wire.go Timezone=a.cfg.Timezone。
go build/vet/test 全绿(gateway/loop/persona);前端 tsc/vite build 75模块绿。红线无写死昔涟头像。
This commit is contained in:
1 parent
950bea9758
commit
7455d09d05
20 files changed
+177
-6
No files matched your search
@@ -16,6 +16,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
@@ -92,6 +93,8 @@ type Loop struct {
|
||||
tracer *trace.Tracer
|
||||
guards *guardrail.Pipeline
|
||||
personaID string
|
||||
timeLocation *time.Location
|
||||
timeZoneName string
|
||||
maxRounds int
|
||||
toolExec string
|
||||
toolParallelism int
|
||||
@@ -126,6 +129,7 @@ func New(opts Options) *Loop {
|
||||
if compactor == nil {
|
||||
compactor = defaultCompactor{}
|
||||
}
|
||||
timeLocation, timeZoneName := loadTimezone(opts.Timezone, sourcedLog(opts.Log))
|
||||
return &Loop{
|
||||
agent: opts.Agent,
|
||||
tools: opts.Tools,
|
||||
@@ -139,6 +143,8 @@ func New(opts Options) *Loop {
|
||||
tracer: opts.Tracer,
|
||||
guards: opts.Guards,
|
||||
personaID: opts.PersonaID,
|
||||
timeLocation: timeLocation,
|
||||
timeZoneName: timeZoneName,
|
||||
maxRounds: maxRounds,
|
||||
toolExec: toolExec,
|
||||
toolParallelism: toolParallelism,
|
||||
|
||||
@@ -94,6 +94,13 @@ func formatMemoryBlock(results []memory.Result, marker string) string {
|
||||
// when nothing is configured.
|
||||
func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, round int) []model.Message {
|
||||
var leading []model.Message
|
||||
// Per-round "current time" so the model naturally knows the wall clock (not a
|
||||
// first-turn snapshot). Keyed on a configured timezone so tests and no-TZ runs
|
||||
// keep their exact leading-message counts. It sits before working memory / the
|
||||
// memory block and can still be projected by transformContext.
|
||||
if l.timeLocation != nil {
|
||||
leading = append(leading, l.currentTimeBlock())
|
||||
}
|
||||
if l.working != nil {
|
||||
if s := l.working.Summarize(); s != "" {
|
||||
leading = append(leading, model.TextMessage(model.RoleSystem, "工作记忆: "+s))
|
||||
|
||||
@@ -58,6 +58,10 @@ type Options struct {
|
||||
Guards *guardrail.Pipeline
|
||||
// PersonaID scopes memory retrieval and extraction. Blank skips both.
|
||||
PersonaID string
|
||||
// Timezone is the IANA zone used to render the per-round "current time"
|
||||
// system message (e.g. "Asia/Shanghai"). Empty disables time injection
|
||||
// entirely (backward compatible); a name that fails to load also disables it.
|
||||
Timezone string
|
||||
// MaxRounds is the tool-loop upper bound. Non-positive selects
|
||||
// DefaultMaxRounds (8).
|
||||
MaxRounds int
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package loop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// weekdaysCN maps time.Weekday (0=Sunday) to a Chinese weekday label.
|
||||
var weekdaysCN = [...]string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
|
||||
// loadTimezone resolves the configured IANA timezone into a *time.Location for
|
||||
// the per-round time injection. An empty name (or one that fails to load) yields
|
||||
// nil, disabling injection entirely (backward compatible, graceful degradation).
|
||||
func loadTimezone(name string, log *slog.Logger) (*time.Location, string) {
|
||||
if name == "" {
|
||||
return nil, ""
|
||||
}
|
||||
loc, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
log.Warn("timezone load failed; time injection disabled", "tz", name, "error", err)
|
||||
return nil, ""
|
||||
}
|
||||
return loc, name
|
||||
}
|
||||
|
||||
// currentTimeBlock renders one per-round "current time" system message so the
|
||||
// model knows the wall-clock time each round (not a first-turn snapshot). It is
|
||||
// called from assembleContext only when a timezone was configured. It provides
|
||||
// the raw clock — whether it is "late and time to sleep" is the persona prompt's
|
||||
// job, not a hard-coded rule here.
|
||||
func (l *Loop) currentTimeBlock() model.Message {
|
||||
now := time.Now().In(l.timeLocation)
|
||||
text := fmt.Sprintf("当前时间:%s %s,时区 %s",
|
||||
now.Format("2006-01-02 15:04"),
|
||||
weekdaysCN[now.Weekday()],
|
||||
l.timeZoneName,
|
||||
)
|
||||
return model.TextMessage(model.RoleSystem, text)
|
||||
}
|
||||
Reference in new issue
Block a user