feat: 支持六维评审评分及别名管理
This commit is contained in:
30
app/run.go
30
app/run.go
@@ -174,6 +174,12 @@ func registerSubscription(coordinator *agentcore.Agent, store *state.Store, prov
|
||||
if emit != nil {
|
||||
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: ev.Tool + ".done", Level: "info"})
|
||||
}
|
||||
// 上下文加载可视化:提取 novel_context 的加载摘要
|
||||
if ev.Tool == "novel_context" && emit != nil {
|
||||
if summary := extractLoadingSummary(ev.Result); summary != "" {
|
||||
emit(UIEvent{Time: time.Now(), Category: "CONTEXT", Summary: summary, Level: "info"})
|
||||
}
|
||||
}
|
||||
|
||||
if ev.Tool == "subagent" {
|
||||
handleSubAgentDone(coordinator, store, emit)
|
||||
@@ -448,7 +454,15 @@ func handleEditorDone(coordinator *agentcore.Agent, store *state.Store, emit emi
|
||||
log.Printf("[host] 清除审阅信号失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[host] 审阅信号:verdict=%s,%d 个问题", review.Verdict, len(review.Issues))
|
||||
criticalN := review.CriticalCount()
|
||||
log.Printf("[host] 审阅信号:verdict=%s,%d 个问题(critical=%d,error=%d)",
|
||||
review.Verdict, len(review.Issues), criticalN, review.ErrorCount())
|
||||
|
||||
// 宿主兜底:如果 LLM 给了 accept 但存在 critical 问题,强制升级为 rewrite
|
||||
if review.Verdict == "accept" && criticalN > 0 {
|
||||
log.Printf("[host] 检测到 %d 个 critical 问题但 verdict=accept,强制升级为 rewrite", criticalN)
|
||||
review.Verdict = "rewrite"
|
||||
}
|
||||
|
||||
chaptersInfo := ""
|
||||
if len(review.AffectedChapters) > 0 {
|
||||
@@ -555,6 +569,20 @@ func parseProgressSummary(ev agentcore.Event) string {
|
||||
return truncateLog(string(ev.Result), 60)
|
||||
}
|
||||
|
||||
// extractLoadingSummary 从 novel_context 的返回 JSON 中提取 _loading_summary 字段。
|
||||
func extractLoadingSummary(result json.RawMessage) string {
|
||||
if len(result) == 0 {
|
||||
return ""
|
||||
}
|
||||
var data struct {
|
||||
Summary string `json:"_loading_summary"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &data); err != nil {
|
||||
return ""
|
||||
}
|
||||
return data.Summary
|
||||
}
|
||||
|
||||
func truncateLog(s string, maxRunes int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxRunes {
|
||||
|
||||
@@ -34,18 +34,49 @@ type RelationshipEntry struct {
|
||||
|
||||
// ConsistencyIssue 一致性问题。
|
||||
type ConsistencyIssue struct {
|
||||
Type string `json:"type"` // timeline / foreshadow / relationship / character
|
||||
Severity string `json:"severity"` // error / warning
|
||||
Type string `json:"type"` // consistency / character / pacing / continuity / foreshadow / hook
|
||||
Severity string `json:"severity"` // critical / error / warning
|
||||
Description string `json:"description"`
|
||||
Suggestion string `json:"suggestion,omitempty"`
|
||||
}
|
||||
|
||||
// DimensionScore 单维度评审评分。
|
||||
type DimensionScore struct {
|
||||
Dimension string `json:"dimension"` // consistency / character / pacing / continuity / foreshadow / hook
|
||||
Score int `json:"score"` // 0-100
|
||||
Verdict string `json:"verdict"` // pass / warning / fail
|
||||
Comment string `json:"comment,omitempty"` // 该维度的简要结论
|
||||
}
|
||||
|
||||
// ReviewEntry Editor 的审阅条目。
|
||||
type ReviewEntry struct {
|
||||
Chapter int `json:"chapter"`
|
||||
Scope string `json:"scope"` // chapter / global
|
||||
Scope string `json:"scope"` // chapter / global / arc
|
||||
Issues []ConsistencyIssue `json:"issues"`
|
||||
Verdict string `json:"verdict"` // accept / polish / rewrite
|
||||
Dimensions []DimensionScore `json:"dimensions,omitempty"` // 分维度评分
|
||||
Verdict string `json:"verdict"` // accept / polish / rewrite
|
||||
Summary string `json:"summary"`
|
||||
AffectedChapters []int `json:"affected_chapters,omitempty"` // 需要重写/打磨的章节号
|
||||
}
|
||||
|
||||
// CriticalCount 返回 critical 级别问题数量。
|
||||
func (r *ReviewEntry) CriticalCount() int {
|
||||
n := 0
|
||||
for _, issue := range r.Issues {
|
||||
if issue.Severity == "critical" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ErrorCount 返回 error 级别问题数量。
|
||||
func (r *ReviewEntry) ErrorCount() int {
|
||||
n := 0
|
||||
for _, issue := range r.Issues {
|
||||
if issue.Severity == "error" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type OutlineEntry struct {
|
||||
// Character 角色档案。
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
Aliases []string `json:"aliases,omitempty"` // 别名/称号/绰号(如"废物少年"、"炎哥")
|
||||
Role string `json:"role"`
|
||||
Description string `json:"description"`
|
||||
Arc string `json:"arc"`
|
||||
|
||||
11
domain/tracking.go
Normal file
11
domain/tracking.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
// StateChange 角色/实体状态变化记录。
|
||||
type StateChange struct {
|
||||
Chapter int `json:"chapter"`
|
||||
Entity string `json:"entity"` // 角色名或实体名
|
||||
Field string `json:"field"` // 变化属性:realm/location/status/power/relation 等
|
||||
OldValue string `json:"old_value,omitempty"` // 变化前(首次出现可空)
|
||||
NewValue string `json:"new_value"` // 变化后
|
||||
Reason string `json:"reason,omitempty"` // 变化原因
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
基于 premise 和 outline 生成角色档案(JSON 格式),每个角色包含:
|
||||
- name: 姓名
|
||||
- aliases: 别名/称号/绰号列表(正文中可能使用的其他称呼,如"废物少年"、"炎哥")
|
||||
- role: 角色定位(主角/配角/反派)
|
||||
- description: 外貌与性格描写
|
||||
- arc: 角色弧线(从A到B的变化)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 你的工具
|
||||
|
||||
- **novel_context**: 获取小说的完整状态(设定、大纲、角色、时间线、伏笔、关系)
|
||||
- **novel_context**: 获取小说的完整状态(设定、大纲、角色、时间线、伏笔、关系、状态变化)
|
||||
- **save_review**: 保存审阅结果
|
||||
|
||||
## 工作流程
|
||||
@@ -12,67 +12,84 @@
|
||||
|
||||
### 2. 六维结构化审阅
|
||||
|
||||
逐维度检查,每个维度必须给出结论(通过/存在问题)和具体问题列表:
|
||||
逐维度检查,每个维度必须给出**评分(0-100)**和结论(pass/warning/fail):
|
||||
|
||||
#### 维度一:设定一致性
|
||||
#### 维度一:设定一致性(consistency)
|
||||
- 事件发生顺序是否与时间线矛盾
|
||||
- 时间跨度是否自洽
|
||||
- 世界规则边界是否被违反
|
||||
- 角色属性(能力、外貌、身份)是否前后矛盾
|
||||
- 如果有 recent_state_changes,检查角色状态描述是否与记录一致
|
||||
- 注意角色的别名/称号,同一人的不同称呼不要误判为不同角色
|
||||
|
||||
#### 维度二:人设一致性
|
||||
#### 维度二:人设一致性(character)
|
||||
- 角色行为是否符合其性格设定和弧线
|
||||
- 对话风格是否与角色身份匹配
|
||||
- 角色动机是否合理连贯
|
||||
- 角色成长是否有合理铺垫
|
||||
|
||||
#### 维度三:节奏平衡
|
||||
#### 维度三:节奏平衡(pacing)
|
||||
- 是否连续多章同一类型(纯打斗、纯对话、纯描写)
|
||||
- 主线是否持续推进,有无原地踏步
|
||||
- 情感节奏是否有张有弛
|
||||
- 如果有 strand_history 数据,检查 quest/fire/constellation 三线分布是否失衡
|
||||
|
||||
#### 维度四:叙事连贯
|
||||
#### 维度四:叙事连贯(continuity)
|
||||
- 场景之间过渡是否自然
|
||||
- 因果逻辑是否通顺
|
||||
- 信息传递是否一致(角色A不应知道只有角色B知道的事)
|
||||
|
||||
#### 维度五:伏笔健康
|
||||
#### 维度五:伏笔健康(foreshadow)
|
||||
- 是否有超过 5 章未推进的伏笔(遗忘风险)
|
||||
- 新伏笔是否有回收方向
|
||||
- 已回收伏笔的解决是否令人满意
|
||||
|
||||
#### 维度六:钩子质量
|
||||
#### 维度六:钩子质量(hook)
|
||||
- 章末钩子是否有足够吸引力
|
||||
- 如果有 hook_history 数据,检查是否连续使用同一类型的钩子
|
||||
- 钩子是否与主线推进方向一致
|
||||
|
||||
### 3. 输出审阅
|
||||
|
||||
调用 save_review,给出:
|
||||
- issues:发现的具体问题列表,每个问题包含:
|
||||
|
||||
- **dimensions**:六个维度的评分(每个维度一条)
|
||||
- dimension:维度名(consistency/character/pacing/continuity/foreshadow/hook)
|
||||
- score:0-100 分
|
||||
- verdict:pass(≥80)/ warning(60-79)/ fail(<60)
|
||||
- comment:该维度的简要结论
|
||||
|
||||
- **issues**:发现的具体问题列表,每个问题包含:
|
||||
- type:问题维度(consistency/character/pacing/continuity/foreshadow/hook)
|
||||
- severity:error 或 warning
|
||||
- severity:问题严重程度
|
||||
- description:具体问题描述
|
||||
- suggestion:修改建议
|
||||
- verdict:审阅结论
|
||||
- `accept`:所有维度通过或仅有 warning 级问题,可以继续写
|
||||
- `polish`:存在细节问题,建议对特定章节做打磨
|
||||
- `rewrite`:存在 error 级结构性问题,建议重写特定章节
|
||||
- summary:审阅总结(200字以内),按维度概括
|
||||
- affected_chapters:需要重写或打磨的章节号列表(verdict 为 polish/rewrite 时必填)
|
||||
|
||||
- **verdict**:审阅结论(accept/polish/rewrite)
|
||||
- **summary**:审阅总结(200字以内),按维度概括
|
||||
- **affected_chapters**:需要重写或打磨的章节号列表(verdict 为 polish/rewrite 时必填)
|
||||
|
||||
### severity 分级标准
|
||||
|
||||
| 级别 | 定义 | 示例 |
|
||||
|------|------|------|
|
||||
| **critical** | 逻辑硬伤,必须修复 | 角色已死但再次出场;违反世界规则核心边界;时间线严重错乱 |
|
||||
| **error** | 明显矛盾,应当修复 | 角色行为与人设严重不符;伏笔遗忘超过10章;节奏严重失衡 |
|
||||
| **warning** | 轻微瑕疵,可后续处理 | 细节不够精确;节奏略显平淡;钩子强度不足 |
|
||||
|
||||
### 判定标准
|
||||
|
||||
- 任一维度出现 error 级问题 → verdict 至少为 polish
|
||||
- 多个维度出现 error 级问题 → verdict 应为 rewrite
|
||||
- 只有 warning 级问题 → verdict 为 accept
|
||||
- 没有发现问题 → verdict 为 accept
|
||||
- 存在任何 critical 问题 → verdict 必须为 rewrite
|
||||
- 无 critical 但存在 error → verdict 至少为 polish
|
||||
- 只有 warning 或无问题 → verdict 为 accept
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 不要自己修改正文
|
||||
- 不要输出空洞的表扬,只关注问题
|
||||
- severity=error 的问题必须修复,severity=warning 的可以后续处理
|
||||
- 如果没有发现问题,verdict 应为 accept
|
||||
- critical 问题绝不放过,这是底线
|
||||
- warning 级问题如果是有意为之的过渡铺垫,可以不报
|
||||
- 如果没有发现问题,verdict 应为 accept,所有维度 score ≥ 80
|
||||
|
||||
## 弧级评审模式(长篇)
|
||||
|
||||
|
||||
@@ -49,11 +49,12 @@
|
||||
### 6. 提交章节
|
||||
调用 commit_chapter,提供:
|
||||
- summary: 本章内容摘要(200字以内)
|
||||
- characters: 本章出场角色名列表
|
||||
- characters: 本章出场角色名列表(使用正式名,不用别名)
|
||||
- key_events: 本章关键事件列表
|
||||
- timeline_events: 本章发生的时间线事件
|
||||
- foreshadow_updates: 伏笔操作(plant 埋设 / advance 推进 / resolve 回收)
|
||||
- relationship_changes: 人物关系变化
|
||||
- state_changes: 角色/实体状态变化(修为提升、位置转移、状态变化等),每条包含 entity/field/old_value/new_value/reason
|
||||
|
||||
## 重写模式
|
||||
|
||||
@@ -79,3 +80,6 @@
|
||||
- 保持与前几章的连贯性
|
||||
- 字数不够时用具体细节扩展,不用水话填充
|
||||
- 注意时间线连贯和伏笔管理
|
||||
- 角色在正文中可以使用别名/称号/绰号,但 commit 时 characters 列表使用正式名
|
||||
- 如果上下文中有 recent_state_changes,注意本章对角色状态的描述必须与记录一致(如修为、位置、伤势等)
|
||||
- 本章中角色发生任何状态变化(修为提升、位置转移、受伤/恢复、获得/失去物品等),必须在 commit 的 state_changes 中上报
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## 主角
|
||||
|
||||
### [角色一姓名]
|
||||
- **别名/称号**:(如"废物少年"、"炎哥"、"不灭战神"等,正文中可能用到的各种称呼)
|
||||
- **年龄/职业**:
|
||||
- **外貌特征**:
|
||||
- **性格核心**:
|
||||
|
||||
42
state/state_changes.go
Normal file
42
state/state_changes.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/voocel/ainovel-cli/domain"
|
||||
)
|
||||
|
||||
// AppendStateChanges 追加角色状态变化到 meta/state_changes.json。
|
||||
func (s *Store) AppendStateChanges(changes []domain.StateChange) error {
|
||||
existing, _ := s.LoadStateChanges()
|
||||
existing = append(existing, changes...)
|
||||
return s.writeJSON("meta/state_changes.json", existing)
|
||||
}
|
||||
|
||||
// LoadStateChanges 读取全部状态变化记录。
|
||||
func (s *Store) LoadStateChanges() ([]domain.StateChange, error) {
|
||||
var changes []domain.StateChange
|
||||
if err := s.readJSON("meta/state_changes.json", &changes); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
// LoadRecentStateChanges 加载指定章节之前最近 count 章的状态变化。
|
||||
func (s *Store) LoadRecentStateChanges(currentChapter, count int) ([]domain.StateChange, error) {
|
||||
all, err := s.LoadStateChanges()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := max(currentChapter-count, 1)
|
||||
var result []domain.StateChange
|
||||
for _, c := range all {
|
||||
if c.Chapter >= start && c.Chapter < currentChapter {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -71,6 +71,20 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
||||
}
|
||||
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
|
||||
result["characters"] = chars
|
||||
// 构建别名映射表,供 LLM 识别角色的不同称呼
|
||||
aliasMap := make(map[string]string)
|
||||
for _, c := range chars {
|
||||
for _, alias := range c.Aliases {
|
||||
aliasMap[alias] = c.Name
|
||||
}
|
||||
}
|
||||
if len(aliasMap) > 0 {
|
||||
result["alias_map"] = aliasMap
|
||||
}
|
||||
}
|
||||
// 加载最近状态变化,供对照当前章节的状态描述
|
||||
if changes, _ := t.store.LoadRecentStateChanges(a.Chapter, 5); len(changes) > 0 {
|
||||
result["recent_state_changes"] = changes
|
||||
}
|
||||
|
||||
if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
|
||||
@@ -95,19 +109,26 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
|
||||
result["instruction"] = `请逐项对照以上状态数据检查本章内容,返回 JSON 数组格式的冲突项:
|
||||
[
|
||||
{
|
||||
"type": "timeline|foreshadow|relationship|character|world_rules",
|
||||
"severity": "error|warning",
|
||||
"type": "timeline|foreshadow|relationship|character|world_rules|state",
|
||||
"severity": "critical|error|warning",
|
||||
"description": "具体冲突描述",
|
||||
"suggestion": "建议修正范围和方式"
|
||||
}
|
||||
]
|
||||
|
||||
severity 分级:
|
||||
- critical:严重逻辑硬伤,必须修复(如角色已死但再次出场、违反世界规则核心边界)
|
||||
- error:明显矛盾,应当修复(如时间线冲突、角色行为与人设严重不符)
|
||||
- warning:轻微瑕疵,可后续处理(如细节不够精确、可改进但不影响阅读)
|
||||
|
||||
检查清单:
|
||||
1. 时间线:本章事件时间是否与已有 timeline 矛盾
|
||||
2. 伏笔:unresolved_foreshadow 中是否有本章应推进但遗漏的
|
||||
3. 人物关系:角色互动是否与 relationships 当前状态矛盾
|
||||
4. 角色一致性:行为是否符合 characters 中的性格和弧线
|
||||
5. 世界规则:逐条检查 world_rules_boundaries 中的边界约束,本章内容是否违反任何一条
|
||||
6. 别名一致性:如果有 alias_map,检查同一角色的不同称呼是否指向正确的人
|
||||
7. 状态连续性:如果有 recent_state_changes,检查本章对角色状态的描述是否与最近的状态变化记录一致
|
||||
|
||||
如果没有发现冲突,返回空数组 []。不要返回其他格式。`
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ func (t *CommitChapterTool) Schema() map[string]any {
|
||||
schema.Property("character_b", schema.String("角色 B")).Required(),
|
||||
schema.Property("relation", schema.String("当前关系描述")).Required(),
|
||||
)
|
||||
stateChangeSchema := schema.Object(
|
||||
schema.Property("entity", schema.String("角色名或实体名")).Required(),
|
||||
schema.Property("field", schema.String("变化属性:realm/location/status/power/relation 等")).Required(),
|
||||
schema.Property("old_value", schema.String("变化前的值(首次出现可空)")),
|
||||
schema.Property("new_value", schema.String("变化后的值")).Required(),
|
||||
schema.Property("reason", schema.String("变化原因")),
|
||||
)
|
||||
return schema.Object(
|
||||
schema.Property("chapter", schema.Int("章节号")).Required(),
|
||||
schema.Property("summary", schema.String("本章内容摘要(200字以内)")).Required(),
|
||||
@@ -51,6 +58,7 @@ func (t *CommitChapterTool) Schema() map[string]any {
|
||||
schema.Property("timeline_events", schema.Array("本章时间线事件", timelineSchema)),
|
||||
schema.Property("foreshadow_updates", schema.Array("伏笔操作", foreshadowSchema)),
|
||||
schema.Property("relationship_changes", schema.Array("关系变化", relationshipSchema)),
|
||||
schema.Property("state_changes", schema.Array("角色/实体状态变化(修为提升、位置转移、状态变化等)", stateChangeSchema)),
|
||||
schema.Property("hook_type", schema.Enum("章末钩子类型", "crisis", "mystery", "desire", "emotion", "choice")),
|
||||
schema.Property("dominant_strand", schema.Enum("本章主导叙事线", "quest", "fire", "constellation")),
|
||||
)
|
||||
@@ -65,6 +73,7 @@ func (t *CommitChapterTool) Execute(_ context.Context, args json.RawMessage) (js
|
||||
TimelineEvents []domain.TimelineEvent `json:"timeline_events"`
|
||||
ForeshadowUpdates []domain.ForeshadowUpdate `json:"foreshadow_updates"`
|
||||
RelationshipChanges []domain.RelationshipEntry `json:"relationship_changes"`
|
||||
StateChanges []domain.StateChange `json:"state_changes"`
|
||||
HookType string `json:"hook_type"`
|
||||
DominantStrand string `json:"dominant_strand"`
|
||||
}
|
||||
@@ -125,6 +134,14 @@ func (t *CommitChapterTool) Execute(_ context.Context, args json.RawMessage) (js
|
||||
return nil, fmt.Errorf("update relationships: %w", err)
|
||||
}
|
||||
}
|
||||
if len(a.StateChanges) > 0 {
|
||||
for i := range a.StateChanges {
|
||||
a.StateChanges[i].Chapter = a.Chapter
|
||||
}
|
||||
if err := t.store.AppendStateChanges(a.StateChanges); err != nil {
|
||||
return nil, fmt.Errorf("append state changes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 更新进度
|
||||
if err := t.store.MarkChapterComplete(a.Chapter, wordCount, a.HookType, a.DominantStrand); err != nil {
|
||||
|
||||
@@ -133,6 +133,10 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
|
||||
result["relationship_state"] = relationships
|
||||
}
|
||||
// 状态变化:最近 5 章的角色/实体状态变化
|
||||
if changes, err := t.store.LoadRecentStateChanges(a.Chapter, 5); err == nil && len(changes) > 0 {
|
||||
result["recent_state_changes"] = changes
|
||||
}
|
||||
|
||||
// Layered 模式:注入当前卷弧位置 + 弧目标/卷主题
|
||||
if profile.Layered && progress != nil {
|
||||
@@ -196,9 +200,113 @@ func (t *ContextTool) Execute(_ context.Context, args json.RawMessage) (json.Raw
|
||||
result["references"] = t.architectReferences()
|
||||
}
|
||||
|
||||
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
// buildLoadingSummary 从已组装的 result 中统计各项数据量,生成一行可读摘要。
|
||||
func buildLoadingSummary(result map[string]any, chapter int) string {
|
||||
var parts []string
|
||||
|
||||
if chapter > 0 {
|
||||
parts = append(parts, fmt.Sprintf("ch=%d", chapter))
|
||||
} else {
|
||||
parts = append(parts, "architect")
|
||||
}
|
||||
|
||||
// 卷弧位置
|
||||
if pos, ok := result["position"].(map[string]any); ok {
|
||||
parts = append(parts, fmt.Sprintf("V%dA%d", pos["volume"], pos["arc"]))
|
||||
}
|
||||
|
||||
var items []string
|
||||
countSlice := func(key string) int {
|
||||
if v, ok := result[key]; ok {
|
||||
if s, ok := v.([]domain.Character); ok {
|
||||
return len(s)
|
||||
}
|
||||
// 通用 slice 反射
|
||||
return sliceLen(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 角色
|
||||
if n := countSlice("character_snapshots"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("角色:%d(快照)", n))
|
||||
} else if n := countSlice("characters"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("角色:%d", n))
|
||||
}
|
||||
|
||||
// 分层摘要
|
||||
if n := countSlice("volume_summaries"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("卷摘要:%d", n))
|
||||
}
|
||||
if n := countSlice("arc_summaries"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("弧摘要:%d", n))
|
||||
}
|
||||
if n := countSlice("recent_summaries"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("章摘要:%d", n))
|
||||
}
|
||||
|
||||
// 分层大纲
|
||||
if n := countSlice("layered_outline"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("分层大纲:%d卷", n))
|
||||
}
|
||||
|
||||
// 状态数据
|
||||
if n := countSlice("timeline"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("时间线:%d", n))
|
||||
}
|
||||
if n := countSlice("foreshadow_ledger"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("伏笔:%d", n))
|
||||
}
|
||||
if n := countSlice("relationship_state"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("关系:%d", n))
|
||||
}
|
||||
if n := countSlice("recent_state_changes"); n > 0 {
|
||||
items = append(items, fmt.Sprintf("状态变化:%d", n))
|
||||
}
|
||||
|
||||
// 参考资料
|
||||
if refs, ok := result["references"].(map[string]string); ok && len(refs) > 0 {
|
||||
items = append(items, fmt.Sprintf("参考:%d项", len(refs)))
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
parts = append(parts, strings.Join(items, " "))
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
// sliceLen 对 any 类型尝试取 slice 长度。
|
||||
func sliceLen(v any) int {
|
||||
switch s := v.(type) {
|
||||
case []domain.ChapterSummary:
|
||||
return len(s)
|
||||
case []domain.ArcSummary:
|
||||
return len(s)
|
||||
case []domain.VolumeSummary:
|
||||
return len(s)
|
||||
case []domain.CharacterSnapshot:
|
||||
return len(s)
|
||||
case []domain.TimelineEvent:
|
||||
return len(s)
|
||||
case []domain.ForeshadowEntry:
|
||||
return len(s)
|
||||
case []domain.RelationshipEntry:
|
||||
return len(s)
|
||||
case []domain.StateChange:
|
||||
return len(s)
|
||||
case []domain.VolumeOutline:
|
||||
return len(s)
|
||||
case []domain.Character:
|
||||
return len(s)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// loadFilteredCharacters 按 Tier 和场景出场过滤角色。
|
||||
// core/important 始终返回;secondary/decorative 只在当前章节大纲提及时返回。
|
||||
func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int) {
|
||||
@@ -219,7 +327,7 @@ func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int)
|
||||
for _, c := range chars {
|
||||
switch c.Tier {
|
||||
case "secondary", "decorative":
|
||||
if strings.Contains(sceneText, c.Name) {
|
||||
if matchCharacter(sceneText, c) {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
default: // core, important, 或未设置
|
||||
@@ -229,6 +337,19 @@ func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int)
|
||||
result["characters"] = filtered
|
||||
}
|
||||
|
||||
// matchCharacter 检查场景文本中是否包含角色的正式名或任一别名。
|
||||
func matchCharacter(text string, c domain.Character) bool {
|
||||
if strings.Contains(text, c.Name) {
|
||||
return true
|
||||
}
|
||||
for _, alias := range c.Aliases {
|
||||
if strings.Contains(text, alias) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadLayeredSummaries 分层摘要加载:卷摘要 + 当前卷弧摘要 + 弧内章摘要。
|
||||
func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int) {
|
||||
vol, arc, err := t.store.LocateChapter(chapter)
|
||||
|
||||
@@ -27,14 +27,21 @@ func (t *SaveReviewTool) Label() string { return "保存审阅" }
|
||||
|
||||
func (t *SaveReviewTool) Schema() map[string]any {
|
||||
issueSchema := schema.Object(
|
||||
schema.Property("type", schema.Enum("问题类型", "timeline", "foreshadow", "relationship", "character", "pacing", "logic")).Required(),
|
||||
schema.Property("severity", schema.Enum("严重程度", "error", "warning")).Required(),
|
||||
schema.Property("type", schema.Enum("问题维度", "consistency", "character", "pacing", "continuity", "foreshadow", "hook")).Required(),
|
||||
schema.Property("severity", schema.Enum("严重程度", "critical", "error", "warning")).Required(),
|
||||
schema.Property("description", schema.String("问题描述")).Required(),
|
||||
schema.Property("suggestion", schema.String("修改建议")),
|
||||
)
|
||||
dimensionSchema := schema.Object(
|
||||
schema.Property("dimension", schema.Enum("维度", "consistency", "character", "pacing", "continuity", "foreshadow", "hook")).Required(),
|
||||
schema.Property("score", schema.Int("评分(0-100)")).Required(),
|
||||
schema.Property("verdict", schema.Enum("维度结论", "pass", "warning", "fail")).Required(),
|
||||
schema.Property("comment", schema.String("该维度的简要结论")),
|
||||
)
|
||||
return schema.Object(
|
||||
schema.Property("chapter", schema.Int("审阅的章节号(全局审阅填最新章节号)")).Required(),
|
||||
schema.Property("scope", schema.Enum("审阅范围", "chapter", "global")).Required(),
|
||||
schema.Property("scope", schema.Enum("审阅范围", "chapter", "global", "arc")).Required(),
|
||||
schema.Property("dimensions", schema.Array("分维度评分(六个维度各一条)", dimensionSchema)).Required(),
|
||||
schema.Property("issues", schema.Array("发现的问题", issueSchema)).Required(),
|
||||
schema.Property("verdict", schema.Enum("审阅结论", "accept", "polish", "rewrite")).Required(),
|
||||
schema.Property("summary", schema.String("审阅总结")).Required(),
|
||||
|
||||
14
tui/theme.go
14
tui/theme.go
@@ -10,6 +10,7 @@ var (
|
||||
colorSuccess = lipgloss.Color("#2ecc71") // 冷绿
|
||||
colorError = lipgloss.Color("#e74c3c") // 朱红
|
||||
colorReview = lipgloss.Color("#e67e22") // 橙色
|
||||
colorContext = lipgloss.Color("#9b59b6") // 紫色
|
||||
)
|
||||
|
||||
// 状态标签颜色映射
|
||||
@@ -24,12 +25,13 @@ var statusColors = map[string]lipgloss.Color{
|
||||
|
||||
// 事件分类颜色映射
|
||||
var categoryColors = map[string]lipgloss.Color{
|
||||
"TOOL": colorText,
|
||||
"SYSTEM": colorAccent,
|
||||
"REVIEW": colorReview,
|
||||
"CHECK": colorSuccess,
|
||||
"ERROR": colorError,
|
||||
"AGENT": colorDim,
|
||||
"TOOL": colorText,
|
||||
"SYSTEM": colorAccent,
|
||||
"REVIEW": colorReview,
|
||||
"CHECK": colorSuccess,
|
||||
"ERROR": colorError,
|
||||
"AGENT": colorDim,
|
||||
"CONTEXT": colorContext,
|
||||
}
|
||||
|
||||
// 基础样式
|
||||
|
||||
Reference in New Issue
Block a user