新增工作日誌功能,記錄MCP與LLM之間的對話歷史,並提供查詢及清除日誌的工具。更新任務工具以在任務規劃、分析、反思及執行過程中自動記錄日誌,確保系統的可追溯性和完整性。

This commit is contained in:
siage 2025-04-11 16:51:29 +08:00
parent 3d0f002dd8
commit 74f700e3cd
7 changed files with 1631 additions and 1 deletions

118
README.md
View File

@ -9,6 +9,7 @@
3. **依賴管理**:處理任務間的依賴關係,確保正確的執行順序
4. **執行追蹤**:監控任務執行進度和狀態
5. **任務驗證**:確保任務符合預期要求
6. **工作日誌**:記錄和查詢對話歷史,提供任務執行過程的完整紀錄
## 任務管理工作流程
@ -23,6 +24,123 @@
7. **檢驗任務 (verify_task)**:檢查任務完成情況
8. **完成任務 (complete_task)**:標記任務完成並提供報告
## 工作日誌功能
### 功能概述
工作日誌系統記錄 MCP 與 LLM 之間的關鍵對話內容,主要目的是:
1. **保存執行脈絡**:記錄任務執行過程中的關鍵決策和事件
2. **提供可追溯性**:方便回溯查看歷史操作和決策理由
3. **知識累積**:積累經驗知識,避免重複解決相同問題
4. **效能分析**:提供數據支持,幫助分析系統效能和改進方向
系統會自動在以下關鍵時刻記錄日誌:
- 任務執行開始時
- 關鍵決策點
- 任務驗證過程中
- 任務完成時
### 如何使用日誌查詢工具
系統提供兩個主要的日誌管理工具:
#### 1. 查詢日誌 (list_conversation_log)
```javascript
const logResult = await mcp.mcp_shrimp_task_manager.list_conversation_log({
taskId: "任務ID", // 選填,按特定任務過濾
startDate: "2025-01-01T00:00:00Z", // 選填開始日期ISO格式
endDate: "2025-12-31T23:59:59Z", // 選填結束日期ISO格式
limit: 20, // 選填每頁顯示數量預設20最大100
offset: 0, // 選填分頁起始位置預設0
});
```
參數說明:
- `taskId`:按任務 ID 過濾日誌
- `startDate`查詢開始日期ISO 格式字串
- `endDate`查詢結束日期ISO 格式字串
- `limit`:每頁顯示的記錄數量,預設 20最大 100
- `offset`:分頁偏移量,用於實現分頁查詢
#### 2. 清除日誌 (clear_conversation_log)
```javascript
const clearResult = await mcp.mcp_shrimp_task_manager.clear_conversation_log({
confirm: true, // 必填,確認刪除操作
});
```
注意:清除操作不可逆,請謹慎使用。
### 日誌數據結構
工作日誌的核心數據結構為 `ConversationEntry`
```typescript
interface ConversationEntry {
id: string; // 唯一識別符
timestamp: Date; // 記錄時間
participant: ConversationParticipant; // 對話參與者MCP或LLM
summary: string; // 消息摘要,僅記錄關鍵信息
relatedTaskId?: string; // 關聯的任務ID選填
context?: string; // 額外上下文信息(選填)
}
enum ConversationParticipant {
MCP = "MCP", // 系統方
LLM = "LLM", // 模型方
}
```
日誌以 JSON 格式存儲在 `data/conversation_log.json` 文件中,當記錄數量超過閾值時,系統會自動將舊日誌歸檔並創建新的日誌文件。
### 開發者指南:擴展或修改日誌功能
#### 關鍵文件
1. **類型定義**`src/types/index.ts`
- 包含 `ConversationEntry``ConversationParticipant` 等核心類型定義
2. **模型層**`src/models/conversationLogModel.ts`
- 包含所有日誌相關的數據操作函數
- 日誌文件的讀寫、查詢、歸檔等功能
3. **工具層**`src/tools/logTools.ts`
- 提供給外部調用的日誌工具函數
- 實現格式化輸出和參數處理
4. **摘要提取**`src/utils/summaryExtractor.ts`
- 從完整對話中提取關鍵信息的工具
- 使用關鍵詞匹配和重要性評分算法
#### 如何擴展
1. **添加新的日誌查詢方式**
- 在 `conversationLogModel.ts` 中添加新的查詢函數
- 在 `logTools.ts` 中創建相應的工具函數
- 在 `index.ts` 中註冊新工具
2. **修改日誌存儲方式**
- 日誌默認以 JSON 文件形式存儲,可修改 `conversationLogModel.ts` 改用數據庫存儲
- 同時更新相關的讀寫函數
3. **優化摘要提取算法**
- 可在 `summaryExtractor.ts` 中增強或替換摘要提取算法
- 考慮添加基於機器學習的摘要方法
4. **添加新的日誌觸發點**
- 在關鍵流程中調用 `addConversationEntry` 函數添加新的日誌記錄點
## 任務依賴關係
系統支持兩種方式指定任務依賴:

View File

@ -23,6 +23,14 @@ import {
deleteTaskSchema,
} from "./tools/taskTools.js";
// 導入日誌工具函數
import {
listConversationLog,
listConversationLogSchema,
clearConversationLog,
clearConversationLogSchema,
} from "./tools/logTools.js";
// 導入提示模板
import {
planTaskPrompt,
@ -199,6 +207,54 @@ async function main() {
}
);
// 註冊日誌查詢工具
server.tool(
"list_conversation_log",
"查詢系統對話日誌,支持按任務 ID 或時間範圍過濾,提供分頁功能處理大量記錄",
{
taskId: z
.string()
.optional()
.describe("按任務 ID 過濾對話記錄(選填)"),
startDate: z
.string()
.optional()
.describe("起始日期過濾,格式為 ISO 日期字串(選填)"),
endDate: z
.string()
.optional()
.describe("結束日期過濾,格式為 ISO 日期字串(選填)"),
limit: z
.number()
.int()
.positive()
.max(100)
.default(20)
.describe("返回結果數量限制,最大 100預設20"),
offset: z
.number()
.int()
.nonnegative()
.default(0)
.describe("分頁偏移量預設0"),
},
async (args) => {
return await listConversationLog(args);
}
);
// 註冊日誌清除工具
server.tool(
"clear_conversation_log",
"清除所有對話日誌記錄,需要明確確認以避免意外操作",
{
confirm: z.boolean().describe("確認刪除所有日誌記錄(此操作不可逆)"),
},
async (args) => {
return await clearConversationLog(args);
}
);
// 註冊提示
server.prompt(
"plan_task_prompt",

View File

@ -0,0 +1,462 @@
import { ConversationEntry, ConversationParticipant } from "../types/index.js";
import fs from "fs/promises";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { fileURLToPath } from "url";
import { extractSummary } from "../utils/summaryExtractor.js";
// 確保獲取專案資料夾路徑
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, "../..");
// 數據文件路徑
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data");
const CONVERSATION_LOG_FILE = path.join(DATA_DIR, "conversation_log.json");
// 配置參數
const MAX_LOG_ENTRIES = 10000; // 單個日誌文件最大條目數
const MAX_ARCHIVED_LOGS = 5; // 最大歸檔日誌文件數
const LOG_ENTRY_TRIM_THRESHOLD = 8000; // 當日誌超過該條目數時進行精簡
// 確保數據目錄存在
async function ensureDataDir() {
try {
await fs.access(DATA_DIR);
} catch (error) {
await fs.mkdir(DATA_DIR, { recursive: true });
}
try {
await fs.access(CONVERSATION_LOG_FILE);
} catch (error) {
await fs.writeFile(CONVERSATION_LOG_FILE, JSON.stringify({ entries: [] }));
}
}
// 讀取所有對話日誌
async function readConversationLog(): Promise<ConversationEntry[]> {
await ensureDataDir();
const data = await fs.readFile(CONVERSATION_LOG_FILE, "utf-8");
const entries = JSON.parse(data).entries;
// 將日期字串轉換回 Date 物件
return entries.map((entry: any) => ({
...entry,
timestamp: entry.timestamp ? new Date(entry.timestamp) : new Date(),
}));
}
// 寫入所有對話日誌
async function writeConversationLog(
entries: ConversationEntry[]
): Promise<void> {
await ensureDataDir();
await fs.writeFile(
CONVERSATION_LOG_FILE,
JSON.stringify({ entries }, null, 2)
);
}
// 獲取所有對話日誌
export async function getAllConversationEntries(): Promise<
ConversationEntry[]
> {
return await readConversationLog();
}
// 根據 ID 獲取對話日誌條目
export async function getConversationEntryById(
entryId: string
): Promise<ConversationEntry | null> {
const entries = await readConversationLog();
return entries.find((entry) => entry.id === entryId) || null;
}
// 添加新的對話日誌條目
export async function addConversationEntry(
participant: ConversationParticipant,
summary: string,
relatedTaskId?: string,
context?: string
): Promise<ConversationEntry> {
const entries = await readConversationLog();
// 如果日誌條目超過閾值,進行日誌輪換
if (entries.length >= MAX_LOG_ENTRIES) {
await rotateLogFile();
return addConversationEntry(participant, summary, relatedTaskId, context);
}
// 如果日誌條目超過精簡閾值,進行精簡處理
if (entries.length >= LOG_ENTRY_TRIM_THRESHOLD) {
await trimLogEntries();
}
// 摘要太長時自動縮減
const processedSummary =
summary.length > 500 ? extractSummary(summary, 300) : summary;
const newEntry: ConversationEntry = {
id: uuidv4(),
timestamp: new Date(),
participant,
summary: processedSummary,
relatedTaskId,
context,
};
entries.push(newEntry);
await writeConversationLog(entries);
return newEntry;
}
// 更新對話日誌條目
export async function updateConversationEntry(
entryId: string,
updates: Partial<ConversationEntry>
): Promise<ConversationEntry | null> {
const entries = await readConversationLog();
const entryIndex = entries.findIndex((entry) => entry.id === entryId);
if (entryIndex === -1) {
return null;
}
entries[entryIndex] = {
...entries[entryIndex],
...updates,
};
await writeConversationLog(entries);
return entries[entryIndex];
}
// 獲取特定任務的對話日誌條目
export async function getConversationEntriesByTaskId(
taskId: string
): Promise<ConversationEntry[]> {
const entries = await readConversationLog();
return entries.filter((entry) => entry.relatedTaskId === taskId);
}
// 刪除對話日誌條目
export async function deleteConversationEntry(
entryId: string
): Promise<{ success: boolean; message: string }> {
const entries = await readConversationLog();
const initialLength = entries.length;
const filteredEntries = entries.filter((entry) => entry.id !== entryId);
if (filteredEntries.length === initialLength) {
return {
success: false,
message: `找不到 ID 為 ${entryId} 的對話日誌條目`,
};
}
await writeConversationLog(filteredEntries);
return { success: true, message: "對話日誌條目已成功刪除" };
}
// 根據時間範圍獲取對話日誌條目
export async function getConversationEntriesByDateRange(
startDate: Date,
endDate: Date
): Promise<ConversationEntry[]> {
const entries = await readConversationLog();
return entries.filter((entry) => {
const entryTime = entry.timestamp.getTime();
return entryTime >= startDate.getTime() && entryTime <= endDate.getTime();
});
}
// 清除所有對話日誌
export async function clearAllConversationEntries(): Promise<void> {
await writeConversationLog([]);
}
// 獲取分頁的對話日誌條目
export async function getPaginatedConversationEntries(
limit: number = 10,
offset: number = 0,
taskId?: string,
startDate?: Date,
endDate?: Date
): Promise<{ entries: ConversationEntry[]; total: number }> {
let entries = await readConversationLog();
// 根據任務 ID 過濾
if (taskId) {
entries = entries.filter((entry) => entry.relatedTaskId === taskId);
}
// 根據日期範圍過濾
if (startDate && endDate) {
entries = entries.filter((entry) => {
const entryTime = entry.timestamp.getTime();
return entryTime >= startDate.getTime() && entryTime <= endDate.getTime();
});
} else if (startDate) {
entries = entries.filter(
(entry) => entry.timestamp.getTime() >= startDate.getTime()
);
} else if (endDate) {
entries = entries.filter(
(entry) => entry.timestamp.getTime() <= endDate.getTime()
);
}
// 按時間排序(降序,最新的在前)
entries.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
const total = entries.length;
const paginatedEntries = entries.slice(offset, offset + limit);
return { entries: paginatedEntries, total };
}
/**
* -
*/
async function rotateLogFile(): Promise<void> {
console.log("執行日誌輪換操作...");
try {
// 確保目錄存在
await ensureDataDir();
// 檢查當前日誌文件是否存在
try {
await fs.access(CONVERSATION_LOG_FILE);
} catch (error) {
// 如果不存在,創建空文件並返回
await fs.writeFile(
CONVERSATION_LOG_FILE,
JSON.stringify({ entries: [] })
);
return;
}
// 生成歸檔文件名 (conversation_log_時間戳.json)
const timestamp = new Date()
.toISOString()
.replace(/:/g, "-")
.replace(/\..+/, "");
const archiveFileName = `conversation_log_${timestamp}.json`;
const archiveFilePath = path.join(DATA_DIR, archiveFileName);
// 將當前日誌複製到歸檔文件
await fs.copyFile(CONVERSATION_LOG_FILE, archiveFilePath);
// 創建新的空日誌文件
await fs.writeFile(CONVERSATION_LOG_FILE, JSON.stringify({ entries: [] }));
// 清理過多的歸檔文件
await cleanupArchivedLogs();
console.log(`日誌輪換完成,歸檔文件: ${archiveFileName}`);
} catch (error) {
console.error("日誌輪換過程中發生錯誤:", error);
// 即使輪換失敗,也要確保主日誌文件存在
try {
await fs.writeFile(
CONVERSATION_LOG_FILE,
JSON.stringify({ entries: [] })
);
} catch (innerError) {
console.error("創建新日誌文件失敗:", innerError);
}
}
}
/**
*
*/
async function cleanupArchivedLogs(): Promise<void> {
try {
// 讀取數據目錄中的所有文件
const files = await fs.readdir(DATA_DIR);
// 過濾出歸檔日誌文件
const archivedLogs = files
.filter(
(file) => file.startsWith("conversation_log_") && file.endsWith(".json")
)
.map((file) => ({
name: file,
path: path.join(DATA_DIR, file),
// 從文件名中提取時間戳
timestamp: file.replace("conversation_log_", "").replace(".json", ""),
}))
// 按時間戳降序排序(最新的在前)
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
// 如果歸檔日誌文件數量超過最大保留數量,刪除最舊的
if (archivedLogs.length > MAX_ARCHIVED_LOGS) {
const logsToDelete = archivedLogs.slice(MAX_ARCHIVED_LOGS);
for (const log of logsToDelete) {
try {
await fs.unlink(log.path);
console.log(`已刪除過舊的日誌歸檔: ${log.name}`);
} catch (error) {
console.error(`刪除日誌歸檔 ${log.name} 失敗:`, error);
}
}
}
} catch (error) {
console.error("清理歸檔日誌時發生錯誤:", error);
}
}
/**
*
*/
async function trimLogEntries(): Promise<void> {
try {
const entries = await readConversationLog();
// 如果條目數量未達到精簡閾值,不進行處理
if (entries.length < LOG_ENTRY_TRIM_THRESHOLD) {
return;
}
console.log(
`日誌條目數量(${entries.length})超過精簡閾值(${LOG_ENTRY_TRIM_THRESHOLD}),進行精簡處理...`
);
// 策略保留最新的75%條目,優先移除一般日誌,保留錯誤和重要操作
const entriesToKeep = Math.floor(entries.length * 0.75);
// 先按重要性對條目進行分類
const errorEntries = entries.filter(
(entry) =>
entry.context?.includes("錯誤") ||
entry.context?.includes("失敗") ||
entry.summary.includes("錯誤") ||
entry.summary.includes("失敗")
);
const taskEntries = entries.filter(
(entry) =>
entry.relatedTaskId && !errorEntries.some((e) => e.id === entry.id)
);
const generalEntries = entries.filter(
(entry) =>
!errorEntries.some((e) => e.id === entry.id) &&
!taskEntries.some((e) => e.id === entry.id)
);
// 確定每類保留多少條目
const totalToRemove = entries.length - entriesToKeep;
// 優先從一般日誌中移除
let trimmedGeneralEntries = generalEntries;
if (generalEntries.length > totalToRemove) {
// 按時間排序,移除最舊的
trimmedGeneralEntries = generalEntries
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
)
.slice(0, generalEntries.length - totalToRemove);
} else {
// 如果一般日誌不夠,需要從任務日誌中移除
trimmedGeneralEntries = [];
const remainingToRemove = totalToRemove - generalEntries.length;
if (remainingToRemove > 0 && taskEntries.length > remainingToRemove) {
const trimmedTaskEntries = taskEntries
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
)
.slice(0, taskEntries.length - remainingToRemove);
// 合併保留的條目
const newEntries = [
...errorEntries,
...trimmedTaskEntries,
...trimmedGeneralEntries,
];
await writeConversationLog(newEntries);
console.log(
`日誌精簡完成: 從 ${entries.length} 條減少到 ${newEntries.length}`
);
return;
}
}
// 合併保留的條目
const newEntries = [
...errorEntries,
...taskEntries,
...trimmedGeneralEntries,
];
await writeConversationLog(newEntries);
console.log(
`日誌精簡完成: 從 ${entries.length} 條減少到 ${newEntries.length}`
);
} catch (error) {
console.error("精簡日誌條目時發生錯誤:", error);
}
}
/**
*
*/
export async function getArchivedLogFiles(): Promise<string[]> {
try {
// 確保目錄存在
await ensureDataDir();
const files = await fs.readdir(DATA_DIR);
// 過濾出歸檔日誌文件
return files
.filter(
(file) => file.startsWith("conversation_log_") && file.endsWith(".json")
)
.sort()
.reverse(); // 最新的在前
} catch (error) {
console.error("獲取歸檔日誌文件列表時發生錯誤:", error);
return [];
}
}
/**
*
*/
export async function readArchivedLog(
archiveFileName: string
): Promise<ConversationEntry[]> {
// 安全性檢查:確保文件名格式正確
if (!archiveFileName.match(/^conversation_log_[\d-]+T[\d-]+\.json$/)) {
throw new Error("無效的歸檔日誌文件名");
}
const archiveFilePath = path.join(DATA_DIR, archiveFileName);
try {
await fs.access(archiveFilePath);
const data = await fs.readFile(archiveFilePath, "utf-8");
const entries = JSON.parse(data).entries;
// 將日期字串轉換回 Date 物件
return entries.map((entry: any) => ({
...entry,
timestamp: entry.timestamp ? new Date(entry.timestamp) : new Date(),
}));
} catch (error) {
console.error(`讀取歸檔日誌 ${archiveFileName} 時發生錯誤:`, error);
return [];
}
}

434
src/tools/logTools.ts Normal file
View File

@ -0,0 +1,434 @@
import { z } from "zod";
import {
getAllConversationEntries,
getConversationEntriesByTaskId,
getPaginatedConversationEntries,
clearAllConversationEntries,
getArchivedLogFiles,
readArchivedLog,
} from "../models/conversationLogModel.js";
import { getTaskById } from "../models/taskModel.js";
import { ListConversationLogArgs } from "../types/index.js";
// 列出對話日誌工具
export const listConversationLogSchema = z.object({
taskId: z.string().optional().describe("按任務 ID 過濾對話記錄(選填)"),
startDate: z
.string()
.optional()
.describe("起始日期過濾,格式為 ISO 日期字串(選填)"),
endDate: z
.string()
.optional()
.describe("結束日期過濾,格式為 ISO 日期字串(選填)"),
limit: z
.number()
.int()
.positive()
.max(100)
.default(20)
.describe("返回結果數量限制,最大 100預設20"),
offset: z
.number()
.int()
.nonnegative()
.default(0)
.describe("分頁偏移量預設0"),
});
export async function listConversationLog({
taskId,
startDate,
endDate,
limit,
offset,
}: z.infer<typeof listConversationLogSchema>) {
// 將日期字串轉換為 Date 物件
const startDateObj = startDate ? new Date(startDate) : undefined;
const endDateObj = endDate ? new Date(endDate) : undefined;
// 驗證日期格式
if (startDate && isNaN(startDateObj?.getTime() ?? NaN)) {
return {
content: [
{
type: "text" as const,
text: `## 參數錯誤\n\n起始日期格式無效。請使用 ISO 日期字串格式例如2023-01-01T00:00:00Z`,
},
],
isError: true,
};
}
if (endDate && isNaN(endDateObj?.getTime() ?? NaN)) {
return {
content: [
{
type: "text" as const,
text: `## 參數錯誤\n\n結束日期格式無效。請使用 ISO 日期字串格式例如2023-01-01T00:00:00Z`,
},
],
isError: true,
};
}
// 如果指定了任務 ID檢查任務是否存在
let taskInfo = "";
if (taskId) {
const task = await getTaskById(taskId);
if (!task) {
return {
content: [
{
type: "text" as const,
text: `## 參數錯誤\n\n找不到 ID 為 \`${taskId}\` 的任務。請使用「list_tasks」工具確認有效的任務 ID 後再試。`,
},
],
isError: true,
};
}
taskInfo = `任務:${task.name} (ID: \`${task.id}\`)`;
}
// 獲取分頁的對話日誌
const { entries, total } = await getPaginatedConversationEntries(
limit,
offset,
taskId,
startDateObj,
endDateObj
);
if (entries.length === 0) {
return {
content: [
{
type: "text" as const,
text: `## 查詢結果\n\n未找到符合條件的對話日誌記錄。${
taskId ? `\n\n${taskInfo}` : ""
}`,
},
],
};
}
// 構建過濾條件描述
const filterDescs = [];
if (taskInfo) filterDescs.push(taskInfo);
if (startDateObj) filterDescs.push(`開始日期:${startDateObj.toISOString()}`);
if (endDateObj) filterDescs.push(`結束日期:${endDateObj.toISOString()}`);
const filterDesc =
filterDescs.length > 0 ? `\n\n過濾條件${filterDescs.join("")}` : "";
// 構建分頁信息
const pageInfo = `\n\n當前顯示${offset + 1}${Math.min(
offset + limit,
total
)} ${total} `;
// 構建分頁導航提示
let navTips = "";
if (total > limit) {
const prevPageAvailable = offset > 0;
const nextPageAvailable = offset + limit < total;
navTips = "\n\n分頁導航";
if (prevPageAvailable) {
const prevOffset = Math.max(0, offset - limit);
navTips += `\n- 上一頁:使用 offset=${prevOffset}`;
}
if (nextPageAvailable) {
const nextOffset = offset + limit;
navTips += `\n- 下一頁:使用 offset=${nextOffset}`;
}
}
// 格式化對話日誌列表
const formattedEntries = entries
.map((entry, index) => {
const entryNumber = offset + index + 1;
return `### ${entryNumber}. ${entry.participant} (${new Date(
entry.timestamp
).toISOString()})\n${
entry.relatedTaskId ? `關聯任務:\`${entry.relatedTaskId}\`\n` : ""
}${entry.context ? `上下文:${entry.context}\n` : ""}${
entry.summary
}`;
})
.join("\n\n");
const result = `## 對話日誌查詢結果${filterDesc}${pageInfo}\n\n${formattedEntries}${navTips}`;
return {
content: [
{
type: "text" as const,
text: result,
},
],
};
}
// 清除所有對話日誌工具
export const clearConversationLogSchema = z.object({
confirm: z.boolean().describe("確認刪除所有日誌記錄(此操作不可逆)"),
});
export async function clearConversationLog({
confirm,
}: z.infer<typeof clearConversationLogSchema>) {
if (!confirm) {
return {
content: [
{
type: "text" as const,
text: `## 操作取消\n\n未確認清除操作。如要清除所有對話日誌請將 confirm 參數設為 true。`,
},
],
};
}
// 執行清除操作
await clearAllConversationEntries();
return {
content: [
{
type: "text" as const,
text: `## 操作成功\n\n所有對話日誌已成功清除。`,
},
],
};
}
// 列出歸檔日誌工具
export const listArchivedLogsSchema = z.object({
includeDetails: z
.boolean()
.default(false)
.describe("是否包含歸檔文件的詳細信息(預設:否)"),
});
export async function listArchivedLogs({
includeDetails = false,
}: z.infer<typeof listArchivedLogsSchema>) {
const archiveFiles = await getArchivedLogFiles();
let content = "";
if (archiveFiles.length === 0) {
content = "## 日誌歸檔\n\n目前沒有任何日誌歸檔文件。";
} else {
content = "## 日誌歸檔文件\n\n";
if (includeDetails) {
// 為每個歸檔文件獲取更多詳細信息
const detailedFiles = await Promise.all(
archiveFiles.map(async (file) => {
try {
const entries = await readArchivedLog(file);
const timestamp = file
.replace("conversation_log_", "")
.replace(".json", "");
const formattedDate = new Date(
timestamp.replace(/-/g, ":").replace("T", " ")
).toLocaleString();
return {
filename: file,
date: formattedDate,
entriesCount: entries.length,
firstEntry: entries.length > 0 ? entries[0] : null,
lastEntry:
entries.length > 0 ? entries[entries.length - 1] : null,
};
} catch (error) {
return {
filename: file,
date: "未知",
entriesCount: 0,
error: (error as Error).message,
};
}
})
);
// 格式化輸出
content += detailedFiles
.map((file, index) => {
let fileInfo = `${index + 1}. **${file.filename}**\n`;
fileInfo += ` - 創建日期: ${file.date}\n`;
fileInfo += ` - 條目數量: ${file.entriesCount}\n`;
if (file.firstEntry) {
const firstDate = new Date(
file.firstEntry.timestamp
).toLocaleString();
fileInfo += ` - 最早條目: ${firstDate}\n`;
}
if (file.lastEntry) {
const lastDate = new Date(
file.lastEntry.timestamp
).toLocaleString();
fileInfo += ` - 最晚條目: ${lastDate}\n`;
}
if (file.error) {
fileInfo += ` - 錯誤: ${file.error}\n`;
}
return fileInfo;
})
.join("\n");
} else {
// 簡單列出歸檔文件名
content += archiveFiles
.map((file, index) => {
// 從文件名提取日期
const timestamp = file
.replace("conversation_log_", "")
.replace(".json", "");
const formattedDate = new Date(
timestamp.replace(/-/g, ":").replace("T", " ")
).toLocaleString();
return `${index + 1}. **${file}** (${formattedDate})`;
})
.join("\n");
}
content += "\n\n使用讀取歸檔日誌工具可查看特定歸檔文件的內容。";
}
return {
content: [
{
type: "text" as const,
text: content,
},
],
};
}
// 讀取特定歸檔日誌工具
export const readArchivedLogSchema = z.object({
filename: z
.string()
.describe("歸檔日誌文件名,格式為 'conversation_log_[timestamp].json'"),
limit: z
.number()
.int()
.positive()
.max(100)
.default(50)
.describe("返回結果數量限制,最大 100預設50"),
offset: z
.number()
.int()
.nonnegative()
.default(0)
.describe("分頁起始位置預設0"),
});
export async function readArchivedLogTool({
filename,
limit = 50,
offset = 0,
}: z.infer<typeof readArchivedLogSchema>) {
try {
// 安全性檢查:確保文件名格式正確
if (!filename.match(/^conversation_log_[\d-]+T[\d-]+\.json$/)) {
return {
content: [
{
type: "text" as const,
text: "## 錯誤\n\n無效的歸檔日誌文件名。正確格式為 'conversation_log_[timestamp].json'。",
},
],
isError: true,
};
}
const entries = await readArchivedLog(filename);
// 分頁處理
const paginatedEntries = entries.slice(offset, offset + limit);
const total = entries.length;
let content = `## 歸檔日誌: ${filename}\n\n`;
if (paginatedEntries.length === 0) {
content += "此歸檔文件沒有任何日誌條目。";
} else {
// 添加分頁信息
content += `顯示 ${Math.min(total, offset + 1)}-${Math.min(
total,
offset + limit
)} ${total} \n\n`;
// 格式化日誌條目
content += paginatedEntries
.map((entry, index) => {
const date = new Date(entry.timestamp).toLocaleString();
let entryContent = `### ${offset + index + 1}. ${date} (${
entry.participant
})\n`;
if (entry.context) {
entryContent += `**上下文:** ${entry.context}\n`;
}
if (entry.relatedTaskId) {
entryContent += `**相關任務:** ${entry.relatedTaskId}\n`;
}
entryContent += `\n${entry.summary}\n`;
return entryContent;
})
.join("\n\n");
// 添加導航鏈接
if (offset > 0 || offset + limit < total) {
content += "\n\n### 分頁導航\n";
if (offset > 0) {
const prevOffset = Math.max(0, offset - limit);
content += `- 上一頁 (${prevOffset + 1}-${Math.min(
total,
prevOffset + limit
)})\n`;
}
if (offset + limit < total) {
const nextOffset = offset + limit;
content += `- 下一頁 (${nextOffset + 1}-${Math.min(
total,
nextOffset + limit
)})\n`;
}
}
}
return {
content: [
{
type: "text" as const,
text: content,
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `## 錯誤\n\n讀取歸檔日誌時發生錯誤: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
}

View File

@ -7,7 +7,9 @@ import {
batchCreateOrUpdateTasks,
deleteTask as modelDeleteTask,
} from "../models/taskModel.js";
import { TaskStatus } from "../types/index.js";
import { TaskStatus, ConversationParticipant } from "../types/index.js";
import { addConversationEntry } from "../models/conversationLogModel.js";
import { extractSummary } from "../utils/summaryExtractor.js";
// 開始規劃工具
export const planTaskSchema = z.object({
@ -24,6 +26,20 @@ export async function planTask({
description,
requirements,
}: z.infer<typeof planTaskSchema>) {
// 記錄任務規劃開始
try {
// 使用摘要提取工具處理較長的描述
const descriptionSummary = extractSummary(description, 100);
await addConversationEntry(
ConversationParticipant.MCP,
`開始新任務規劃,描述:${descriptionSummary}`,
undefined,
"任務規劃"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
let prompt = `## 任務分析請求\n\n請仔細分析以下任務問題理解其核心要求、範圍和約束條件\n\n\`\`\`\n${description}\n\`\`\`\n\n`;
if (requirements) {
@ -68,6 +84,23 @@ export async function analyzeTask({
initialConcept,
previousAnalysis,
}: z.infer<typeof analyzeTaskSchema>) {
// 記錄任務分析
try {
// 使用摘要提取工具處理較長的概念描述
const conceptSummary = extractSummary(initialConcept, 100);
await addConversationEntry(
ConversationParticipant.MCP,
`開始分析任務:${extractSummary(
summary,
100
)}${conceptSummary}`,
undefined,
"任務分析"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
let prompt = `## 代碼庫分析任務\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n已收到您的初步解答構想\n\n\`\`\`\n${initialConcept}\n\`\`\`\n\n`;
prompt += `## 技術審核指引\n\n請執行以下分析步驟\n\n1. 檢查現有程式碼庫中的相似實現或可重用組件
@ -107,6 +140,23 @@ export async function reflectTask({
summary,
analysis,
}: z.infer<typeof reflectTaskSchema>) {
// 記錄任務反思
try {
// 使用摘要提取工具處理較長的分析
const analysisSummary = extractSummary(analysis, 100);
await addConversationEntry(
ConversationParticipant.MCP,
`開始反思任務解決方案:${extractSummary(
summary,
50
)}${analysisSummary}`,
undefined,
"任務反思"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const prompt = `## 解決方案反思與評估\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n### 詳細分析結果\n\`\`\`\n${analysis}\n\`\`\`\n\n## 批判性評估指引\n\n請從以下多個維度對您的解決方案進行全面且批判性的審查\n\n### 1. 技術完整性評估\n- 方案是否存在技術缺陷或邏輯漏洞?
-
-
@ -166,9 +216,36 @@ export async function splitTasks({
isOverwrite,
tasks,
}: z.infer<typeof splitTasksSchema>) {
// 記錄任務拆分
try {
await addConversationEntry(
ConversationParticipant.MCP,
`拆分任務:${isOverwrite ? "覆蓋模式" : "新增模式"},任務數量:${
tasks.length
}`,
undefined,
"任務拆分"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
// 批量創建任務
const createdTasks = await batchCreateOrUpdateTasks(tasks, isOverwrite);
// 記錄任務創建成功
try {
const taskNames = createdTasks.map((task) => task.name).join(", ");
await addConversationEntry(
ConversationParticipant.MCP,
`成功創建任務:${taskNames}`,
undefined,
"任務創建"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
// 獲取所有任務,用於顯示完整的依賴關係
const allTasks = await getAllTasks();
@ -229,6 +306,18 @@ export async function splitTasks({
// 列出任務工具
export async function listTasks() {
// 記錄查詢任務列表
try {
await addConversationEntry(
ConversationParticipant.MCP,
"查詢所有任務列表",
undefined,
"任務列表查詢"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const tasks = await getAllTasks();
if (tasks.length === 0) {
@ -316,6 +405,18 @@ export async function executeTask({
const task = await getTaskById(taskId);
if (!task) {
// 記錄錯誤日誌
try {
await addConversationEntry(
ConversationParticipant.MCP,
`執行任務失敗找不到ID為 ${taskId} 的任務`,
undefined,
"錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -328,6 +429,18 @@ export async function executeTask({
}
if (task.status === TaskStatus.COMPLETED) {
// 記錄已完成任務的嘗試執行
try {
await addConversationEntry(
ConversationParticipant.MCP,
`嘗試執行已完成的任務:${task.name} (ID: ${task.id})`,
task.id,
"狀態通知"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -352,6 +465,18 @@ export async function executeTask({
: `ID: \`${id}\``;
});
// 記錄任務被阻擋的情況
try {
await addConversationEntry(
ConversationParticipant.MCP,
`任務 ${task.name} (ID: ${task.id}) 被依賴阻擋,等待完成的依賴任務數量: ${blockedBy.length}`,
task.id,
"依賴阻擋"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -370,6 +495,18 @@ export async function executeTask({
// 更新任務狀態為進行中
await updateTaskStatus(taskId, TaskStatus.IN_PROGRESS);
// 記錄任務開始執行
try {
await addConversationEntry(
ConversationParticipant.MCP,
`開始執行任務:${task.name} (ID: ${task.id})`,
task.id,
"任務啟動"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const prompt = `## 任務執行指示\n\n### 任務詳情\n\n- **名稱:** ${
task.name
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
@ -408,6 +545,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
const task = await getTaskById(taskId);
if (!task) {
// 記錄錯誤日誌
try {
await addConversationEntry(
ConversationParticipant.MCP,
`驗證任務失敗找不到ID為 ${taskId} 的任務`,
undefined,
"錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -420,6 +569,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
}
if (task.status !== TaskStatus.IN_PROGRESS) {
// 記錄狀態錯誤
try {
await addConversationEntry(
ConversationParticipant.MCP,
`驗證任務狀態錯誤:任務 ${task.name} (ID: ${task.id}) 當前狀態為 ${task.status},不處於進行中狀態`,
task.id,
"狀態錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -431,6 +592,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
};
}
// 記錄開始驗證
try {
await addConversationEntry(
ConversationParticipant.MCP,
`開始驗證任務:${task.name} (ID: ${task.id})`,
task.id,
"任務驗證"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const prompt = `## 任務驗證評估\n\n### 任務資料\n\n- **名稱:** ${
task.name
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
@ -476,6 +649,18 @@ export async function completeTask({
const task = await getTaskById(taskId);
if (!task) {
// 記錄錯誤日誌
try {
await addConversationEntry(
ConversationParticipant.MCP,
`完成任務失敗找不到ID為 ${taskId} 的任務`,
undefined,
"錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -488,6 +673,18 @@ export async function completeTask({
}
if (task.status !== TaskStatus.IN_PROGRESS) {
// 記錄狀態錯誤
try {
await addConversationEntry(
ConversationParticipant.MCP,
`完成任務狀態錯誤:任務 ${task.name} (ID: ${task.id}) 當前狀態為 ${task.status},不是進行中狀態`,
task.id,
"狀態錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -502,6 +699,18 @@ export async function completeTask({
// 更新任務狀態為已完成
await updateTaskStatus(taskId, TaskStatus.COMPLETED);
// 記錄任務完成
try {
await addConversationEntry(
ConversationParticipant.MCP,
`任務成功完成:${task.name} (ID: ${task.id})`,
task.id,
"任務完成"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const prompt = `## 任務完成確認\n\n任務 "${task.name}" (ID: \`${
task.id
}\`) 已於 ${new Date().toISOString()} 成功標記為完成。\n\n## 任務報告要求\n\n請提供全面且結構化的任務完成報告必須包含以下章節\n\n### 1. 任務概述 (20%)\n- 簡要說明任務目標及其在整體系統中的角色\n- 概述任務的範圍和界限\n- 說明任務的重要性和價值\n\n### 2. 實施摘要 (30%)\n- 詳述採用的技術方案和架構決策\n- 說明關鍵算法和數據結構的選擇\n- 列出使用的外部依賴和API\n\n### 3. 挑戰與解決方案 (20%)\n- 描述在實施過程中遇到的主要技術挑戰\n- 解釋每個挑戰的解決方案及其理由\n- 討論探索過但未採用的替代方案\n\n### 4. 質量保證措施 (15%)\n- 總結執行的測試類型和範圍\n- 報告性能測量結果(如適用)\n- 描述實施的安全措施(如適用)\n\n### 5. 後續步驟與建議 (15%)\n- 提出可能的進一步改進或優化\n- 識別潛在的風險或技術債務\n- 建議下一步行動和優先事項`;
@ -527,6 +736,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
const task = await getTaskById(taskId);
if (!task) {
// 記錄錯誤日誌
try {
await addConversationEntry(
ConversationParticipant.MCP,
`刪除任務失敗找不到ID為 ${taskId} 的任務`,
undefined,
"錯誤"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -539,6 +760,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
}
if (task.status === TaskStatus.COMPLETED) {
// 記錄操作被拒絕
try {
await addConversationEntry(
ConversationParticipant.MCP,
`刪除操作被拒絕:嘗試刪除已完成的任務 ${task.name} (ID: ${task.id})`,
task.id,
"操作被拒絕"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{
@ -550,8 +783,34 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
};
}
// 記錄要刪除的任務
try {
await addConversationEntry(
ConversationParticipant.MCP,
`正在刪除任務:${task.name} (ID: ${task.id})`,
task.id,
"任務刪除"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
const result = await modelDeleteTask(taskId);
// 記錄刪除結果
try {
await addConversationEntry(
ConversationParticipant.MCP,
`任務刪除${result.success ? "成功" : "失敗"}${task.name} (ID: ${
task.id
})${result.message}`,
result.success ? undefined : task.id,
result.success ? "任務刪除成功" : "任務刪除失敗"
);
} catch (error) {
console.error("記錄對話日誌時發生錯誤:", error);
}
return {
content: [
{

View File

@ -70,3 +70,28 @@ export interface VerifyTaskArgs {
export interface CompleteTaskArgs {
taskId: string; // 待標記為完成的任務唯一標識符必須是狀態為「進行中」的有效任務ID
}
// 對話參與者類型:定義對話中的參與方身份
export enum ConversationParticipant {
MCP = "MCP", // 系統方MCP
LLM = "LLM", // 模型方LLM
}
// 對話日誌條目:記錄 MCP 與 LLM 之間的重要對話內容
export interface ConversationEntry {
id: string; // 日誌條目的唯一標識符
timestamp: Date; // 記錄的時間戳
participant: ConversationParticipant; // 對話參與者MCP 或 LLM
summary: string; // 消息摘要,只記錄關鍵信息點而非完整對話
relatedTaskId?: string; // 關聯的任務 ID選填用於將對話與特定任務關聯
context?: string; // 額外的上下文信息(選填),提供對話發生的背景
}
// 對話日誌列表參數:用於查詢對話日誌的參數
export interface ListConversationLogArgs {
taskId?: string; // 按任務 ID 過濾(選填)
startDate?: Date; // 起始日期過濾(選填)
endDate?: Date; // 結束日期過濾(選填)
limit?: number; // 返回結果數量限制(選填)
offset?: number; // 分頁偏移量(選填)
}

View File

@ -0,0 +1,276 @@
/**
*
*
* 使
* 1.
* 2.
* 3.
*/
// 定義關鍵詞與其權重
const KEYWORDS = {
// 任務相關
任務: 1.5,
功能: 1.3,
實現: 1.3,
開發: 1.3,
完成: 1.2,
執行: 1.2,
驗證: 1.2,
錯誤: 1.5,
問題: 1.5,
修復: 1.5,
失敗: 1.8,
成功: 1.5,
依賴: 1.2,
阻擋: 1.4,
風險: 1.4,
優化: 1.3,
改進: 1.3,
// 決策相關
決定: 1.6,
選擇: 1.5,
決策: 1.6,
方案: 1.5,
架構: 1.5,
設計: 1.4,
結構: 1.4,
// 技術相關
代碼: 1.3,
測試: 1.3,
函數: 1.2,
接口: 1.2,
類型: 1.2,
模塊: 1.2,
組件: 1.2,
數據: 1.3,
文件: 1.2,
路徑: 1.1,
// 系統狀態
狀態: 1.3,
啟動: 1.3,
停止: 1.3,
創建: 1.3,
刪除: 1.4,
更新: 1.3,
查詢: 1.2,
// 負面信息(需要重點關注)
警告: 1.8,
異常: 1.8,
崩潰: 2.0,
嚴重: 1.8,
危險: 1.8,
緊急: 1.9,
};
/**
*
*
* @param text
* @param maxLength
* @returns
*/
export function extractSummary(text: string, maxLength: number = 200): string {
// 防禦性檢查
if (!text || text.trim().length === 0) {
return "";
}
// 將文本分割為句子
const sentences = splitIntoSentences(text);
// 如果只有一個句子且小於最大長度,直接返回
if (sentences.length === 1 && sentences[0].length <= maxLength) {
return sentences[0];
}
// 為每個句子評分
const scoredSentences = sentences.map((sentence, index) => ({
text: sentence,
score: scoreSentence(sentence, index, sentences.length),
index,
}));
// 按評分排序
scoredSentences.sort((a, b) => b.score - a.score);
// 選擇評分最高的句子,直到達到最大長度
let summary = "";
let sentencesToInclude: { text: string; index: number }[] = [];
for (const scored of scoredSentences) {
if ((summary + scored.text).length <= maxLength) {
sentencesToInclude.push({
text: scored.text,
index: scored.index,
});
} else {
// 如果還沒有選中任何句子,選擇第一個句子並截斷
if (sentencesToInclude.length === 0) {
return scored.text.substring(0, maxLength);
}
break;
}
}
// 按原文順序排列選中的句子
sentencesToInclude.sort((a, b) => a.index - b.index);
// 組合成最終摘要
summary = sentencesToInclude.map((s) => s.text).join(" ");
// 如果摘要仍然太長,進行截斷
if (summary.length > maxLength) {
summary = summary.substring(0, maxLength - 3) + "...";
}
return summary;
}
/**
*
*
* @param text
* @returns
*/
function splitIntoSentences(text: string): string[] {
// 使用正則表達式分割句子
// 匹配中文和英文的句號、問號、驚嘆號,以及換行符
const sentenceSplitters = /(?<=[。.!?\n])\s*/g;
const sentences = text
.split(sentenceSplitters)
.filter((s) => s.trim().length > 0);
return sentences;
}
/**
*
*
* @param sentence
* @param index
* @param totalSentences
* @returns
*/
function scoreSentence(
sentence: string,
index: number,
totalSentences: number
): number {
let score = 1.0;
// 位置因素:文檔開頭和結尾的句子通常更重要
if (index === 0 || index === totalSentences - 1) {
score *= 1.5;
} else if (
index < Math.ceil(totalSentences * 0.2) ||
index >= Math.floor(totalSentences * 0.8)
) {
score *= 1.25;
}
// 句子長度因素:過短的句子可能信息量較少,過長的句子可能包含太多信息
const wordCount = sentence.split(/\s+/).length;
if (wordCount < 3) {
score *= 0.8;
} else if (wordCount > 25) {
score *= 0.9;
} else if (wordCount >= 5 && wordCount <= 15) {
score *= 1.2;
}
// 關鍵詞因素:包含關鍵詞的句子更重要
for (const [keyword, weight] of Object.entries(KEYWORDS)) {
if (sentence.includes(keyword)) {
score *= weight;
}
}
// 句子結構因素:特殊句式可能更重要
if (
sentence.includes("總結") ||
sentence.includes("結論") ||
sentence.includes("因此") ||
sentence.includes("所以")
) {
score *= 1.5;
}
// 數字和專有名詞因素:包含數字和專有名詞的句子通常更重要
if (/\d+/.test(sentence)) {
score *= 1.3;
}
return score;
}
/**
*
*
* @param content
* @param maxLength
* @returns
*/
export function extractTitle(content: string, maxLength: number = 50): string {
// 防禦性檢查
if (!content || content.trim().length === 0) {
return "";
}
// 分割為句子
const sentences = splitIntoSentences(content);
if (sentences.length === 0) {
return "";
}
// 先考慮第一個句子
let title = sentences[0];
// 如果第一個句子太長,找到第一個逗號或其他分隔符截斷
if (title.length > maxLength) {
const firstPart = title.split(/[,:]/)[0];
if (firstPart && firstPart.length < maxLength) {
title = firstPart;
} else {
title = title.substring(0, maxLength - 3) + "...";
}
}
return title;
}
/**
*
*
* @param messages
* @param maxLength
* @returns
*/
export function extractSummaryFromConversation(
messages: Array<{ role: string; content: string }>,
maxLength: number = 200
): string {
// 防禦性檢查
if (!messages || messages.length === 0) {
return "";
}
// 如果只有一條消息,直接提取其摘要
if (messages.length === 1) {
return extractSummary(messages[0].content, maxLength);
}
// 連接所有消息,但保留角色信息
const combinedText = messages
.map((msg) => `${msg.role}: ${msg.content}`)
.join("\n");
// 從組合文本提取摘要
const summary = extractSummary(combinedText, maxLength);
return summary;
}