feat: 支持六维评审评分及别名管理
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user