log_dialog
Record a dialog with title, request, code changes, decisions, and todos into a daily log chain for persistent project context.
Instructions
【记录日志】记录一次对话到传递链(daily + recent-5 + summary-10 + log-state.json)。
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | 对话简明标题 | |
| request | Yes | 清洗后的用户需求 | |
| changes | No | 代码变更文件列表 | |
| decisions | No | 本次技术决策 | |
| todos | No | 遗留待办项 |
Implementation Reference
- src/index.ts:137-168 (registration)Tool registration: 'log_dialog' is registered in ListToolsRequestSchema with name, description, and inputSchema (title, request required; changes, decisions, todos optional).
{ name: 'log_dialog', description: '【记录日志】记录一次对话到传递链(daily + recent-5 + summary-10 + log-state.json)。', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '对话简明标题', }, request: { type: 'string', description: '清洗后的用户需求', }, changes: { type: 'array', items: { type: 'string' }, description: '代码变更文件列表', }, decisions: { type: 'array', items: { type: 'string' }, description: '本次技术决策', }, todos: { type: 'array', items: { type: 'string' }, description: '遗留待办项', }, }, required: ['title', 'request'], }, - src/index.ts:246-247 (registration)Tool routing: 'log_dialog' is routed in CallToolRequestSchema switch statement to handleLogDialog method.
case 'log_dialog': return this.handleLogDialog(args); - src/index.ts:380-428 (handler)Main handler for log_dialog: validates required args (title, request), gets promptsDir, generates entry ID, then calls 5 helper methods to update daily log, recent-5, summary-10, log-state.json, and todos.md.
private async handleLogDialog(args: any) { const title = typeof args?.title === 'string' ? args.title : ''; const request = typeof args?.request === 'string' ? args.request : ''; const changes: string[] = Array.isArray(args?.changes) ? args.changes : []; const decisions: string[] = Array.isArray(args?.decisions) ? args.decisions : []; const todos: string[] = Array.isArray(args?.todos) ? args.todos : []; if (!title || !request) { return { content: [{ type: 'text', text: '❌ "title" 和 "request" 是必填参数。' }], isError: true, }; } try { const promptsDir = getPromptsDir(); const today = new Date().toISOString().slice(0, 10); const entryId = this.getNextEntryId(promptsDir); // 1. 更新 daily 日志 this.appendDailyLog(promptsDir, today, entryId, title, request, changes, decisions, todos); // 2. 更新 recent-5 this.updateRecent5(promptsDir, entryId, today, title, request, changes, decisions, todos); // 3. 更新 summary-10 this.updateSummary10(promptsDir, entryId, today, request, changes, decisions, todos); // 4. 更新 log-state.json this.updateLogState(promptsDir, entryId, today, request, changes, decisions, todos); // 5. 更新 todos.md(如果有待办) if (todos.length > 0) { this.appendTodos(promptsDir, todos); } return { content: [{ type: 'text', text: `✅ 对话日志已记录。\n\n- Entry-${String(entryId).padStart(3, '0')}\n- 日期: ${today}\n- 标题: ${title}\n- daily: 已追加\n- recent-5: 已更新\n- summary-10: 已更新`, }], }; } catch (error: any) { return { content: [{ type: 'text', text: `❌ 记录日志失败: ${error.message || error}` }], isError: true, }; } } - src/index.ts:552-561 (helper)Helper: reads nextEntryId from log-state.json to generate sequential entry IDs.
private getNextEntryId(promptsDir: string): number { const statePath = path.join(promptsDir, 'log-state.json'); try { if (fs.existsSync(statePath)) { const state = JSON.parse(fs.readFileSync(statePath, 'utf-8')); return state.nextEntryId || 1; } } catch { /* ignore */ } return 1; } - src/index.ts:563-745 (helper)Helper methods that write to daily/<date>.md, recent-5.md, summary-10.md, log-state.json, and todos.md respectively to persist dialog log data.
private appendDailyLog( promptsDir: string, today: string, entryId: number, title: string, request: string, changes: string[], decisions: string[], todos: string[] ) { const dailyDir = path.join(promptsDir, 'daily'); if (!fs.existsSync(dailyDir)) fs.mkdirSync(dailyDir, { recursive: true }); const dailyPath = path.join(dailyDir, `${today}.md`); const entry = [ '', `## Entry-${String(entryId).padStart(3, '0')}`, `- 时间: ${new Date().toISOString()}`, `- 标题: ${title}`, `- 清洗后需求: ${request}`, changes.length > 0 ? `- 代码变更: ${changes.join(', ')}` : '', decisions.length > 0 ? `- 技术决策: ${decisions.join('; ')}` : '', todos.length > 0 ? `- 待办: ${todos.join('; ')}` : '', '', ].filter(Boolean).join('\n'); fs.appendFileSync(dailyPath, entry, 'utf-8'); } private updateRecent5( promptsDir: string, entryId: number, today: string, title: string, request: string, changes: string[], decisions: string[], todos: string[] ) { const recentPath = path.join(promptsDir, 'recent-5.md'); const newEntry = [ `## Entry-${String(entryId).padStart(3, '0')}`, `- 日期: ${today}`, `- 清洗后需求: ${request}`, changes.length > 0 ? `- 代码变更:\n${changes.map(c => ` - ${c}`).join('\n')}` : '- 代码变更: (无)', decisions.length > 0 ? `- 技术决策:\n${decisions.map(d => ` - ${d}`).join('\n')}` : '- 技术决策: (无)', todos.length > 0 ? `- 待办:\n${todos.map(t => ` - ${t}`).join('\n')}` : '- 待办: (无)', '', ].join('\n'); // 读取现有内容,保留 header,追加新条目,只保留最近 5 条 let content = ''; if (fs.existsSync(recentPath)) { content = fs.readFileSync(recentPath, 'utf-8'); } // 提取 header(第一个 ## 之前的内容) const headerMatch = content.match(/^.*?(?=\n## Entry-)/s); const header = headerMatch ? headerMatch[0].trim() : `# 最近 5 条对话与操作(动态窗口)\n\n> 规则:每次新增 1 条,超过 5 条时删除最旧 1 条,仅保留最近 5 条。\n`; // 提取现有条目 const entries = content.split(/\n(?=## Entry-)/).filter(e => e.startsWith('## Entry-')); entries.push(newEntry); // 只保留最近 5 条 const recentEntries = entries.slice(-5); const updated = `${header}\n\n${recentEntries.join('\n')}\n`; fs.writeFileSync(recentPath, updated, 'utf-8'); } private updateSummary10( promptsDir: string, entryId: number, today: string, request: string, changes: string[], decisions: string[], todos: string[] ) { const summaryPath = path.join(promptsDir, 'summary-10.md'); let content = ''; if (fs.existsSync(summaryPath)) { content = fs.readFileSync(summaryPath, 'utf-8'); } if (!content) { content = `# 近 10 条对话状态摘要(Stateful)\n\n## 窗口元数据\n- window_id: W-0001\n- 统计范围: Entry-001 ~ Entry-010\n- 当前已收录: 0 / 10\n\n## Stateful 摘要\n### Current State\n- 项目初始化完成。\n\n### Decisions Kept\n- (暂无)\n\n### Invalidated Decisions\n- (暂无)\n\n### Open TODO\n- (暂无)\n\n### Carry Forward\n- (暂无)\n`; } // 更新窗口计数 const countMatch = content.match(/当前已收录:\s*(\d+)\s*\/\s*10/); let count = countMatch ? parseInt(countMatch[1]) : 0; count = Math.min(count + 1, 10); content = content.replace(/当前已收录:\s*\d+\s*\/\s*10/, `当前已收录: ${count} / 10`); // 更新 Current State const stateSection = content.match(/### Current State\n([\s\S]*?)(?=\n### Decisions Kept)/); if (stateSection) { const newState = `### Current State\n- Entry-${String(entryId).padStart(3, '0')} (${today}): ${request}\n- Window progress: ${count}/10`; content = content.replace(/### Current State\n[\s\S]*?(?=\n### Decisions Kept)/, newState + '\n'); } // 更新 Decisions Kept if (decisions.length > 0) { const keptSection = content.match(/### Decisions Kept\n([\s\S]*?)(?=\n### Invalidated Decisions)/); if (keptSection) { const newDecisions = decisions.map(d => `- ${d}`).join('\n'); const existingDecisions = keptSection[1].trim(); if (existingDecisions === '(暂无)') { content = content.replace(/### Decisions Kept\n\(暂无\)/, `### Decisions Kept\n${newDecisions}`); } else { content = content.replace(/### Decisions Kept\n[\s\S]*?(?=\n### Invalidated Decisions)/, `### Decisions Kept\n${existingDecisions}\n${newDecisions}\n`); } } } // 更新 Open TODO if (todos.length > 0) { const todoSection = content.match(/### Open TODO\n([\s\S]*?)(?=\n### Carry Forward)/); if (todoSection) { const newTodos = todos.map(t => `- ${t}`).join('\n'); const existingTodos = todoSection[1].trim(); if (existingTodos === '(暂无)') { content = content.replace(/### Open TODO\n\(暂无\)/, `### Open TODO\n${newTodos}`); } else { content = content.replace(/### Open TODO\n[\s\S]*?(?=\n### Carry Forward)/, `### Open TODO\n${existingTodos}\n${newTodos}\n`); } } } fs.writeFileSync(summaryPath, content, 'utf-8'); } private updateLogState( promptsDir: string, entryId: number, today: string, request: string, changes: string[], decisions: string[], todos: string[] ) { const statePath = path.join(promptsDir, 'log-state.json'); let state: any = { nextEntryId: 1, windowId: 'W-0001', windowStartEntry: 1, windowCount: 0, windowEntries: [], }; if (fs.existsSync(statePath)) { try { state = JSON.parse(fs.readFileSync(statePath, 'utf-8')); } catch { /* use default */ } } // 添加新条目 state.windowEntries.push({ id: entryId, date: today, request, changes, decisions, todos, }); state.windowCount = state.windowEntries.length; state.nextEntryId = entryId + 1; // 如果达到 10 条,滚动窗口 if (state.windowCount >= 10) { const windowNum = parseInt(state.windowId.replace('W-', '')) || 1; state.windowId = `W-${String(windowNum + 1).padStart(4, '0')}`; state.windowStartEntry = entryId + 1; state.windowCount = 0; state.windowEntries = []; } fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8'); } private appendTodos(promptsDir: string, todos: string[]) { const todosPath = path.join(promptsDir, 'todos.md'); let content = ''; if (fs.existsSync(todosPath)) { content = fs.readFileSync(todosPath, 'utf-8'); } else { content = `# 待办事项\n\n## 进行中\n\n*(暂无)*\n\n## 已完成\n\n*(暂无)*\n`; } const inProgressMarker = '## 进行中'; const idx = content.indexOf(inProgressMarker); if (idx !== -1) { const afterMarker = content.indexOf('\n', idx) + 1; const newTodos = todos.map(t => `- [ ] ${t}`).join('\n'); content = content.slice(0, afterMarker) + `\n${newTodos}` + content.slice(afterMarker); } fs.writeFileSync(todosPath, content, 'utf-8'); }