package room

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/sdaduanbilei/agent-team/internal/agent"
	"github.com/sdaduanbilei/agent-team/internal/llm"
	"github.com/sdaduanbilei/agent-team/internal/skill"
	"github.com/sdaduanbilei/agent-team/internal/store"
	"github.com/sdaduanbilei/agent-team/internal/user"
	"gopkg.in/yaml.v3"
)

type RoomType string

const (
	TypeProject RoomType = "project"
)

type Status string

const (
	StatusPending  Status = "pending"
	StatusThinking Status = "thinking"
	StatusWorking  Status = "working"
)

type Config struct {
	Name    string   `yaml:"name"`
	Type    RoomType `yaml:"type"`
	Master  string   `yaml:"master"`  // agent name
	Members []string `yaml:"members"` // agent names
	Color   string   `yaml:"color"`   // avatar color
	Team    string   `yaml:"team"`    // installed team name
}

type Room struct {
	Config    Config
	Dir       string
	master    *agent.Agent
	members   map[string]*agent.Agent
	skillMeta []skill.Meta
	User      *user.User
	Status    Status
	ActiveAgent string // for working status display
	Broadcast func(Event) // set by api layer

	// master 的会话历史，保持多轮对话上下文
	masterHistory []llm.Message
	historyMu     sync.Mutex

	Mode               string            // "plan" | "build"
	pendingAssignments map[string]string // plan 模式下暂存的待执行任务
	pendingPlanReply   string            // master 规划原文，用于生成计划文档

	// Build 模式下成员对话跟踪
	memberConvos     map[string][]llm.Message // 成员名 -> 多轮对话历史
	memberArtifacts  map[string]string        // 成员名 -> 已产出的文档文件名（用于后续覆盖更新）
	lastActiveMember string                   // 最后一个发出提问的成员
	planFilename     string                   // 当前任务计划文件名
	systemRules      string                   // SYSTEM.md 全局规则

	projectTemplate *ProjectTemplate // 项目模板（可为 nil）

	Store          *store.Store
	currentGroupID int64 // 当前用户消息的 group_id

	cancelFunc context.CancelFunc
	cancelMu   sync.Mutex
}

type EventType string

const (
	EvtAgentMessage  EventType = "agent_message"
	EvtTaskAssign    EventType = "task_assign"
	EvtReview        EventType = "review"
	EvtRoomStatus    EventType = "room_status"
	EvtTasksUpdate   EventType = "tasks_update"
	EvtWorkspaceFile EventType = "workspace_file"
	EvtModeChange    EventType = "mode_change"
	EvtArtifact      EventType = "artifact"
	EvtTaskDone      EventType = "task_done"
	EvtScheduleRun   EventType = "schedule_run"
	EvtTokenUsage    EventType = "token_usage"
	EvtFileRead      EventType = "file_read"
)

type Event struct {
	Type        EventType `json:"type"`
	RoomID      string    `json:"room_id"`
	Agent       string    `json:"agent,omitempty"`
	Role        string    `json:"role,omitempty"` // master | member | challenge
	Content     string    `json:"content,omitempty"`
	Streaming   bool      `json:"streaming,omitempty"`
	From        string    `json:"from,omitempty"`
	To          string    `json:"to,omitempty"`
	Task        string    `json:"task,omitempty"`
	Feedback    string    `json:"feedback,omitempty"`
	Status      Status    `json:"status,omitempty"`
	ActiveAgent string    `json:"active_agent,omitempty"`
	Action      string    `json:"action,omitempty"`
	Filename         string `json:"filename,omitempty"`
	Mode             string `json:"mode,omitempty"`
	Title            string `json:"title,omitempty"`
	PromptTokens     int    `json:"prompt_tokens,omitempty"`
	CompletionTokens int    `json:"completion_tokens,omitempty"`
	TotalTokens      int    `json:"total_tokens,omitempty"`
}

// ProjectFile 项目模板中的单个文件
type ProjectFile struct {
	Path    string // 如 "创作需求书.md"
	Owner   string // 负责的 agent 名
	Phase   int    // 阶段编号
	IsDir   bool   // 是否为目录
	Dynamic bool   // ... 标记，可动态扩展
}

// ProjectTemplate 从 TEAM.md 解析的项目模板
type ProjectTemplate struct {
	Files []ProjectFile
}

// parseProjectTemplate 从 TEAM.md body 中提取 project-template 代码块并解析
func parseProjectTemplate(body string) *ProjectTemplate {
	// 提取 ```project-template ... ``` 代码块
	re := regexp.MustCompile("(?s)```project-template\\s*\\n(.+?)```")
	match := re.FindStringSubmatch(body)
	if match == nil {
		return nil
	}
	block := match[1]

	// 逐行解析
	lineRe := regexp.MustCompile(`[├└─│\s]+(.+?)\.md\s+@(\S+)\s*(?:phase:(\d+))?`)
	dirRe := regexp.MustCompile(`[├└─│\s]+(.+?)/\s*$`)
	dynamicRe := regexp.MustCompile(`[├└─│\s]+\.\.\.\s+@(\S+)\s*(?:phase:(\d+))?`)

	var files []ProjectFile
	for _, line := range strings.Split(block, "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "workspace/") {
			continue
		}

		// 动态文件（...）
		if m := dynamicRe.FindStringSubmatch(line); m != nil {
			phase := 0
			if m[2] != "" {
				phase, _ = strconv.Atoi(m[2])
			}
			files = append(files, ProjectFile{
				Path: "...", Owner: m[1], Phase: phase, Dynamic: true,
			})
			continue
		}

		// 目录
		if m := dirRe.FindStringSubmatch(line); m != nil {
			files = append(files, ProjectFile{
				Path: m[1] + "/", IsDir: true,
			})
			continue
		}

		// 普通文件
		if m := lineRe.FindStringSubmatch(line); m != nil {
			phase := 0
			if m[3] != "" {
				phase, _ = strconv.Atoi(m[3])
			}
			files = append(files, ProjectFile{
				Path: strings.TrimSpace(m[1]) + ".md", Owner: m[2], Phase: phase,
			})
		}
	}

	if len(files) == 0 {
		return nil
	}
	return &ProjectTemplate{Files: files}
}

type BoardEntry struct {
	Author  string
	Content string
	Type    string // "draft" | "challenge"
}

type SharedBoard struct {
	mu      sync.RWMutex
	entries []BoardEntry
}

func (b *SharedBoard) Add(author, content, typ string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.entries = append(b.entries, BoardEntry{Author: author, Content: content, Type: typ})
}

func (b *SharedBoard) ToContext() string {
	b.mu.RLock()
	defer b.mu.RUnlock()
	if len(b.entries) == 0 {
		return ""
	}
	var sb strings.Builder
	sb.WriteString("<team_board>\n")
	for _, e := range b.entries {
		fmt.Fprintf(&sb, "  <entry type=\"%s\" author=\"%s\">\n%s\n  </entry>\n", e.Type, e.Author, e.Content)
	}
	sb.WriteString("</team_board>")
	return sb.String()
}

func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
	data, err := os.ReadFile(filepath.Join(roomDir, "room.md"))
	if err != nil {
		return nil, err
	}
	cfg, err := parseRoomConfig(data)
	if err != nil {
		return nil, err
	}

	r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent), Mode: "plan", Status: StatusPending}

	// 读取全局系统规则（项目根目录的 SYSTEM.md）
	// agentsDir 的父目录就是项目根目录
	projectRoot := filepath.Dir(agentsDir)
	if data, err := os.ReadFile(filepath.Join(projectRoot, "SYSTEM.md")); err == nil {
		r.systemRules = string(data)
	}

	if cfg.Master != "" {
		// 团队知识库目录
		var knowledgeDir string
		if cfg.Team != "" {
			kd := filepath.Join(agentsDir, cfg.Team, "knowledge")
			if info, err := os.Stat(kd); err == nil && info.IsDir() {
				knowledgeDir = kd
			}
		}

		agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master)
		r.master, err = agent.Load(agentPath)
		if err != nil {
			return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
		}
		r.master.KnowledgeDir = knowledgeDir

		for _, name := range cfg.Members {
			a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name))
			if err != nil {
				return nil, fmt.Errorf("load member %s: %w", name, err)
			}
			a.KnowledgeDir = knowledgeDir
			r.members[name] = a
		}
	}

	// 加载项目模板（如果有 TEAM.md）
	if cfg.Team != "" {
		teamMDPath := filepath.Join(agentsDir, cfg.Team, "TEAM.md")
		if teamData, err := os.ReadFile(teamMDPath); err == nil {
			// 跳过 frontmatter，提取 body
			body := string(teamData)
			if strings.HasPrefix(body, "---") {
				parts := strings.SplitN(body, "---", 3)
				if len(parts) >= 3 {
					body = parts[2]
				}
			}
			if pt := parseProjectTemplate(body); pt != nil {
				r.projectTemplate = pt
				log.Printf("[room %s] 已加载项目模板，包含 %d 个文件定义", cfg.Name, len(pt.Files))
			}
		}
	}

	r.skillMeta, _ = skill.Discover(skillsDir)
	return r, nil
}

func (r *Room) emit(e Event) {
	e.RoomID = r.Config.Name
	if r.Broadcast != nil {
		r.Broadcast(e)
	}
	// 持久化非 streaming 消息到 DB
	if r.Store != nil {
		gid := &r.currentGroupID
		switch e.Type {
		case EvtAgentMessage:
			// 跳过 streaming、空内容、临时状态消息、tool_use 状态
			if !e.Streaming && e.Content != "" && e.Role != "tool_use" &&
				!strings.HasPrefix(e.Content, "正在处理:") && !strings.HasPrefix(e.Content, "正在处理: ") {
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: e.Agent, Role: e.Role,
					Content: e.Content, GroupID: gid,
				})
			}
		case EvtArtifact:
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: e.Agent, Role: "artifact",
				Content: e.Title, Filename: e.Filename, Title: e.Title, GroupID: gid,
			})
		case EvtTaskAssign:
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: e.From, Role: "task_assign",
				Content: e.Task, Title: e.To, GroupID: gid,
			})
		}
	}
}

func (r *Room) emitUsage(agentName string, usage llm.Usage) {
	if usage.TotalTokens > 0 {
		r.emit(Event{
			Type:             EvtTokenUsage,
			Agent:            agentName,
			PromptTokens:     usage.PromptTokens,
			CompletionTokens: usage.CompletionTokens,
			TotalTokens:      usage.TotalTokens,
		})
		if r.Store != nil {
			r.Store.InsertTokenUsage(r.Config.Name, agentName, usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens)
		}
	}
}

func (r *Room) setMode(mode string) {
	r.Mode = mode
	r.emit(Event{Type: EvtModeChange, Mode: mode})
}

func (r *Room) setStatus(s Status, activeAgent, action string) {
	r.Status = s
	r.ActiveAgent = activeAgent
	r.emit(Event{Type: EvtRoomStatus, Status: s, ActiveAgent: activeAgent, Action: action})
}

func (r *Room) Stop() {
	r.cancelMu.Lock()
	defer r.cancelMu.Unlock()
	if r.cancelFunc != nil {
		r.cancelFunc()
		r.cancelFunc = nil
	}
	r.setStatus(StatusPending, "", "")
}

// AppendHistory persists a message to today's history file.
func (r *Room) AppendHistory(role, agentName, content string) {
	dir := filepath.Join(r.Dir, "history")
	os.MkdirAll(dir, 0755)
	filename := filepath.Join(dir, time.Now().Format("2006-01-02")+".md")
	line := fmt.Sprintf("\n**[%s] %s** (%s)\n\n%s\n", time.Now().Format("15:04:05"), agentName, role, content)
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	f.WriteString(line)
}

func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]string, board *SharedBoard, skillXML string) map[string]string {
	results := make(map[string]string)
	var mu sync.Mutex
	var wg sync.WaitGroup

	// 构建 tool 定义
	tools := skill.ToTools(r.skillMeta)

	for memberName, task := range assignments {
		wg.Add(1)
		go func(name, t string) {
			defer wg.Done()
			member, ok := r.members[name]
			if !ok {
				return
			}
			r.setStatus(StatusWorking, member.Config.Name, t)
			r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: name, Task: t})

			boardCtx := board.ToContext()
			// 有 tools 时不注入 skill XML（避免 LLM 用 XML 格式而不是 function calling）
			var extraCtx string
			if len(tools) == 0 {
				extraCtx = skillXML
			}
			if boardCtx != "" {
				if extraCtx != "" {
					extraCtx = boardCtx + "\n\n" + extraCtx
				} else {
					extraCtx = boardCtx
				}
			}
			// 注入团队成员列表，让成员知道队友是谁
			teamCtx := r.buildTeamXML()
			if teamCtx != "" {
				if extraCtx != "" {
					extraCtx = teamCtx + "\n\n" + extraCtx
				} else {
					extraCtx = teamCtx
				}
			}
			if r.systemRules != "" {
				if extraCtx != "" {
					extraCtx = r.systemRules + "\n\n" + extraCtx
				} else {
					extraCtx = r.systemRules
				}
			}
			// 注入项目模板上下文
			if projectCtx := r.buildProjectContext(name); projectCtx != "" {
				if extraCtx != "" {
					extraCtx = extraCtx + "\n\n" + projectCtx
				} else {
					extraCtx = projectCtx
				}
			}
			memberSystem := member.BuildSystemPrompt(extraCtx)
			// 将 workspace 文件内容注入任务消息，让成员能看到已有文档
			taskMsg := t
			if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
				taskMsg = t + "\n\n" + wsCtx
			}
			// 如果该成员已有文档产出，提示在原文档基础上修改
			if r.memberArtifacts != nil {
				if _, hasDoc := r.memberArtifacts[name]; hasDoc {
					taskMsg += "\n\n<important>你之前已经产出过文档。请在原文档基础上进行补充和修改，不要重新写一份全新的文档。保留原有内容中仍然有效的部分，合并新的调研结果。</important>"
				}
			}
			// 标记该成员已查阅所有 workspace 文件，更新状态
			wsFiles := r.listWorkspaceFiles()
			for i, f := range wsFiles {
				r.emit(Event{Type: EvtFileRead, Agent: name, Filename: f})
				if i == 0 {
					r.setStatus(StatusWorking, name, fmt.Sprintf("正在阅读 %s ...", f))
				}
			}
			memberMsgs := []llm.Message{
				llm.NewMsg("system", memberSystem),
				llm.NewMsg("user", taskMsg),
			}

			// tool calling 循环：最多 10 轮
			var finalReply string
			for round := 0; round < 10; round++ {
				result, err := member.ChatWithTools(ctx, memberMsgs, tools, nil)
				if err != nil {
					mu.Lock()
					results[name] = fmt.Sprintf("[error] %v", err)
					mu.Unlock()
					return
				}
				r.emitUsage(name, result.Usage)

				// 没有 tool calls，正常结束
				if len(result.ToolCalls) == 0 {
					finalReply = result.Content
					break
				}

				// 有 tool calls，执行并将结果反馈
				// 先把 assistant 的 tool_call 消息加入历史
				assistantMsg := llm.Message{
					Role:      "assistant",
					Content:   result.Content,
					ToolCalls: result.ToolCalls,
				}
				memberMsgs = append(memberMsgs, assistantMsg)

				// 发一条 tool_use 状态消息（带 streaming 标记，前端显示动画）
				var toolNames []string
				for _, tc := range result.ToolCalls {
					toolNames = append(toolNames, tc.Function.Name)
				}
				r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use",
					Content: strings.Join(toolNames, ", "), Streaming: true})

				for _, tc := range result.ToolCalls {
					toolResult := r.executeToolCall(tc)
					memberMsgs = append(memberMsgs, llm.NewToolResultMsg(tc.ID, toolResult))
				}

				// 搜索完成，进入整理阶段
				r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use",
					Content: "thinking", Streaming: true})
			}

			// 关闭 tool_use 状态
			r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use",
				Content: "", Streaming: false})

			// tool calling 循环耗尽后仍无文本回复，强制无 tools 调用生成总结
			if finalReply == "" {
				memberMsgs = append(memberMsgs, llm.NewMsg("user", "请根据以上所有工具调用结果，直接输出完整的任务回复。不要再调用任何工具。"))
				result, err := member.ChatWithTools(ctx, memberMsgs, nil, nil)
				if err == nil && result.Content != "" {
					finalReply = result.Content
					r.emitUsage(name, result.Usage)
				}
			}
			if finalReply == "" {
				finalReply = "[任务执行完成，但未产生文本回复]"
			}

			if r.memberConvos == nil {
				r.memberConvos = make(map[string][]llm.Message)
			}
			r.memberConvos[name] = memberMsgs
			r.memberConvos[name] = append(r.memberConvos[name], llm.NewMsg("assistant", finalReply))

			// 文件保存路由：模板路由成功时，生成简短交流消息
			savedPath, routed := r.saveAgentOutput(name, finalReply, t)
			if routed {
				// 文档已保存 → 存 document part 到数据库
				if r.Store != nil {
					title := extractTitle(finalReply)
					r.Store.InsertMessage(&store.Message{
						RoomID: r.Config.Name, Agent: name, Role: "member",
						Content: title, Filename: savedPath, PartType: "document",
						GroupID: &r.currentGroupID,
					})
				}
				// 生成简短交流消息 → 存 text part
				briefMsg := r.generateBriefMessage(ctx, member, name, finalReply, t)
				if briefMsg != "" {
					r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: briefMsg})
					if r.Store != nil {
						r.Store.InsertMessage(&store.Message{
							RoomID: r.Config.Name, Agent: name, Role: "member",
							Content: briefMsg, PartType: "text",
							GroupID: &r.currentGroupID,
						})
					}
				}
				mu.Lock()
				results[name] = briefMsg
				mu.Unlock()
				r.AppendHistory("member", name, finalReply)
				if strings.TrimSpace(briefMsg) != "" {
					board.Add(name, briefMsg, "draft")
				}
			} else {
				mu.Lock()
				results[name] = finalReply
				mu.Unlock()
				r.AppendHistory("member", name, finalReply)
				if strings.TrimSpace(finalReply) != "" {
					board.Add(name, finalReply, "draft")
				}
			}

			// 异步保存成员记忆
			go r.updateMemberMemory(context.Background(), member, t, finalReply)
		}(memberName, task)
	}
	wg.Wait()

	// 检查成员结果中是否有 @其他成员 的委派请求，自动分派第二轮
	secondRound := make(map[string]string)
	for _, result := range results {
		subAssignments := parseAssignments(result)
		for name, task := range subAssignments {
			if _, isMember := r.members[name]; isMember {
				// 避免重复：本轮已执行过的成员不再分派
				if _, alreadyDone := results[name]; !alreadyDone {
					secondRound[name] = task
				}
			}
		}
	}
	if len(secondRound) > 0 {
		log.Printf("[room %s] 检测到成员间委派，执行第二轮: %v", r.Config.Name, secondRound)
		var wg2 sync.WaitGroup
		for memberName, task := range secondRound {
			wg2.Add(1)
			go func(name, t string) {
				defer wg2.Done()
				member, ok := r.members[name]
				if !ok {
					return
				}
				r.setStatus(StatusWorking, member.Config.Name, t)
				r.emit(Event{Type: EvtTaskAssign, From: "team", To: name, Task: t})

				boardCtx := board.ToContext()
				var extraCtx string
				if len(tools) == 0 {
					extraCtx = skillXML
				}
				if boardCtx != "" {
					if extraCtx != "" {
						extraCtx = boardCtx + "\n\n" + extraCtx
					} else {
						extraCtx = boardCtx
					}
				}
				// 注入团队成员列表
				teamCtx := r.buildTeamXML()
				if teamCtx != "" {
					if extraCtx != "" {
						extraCtx = teamCtx + "\n\n" + extraCtx
					} else {
						extraCtx = teamCtx
					}
				}
				if r.systemRules != "" {
					if extraCtx != "" {
						extraCtx = r.systemRules + "\n\n" + extraCtx
					} else {
						extraCtx = r.systemRules
					}
				}
				// 注入项目模板上下文
				if projectCtx := r.buildProjectContext(name); projectCtx != "" {
					if extraCtx != "" {
						extraCtx = extraCtx + "\n\n" + projectCtx
					} else {
						extraCtx = projectCtx
					}
				}
				memberSystem := member.BuildSystemPrompt(extraCtx)
				taskMsg := t
				if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
					taskMsg = t + "\n\n" + wsCtx
				}
				// 如果该成员已有文档产出，提示在原文档基础上修改
				if r.memberArtifacts != nil {
					if _, hasDoc := r.memberArtifacts[name]; hasDoc {
						taskMsg += "\n\n<important>你之前已经产出过文档。请在原文档基础上进行补充和修改，不要重新写一份全新的文档。保留原有内容中仍然有效的部分，合并新的调研结果。</important>"
					}
				}
				// 标记该成员已查阅所有 workspace 文件，更新状态
				wsFiles2 := r.listWorkspaceFiles()
				for i, f := range wsFiles2 {
					r.emit(Event{Type: EvtFileRead, Agent: name, Filename: f})
					if i == 0 {
						r.setStatus(StatusWorking, name, fmt.Sprintf("正在阅读 %s ...", f))
					}
				}
				memberMsgs := []llm.Message{
					llm.NewMsg("system", memberSystem),
					llm.NewMsg("user", taskMsg),
				}

				var finalReply string
				for round := 0; round < 10; round++ {
					result, err := member.ChatWithTools(ctx, memberMsgs, tools, nil)
					if err != nil {
						mu.Lock()
						results[name] = fmt.Sprintf("[error] %v", err)
						mu.Unlock()
						return
					}
					r.emitUsage(name, result.Usage)
					if len(result.ToolCalls) == 0 {
						finalReply = result.Content
						break
					}
					assistantMsg := llm.Message{Role: "assistant", Content: result.Content, ToolCalls: result.ToolCalls}
					memberMsgs = append(memberMsgs, assistantMsg)
					r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use", Content: "", Streaming: true})
					for _, tc := range result.ToolCalls {
						toolResult := r.executeToolCall(tc)
						memberMsgs = append(memberMsgs, llm.NewToolResultMsg(tc.ID, toolResult))
					}
					r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use", Content: "thinking", Streaming: true})
				}
				r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "tool_use", Content: "", Streaming: false})

				if finalReply == "" {
					memberMsgs = append(memberMsgs, llm.NewMsg("user", "请根据以上所有工具调用结果，直接输出完整的任务回复。不要再调用任何工具。"))
					result, err := member.ChatWithTools(ctx, memberMsgs, nil, nil)
					if err == nil && result.Content != "" {
						finalReply = result.Content
						r.emitUsage(name, result.Usage)
					}
				}
				if finalReply == "" {
					finalReply = "[任务执行完成，但未产生文本回复]"
				}

				// 文件保存路由：模板路由成功时，results/board 只存简短摘要
				statusMsg, routed := r.saveAgentOutput(name, finalReply, t)
				if routed {
					mu.Lock()
					results[name] = statusMsg
					mu.Unlock()
					r.AppendHistory("member", name, finalReply)
					if strings.TrimSpace(statusMsg) != "" {
						board.Add(name, statusMsg, "draft")
					}
				} else {
					mu.Lock()
					results[name] = finalReply
					mu.Unlock()
					r.AppendHistory("member", name, finalReply)
					if strings.TrimSpace(finalReply) != "" {
						board.Add(name, finalReply, "draft")
					}
				}

				go r.updateMemberMemory(context.Background(), member, t, finalReply)
			}(memberName, task)
		}
		wg2.Wait()
	}

	return results
}

func (r *Room) runChallengeRound(ctx context.Context, board *SharedBoard, skillXML string) {
	var challengers []string
	for name, member := range r.members {
		if member.Config.CanChallenge {
			challengers = append(challengers, name)
		}
	}
	if len(challengers) == 0 {
		return
	}

	boardCtx := board.ToContext()
	// 没有看板内容且没有 workspace 文件，跳过 challenge
	if boardCtx == "" && len(r.listWorkspaceFiles()) == 0 {
		return
	}

	var wg sync.WaitGroup
	for _, name := range challengers {
		wg.Add(1)
		go func(n string) {
			defer wg.Done()
			member := r.members[n]
			extraCtx := boardCtx + "\n\n" + skillXML
			// 注入 workspace 文件内容，让 challenger 能看到实际文档
			if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
				extraCtx = wsCtx + "\n\n" + extraCtx
			}
			// 注入团队成员列表
			teamCtx := r.buildTeamXML()
			if teamCtx != "" {
				extraCtx = teamCtx + "\n\n" + extraCtx
			}
			if r.systemRules != "" {
				extraCtx = r.systemRules + "\n\n" + extraCtx
			}
			// 注入项目模板上下文
			if projectCtx := r.buildProjectContext(n); projectCtx != "" {
				extraCtx = extraCtx + "\n\n" + projectCtx
			}
			memberSystem := member.BuildSystemPrompt(extraCtx)
			memberMsgs := []llm.Message{
				llm.NewMsg("system", memberSystem+"\n\n审阅 workspace 中的文档内容（而非看板摘要）。如果发现问题或需要质疑，请输出 CHALLENGE:你的具体意见。如果没有问题，输出 AGREE。注意：只评审你职责范围内的内容。禁止使用@提及任何人，禁止建议分配任务。"),
				llm.NewMsg("user", "请审阅 workspace 中的文档并给出你的专业反馈。"),
			}
			var reply strings.Builder
			_, usage, err := member.ChatWithUsage(ctx, memberMsgs, func(token string) {
				reply.WriteString(token)
				r.emit(Event{Type: EvtAgentMessage, Agent: n, Role: "challenge", Content: token, Streaming: true})
			})
			if err != nil {
				return
			}
			r.emitUsage(n, usage)
			r.emit(Event{Type: EvtAgentMessage, Agent: n, Role: "challenge", Content: "", Streaming: false})
			result := reply.String()
			// 持久化完整的 challenge 回复
			if r.Store != nil && result != "" {
				gid := &r.currentGroupID
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: n, Role: "challenge",
					Content: result, GroupID: gid,
				})
			}
			if strings.Contains(result, "CHALLENGE:") {
				board.Add(n, result, "challenge")
				r.AppendHistory("challenge", n, result)
			}
		}(name)
	}
	wg.Wait()
}

// Handle processes a user message through master orchestration.
func (r *Room) Handle(ctx context.Context, userMsg string) error {
	return r.HandleUserMessage(ctx, "user", userMsg)
}

// parseUserMentions 从用户消息中提取 @agent 指派。
// 返回指派 map 和去除 @agent 后的剩余消息。
func parseUserMentions(text string, validMembers map[string]*agent.Agent) map[string]string {
	assignments := make(map[string]string)
	for _, line := range strings.Split(text, "\n") {
		line = strings.TrimSpace(line)
		if !strings.HasPrefix(line, "@") {
			continue
		}
		rest := strings.TrimPrefix(line, "@")
		idx := strings.IndexAny(rest, " \t")
		if idx <= 0 {
			// 只有 @name 没有任务内容，跳过
			continue
		}
		name := strings.TrimSpace(rest[:idx])
		task := strings.TrimSpace(rest[idx+1:])
		if _, ok := validMembers[name]; ok && task != "" {
			assignments[name] = task
		}
	}
	return assignments
}

// HandleUserMessage 处理用户消息。
// 如果用户消息中包含 @agent，直接将任务分配给对应 agent，不经过 master。
func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error {
	if r.master == nil {
		return fmt.Errorf("room has no master agent configured")
	}
	r.AppendHistory("user", userName, userMsg)

	// 持久化 user 消息，记录 group_id
	if r.Store != nil {
		id, _ := r.Store.InsertMessage(&store.Message{
			RoomID: r.Config.Name, Agent: userName, Role: "user", Content: userMsg,
		})
		r.currentGroupID = id
	}

	ctx, cancel := context.WithCancel(ctx)
	r.cancelMu.Lock()
	r.cancelFunc = cancel
	r.cancelMu.Unlock()
	defer func() {
		r.cancelMu.Lock()
		r.cancelFunc = nil
		r.cancelMu.Unlock()
	}()

	// 检测用户是否直接 @agent 指派任务
	userAssignments := parseUserMentions(userMsg, r.members)
	if len(userAssignments) > 0 {
		return r.handleDirectAssign(ctx, userAssignments)
	}

	// Build 模式下，如果有 plan 阶段暂存的待执行任务，直接执行
	if r.Mode == "build" && len(r.pendingAssignments) > 0 {
		log.Printf("[room %s] build 模式，执行 %d 个暂存任务", r.Config.Name, len(r.pendingAssignments))
		assignments := r.pendingAssignments
		r.pendingAssignments = nil
		r.pendingPlanReply = ""

		skillXML := skill.ToXML(r.skillMeta)
		board := &SharedBoard{}
		r.setStatus(StatusWorking, "", "")
		r.runMembersParallel(ctx, assignments, board, skillXML)
		r.runChallengeRound(ctx, board, skillXML)

		r.setStatus(StatusPending, "", "")
		return nil
	}

	// Build 模式下，成员发出了提问，用户回复 → 转发给该成员继续对话
	if r.Mode == "build" && r.lastActiveMember != "" {
		memberName := r.lastActiveMember
		member, ok := r.members[memberName]
		if !ok {
			return fmt.Errorf("member %s not found", memberName)
		}
		log.Printf("[room %s] build 模式，将用户回复转发给 %s", r.Config.Name, memberName)
		r.setStatus(StatusWorking, member.Config.Name, "")

		// 追加用户回复到成员对话历史
		if r.memberConvos == nil {
			r.memberConvos = make(map[string][]llm.Message)
		}
		r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("user", userMsg))

		// 追加沟通记录到任务计划文档
		r.appendPlanLog(userName, memberName, userMsg)

		// 让成员继续对话
		var memberReply strings.Builder
		_, usage, err := member.ChatWithUsage(ctx, r.memberConvos[memberName], func(token string) {
			memberReply.WriteString(token)
		})
		if err != nil {
			r.setStatus(StatusPending, "", "")
			return err
		}
		r.emitUsage(memberName, usage)
		result := memberReply.String()
		r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("assistant", result))
		r.AppendHistory("member", memberName, result)

		// 追加成员回复到任务计划文档
		r.appendPlanLog(memberName, userName, result)

		// 智能判断输出类型（含项目模板路由）
		_, routed := r.saveAgentOutput(memberName, result, "")
		if routed {
			r.lastActiveMember = "" // 文档产出，对话结束
		} else if !isDocument(result) {
			// lastActiveMember 保持不变，用户可以继续回复
		} else {
			r.lastActiveMember = "" // 文档产出，对话结束
		}

		r.setStatus(StatusPending, "", "")
		return nil
	}

	r.setStatus(StatusThinking, "", "")

	// 构建 system prompt
	teamXML := r.buildTeamXML()
	skillXML := skill.ToXML(r.skillMeta) // 成员执行任务时使用
	// master 只需要知道 skill 名称和描述，不需要具体调用方式
	skillSummary := r.buildSkillSummary()
	var userXML string
	if r.User != nil {
		userXML = r.User.BuildUserXML()
	}
	extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillSummary
	if r.systemRules != "" {
		extraContext = r.systemRules + "\n\n" + extraContext
	}
	// 注入项目模板上下文到 master
	if projectCtx := r.buildProjectContext(r.master.Config.Name); projectCtx != "" {
		extraContext = extraContext + "\n\n" + projectCtx
	}
	systemPrompt := r.master.BuildSystemPrompt(extraContext)
	sysMsg := llm.NewMsg("system", systemPrompt+fmt.Sprintf("\n\n当前用户：%s\n当前模式：%s", userName, r.Mode))

	// 使用持久化的会话历史
	r.historyMu.Lock()
	// 始终更新 system prompt（可能 SOUL.md 改了）
	if len(r.masterHistory) == 0 {
		r.masterHistory = []llm.Message{sysMsg}
	} else {
		r.masterHistory[0] = sysMsg
	}
	r.masterHistory = append(r.masterHistory, llm.NewMsg("user", userMsg))

	// 限制历史长度，保留 system + 最近 20 轮对话
	if len(r.masterHistory) > 41 {
		r.masterHistory = append(r.masterHistory[:1], r.masterHistory[len(r.masterHistory)-40:]...)
	}

	masterMsgs := make([]llm.Message, len(r.masterHistory))
	copy(masterMsgs, r.masterHistory)
	r.historyMu.Unlock()

	// Master 规划循环
	for iteration := 0; iteration < 12; iteration++ {
		log.Printf("[room %s] master iteration %d, sending to LLM...", r.Config.Name, iteration)
		// Before the master ChatWithUsage call
		if iteration > 0 {
			r.setStatus(StatusWorking, r.master.Config.Name, "正在编写文档...")
		}
		var masterReply strings.Builder
		docStreamCut := false // 文件驱动模式：检测到文档后停止向前端 streaming
		_, usage, err := r.master.ChatWithUsage(ctx, masterMsgs, func(token string) {
			masterReply.WriteString(token)
			// 文件驱动模式：检测到 # 标题 开头的文档后，停止向前端发送
			if r.projectTemplate != nil && !docStreamCut {
				text := masterReply.String()
				trimmed := strings.TrimSpace(text)
				if strings.HasPrefix(trimmed, "# ") || strings.Contains(text, "\n# ") {
					docStreamCut = true
					// 提取文档标题，更新工作状态
					title := extractTitle(text)
					if title != "" {
						r.setStatus(StatusWorking, r.master.Config.Name, fmt.Sprintf("正在编写《%s》...", title))
					} else {
						r.setStatus(StatusWorking, r.master.Config.Name, "正在编写文档...")
					}
					return
				}
			}
			if !docStreamCut {
				r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true})
			}
		})
		if err != nil {
			log.Printf("[room %s] master chat error: %v", r.Config.Name, err)
			return err
		}
		r.emitUsage(r.master.Config.Name, usage)
		reply := masterReply.String()
		log.Printf("[room %s] master reply (%d chars): %.100s...", r.Config.Name, len(reply), reply)

		// 先检查增量编辑指令
		var savedDocTitles []string
		var savedDocFilenames []string
		persistContent := reply

		if editFile, edited := r.applyDocumentEdit(reply); edited {
			persistContent = fmt.Sprintf("已更新《%s》", strings.TrimSuffix(editFile, ".md"))
			savedDocTitles = append(savedDocTitles, strings.TrimSuffix(editFile, ".md"))
			savedDocFilenames = append(savedDocFilenames, editFile)
		} else if docs := splitDocuments(reply); len(docs) > 0 {
			for _, doc := range docs {
				title := extractTitle(doc)
				var filename string
				if r.projectTemplate != nil {
					if tf := r.matchTemplateFile(title); tf != nil {
						filename = tf.Path
					}
				}
				if filename == "" {
					filename = titleToFilename(title, r.master.Config.Name)
				}
				r.saveWorkspace(filename, doc)
				r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: filename, Title: title})
				savedDocTitles = append(savedDocTitles, title)
				savedDocFilenames = append(savedDocFilenames, filename)
			}
			// 计算持久化内容（去掉文档部分）
			if r.projectTemplate != nil {
				stripped := r.stripDocuments(reply)
				if stripped == "" {
					var summary strings.Builder
					for _, t := range savedDocTitles {
						summary.WriteString(fmt.Sprintf("已完成《%s》\n", t))
					}
					stripped = strings.TrimSpace(summary.String())
				}
				persistContent = stripped
			}
		}

		// 发送 streaming 结束信号
		if docStreamCut {
			// 文件驱动模式：streaming 被截断，用 replace 替换前端已累积的内容
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: persistContent, Streaming: false, Action: "replace"})
		} else if len(savedDocTitles) > 0 {
			// 有文档但未被截断（非文件驱动模式），replace 替换
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: persistContent, Streaming: false, Action: "replace"})
		} else {
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false})
		}

		// Part 模型存储：文档和交流分开存
		if r.Store != nil {
			gid := &r.currentGroupID
			// 文档部分 → document part
			for i, title := range savedDocTitles {
				filename := ""
				if i < len(savedDocFilenames) {
					filename = savedDocFilenames[i]
				}
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
					Content: title, Filename: filename, PartType: "document",
					GroupID: gid,
				})
			}
			// 交流部分 → text part
			if persistContent != "" {
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
					Content: persistContent, PartType: "text",
					GroupID: gid,
				})
			}
		}
		assistantMsg := llm.NewMsg("assistant", reply)
		masterMsgs = append(masterMsgs, assistantMsg)
		// 同步到持久化历史
		r.historyMu.Lock()
		r.masterHistory = append(r.masterHistory, assistantMsg)
		r.historyMu.Unlock()
		r.AppendHistory("master", r.master.Config.Name, reply)

		// 解析 @ 指令，只匹配已知成员名
		allMentions := parseAssignments(reply)
		assignments := make(map[string]string)
		for name, task := range allMentions {
			if _, isMember := r.members[name]; isMember {
				assignments[name] = task
			}
		}

		if len(assignments) == 0 {
			if r.Mode == "build" && r.projectTemplate != nil {
				// 检查是否还有待产出的非动态文件
				hasPending := false
				for _, f := range r.projectTemplate.Files {
					if f.IsDir || f.Dynamic {
						continue
					}
					fpath := filepath.Join(r.Dir, "workspace", f.Path)
					if _, err := os.Stat(fpath); os.IsNotExist(err) {
						hasPending = true
						break
					}
				}

				// 判断是否需要自动 continue：
				// - 有待产出文件 → 一定 continue（工作流没做完）
				// - 本轮有文档产出 → continue（刚产出文档，推进下一步）
				// - 都没有 → break（master 自己选择了停下来等用户，尊重它的决定）
				if hasPending || len(savedDocTitles) > 0 {
					stepCtx := r.buildWorkflowStep()
					log.Printf("[room %s] 工作流未完成（pending=%v, docs=%d），提示 master 继续", r.Config.Name, hasPending, len(savedDocTitles))
					var continueMsg string
					if stepCtx != "" {
						continueMsg = stepCtx + "\n\n请继续推进。用 @成员名 分配任务，或自己产出文档（以 # 标题 开头）。不要重复已保存的文档内容。"
					} else {
						continueMsg = "文档已保存。请根据工作流程决定下一步行动。不要重复已保存的文档内容。"
					}
					continueLLMMsg := llm.NewMsg("user", continueMsg)
					masterMsgs = append(masterMsgs, continueLLMMsg)
					r.historyMu.Lock()
					r.masterHistory = append(r.masterHistory, continueLLMMsg)
					r.historyMu.Unlock()
					continue
				}
				// 所有固定文件已完成，本轮也没新文档 → master 自己选择了停下（如等用户确认）
				log.Printf("[room %s] 固定文件已完成，master 无新产出无分配，break 等用户", r.Config.Name)
			} else if len(savedDocTitles) > 0 && r.Mode == "build" {
				// 无 projectTemplate 但有文档产出，原逻辑
				log.Printf("[room %s] master 产出了文档但未分配任务，提示继续", r.Config.Name)
				var fileList strings.Builder
				for _, t := range savedDocTitles {
					fileList.WriteString(fmt.Sprintf("- %s\n", t))
				}
				wsFiles := r.listWorkspaceFiles()
				for _, f := range wsFiles {
					fileList.WriteString(fmt.Sprintf("- %s\n", f))
				}
				continueMsg := fmt.Sprintf("文档已保存到 workspace：\n%s\n请根据工作流程，用 @成员名 分配下一步任务。不要重复输出文档内容。", fileList.String())
				continueLLMMsg := llm.NewMsg("user", continueMsg)
				masterMsgs = append(masterMsgs, continueLLMMsg)
				r.historyMu.Lock()
				r.masterHistory = append(r.masterHistory, continueLLMMsg)
				r.historyMu.Unlock()
				continue
			}
			// 没有分配给任何成员的任务，master 直接回复了用户
			break
		}

		// Plan 模式下不执行任务，提示用户切换到 Build 模式
		if r.Mode != "build" {
			log.Printf("[room %s] plan 模式，拦截 %d 个任务分配", r.Config.Name, len(assignments))
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master",
				Content: "当前是 Plan 模式，无法执行任务。请切换到 Build 模式后发送消息开始执行。"})
			break
		}

		// 并行执行成员任务
		board := &SharedBoard{}
		results := r.runMembersParallel(ctx, assignments, board, skillXML)

		// 质疑轮
		r.runChallengeRound(ctx, board, skillXML)

		// 将结果反馈给 master 审查
		r.setStatus(StatusThinking, r.master.Config.Name, "正在审阅成员结果...")
		var resultsStr strings.Builder
		for memberName, result := range results {
			resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result))
		}
		boardCtx := board.ToContext()
		feedbackMsg := "Team results:\n" + resultsStr.String()
		if boardCtx != "" {
			feedbackMsg += "\n\nTeam board:\n" + boardCtx
		}
		// 文件驱动模式：注入 workspace 文件内容供 master 审阅
		if r.projectTemplate != nil {
			if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
				feedbackMsg += "\n\n" + wsCtx
			}
		} else {
			// 附加当前产出物文件列表
			if wsFiles := r.listWorkspaceFiles(); len(wsFiles) > 0 {
				feedbackMsg += "\n\n当前产出物文件：\n"
				for _, f := range wsFiles {
					feedbackMsg += "- 📎 " + f + "\n"
				}
			}
		}
		// 注入工作流步骤跟踪
		if r.projectTemplate != nil {
			if stepCtx := r.buildWorkflowStep(); stepCtx != "" {
				feedbackMsg = stepCtx + "\n\n" + feedbackMsg
			}
		}
		feedbackMsg += "\n\n请审查以上成员结果，然后**立即行动**（二选一）：\n1. 用 @成员名 分配下一步任务\n2. 自己直接输出文档（以 # 标题 开头，系统自动保存）\n\n**禁止**只做口头评价而不行动。不要重复已保存的文档内容。"
		feedbackLLMMsg := llm.NewMsg("user", feedbackMsg)
		masterMsgs = append(masterMsgs, feedbackLLMMsg)
		// 同步反馈消息到持久化历史
		r.historyMu.Lock()
		r.masterHistory = append(r.masterHistory, feedbackLLMMsg)
		r.historyMu.Unlock()

		// 更新任务列表
		r.updateTasks(masterMsgs)
	}

	r.setStatus(StatusPending, "", "")

	// Auto-update master memory after task completion
	go r.updateMasterMemory(context.Background(), userMsg, masterMsgs)

	return nil
}

// handleDirectAssign 处理用户直接 @agent 指派的任务，跳过 master 规划。
func (r *Room) handleDirectAssign(ctx context.Context, assignments map[string]string) error {
	skillXML := skill.ToXML(r.skillMeta)
	board := &SharedBoard{}

	r.runMembersParallel(ctx, assignments, board, skillXML)
	r.runChallengeRound(ctx, board, skillXML)

	r.setStatus(StatusPending, "", "")
	return nil
}

func (r *Room) updateMasterMemory(ctx context.Context, task string, msgs []llm.Message) {
	// 过滤无意义任务：内容太短（如"你好"、"hi"）
	if len([]rune(strings.TrimSpace(task))) < 10 {
		log.Printf("[memory] 跳过短任务记忆: %q", task)
		return
	}

	summaryPrompt := fmt.Sprintf(`基于这个任务: %q
总结核心经验（最多3条 bullet points）。
如果这个任务没有值得记忆的经验（如简单问候、闲聊），只输出 SKIP。`, task)
	memMsgs := append(msgs, llm.NewMsg("user", summaryPrompt))
	summary, err := r.master.Chat(ctx, memMsgs, nil)
	if err != nil || summary == "" {
		return
	}

	// LLM 判断无值得记忆的内容
	if strings.TrimSpace(summary) == "SKIP" {
		log.Printf("[memory] LLM 判断跳过记忆: %q", task)
		return
	}

	filename := time.Now().Format("2006-01") + ".md"
	existing, _ := os.ReadFile(filepath.Join(r.master.Dir, "memory", filename))
	taskTitle := task
	if len([]rune(taskTitle)) > 50 {
		taskTitle = string([]rune(taskTitle)[:50])
	}
	content := string(existing) + fmt.Sprintf("\n## %s — %s\n\n%s\n", time.Now().Format("2006-01-02"), taskTitle, summary)
	r.master.SaveMemory(filename, content)

	// 自动压缩：当月文件 > 20KB 时触发
	memFile := filepath.Join(r.master.Dir, "memory", filename)
	if info, err := os.Stat(memFile); err == nil && info.Size() > 20*1024 {
		log.Printf("[memory] 当月文件 %s 超过 20KB，触发压缩", filename)
		go r.master.CompressMemory(context.Background())
	}
}

// CompressAllMemory 压缩 room 中所有 agent（master + members）的记忆。
func (r *Room) CompressAllMemory(ctx context.Context) {
	if r.master != nil {
		if err := r.master.CompressMemory(ctx); err != nil {
			log.Printf("[memory] master 压缩失败: %v", err)
		}
	}
	for name, member := range r.members {
		if err := member.CompressMemory(ctx); err != nil {
			log.Printf("[memory] 成员 %s 压缩失败: %v", name, err)
		}
	}
	log.Printf("[memory] 全部 agent 记忆压缩完成")
}

// updateMemberMemory 为成员 agent 保存任务经验。
func (r *Room) updateMemberMemory(ctx context.Context, member *agent.Agent, task, result string) {
	// 过滤无意义任务
	if len([]rune(strings.TrimSpace(task))) < 10 {
		return
	}

	// 限制 result 输入长度，节省 token
	resultRunes := []rune(result)
	if len(resultRunes) > 500 {
		result = string(resultRunes[:500])
	}

	summaryPrompt := fmt.Sprintf(`基于这个任务和结果，总结核心经验（最多3条 bullet points）。
如果没有值得记忆的经验，只输出 SKIP。

任务: %s
结果: %s`, task, result)

	msgs := []llm.Message{
		llm.NewMsg("system", "你是经验总结助手。提取任务中最有价值的经验教训。"),
		llm.NewMsg("user", summaryPrompt),
	}

	summary, err := member.Chat(ctx, msgs, nil)
	if err != nil || summary == "" || strings.TrimSpace(summary) == "SKIP" {
		return
	}

	filename := time.Now().Format("2006-01") + ".md"
	existing, _ := os.ReadFile(filepath.Join(member.Dir, "memory", filename))
	taskTitle := task
	if len([]rune(taskTitle)) > 50 {
		taskTitle = string([]rune(taskTitle)[:50])
	}
	content := string(existing) + fmt.Sprintf("\n## %s — %s\n\n%s\n", time.Now().Format("2006-01-02"), taskTitle, summary)
	member.SaveMemory(filename, content)
	log.Printf("[memory] 成员 %s 记忆已更新", member.Config.Name)
}

// stripDocuments 从文本中去除文档段落，只保留非文档部分（状态/指令文本）。
func (r *Room) stripDocuments(text string) string {
	docs := splitDocuments(text)
	if len(docs) == 0 {
		return text
	}
	result := text
	for _, doc := range docs {
		result = strings.Replace(result, doc, "", 1)
	}
	// 清理多余空行
	for strings.Contains(result, "\n\n\n") {
		result = strings.ReplaceAll(result, "\n\n\n", "\n\n")
	}
	return strings.TrimSpace(result)
}

// splitDocuments 从文本中拆分出独立文档段落。
// 以 \n# 分割，只保留长度超过 200 字符的段落作为文档。
func splitDocuments(text string) []string {
	parts := strings.Split("\n"+text, "\n# ")
	if len(parts) <= 1 {
		return nil
	}
	var docs []string
	for _, part := range parts[1:] {
		doc := "# " + part
		// 去掉尾部可能的分隔符
		doc = strings.TrimSpace(doc)
		if len([]rune(doc)) > 200 {
			docs = append(docs, doc)
		}
	}
	return docs
}

// isDocument 判断内容是否为文档产出物（而非对话/提问）。
// 文档特征：包含 markdown 标题且内容较长。
func isDocument(content string) bool {
	runeLen := len([]rune(content))
	// 有 # 标题 且 超过 300 字
	hasH1 := strings.Contains(content, "\n# ") || strings.HasPrefix(content, "# ")
	if hasH1 && runeLen > 300 {
		return true
	}
	// 有多个 ## 二级标题，说明是结构化文档
	h2Count := strings.Count(content, "\n## ")
	if h2Count >= 2 && runeLen > 200 {
		return true
	}
	// 有 《》 书名号标题 且 内容较长
	if strings.Contains(content, "《") && strings.Contains(content, "》") && runeLen > 300 {
		// 检查是否为结构化内容（多段落或有列表）
		if strings.Count(content, "\n") > 5 {
			return true
		}
	}
	return false
}

func extractTitle(content string) string {
	for _, line := range strings.Split(content, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "# ") {
			return strings.TrimPrefix(line, "# ")
		}
	}
	// 尝试从 《》 书名号提取标题
	if idx := strings.Index(content, "《"); idx != -1 {
		if end := strings.Index(content[idx:], "》"); end != -1 {
			return content[idx+len("《") : idx+end]
		}
	}
	// 尝试 ## 二级标题
	for _, line := range strings.Split(content, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "## ") {
			return strings.TrimPrefix(line, "## ")
		}
	}
	return ""
}

// titleToFilename 从文档标题生成文件名，如 "主角小传：林远" → "主角小传-林远.md"
func titleToFilename(title, agentName string) string {
	if title == "" {
		return fmt.Sprintf("%s-%s.md", agentName, time.Now().Format("20060102-150405"))
	}
	// 替换不适合文件名的字符
	name := strings.NewReplacer(
		"：", "-", ":", "-",
		"/", "-", "\\", "-",
		" ", "-", "　", "-",
		"*", "", "?", "", "\"", "", "<", "", ">", "", "|", "",
	).Replace(title)
	// 去除连续的短横线
	for strings.Contains(name, "--") {
		name = strings.ReplaceAll(name, "--", "-")
	}
	name = strings.Trim(name, "-")
	if name == "" {
		return fmt.Sprintf("%s-%s.md", agentName, time.Now().Format("20060102-150405"))
	}
	return name + ".md"
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// parseAssignments 解析任务分配指令。
// 支持多行任务描述：从 @成员名 开始，到下一个 @成员名 或文本结束为止。
func parseAssignments(text string) map[string]string {
	result := make(map[string]string)
	lines := strings.Split(text, "\n")

	var currentName string
	var currentTask strings.Builder

	flush := func() {
		if currentName != "" {
			task := strings.TrimSpace(currentTask.String())
			if task != "" {
				result[currentName] = task
			}
		}
		currentName = ""
		currentTask.Reset()
	}

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		// 检测行内 @成员名（支持行首和行内位置，如 "要求 @搜索员 补充信息"）
		atIdx := strings.Index(trimmed, "@")
		if atIdx >= 0 {
			rest := trimmed[atIdx+1:]
			if idx := strings.IndexAny(rest, " \t"); idx > 0 {
				// @name 任务内容
				name := strings.TrimSpace(rest[:idx])
				task := strings.TrimSpace(rest[idx+1:])
				flush()
				currentName = name
				if task != "" {
					currentTask.WriteString(task)
				}
				continue
			}
			// @name 单独出现，任务内容在后续行
			name := strings.TrimSpace(rest)
			if name != "" {
				flush()
				currentName = name
				continue
			}
		}
		// ASSIGN:成员名:任务描述（向后兼容）
		if strings.HasPrefix(trimmed, "ASSIGN:") {
			parts := strings.SplitN(strings.TrimPrefix(trimmed, "ASSIGN:"), ":", 2)
			if len(parts) == 2 {
				flush()
				result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
			}
			continue
		}
		// 当前有 @成员名 在收集中，追加后续行作为任务描述
		if currentName != "" && trimmed != "" {
			if currentTask.Len() > 0 {
				currentTask.WriteString("\n")
			}
			currentTask.WriteString(trimmed)
		}
	}
	flush()
	return result
}

// executeToolCall 执行 tool call，返回执行结果
func (r *Room) executeToolCall(tc llm.ToolCall) string {
	var args struct {
		Command string `json:"command"`
	}
	if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
		return fmt.Sprintf("参数解析错误: %v", err)
	}

	// 设置 SKILLS_ROOT 环境变量（指向 skills/ 根目录）
	skillPath := skill.SkillPathByToolName(r.skillMeta, tc.Function.Name)
	skillsRoot := filepath.Dir(skillPath) // skill 的父目录即 skills/
	if skillPath == "" {
		skillsRoot = "skills"
	}
	// 转为绝对路径
	if abs, err := filepath.Abs(skillsRoot); err == nil {
		skillsRoot = abs
	}

	log.Printf("[tool] 执行: %s, 命令: %s", tc.Function.Name, args.Command)

	cmd := exec.Command("bash", "-c", args.Command)
	cmd.Env = append(os.Environ(), "SKILLS_ROOT="+skillsRoot)
	cmd.Dir = r.Dir

	output, err := cmd.CombinedOutput()
	result := string(output)
	if err != nil {
		result = fmt.Sprintf("命令执行错误: %v\n输出:\n%s", err, result)
	}

	// 限制输出长度，避免 token 爆炸
	if len(result) > 10000 {
		result = result[:10000] + "\n... (输出已截断)"
	}

	log.Printf("[tool] 结果 (%d 字符): %.200s", len(result), result)
	return result
}

// buildSkillSummary 为 master 构建简要的 skill 清单（只有名称和描述，不含调用方式）
func (r *Room) buildSkillSummary() string {
	if len(r.skillMeta) == 0 {
		return ""
	}
	var sb strings.Builder
	sb.WriteString("<available_skills>\n")
	sb.WriteString("以下工具可供团队成员使用，你可以在分配任务时提示成员使用对应的工具：\n")
	for _, m := range r.skillMeta {
		fmt.Fprintf(&sb, "  - %s: %s\n", m.Name, m.Description)
	}
	sb.WriteString("</available_skills>")
	return sb.String()
}

// buildWorkflowStep 根据 workspace 中已存在的文件，构建 phase-aware 的工作流进度上下文
func (r *Room) buildWorkflowStep() string {
	if r.projectTemplate == nil {
		return ""
	}
	var completed []string
	var pending []string
	minPendingPhase := 999

	for _, f := range r.projectTemplate.Files {
		if f.IsDir || f.Dynamic {
			continue
		}
		fpath := filepath.Join(r.Dir, "workspace", f.Path)
		if _, err := os.Stat(fpath); err == nil {
			completed = append(completed, f.Path)
		} else {
			pending = append(pending, fmt.Sprintf("%s (@%s, phase:%d)", f.Path, f.Owner, f.Phase))
			if f.Phase < minPendingPhase {
				minPendingPhase = f.Phase
			}
		}
	}

	var sb strings.Builder
	sb.WriteString("<workflow_progress>\n")
	sb.WriteString("已完成的文件：\n")
	for _, f := range completed {
		sb.WriteString(fmt.Sprintf("  [done] %s\n", f))
	}
	sb.WriteString("待产出的文件：\n")
	for _, f := range pending {
		sb.WriteString(fmt.Sprintf("  [todo] %s\n", f))
	}
	sb.WriteString("</workflow_progress>\n\n")

	// 找到当前最小 phase 的待办任务，给出具体指令
	if minPendingPhase < 999 {
		sb.WriteString(fmt.Sprintf("<next_action>\n当前阶段：phase %d\n", minPendingPhase))
		for _, f := range r.projectTemplate.Files {
			if f.IsDir || f.Dynamic {
				continue
			}
			if f.Phase != minPendingPhase {
				continue
			}
			fpath := filepath.Join(r.Dir, "workspace", f.Path)
			if _, err := os.Stat(fpath); err == nil {
				continue // 已完成
			}
			if f.Owner == r.master.Config.Name {
				sb.WriteString(fmt.Sprintf("- 你需要产出：%s（直接输出文档正文，以 # 标题 开头）\n", f.Path))
			} else {
				sb.WriteString(fmt.Sprintf("- 分配给 @%s：%s\n", f.Owner, f.Path))
			}
		}
		sb.WriteString("</next_action>\n")
		sb.WriteString("请按上述指令推进。文档型输出只输出文档正文，系统自动保存。不要重复已完成的文档内容。")
	} else {
		// 所有固定文件已完成
		sb.WriteString("所有模板文件已完成。请根据你的 AGENT.md 工作流程决定下一步行动（如向用户交付、或进入下一阶段）。")
	}

	return sb.String()
}

// matchTemplateFile 按标题匹配模板文件。
// "创作需求书" -> "创作需求书.md"
// "主角小传：林远" -> "主角小传.md"（前缀匹配）
func (r *Room) matchTemplateFile(title string) *ProjectFile {
	if r.projectTemplate == nil || title == "" {
		return nil
	}
	// 精确匹配（去掉 .md 后缀比较）
	for i := range r.projectTemplate.Files {
		f := &r.projectTemplate.Files[i]
		if f.IsDir || f.Dynamic {
			continue
		}
		fname := strings.TrimSuffix(f.Path, ".md")
		if fname == title {
			return f
		}
	}
	// 前缀匹配（如 "主角小传：林远" 匹配 "主角小传.md"）
	for i := range r.projectTemplate.Files {
		f := &r.projectTemplate.Files[i]
		if f.IsDir || f.Dynamic {
			continue
		}
		fname := strings.TrimSuffix(f.Path, ".md")
		if strings.HasPrefix(title, fname) {
			return f
		}
	}
	// 关键词匹配：标题中包含文件名的关键部分，或文件名包含标题
	for i := range r.projectTemplate.Files {
		f := &r.projectTemplate.Files[i]
		if f.IsDir || f.Dynamic {
			continue
		}
		base := strings.TrimSuffix(f.Path, ".md")
		if strings.Contains(base, title) || strings.Contains(title, base) {
			return f
		}
		// 关键词拆分匹配
		keywords := strings.FieldsFunc(base, func(r rune) bool { return r == '与' || r == '和' || r == '·' })
		for _, kw := range keywords {
			if len([]rune(kw)) >= 2 && strings.Contains(title, kw) {
				return f
			}
		}
	}
	return nil
}

// findOwnerFiles 查找某 agent 负责的所有文件
func (r *Room) findOwnerFiles(agentName string) []ProjectFile {
	if r.projectTemplate == nil {
		return nil
	}
	var files []ProjectFile
	for _, f := range r.projectTemplate.Files {
		if f.Owner == agentName && !f.IsDir && !f.Dynamic {
			files = append(files, f)
		}
	}
	return files
}

// splitContentAndStatus 将 agent 输出分为文件内容和状态消息。
// 整个输出作为文件内容，自动生成状态摘要。
func splitContentAndStatus(reply, filename string) (fileContent, statusMsg string) {
	fileContent = reply
	name := strings.TrimSuffix(filename, ".md")
	statusMsg = fmt.Sprintf("已完成《%s》", name)
	return
}

// buildProjectContext 构建项目模板上下文，注入到成员 system prompt
func (r *Room) buildProjectContext(agentName string) string {
	if r.projectTemplate == nil {
		return ""
	}
	var sb strings.Builder
	sb.WriteString("<project_template>\n")
	sb.WriteString("项目文件结构（系统自动管理文件保存）：\n\n")
	for _, f := range r.projectTemplate.Files {
		if f.IsDir {
			sb.WriteString(fmt.Sprintf("  %s (目录)\n", f.Path))
			continue
		}
		if f.Dynamic {
			sb.WriteString(fmt.Sprintf("  ... @%s phase:%d (动态扩展)\n", f.Owner, f.Phase))
			continue
		}
		marker := ""
		if f.Owner == agentName {
			marker = " ← 你负责"
		}
		sb.WriteString(fmt.Sprintf("  %s  @%s  phase:%d%s\n", f.Path, f.Owner, f.Phase, marker))
	}
	sb.WriteString("\n输出规范：直接输出文件的 Markdown 正文内容（以 # 标题 开头），系统会自动保存到对应文件。\n")
	sb.WriteString("不要在文档中夹杂状态描述或对话内容。\n")
	sb.WriteString("</project_template>")
	return sb.String()
}

// saveAgentOutput 统一处理成员产出的文件保存路由。
// 优先走项目模板路由，降级走原有 isDocument 逻辑。
// 返回 (statusMsg, routed)：routed=true 表示走了模板路由，statusMsg 是短摘要。
func (r *Room) saveAgentOutput(name, finalReply, task string) (string, bool) {
	if r.projectTemplate != nil {
		// 按 owner 查找该成员负责的文件
		ownerFiles := r.findOwnerFiles(name)
		if len(ownerFiles) >= 1 {
			var targetFile *ProjectFile
			if len(ownerFiles) == 1 {
				// 只负责一个文件，直接路由
				targetFile = &ownerFiles[0]
			} else {
				// 负责多个文件，多级匹配策略
				title := extractTitle(finalReply)

				// 策略1：标题精确匹配模板文件
				if title != "" {
					if tf := r.matchTemplateFile(title); tf != nil && tf.Owner == name {
						targetFile = tf
					}
				}

				// 策略2：用回复开头文本模糊匹配文件名（LLM 可能不写 # 前缀）
				if targetFile == nil {
					firstLine := strings.TrimSpace(strings.SplitN(finalReply, "\n", 2)[0])
					firstLine = strings.TrimLeft(firstLine, "# ")
					if firstLine != "" {
						for i := range ownerFiles {
							base := strings.TrimSuffix(ownerFiles[i].Path, ".md")
							if strings.Contains(firstLine, base) || strings.Contains(base, firstLine) {
								targetFile = &ownerFiles[i]
								break
							}
						}
					}
				}

				// 策略3：匹配尚未产出的文件（优先 phase 小的）
				if targetFile == nil {
					for i := range ownerFiles {
						fpath := filepath.Join(r.Dir, "workspace", ownerFiles[i].Path)
						if _, err := os.Stat(fpath); os.IsNotExist(err) {
							targetFile = &ownerFiles[i]
							break
						}
					}
				}

				// 策略4：内容够长（>200字），强制路由到最近的模板文件（覆盖更新）
				if targetFile == nil && len([]rune(finalReply)) > 200 {
					// 取最后一个文件（phase 大的）作为更新目标
					targetFile = &ownerFiles[len(ownerFiles)-1]
				}
			}

			if targetFile != nil {
				fileContent, _ := splitContentAndStatus(finalReply, targetFile.Path)
				// 自动补标题：如果内容没有 # 标题行，从文件名生成一个
				if !strings.HasPrefix(strings.TrimSpace(fileContent), "# ") {
					docTitle := strings.TrimSuffix(targetFile.Path, ".md")
					fileContent = "# " + docTitle + "\n\n" + fileContent
				}
				r.saveWorkspace(targetFile.Path, fileContent)
				// 不再 emit EvtAgentMessage，交给调用方通过 generateBriefMessage 生成
				if r.memberArtifacts == nil {
					r.memberArtifacts = make(map[string]string)
				}
				r.memberArtifacts[name] = targetFile.Path
				r.emit(Event{Type: EvtTaskDone, Agent: name, Task: task})
				return targetFile.Path, true
			}
		}
	}

	// 降级：原有 isDocument 逻辑，也走 Part 模型
	if isDocument(finalReply) {
		title := extractTitle(finalReply)
		if r.memberArtifacts == nil {
			r.memberArtifacts = make(map[string]string)
		}
		var filename string
		if existing, ok := r.memberArtifacts[name]; ok {
			filename = existing
		} else {
			filename = titleToFilename(title, name)
			r.memberArtifacts[name] = filename
		}
		r.saveWorkspace(filename, finalReply)
		r.emit(Event{Type: EvtArtifact, Agent: name, Filename: filename, Title: title})
		r.emit(Event{Type: EvtTaskDone, Agent: name, Task: task})
		// 返回 routed=true，让调用方通过 generateBriefMessage 生成简短消息
		return filename, true
	}
	// 纯交流型输出
	r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: finalReply})
	r.lastActiveMember = name
	r.emit(Event{Type: EvtTaskDone, Agent: name, Task: task})
	return "", false
}

// generateBriefMessage 成员完成文档后，追加一次短 LLM 调用生成包含关键结论的交流消息
func (r *Room) generateBriefMessage(ctx context.Context, ag *agent.Agent, name, docContent, task string) string {
	title := extractTitle(docContent)
	prompt := fmt.Sprintf(
		"你刚完成了文档《%s》（任务：%s）。\n"+
			"请用1句话简短告知团队完成情况，包含关键结论。末尾写 @master 请查阅。\n"+
			"只输出这一句话，不要其他内容。", title, task)
	msgs := []llm.Message{
		llm.NewMsg("system", "你是"+name+"，用一句话简短汇报工作结果。"),
		llm.NewMsg("user", prompt),
	}
	reply, _, err := ag.ChatWithUsage(ctx, msgs, nil)
	if err != nil {
		return fmt.Sprintf("《%s》已完成，@master 请查阅。", title)
	}
	return strings.TrimSpace(reply)
}

// applyDocumentEdit 解析并应用文档编辑指令
// 格式：<<<EDIT 文件名.md>>>
//
//	<<<FIND>>>
//	要替换的旧文本
//	<<<REPLACE>>>
//	新文本
//	<<<END>>>
func (r *Room) applyDocumentEdit(content string) (filename string, applied bool) {
	editRe := regexp.MustCompile(`(?s)<<<EDIT\s+(.+?)>>>\s*<<<FIND>>>\s*(.+?)\s*<<<REPLACE>>>\s*(.+?)\s*<<<END>>>`)
	matches := editRe.FindAllStringSubmatch(content, -1)
	if len(matches) == 0 {
		return "", false
	}
	for _, m := range matches {
		fname := strings.TrimSpace(m[1])
		oldText := strings.TrimSpace(m[2])
		newText := strings.TrimSpace(m[3])
		fpath := filepath.Join(r.Dir, "workspace", fname)
		data, err := os.ReadFile(fpath)
		if err != nil {
			continue
		}
		original := string(data)
		if !strings.Contains(original, oldText) {
			continue
		}
		updated := strings.Replace(original, oldText, newText, 1)
		r.saveWorkspace(fname, updated) // saveWorkspace 内部会自动保存旧版本
		filename = fname
		applied = true
	}
	return
}

func (r *Room) buildTeamXML() string {
	var sb strings.Builder
	sb.WriteString("<team_members>\n")
	for name, a := range r.members {
		fmt.Fprintf(&sb, "  <member>\n    <name>%s</name>\n    <description>%s</description>\n  </member>\n", name, a.Config.Description)
	}
	sb.WriteString("</team_members>")
	return sb.String()
}

// appendPlanLog 将沟通记录追加到任务计划文档
func (r *Room) appendPlanLog(from, to, content string) {
	if r.planFilename == "" {
		return
	}
	path := filepath.Join(r.Dir, "workspace", r.planFilename)
	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	entry := fmt.Sprintf("\n---\n**[%s] %s → %s**\n\n%s\n",
		time.Now().Format("15:04:05"), from, to, content)
	f.WriteString(entry)
}

func (r *Room) saveWorkspace(filename, content string) {
	dir := filepath.Join(r.Dir, "workspace")
	os.MkdirAll(dir, 0755)
	fpath := filepath.Join(dir, filename)

	// 版本追踪：保存旧版本到数据库
	if r.Store != nil {
		if old, err := os.ReadFile(fpath); err == nil && len(old) > 0 {
			r.Store.InsertFileVersion(r.Config.Name, filename, string(old), "")
		}
	}

	os.WriteFile(fpath, []byte(content), 0644)
	// 通知前端 workspace 文件更新
	r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Action: "updated"})
}

func (r *Room) listWorkspaceFiles() []string {
	dir := filepath.Join(r.Dir, "workspace")
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}
	var files []string
	for _, e := range entries {
		if !e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
			files = append(files, e.Name())
		}
	}
	return files
}

// buildWorkspaceContext 读取 workspace 目录下所有文件内容，作为成员上下文。
func (r *Room) buildWorkspaceContext() string {
	wsDir := filepath.Join(r.Dir, "workspace")
	entries, err := os.ReadDir(wsDir)
	if err != nil {
		return ""
	}
	var sb strings.Builder
	for _, e := range entries {
		if e.IsDir() || strings.HasPrefix(e.Name(), ".") {
			continue
		}
		content, err := os.ReadFile(filepath.Join(wsDir, e.Name()))
		if err != nil || len(content) == 0 {
			continue
		}
		// 限制单个文件最大 8000 字符
		text := string(content)
		if len([]rune(text)) > 8000 {
			text = string([]rune(text)[:8000]) + "\n...(截断)"
		}
		sb.WriteString(fmt.Sprintf("\n--- 📎 %s ---\n%s\n", e.Name(), text))
	}
	if sb.Len() == 0 {
		return ""
	}
	return "<workspace_files>\n以下是团队已产出的文档，可供参考和评审：" + sb.String() + "</workspace_files>"
}

func (r *Room) updateTasks(msgs []llm.Message) {
	// 从对话中提取任务列表并保存
	var tasks strings.Builder
	tasks.WriteString("# Tasks\n\n")
	for _, m := range msgs {
		if m.Role == "assistant" {
			assignments := parseAssignments(m.Content)
			for name, task := range assignments {
				tasks.WriteString(fmt.Sprintf("- [ ] [%s] %s\n", name, task))
			}
		}
	}
	content := tasks.String()
	os.WriteFile(filepath.Join(r.Dir, "tasks.md"), []byte(content), 0644)
	r.emit(Event{Type: EvtTasksUpdate, Content: content})
}

func parseRoomConfig(data []byte) (Config, error) {
	var cfg Config
	if !bytes.HasPrefix(data, []byte("---")) {
		return cfg, fmt.Errorf("missing frontmatter")
	}
	parts := bytes.SplitN(data, []byte("---"), 3)
	if len(parts) < 3 {
		return cfg, fmt.Errorf("invalid frontmatter")
	}
	return cfg, yaml.Unmarshal(parts[1], &cfg)
}

// resolveAgentPath finds agent dir: prefers agentsDir/team/name, falls back to agentsDir/name
func resolveAgentPath(agentsDir, team, name string) string {
	if team != "" {
		p := filepath.Join(agentsDir, team, name)
		if _, err := os.Stat(p); err == nil {
			return p
		}
	}
	return filepath.Join(agentsDir, name)
}
