172 lines
5.9 KiB
Go
172 lines
5.9 KiB
Go
package tools
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/voocel/agentcore/schema"
|
||
"github.com/voocel/ainovel-cli/domain"
|
||
"github.com/voocel/ainovel-cli/state"
|
||
)
|
||
|
||
// SaveFoundationTool 保存基础设定(premise/outline/characters),Architect 专用。
|
||
type SaveFoundationTool struct {
|
||
store *state.Store
|
||
}
|
||
|
||
func NewSaveFoundationTool(store *state.Store) *SaveFoundationTool {
|
||
return &SaveFoundationTool{store: store}
|
||
}
|
||
|
||
func (t *SaveFoundationTool) Name() string { return "save_foundation" }
|
||
func (t *SaveFoundationTool) Description() string {
|
||
return "保存小说基础设定。参数固定为 {type, content, scale?}。type 可选 premise / outline / layered_outline / characters / world_rules。premise 时 content 必须是 Markdown 字符串;outline、layered_outline、characters、world_rules 时 content 优先直接传 JSON 数组或对象,不要再手动包一层转义字符串;工具也兼容传入 JSON 字符串。scale 可选,仅允许 short / mid / long。"
|
||
}
|
||
func (t *SaveFoundationTool) Label() string { return "保存设定" }
|
||
|
||
func (t *SaveFoundationTool) Schema() map[string]any {
|
||
return schema.Object(
|
||
schema.Property("type", schema.Enum("设定类型", "premise", "outline", "layered_outline", "characters", "world_rules")).Required(),
|
||
schema.Property("content", map[string]any{
|
||
"description": "内容。premise 传 Markdown 字符串;outline/layered_outline/characters/world_rules 直接传 JSON 数组或对象即可,也兼容传 JSON 字符串。不要把数组再次手动转义成难读的字符串。",
|
||
}).Required(),
|
||
schema.Property("scale", schema.Enum("规划级别", "short", "mid", "long")),
|
||
)
|
||
}
|
||
|
||
func (t *SaveFoundationTool) Execute(_ context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||
var a struct {
|
||
Type string `json:"type"`
|
||
Content json.RawMessage `json:"content"`
|
||
Scale string `json:"scale"`
|
||
}
|
||
if err := json.Unmarshal(args, &a); err != nil {
|
||
return nil, fmt.Errorf("invalid args: %w", err)
|
||
}
|
||
content, err := normalizeFoundationContent(a.Content)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if a.Scale != "" {
|
||
switch domain.PlanningTier(a.Scale) {
|
||
case domain.PlanningTierShort, domain.PlanningTierMid, domain.PlanningTierLong:
|
||
default:
|
||
return nil, fmt.Errorf("invalid scale %q, expected short/mid/long", a.Scale)
|
||
}
|
||
if err := t.store.SetPlanningTier(domain.PlanningTier(a.Scale)); err != nil {
|
||
return nil, fmt.Errorf("save planning tier: %w", err)
|
||
}
|
||
}
|
||
|
||
result := map[string]any{"saved": true, "type": a.Type, "scale": a.Scale}
|
||
|
||
switch a.Type {
|
||
case "premise":
|
||
if err := t.store.SavePremise(content); err != nil {
|
||
return nil, fmt.Errorf("save premise: %w", err)
|
||
}
|
||
_ = t.store.UpdatePhase(domain.PhasePremise)
|
||
|
||
case "outline":
|
||
var entries []domain.OutlineEntry
|
||
if err := json.Unmarshal([]byte(content), &entries); err != nil {
|
||
return nil, fmt.Errorf("parse outline JSON: %w", err)
|
||
}
|
||
if err := t.store.SaveOutline(entries); err != nil {
|
||
return nil, fmt.Errorf("save outline: %w", err)
|
||
}
|
||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||
_ = t.store.SetTotalChapters(len(entries))
|
||
if domain.PlanningTier(a.Scale) != domain.PlanningTierLong {
|
||
_ = t.store.SetLayered(false)
|
||
_ = t.store.UpdateVolumeArc(0, 0)
|
||
_ = t.store.ClearLayeredOutline()
|
||
}
|
||
result["chapters"] = len(entries)
|
||
|
||
case "layered_outline":
|
||
var volumes []domain.VolumeOutline
|
||
if err := json.Unmarshal([]byte(content), &volumes); err != nil {
|
||
return nil, fmt.Errorf("parse layered_outline JSON: %w", err)
|
||
}
|
||
if err := t.store.SaveLayeredOutline(volumes); err != nil {
|
||
return nil, fmt.Errorf("save layered_outline: %w", err)
|
||
}
|
||
flat := domain.FlattenOutline(volumes)
|
||
if err := t.store.SaveOutline(flat); err != nil {
|
||
return nil, fmt.Errorf("save flattened outline: %w", err)
|
||
}
|
||
total := domain.TotalChapters(volumes)
|
||
_ = t.store.UpdatePhase(domain.PhaseOutline)
|
||
_ = t.store.SetTotalChapters(total)
|
||
_ = t.store.SetLayered(true)
|
||
if len(volumes) > 0 && len(volumes[0].Arcs) > 0 {
|
||
_ = t.store.UpdateVolumeArc(volumes[0].Index, volumes[0].Arcs[0].Index)
|
||
}
|
||
result["volumes"] = len(volumes)
|
||
result["chapters"] = total
|
||
|
||
case "characters":
|
||
var chars []domain.Character
|
||
if err := json.Unmarshal([]byte(content), &chars); err != nil {
|
||
return nil, fmt.Errorf("parse characters JSON: %w", err)
|
||
}
|
||
if err := t.store.SaveCharacters(chars); err != nil {
|
||
return nil, fmt.Errorf("save characters: %w", err)
|
||
}
|
||
result["count"] = len(chars)
|
||
|
||
case "world_rules":
|
||
var rules []domain.WorldRule
|
||
if err := json.Unmarshal([]byte(content), &rules); err != nil {
|
||
return nil, fmt.Errorf("parse world_rules JSON: %w", err)
|
||
}
|
||
if err := t.store.SaveWorldRules(rules); err != nil {
|
||
return nil, fmt.Errorf("save world_rules: %w", err)
|
||
}
|
||
result["count"] = len(rules)
|
||
|
||
default:
|
||
return nil, fmt.Errorf("unknown type %q, expected premise/outline/layered_outline/characters/world_rules", a.Type)
|
||
}
|
||
|
||
// 返回剩余未完成项,引导 Architect 继续
|
||
result["remaining"] = t.remaining()
|
||
return json.Marshal(result)
|
||
}
|
||
|
||
func normalizeFoundationContent(raw json.RawMessage) (string, error) {
|
||
if len(raw) == 0 {
|
||
return "", fmt.Errorf("content is required")
|
||
}
|
||
|
||
var text string
|
||
if err := json.Unmarshal(raw, &text); err == nil {
|
||
return text, nil
|
||
}
|
||
|
||
if !json.Valid(raw) {
|
||
return "", fmt.Errorf("invalid content: expected Markdown string or valid JSON value")
|
||
}
|
||
return string(raw), nil
|
||
}
|
||
|
||
// remaining 检查基础设定中还缺少哪些必要项。
|
||
func (t *SaveFoundationTool) remaining() []string {
|
||
var missing []string
|
||
if p, _ := t.store.LoadPremise(); p == "" {
|
||
missing = append(missing, "premise")
|
||
}
|
||
if o, _ := t.store.LoadOutline(); len(o) == 0 {
|
||
missing = append(missing, "outline")
|
||
}
|
||
if c, _ := t.store.LoadCharacters(); len(c) == 0 {
|
||
missing = append(missing, "characters")
|
||
}
|
||
if r, _ := t.store.LoadWorldRules(); len(r) == 0 {
|
||
missing = append(missing, "world_rules")
|
||
}
|
||
return missing
|
||
}
|