feat: 支持六维评审评分及别名管理

This commit is contained in:
voocel
2026-03-12 22:25:34 +08:00
parent bce0adeff1
commit 16e790a372
14 changed files with 344 additions and 40 deletions

View File

@@ -174,6 +174,12 @@ func registerSubscription(coordinator *agentcore.Agent, store *state.Store, prov
if emit != nil { if emit != nil {
emit(UIEvent{Time: time.Now(), Category: "TOOL", Summary: ev.Tool + ".done", Level: "info"}) 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" { if ev.Tool == "subagent" {
handleSubAgentDone(coordinator, store, emit) 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] 清除审阅信号失败: %v", err)
} }
log.Printf("[host] 审阅信号verdict=%s%d 个问题", review.Verdict, len(review.Issues)) criticalN := review.CriticalCount()
log.Printf("[host] 审阅信号verdict=%s%d 个问题critical=%derror=%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 := "" chaptersInfo := ""
if len(review.AffectedChapters) > 0 { if len(review.AffectedChapters) > 0 {
@@ -555,6 +569,20 @@ func parseProgressSummary(ev agentcore.Event) string {
return truncateLog(string(ev.Result), 60) 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 { func truncateLog(s string, maxRunes int) string {
runes := []rune(s) runes := []rune(s)
if len(runes) <= maxRunes { if len(runes) <= maxRunes {

View File

@@ -34,18 +34,49 @@ type RelationshipEntry struct {
// ConsistencyIssue 一致性问题。 // ConsistencyIssue 一致性问题。
type ConsistencyIssue struct { type ConsistencyIssue struct {
Type string `json:"type"` // timeline / foreshadow / relationship / character Type string `json:"type"` // consistency / character / pacing / continuity / foreshadow / hook
Severity string `json:"severity"` // error / warning Severity string `json:"severity"` // critical / error / warning
Description string `json:"description"` Description string `json:"description"`
Suggestion string `json:"suggestion,omitempty"` 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 的审阅条目。 // ReviewEntry Editor 的审阅条目。
type ReviewEntry struct { type ReviewEntry struct {
Chapter int `json:"chapter"` Chapter int `json:"chapter"`
Scope string `json:"scope"` // chapter / global Scope string `json:"scope"` // chapter / global / arc
Issues []ConsistencyIssue `json:"issues"` 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"` Summary string `json:"summary"`
AffectedChapters []int `json:"affected_chapters,omitempty"` // 需要重写/打磨的章节号 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
}

View File

@@ -18,6 +18,7 @@ type OutlineEntry struct {
// Character 角色档案。 // Character 角色档案。
type Character struct { type Character struct {
Name string `json:"name"` Name string `json:"name"`
Aliases []string `json:"aliases,omitempty"` // 别名/称号/绰号(如"废物少年"、"炎哥"
Role string `json:"role"` Role string `json:"role"`
Description string `json:"description"` Description string `json:"description"`
Arc string `json:"arc"` Arc string `json:"arc"`

11
domain/tracking.go Normal file
View 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"` // 变化原因
}

View File

@@ -50,6 +50,7 @@
基于 premise 和 outline 生成角色档案JSON 格式),每个角色包含: 基于 premise 和 outline 生成角色档案JSON 格式),每个角色包含:
- name: 姓名 - name: 姓名
- aliases: 别名/称号/绰号列表(正文中可能使用的其他称呼,如"废物少年"、"炎哥"
- role: 角色定位(主角/配角/反派) - role: 角色定位(主角/配角/反派)
- description: 外貌与性格描写 - description: 外貌与性格描写
- arc: 角色弧线从A到B的变化 - arc: 角色弧线从A到B的变化

View File

@@ -2,7 +2,7 @@
## 你的工具 ## 你的工具
- **novel_context**: 获取小说的完整状态(设定、大纲、角色、时间线、伏笔、关系) - **novel_context**: 获取小说的完整状态(设定、大纲、角色、时间线、伏笔、关系、状态变化
- **save_review**: 保存审阅结果 - **save_review**: 保存审阅结果
## 工作流程 ## 工作流程
@@ -12,67 +12,84 @@
### 2. 六维结构化审阅 ### 2. 六维结构化审阅
逐维度检查,每个维度必须给出结论(通过/存在问题)和具体问题列表 逐维度检查,每个维度必须给出**评分0-100**和结论pass/warning/fail
#### 维度一:设定一致性 #### 维度一:设定一致性consistency
- 事件发生顺序是否与时间线矛盾 - 事件发生顺序是否与时间线矛盾
- 时间跨度是否自洽 - 时间跨度是否自洽
- 世界规则边界是否被违反 - 世界规则边界是否被违反
- 角色属性(能力、外貌、身份)是否前后矛盾 - 角色属性(能力、外貌、身份)是否前后矛盾
- 如果有 recent_state_changes检查角色状态描述是否与记录一致
- 注意角色的别名/称号,同一人的不同称呼不要误判为不同角色
#### 维度二:人设一致性 #### 维度二:人设一致性character
- 角色行为是否符合其性格设定和弧线 - 角色行为是否符合其性格设定和弧线
- 对话风格是否与角色身份匹配 - 对话风格是否与角色身份匹配
- 角色动机是否合理连贯 - 角色动机是否合理连贯
- 角色成长是否有合理铺垫
#### 维度三:节奏平衡 #### 维度三:节奏平衡pacing
- 是否连续多章同一类型(纯打斗、纯对话、纯描写) - 是否连续多章同一类型(纯打斗、纯对话、纯描写)
- 主线是否持续推进,有无原地踏步 - 主线是否持续推进,有无原地踏步
- 情感节奏是否有张有弛 - 情感节奏是否有张有弛
- 如果有 strand_history 数据,检查 quest/fire/constellation 三线分布是否失衡 - 如果有 strand_history 数据,检查 quest/fire/constellation 三线分布是否失衡
#### 维度四:叙事连贯 #### 维度四:叙事连贯continuity
- 场景之间过渡是否自然 - 场景之间过渡是否自然
- 因果逻辑是否通顺 - 因果逻辑是否通顺
- 信息传递是否一致角色A不应知道只有角色B知道的事 - 信息传递是否一致角色A不应知道只有角色B知道的事
#### 维度五:伏笔健康 #### 维度五:伏笔健康foreshadow
- 是否有超过 5 章未推进的伏笔(遗忘风险) - 是否有超过 5 章未推进的伏笔(遗忘风险)
- 新伏笔是否有回收方向 - 新伏笔是否有回收方向
- 已回收伏笔的解决是否令人满意 - 已回收伏笔的解决是否令人满意
#### 维度六:钩子质量 #### 维度六:钩子质量hook
- 章末钩子是否有足够吸引力 - 章末钩子是否有足够吸引力
- 如果有 hook_history 数据,检查是否连续使用同一类型的钩子 - 如果有 hook_history 数据,检查是否连续使用同一类型的钩子
- 钩子是否与主线推进方向一致 - 钩子是否与主线推进方向一致
### 3. 输出审阅 ### 3. 输出审阅
调用 save_review给出 调用 save_review给出
- issues发现的具体问题列表每个问题包含
- **dimensions**:六个维度的评分(每个维度一条)
- dimension维度名consistency/character/pacing/continuity/foreshadow/hook
- score0-100 分
- verdictpass≥80/ warning60-79/ fail<60
- comment该维度的简要结论
- **issues**发现的具体问题列表每个问题包含
- type问题维度consistency/character/pacing/continuity/foreshadow/hook - type问题维度consistency/character/pacing/continuity/foreshadow/hook
- severityerror 或 warning - severity问题严重程度
- description具体问题描述 - description具体问题描述
- suggestion修改建议 - suggestion修改建议
- verdict审阅结论
- `accept`:所有维度通过或仅有 warning 级问题,可以继续写 - **verdict**审阅结论accept/polish/rewrite
- `polish`:存在细节问题,建议对特定章节做打磨 - **summary**审阅总结200字以内按维度概括
- `rewrite`:存在 error 级结构性问题,建议重写特定章节 - **affected_chapters**需要重写或打磨的章节号列表verdict polish/rewrite 时必填
- summary审阅总结200字以内按维度概括
- affected_chapters需要重写或打磨的章节号列表verdict 为 polish/rewrite 时必填) ### severity 分级标准
| 级别 | 定义 | 示例 |
|------|------|------|
| **critical** | 逻辑硬伤必须修复 | 角色已死但再次出场违反世界规则核心边界时间线严重错乱 |
| **error** | 明显矛盾应当修复 | 角色行为与人设严重不符伏笔遗忘超过10章节奏严重失衡 |
| **warning** | 轻微瑕疵可后续处理 | 细节不够精确节奏略显平淡钩子强度不足 |
### 判定标准 ### 判定标准
- 任一维度出现 error 级问题 → verdict 至少为 polish - 存在任何 critical 问题 verdict 必须为 rewrite
- 多个维度出现 error 级问题 → verdict 应为 rewrite - critical 但存在 error verdict 至少为 polish
- 只有 warning 级问题 → verdict 为 accept - 只有 warning 或无问题 verdict accept
- 没有发现问题 → verdict 为 accept
## 注意事项 ## 注意事项
- 不要自己修改正文 - 不要自己修改正文
- 不要输出空洞的表扬只关注问题 - 不要输出空洞的表扬只关注问题
- severity=error 的问题必须修复severity=warning 的可以后续处理 - critical 问题绝不放过这是底线
- 如果没有发现问题verdict 应为 accept - warning 级问题如果是有意为之的过渡铺垫可以不报
- 如果没有发现问题verdict 应为 accept所有维度 score 80
## 弧级评审模式(长篇) ## 弧级评审模式(长篇)

View File

@@ -49,11 +49,12 @@
### 6. 提交章节 ### 6. 提交章节
调用 commit_chapter提供 调用 commit_chapter提供
- summary: 本章内容摘要200字以内 - summary: 本章内容摘要200字以内
- characters: 本章出场角色名列表 - characters: 本章出场角色名列表(使用正式名,不用别名)
- key_events: 本章关键事件列表 - key_events: 本章关键事件列表
- timeline_events: 本章发生的时间线事件 - timeline_events: 本章发生的时间线事件
- foreshadow_updates: 伏笔操作plant 埋设 / advance 推进 / resolve 回收) - foreshadow_updates: 伏笔操作plant 埋设 / advance 推进 / resolve 回收)
- relationship_changes: 人物关系变化 - relationship_changes: 人物关系变化
- state_changes: 角色/实体状态变化(修为提升、位置转移、状态变化等),每条包含 entity/field/old_value/new_value/reason
## 重写模式 ## 重写模式
@@ -79,3 +80,6 @@
- 保持与前几章的连贯性 - 保持与前几章的连贯性
- 字数不够时用具体细节扩展,不用水话填充 - 字数不够时用具体细节扩展,不用水话填充
- 注意时间线连贯和伏笔管理 - 注意时间线连贯和伏笔管理
- 角色在正文中可以使用别名/称号/绰号,但 commit 时 characters 列表使用正式名
- 如果上下文中有 recent_state_changes注意本章对角色状态的描述必须与记录一致如修为、位置、伤势等
- 本章中角色发生任何状态变化(修为提升、位置转移、受伤/恢复、获得/失去物品等),必须在 commit 的 state_changes 中上报

View File

@@ -3,6 +3,7 @@
## 主角 ## 主角
### [角色一姓名] ### [角色一姓名]
- **别名/称号**:(如"废物少年"、"炎哥"、"不灭战神"等,正文中可能用到的各种称呼)
- **年龄/职业** - **年龄/职业**
- **外貌特征** - **外貌特征**
- **性格核心** - **性格核心**

42
state/state_changes.go Normal file
View 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
}

View File

@@ -71,6 +71,20 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
} }
if chars, _ := t.store.LoadCharacters(); len(chars) > 0 { if chars, _ := t.store.LoadCharacters(); len(chars) > 0 {
result["characters"] = chars 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 { if rules, _ := t.store.LoadWorldRules(); len(rules) > 0 {
@@ -95,19 +109,26 @@ func (t *CheckConsistencyTool) Execute(_ context.Context, args json.RawMessage)
result["instruction"] = `请逐项对照以上状态数据检查本章内容,返回 JSON 数组格式的冲突项: result["instruction"] = `请逐项对照以上状态数据检查本章内容,返回 JSON 数组格式的冲突项:
[ [
{ {
"type": "timeline|foreshadow|relationship|character|world_rules", "type": "timeline|foreshadow|relationship|character|world_rules|state",
"severity": "error|warning", "severity": "critical|error|warning",
"description": "具体冲突描述", "description": "具体冲突描述",
"suggestion": "建议修正范围和方式" "suggestion": "建议修正范围和方式"
} }
] ]
severity 分级:
- critical严重逻辑硬伤必须修复如角色已死但再次出场、违反世界规则核心边界
- error明显矛盾应当修复如时间线冲突、角色行为与人设严重不符
- warning轻微瑕疵可后续处理如细节不够精确、可改进但不影响阅读
检查清单: 检查清单:
1. 时间线:本章事件时间是否与已有 timeline 矛盾 1. 时间线:本章事件时间是否与已有 timeline 矛盾
2. 伏笔unresolved_foreshadow 中是否有本章应推进但遗漏的 2. 伏笔unresolved_foreshadow 中是否有本章应推进但遗漏的
3. 人物关系:角色互动是否与 relationships 当前状态矛盾 3. 人物关系:角色互动是否与 relationships 当前状态矛盾
4. 角色一致性:行为是否符合 characters 中的性格和弧线 4. 角色一致性:行为是否符合 characters 中的性格和弧线
5. 世界规则:逐条检查 world_rules_boundaries 中的边界约束,本章内容是否违反任何一条 5. 世界规则:逐条检查 world_rules_boundaries 中的边界约束,本章内容是否违反任何一条
6. 别名一致性:如果有 alias_map检查同一角色的不同称呼是否指向正确的人
7. 状态连续性:如果有 recent_state_changes检查本章对角色状态的描述是否与最近的状态变化记录一致
如果没有发现冲突,返回空数组 []。不要返回其他格式。` 如果没有发现冲突,返回空数组 []。不要返回其他格式。`

View File

@@ -43,6 +43,13 @@ func (t *CommitChapterTool) Schema() map[string]any {
schema.Property("character_b", schema.String("角色 B")).Required(), schema.Property("character_b", schema.String("角色 B")).Required(),
schema.Property("relation", schema.String("当前关系描述")).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( return schema.Object(
schema.Property("chapter", schema.Int("章节号")).Required(), schema.Property("chapter", schema.Int("章节号")).Required(),
schema.Property("summary", schema.String("本章内容摘要200字以内")).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("timeline_events", schema.Array("本章时间线事件", timelineSchema)),
schema.Property("foreshadow_updates", schema.Array("伏笔操作", foreshadowSchema)), schema.Property("foreshadow_updates", schema.Array("伏笔操作", foreshadowSchema)),
schema.Property("relationship_changes", schema.Array("关系变化", relationshipSchema)), 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("hook_type", schema.Enum("章末钩子类型", "crisis", "mystery", "desire", "emotion", "choice")),
schema.Property("dominant_strand", schema.Enum("本章主导叙事线", "quest", "fire", "constellation")), 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"` TimelineEvents []domain.TimelineEvent `json:"timeline_events"`
ForeshadowUpdates []domain.ForeshadowUpdate `json:"foreshadow_updates"` ForeshadowUpdates []domain.ForeshadowUpdate `json:"foreshadow_updates"`
RelationshipChanges []domain.RelationshipEntry `json:"relationship_changes"` RelationshipChanges []domain.RelationshipEntry `json:"relationship_changes"`
StateChanges []domain.StateChange `json:"state_changes"`
HookType string `json:"hook_type"` HookType string `json:"hook_type"`
DominantStrand string `json:"dominant_strand"` 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) 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. 更新进度 // 5. 更新进度
if err := t.store.MarkChapterComplete(a.Chapter, wordCount, a.HookType, a.DominantStrand); err != nil { if err := t.store.MarkChapterComplete(a.Chapter, wordCount, a.HookType, a.DominantStrand); err != nil {

View File

@@ -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 { if relationships, err := t.store.LoadRelationships(); err == nil && len(relationships) > 0 {
result["relationship_state"] = relationships 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 模式:注入当前卷弧位置 + 弧目标/卷主题 // Layered 模式:注入当前卷弧位置 + 弧目标/卷主题
if profile.Layered && progress != nil { 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["references"] = t.architectReferences()
} }
result["_loading_summary"] = buildLoadingSummary(result, a.Chapter)
return json.Marshal(result) 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 和场景出场过滤角色。 // loadFilteredCharacters 按 Tier 和场景出场过滤角色。
// core/important 始终返回secondary/decorative 只在当前章节大纲提及时返回。 // core/important 始终返回secondary/decorative 只在当前章节大纲提及时返回。
func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int) { 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 { for _, c := range chars {
switch c.Tier { switch c.Tier {
case "secondary", "decorative": case "secondary", "decorative":
if strings.Contains(sceneText, c.Name) { if matchCharacter(sceneText, c) {
filtered = append(filtered, c) filtered = append(filtered, c)
} }
default: // core, important, 或未设置 default: // core, important, 或未设置
@@ -229,6 +337,19 @@ func (t *ContextTool) loadFilteredCharacters(result map[string]any, chapter int)
result["characters"] = filtered 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 分层摘要加载:卷摘要 + 当前卷弧摘要 + 弧内章摘要。 // loadLayeredSummaries 分层摘要加载:卷摘要 + 当前卷弧摘要 + 弧内章摘要。
func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int) { func (t *ContextTool) loadLayeredSummaries(result map[string]any, chapter, summaryWindow int) {
vol, arc, err := t.store.LocateChapter(chapter) vol, arc, err := t.store.LocateChapter(chapter)

View File

@@ -27,14 +27,21 @@ func (t *SaveReviewTool) Label() string { return "保存审阅" }
func (t *SaveReviewTool) Schema() map[string]any { func (t *SaveReviewTool) Schema() map[string]any {
issueSchema := schema.Object( issueSchema := schema.Object(
schema.Property("type", schema.Enum("问题类型", "timeline", "foreshadow", "relationship", "character", "pacing", "logic")).Required(), schema.Property("type", schema.Enum("问题维度", "consistency", "character", "pacing", "continuity", "foreshadow", "hook")).Required(),
schema.Property("severity", schema.Enum("严重程度", "error", "warning")).Required(), schema.Property("severity", schema.Enum("严重程度", "critical", "error", "warning")).Required(),
schema.Property("description", schema.String("问题描述")).Required(), schema.Property("description", schema.String("问题描述")).Required(),
schema.Property("suggestion", schema.String("修改建议")), 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( return schema.Object(
schema.Property("chapter", schema.Int("审阅的章节号(全局审阅填最新章节号)")).Required(), 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("issues", schema.Array("发现的问题", issueSchema)).Required(),
schema.Property("verdict", schema.Enum("审阅结论", "accept", "polish", "rewrite")).Required(), schema.Property("verdict", schema.Enum("审阅结论", "accept", "polish", "rewrite")).Required(),
schema.Property("summary", schema.String("审阅总结")).Required(), schema.Property("summary", schema.String("审阅总结")).Required(),

View File

@@ -10,6 +10,7 @@ var (
colorSuccess = lipgloss.Color("#2ecc71") // 冷绿 colorSuccess = lipgloss.Color("#2ecc71") // 冷绿
colorError = lipgloss.Color("#e74c3c") // 朱红 colorError = lipgloss.Color("#e74c3c") // 朱红
colorReview = lipgloss.Color("#e67e22") // 橙色 colorReview = lipgloss.Color("#e67e22") // 橙色
colorContext = lipgloss.Color("#9b59b6") // 紫色
) )
// 状态标签颜色映射 // 状态标签颜色映射
@@ -24,12 +25,13 @@ var statusColors = map[string]lipgloss.Color{
// 事件分类颜色映射 // 事件分类颜色映射
var categoryColors = map[string]lipgloss.Color{ var categoryColors = map[string]lipgloss.Color{
"TOOL": colorText, "TOOL": colorText,
"SYSTEM": colorAccent, "SYSTEM": colorAccent,
"REVIEW": colorReview, "REVIEW": colorReview,
"CHECK": colorSuccess, "CHECK": colorSuccess,
"ERROR": colorError, "ERROR": colorError,
"AGENT": colorDim, "AGENT": colorDim,
"CONTEXT": colorContext,
} }
// 基础样式 // 基础样式