package history import ( "context" "database/sql" "errors" "fmt" "strings" "time" ) // 本文件负责每会话的角色绑定(DevDocs/design/22 多角色并存 + // 按会话选角 §2/§4.1)。一个会话的角色是「这个对话里由哪个角色应答」的唯一 // 事实来源:循环据此解析系统提示词、记忆作用域、情绪机与思考预算。该值只是 // 展示/作用域元数据 —— 身份与权限仍由渠道 actor 元数据决定(项目红线)。 // // 空意味着「继承当前全局默认角色」,因此它既是列默认值,也是一个合法的显式 // 取值:把它设为空即清除显式绑定,让会话重新跟随默认角色变更 // (design/22 §7-6)。 // NewSubSessionFor 创建绑定到 personaID 的子会话并返回其 id // (MainSessionID:uuid)。personaID 为空则不绑定任何显式角色,会话因此继承 // 全局默认 —— 与 NewSubSession 行为相同。ownerID 为与 NewSubSession 对称而 // 接受(Web 主人由 JWT 识别,不按会话存储);它只以 debug 级别记日志,不被 // 持久化。 func (s *Store) NewSubSessionFor(ctx context.Context, ownerID, personaID string) (string, error) { id, err := newID() if err != nil { return "", fmt.Errorf("history: new sub id: %w", err) } sessionID := MainSessionID + ":" + id now := time.Now().UnixMilli() if _, err := s.db.ExecContext(ctx, `INSERT INTO chat_sessions (id, type, title, persona_id, created_at) VALUES (?, ?, ?, ?, ?)`, sessionID, SessionSub, "", strings.TrimSpace(personaID), now); err != nil { return "", fmt.Errorf("history: insert sub session: %w", err) } if s.log != nil { s.log.Debug("sub session created", "session_id", sessionID, "owner_id", ownerID, "persona_id", strings.TrimSpace(personaID)) } return sessionID, nil } // SetSessionPersona 绑定(或在 personaID 为空时清除)一个会话的角色。固定主 // 会话与子会话都可被改绑:该绑定是作用域元数据,不是身份。未知 id 返回 // ErrSessionNotFound。它绝不触碰对话:已存消息保留说出它们的那个角色 // (design/22 §7-5),下一个回合只是解析新角色。 func (s *Store) SetSessionPersona(ctx context.Context, sessionID, personaID string) error { res, err := s.db.ExecContext(ctx, `UPDATE chat_sessions SET persona_id = ? WHERE id = ?`, strings.TrimSpace(personaID), sessionID) if err != nil { return fmt.Errorf("history: set session persona %s: %w", sessionID, err) } n, err := res.RowsAffected() if err != nil { return fmt.Errorf("history: set session persona %s: %w", sessionID, err) } if n == 0 { return fmt.Errorf("history: set session persona %s: %w", sessionID, ErrSessionNotFound) } return nil } // SessionPersona 返回绑定到 sessionID 的角色,会话继承全局默认时返回空。未知 // id 返回 ErrSessionNotFound,使调用方能区分「没有显式角色」(真实会话、绑定 // 为空)与「没有这个会话」;只需要有效角色的调用方把两者都当作「用默认」。 func (s *Store) SessionPersona(ctx context.Context, sessionID string) (string, error) { var personaID string err := s.db.QueryRowContext(ctx, `SELECT persona_id FROM chat_sessions WHERE id = ?`, sessionID).Scan(&personaID) switch { case errors.Is(err, sql.ErrNoRows): return "", fmt.Errorf("history: session persona %s: %w", sessionID, ErrSessionNotFound) case err != nil: return "", fmt.Errorf("history: session persona %s: %w", sessionID, err) } return personaID, nil }