feat(core): 重构 Plan 模式工具,新增 allowedWritePaths 路径限制
Plan 工具重构: - 移除 plan_mode_respond 工具 - 新增 ask_user_question 工具:向用户提问并获取回复 - 新增 enter_plan_mode 工具:进入计划模式 - 新增 exit_plan_mode 工具:退出计划模式 allowedWritePaths 功能: - AgentFilePermission 新增 allowedWritePaths 字段 - permission-merger 添加 isPathInAllowedWritePaths 检查函数 - executor 在写入操作时检查路径限制
This commit is contained in:
@@ -15,7 +15,7 @@ import type {
|
||||
AgentExecutionResult,
|
||||
ImageData,
|
||||
} from './types.js';
|
||||
import { checkBashPermission } from './permission-merger.js';
|
||||
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
|
||||
import { getProviderRegistry } from '../provider/index.js';
|
||||
|
||||
/**
|
||||
@@ -261,6 +261,17 @@ export class AgentExecutor {
|
||||
if (action === 'deny') {
|
||||
return { allowed: false, reason: `${operation} 操作被禁止` };
|
||||
}
|
||||
|
||||
// 检查 allowedWritePaths 限制(仅对 write 操作)
|
||||
if (operation === 'write' && filePermission.allowedWritePaths) {
|
||||
const filePath = params.path as string;
|
||||
if (filePath && !isPathInAllowedWritePaths(filePath, filePermission.allowedWritePaths)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `写入路径不在允许列表中: ${filePath}。只能写入: ${filePermission.allowedWritePaths.join(', ')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@ function mergeFilePermission(
|
||||
global: AgentFilePermission | undefined,
|
||||
agent: AgentFilePermission | undefined
|
||||
): AgentFilePermission {
|
||||
// allowedWritePaths: Agent 优先,否则用 global,不继承 system
|
||||
const allowedWritePaths = agent?.allowedWritePaths ?? global?.allowedWritePaths;
|
||||
|
||||
return {
|
||||
read: mergeAction(system?.read, global?.read, agent?.read),
|
||||
write: mergeAction(system?.write, global?.write, agent?.write),
|
||||
@@ -92,6 +95,7 @@ function mergeFilePermission(
|
||||
global?.sensitivePaths,
|
||||
agent?.sensitivePaths
|
||||
),
|
||||
allowedWritePaths,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,3 +223,46 @@ export function checkFilePathPermission(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开路径中的 ~ 符号
|
||||
*/
|
||||
function expandTilde(targetPath: string): string {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
if (targetPath.startsWith('~/')) {
|
||||
return targetPath.replace('~/', homeDir + '/');
|
||||
}
|
||||
if (targetPath === '~') {
|
||||
return homeDir;
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件路径是否在允许的写入路径列表中
|
||||
* @param filePath 要检查的文件路径
|
||||
* @param allowedPaths 允许的路径列表(支持通配符和 ~)
|
||||
* @returns true 表示允许,false 表示不允许
|
||||
*/
|
||||
export function isPathInAllowedWritePaths(
|
||||
filePath: string,
|
||||
allowedPaths: string[] | undefined
|
||||
): boolean {
|
||||
// 如果没有配置 allowedWritePaths,表示不限制
|
||||
if (!allowedPaths || allowedPaths.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 展开文件路径中的 ~
|
||||
const expandedFilePath = expandTilde(filePath);
|
||||
|
||||
// 检查是否匹配任一允许的路径
|
||||
for (const allowedPath of allowedPaths) {
|
||||
const expandedAllowedPath = expandTilde(allowedPath);
|
||||
if (matchRule(expandedFilePath, expandedAllowedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ export interface AgentFilePermission {
|
||||
delete?: PermissionAction;
|
||||
/** 敏感路径规则 */
|
||||
sensitivePaths?: PermissionRule[];
|
||||
/**
|
||||
* 允许写入的路径列表(支持通配符和 ~ 展开)
|
||||
* 当 write='allow' 时,如果设置此项则只允许写入这些路径
|
||||
* 写入其他路径会被拒绝
|
||||
* 示例: ['~/.ai-terminal-assistant/plan/*']
|
||||
*/
|
||||
allowedWritePaths?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,11 @@ import {
|
||||
} from './checkpoint/index.js';
|
||||
|
||||
// Plan 工具
|
||||
import { planModeRespondTool } from './plan/index.js';
|
||||
import {
|
||||
askUserQuestionTool,
|
||||
enterPlanModeTool,
|
||||
exitPlanModeTool,
|
||||
} from './plan/index.js';
|
||||
|
||||
// 所有工具列表(用于注册)
|
||||
const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
@@ -116,8 +120,10 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
checkpointDiffTool,
|
||||
checkpointRestoreTool,
|
||||
|
||||
// Plan 工具 (deferLoading: true - 仅 Plan 模式)
|
||||
planModeRespondTool,
|
||||
// Plan 工具
|
||||
enterPlanModeTool, // deferLoading: false - Act 模式可用
|
||||
exitPlanModeTool, // deferLoading: true - 仅 Plan 模式
|
||||
askUserQuestionTool, // deferLoading: false - 通用
|
||||
];
|
||||
|
||||
// 注册所有工具到 registry
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Ask User Question Tool
|
||||
*
|
||||
* 向用户提问以澄清需求或获取决策。
|
||||
* 参考 Claude Code 的 AskUserQuestion 实现。
|
||||
*
|
||||
* 特性:
|
||||
* - 支持 1-4 个问题
|
||||
* - 每个问题支持 2-4 个选项
|
||||
* - 支持单选/多选
|
||||
* - 系统自动添加 "Other" 选项供自由输入
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
|
||||
/**
|
||||
* 选项定义
|
||||
*/
|
||||
export interface QuestionOption {
|
||||
/** 选项标签(1-5个词) */
|
||||
label: string;
|
||||
/** 选项说明 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 问题定义
|
||||
*/
|
||||
export interface Question {
|
||||
/** 问题内容(应以问号结尾) */
|
||||
question: string;
|
||||
/** 简短标签(最多12字符) */
|
||||
header?: string;
|
||||
/** 选项列表(2-4个) */
|
||||
options?: QuestionOption[];
|
||||
/** 是否允许多选,默认 false */
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具参数
|
||||
*/
|
||||
export interface AskUserQuestionParams {
|
||||
questions: Question[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证问题结构
|
||||
*/
|
||||
function validateQuestions(questions: Question[]): string | null {
|
||||
if (!Array.isArray(questions) || questions.length === 0) {
|
||||
return 'questions 必须是非空数组';
|
||||
}
|
||||
|
||||
if (questions.length > 4) {
|
||||
return '最多只能包含 4 个问题';
|
||||
}
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const q = questions[i];
|
||||
|
||||
if (!q.question || typeof q.question !== 'string') {
|
||||
return `问题 ${i + 1}: question 是必需的字符串`;
|
||||
}
|
||||
|
||||
if (q.header && q.header.length > 12) {
|
||||
return `问题 ${i + 1}: header 最多 12 个字符`;
|
||||
}
|
||||
|
||||
if (q.options) {
|
||||
if (!Array.isArray(q.options)) {
|
||||
return `问题 ${i + 1}: options 必须是数组`;
|
||||
}
|
||||
|
||||
if (q.options.length < 2 || q.options.length > 4) {
|
||||
return `问题 ${i + 1}: options 必须包含 2-4 个选项`;
|
||||
}
|
||||
|
||||
for (let j = 0; j < q.options.length; j++) {
|
||||
const opt = q.options[j];
|
||||
if (!opt.label || typeof opt.label !== 'string') {
|
||||
return `问题 ${i + 1} 选项 ${j + 1}: label 是必需的字符串`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化问题为可读输出
|
||||
*/
|
||||
function formatQuestions(questions: Question[]): string {
|
||||
const output: string[] = [];
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const q = questions[i];
|
||||
const questionNumber = questions.length > 1 ? `[${i + 1}/${questions.length}] ` : '';
|
||||
const header = q.header ? `【${q.header}】` : '';
|
||||
|
||||
output.push(`${questionNumber}${header}${q.question}`);
|
||||
|
||||
if (q.options && q.options.length > 0) {
|
||||
output.push('');
|
||||
const selectHint = q.multiSelect ? '(可多选)' : '(单选)';
|
||||
output.push(`选项 ${selectHint}:`);
|
||||
|
||||
q.options.forEach((opt, idx) => {
|
||||
const optionKey = String.fromCharCode(65 + idx); // A, B, C, D
|
||||
const description = opt.description ? ` - ${opt.description}` : '';
|
||||
output.push(` ${optionKey}. ${opt.label}${description}`);
|
||||
});
|
||||
|
||||
// 自动添加 Other 选项
|
||||
const otherKey = String.fromCharCode(65 + q.options.length);
|
||||
output.push(` ${otherKey}. Other - 自定义输入`);
|
||||
}
|
||||
|
||||
if (i < questions.length - 1) {
|
||||
output.push('');
|
||||
output.push('---');
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask User Question 工具
|
||||
*/
|
||||
export const askUserQuestionTool: ToolWithMetadata = {
|
||||
name: 'ask_user_question',
|
||||
description: `向用户提问以澄清需求或获取决策。
|
||||
|
||||
使用场景:
|
||||
- 需要用户在多个方案中做选择
|
||||
- 需要澄清不明确的需求
|
||||
- 需要用户确认关键决策
|
||||
|
||||
参数结构:
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "问题内容(以问号结尾)",
|
||||
"header": "简短标签(最多12字符,可选)",
|
||||
"options": [
|
||||
{ "label": "选项1", "description": "说明(可选)" },
|
||||
{ "label": "选项2", "description": "说明" }
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
约束:
|
||||
- questions: 1-4 个问题
|
||||
- options: 每个问题 2-4 个选项(系统自动添加 "Other" 选项)
|
||||
- header: 最多 12 字符
|
||||
- label: 建议 1-5 个词
|
||||
|
||||
示例:
|
||||
ask_user_question({
|
||||
questions: [{
|
||||
question: "您希望使用哪种状态管理方案?",
|
||||
header: "状态管理",
|
||||
options: [
|
||||
{ label: "Redux", description: "成熟稳定,生态丰富" },
|
||||
{ label: "Zustand", description: "轻量简洁,学习曲线低" },
|
||||
{ label: "Jotai", description: "原子化状态,适合细粒度更新" }
|
||||
],
|
||||
multiSelect: false
|
||||
}]
|
||||
})`,
|
||||
|
||||
metadata: {
|
||||
name: 'ask_user_question',
|
||||
category: 'agent',
|
||||
description: '向用户提问以澄清需求或获取决策',
|
||||
keywords: ['ask', 'question', 'clarify', 'decision', '提问', '澄清', '决策', '选择'],
|
||||
deferLoading: false, // 通用工具,始终可用
|
||||
},
|
||||
|
||||
parameters: {
|
||||
questions: {
|
||||
type: 'array',
|
||||
description: '问题列表(1-4 个问题)',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
||||
const questions = params.questions as Question[];
|
||||
|
||||
// 验证参数
|
||||
const validationError = validateQuestions(questions);
|
||||
if (validationError) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: validationError,
|
||||
};
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
const formattedOutput = formatQuestions(questions);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: formattedOutput,
|
||||
metadata: {
|
||||
type: 'ask_user_question',
|
||||
questionCount: questions.length,
|
||||
questions: questions.map((q, i) => ({
|
||||
index: i,
|
||||
header: q.header,
|
||||
optionCount: q.options?.length ?? 0,
|
||||
multiSelect: q.multiSelect ?? false,
|
||||
})),
|
||||
// 标记需要用户输入
|
||||
requiresUserInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Enter Plan Mode Tool
|
||||
*
|
||||
* 进入计划模式的工具。
|
||||
* 参考 Claude Code 的 EnterPlanMode 实现。
|
||||
*
|
||||
* 在开始非平凡的实现任务前主动使用,让用户在编写代码之前先批准方案。
|
||||
*
|
||||
* 使用场景:
|
||||
* - 新功能实现
|
||||
* - 多种可行方案需要选择
|
||||
* - 代码修改影响范围较大
|
||||
* - 架构决策
|
||||
* - 多文件变更
|
||||
* - 需求不明确需要探索
|
||||
*
|
||||
* 不适用场景:
|
||||
* - 单行修复(错别字、明显 bug)
|
||||
* - 用户给出了非常具体详细的指令
|
||||
* - 纯研究/探索任务
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
|
||||
/**
|
||||
* Enter Plan Mode 工具
|
||||
*/
|
||||
export const enterPlanModeTool: ToolWithMetadata = {
|
||||
name: 'enter_plan_mode',
|
||||
description: `进入计划模式,在开始非平凡的实现任务前主动使用。
|
||||
|
||||
使用场景:
|
||||
- 新功能实现:如 "添加登出按钮"、"实现用户认证"
|
||||
- 多种可行方案:如 "给 API 添加缓存"(Redis/内存/文件)
|
||||
- 代码修改:如 "更新登录流程"、"重构组件"
|
||||
- 架构决策:如 "添加实时更新功能"(WebSocket/SSE/轮询)
|
||||
- 多文件变更:如 "重构认证系统"
|
||||
- 需求不明确:如 "让应用更快"、"修复 checkout 的 bug"
|
||||
|
||||
何时不使用:
|
||||
- 单行修复(错别字、明显 bug、小调整)
|
||||
- 用户给出了非常具体详细的指令
|
||||
- 纯研究/探索任务(使用 Task 工具的 explore agent)
|
||||
|
||||
计划模式中的限制:
|
||||
进入后只能使用只读工具:
|
||||
✅ read_file, list_directory, search_files, grep_content
|
||||
✅ web_search, web_extract
|
||||
✅ task(探索代理)
|
||||
✅ ask_user_question
|
||||
❌ write_file, edit_file, bash(执行命令)
|
||||
|
||||
完成计划后使用 exit_plan_mode 退出并提交方案供用户审批。`,
|
||||
|
||||
metadata: {
|
||||
name: 'enter_plan_mode',
|
||||
category: 'agent',
|
||||
description: '进入计划模式,用于规划实现方案',
|
||||
keywords: ['plan', 'mode', 'enter', '计划', '模式', '规划', '方案'],
|
||||
deferLoading: false, // 始终可用
|
||||
},
|
||||
|
||||
parameters: {
|
||||
// 无参数
|
||||
},
|
||||
|
||||
async execute(_params: Record<string, unknown>): Promise<ToolResult> {
|
||||
return {
|
||||
success: true,
|
||||
output: '已进入计划模式。现在可以使用只读工具探索代码库,完成后使用 exit_plan_mode 提交方案。',
|
||||
metadata: {
|
||||
type: 'enter_plan_mode',
|
||||
// 标记模式切换
|
||||
modeTransition: {
|
||||
from: 'act',
|
||||
to: 'plan',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Exit Plan Mode Tool
|
||||
*
|
||||
* 退出计划模式,将方案提交给用户审批。
|
||||
* 参考 Claude Code 的 ExitPlanMode 实现。
|
||||
*
|
||||
* 使用条件:
|
||||
* 1. 已将计划写入计划文件
|
||||
* 2. 计划清晰无歧义
|
||||
* 3. 如有多种方案或不明确的需求,先用 AskUserQuestion 澄清
|
||||
*
|
||||
* 可选参数:
|
||||
* - launchSwarm: 是否启动多代理协作来实施方案
|
||||
* - teammateCount: 协作代理的数量
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
|
||||
/**
|
||||
* 工具参数
|
||||
*/
|
||||
export interface ExitPlanModeParams {
|
||||
/** 是否启动 swarm 来实施方案 */
|
||||
launchSwarm?: boolean;
|
||||
/** swarm 中的队友数量 */
|
||||
teammateCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit Plan Mode 工具
|
||||
*/
|
||||
export const exitPlanModeTool: ToolWithMetadata = {
|
||||
name: 'exit_plan_mode',
|
||||
description: `退出计划模式,将方案提交给用户审批。
|
||||
|
||||
使用条件(调用前必须满足):
|
||||
1. 已完成方案设计
|
||||
2. 计划清晰无歧义
|
||||
3. 如有多种方案或不明确的需求,先用 ask_user_question 澄清
|
||||
|
||||
何时使用:
|
||||
- 当任务需要规划实现步骤并编写代码时使用
|
||||
- 纯研究/探索任务不要使用此工具
|
||||
|
||||
参数说明:
|
||||
- launchSwarm (可选): 是否启动多代理协作来实施方案
|
||||
- teammateCount (可选): 协作代理的数量(需要 launchSwarm=true)
|
||||
|
||||
示例:
|
||||
1. 简单退出(用户手动执行):
|
||||
exit_plan_mode({})
|
||||
|
||||
2. 启动 swarm 协作实施:
|
||||
exit_plan_mode({
|
||||
launchSwarm: true,
|
||||
teammateCount: 3
|
||||
})`,
|
||||
|
||||
metadata: {
|
||||
name: 'exit_plan_mode',
|
||||
category: 'agent',
|
||||
description: '退出计划模式,提交方案供用户审批',
|
||||
keywords: ['plan', 'mode', 'exit', 'submit', '计划', '模式', '退出', '提交', '审批'],
|
||||
deferLoading: true, // 仅 Plan 模式下可用
|
||||
},
|
||||
|
||||
parameters: {
|
||||
launchSwarm: {
|
||||
type: 'boolean',
|
||||
description: '是否启动多代理协作来实施方案',
|
||||
required: false,
|
||||
},
|
||||
teammateCount: {
|
||||
type: 'number',
|
||||
description: '协作代理的数量(需要 launchSwarm=true)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
||||
const launchSwarm = params.launchSwarm as boolean | undefined;
|
||||
const teammateCount = params.teammateCount as number | undefined;
|
||||
|
||||
// 验证参数
|
||||
if (teammateCount !== undefined) {
|
||||
if (typeof teammateCount !== 'number' || teammateCount < 1) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'teammateCount 必须是大于 0 的数字',
|
||||
};
|
||||
}
|
||||
if (!launchSwarm) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '使用 teammateCount 时需要设置 launchSwarm: true',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 构建输出
|
||||
const output: string[] = ['计划模式已结束,方案已提交审批。'];
|
||||
|
||||
if (launchSwarm) {
|
||||
const count = teammateCount ?? 2;
|
||||
output.push('');
|
||||
output.push(`配置: 启动 Swarm 协作模式,${count} 个代理将并行实施方案。`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output.join('\n'),
|
||||
metadata: {
|
||||
type: 'exit_plan_mode',
|
||||
// 标记模式切换
|
||||
modeTransition: {
|
||||
from: 'plan',
|
||||
to: 'act',
|
||||
},
|
||||
// Swarm 配置
|
||||
swarm: launchSwarm
|
||||
? {
|
||||
enabled: true,
|
||||
teammateCount: teammateCount ?? 2,
|
||||
}
|
||||
: {
|
||||
enabled: false,
|
||||
},
|
||||
// 标记需要用户审批
|
||||
requiresApproval: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -4,4 +4,8 @@
|
||||
* Plan 模式专用工具集合
|
||||
*/
|
||||
|
||||
export { planModeRespondTool } from './plan_mode_respond.js';
|
||||
export { askUserQuestionTool } from './ask_user_question.js';
|
||||
export { enterPlanModeTool } from './enter_plan_mode.js';
|
||||
export { exitPlanModeTool } from './exit_plan_mode.js';
|
||||
export type { Question, QuestionOption, AskUserQuestionParams } from './ask_user_question.js';
|
||||
export type { ExitPlanModeParams } from './exit_plan_mode.js';
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Plan Mode Respond Tool
|
||||
*
|
||||
* Plan 模式专用响应工具,用于结构化输出计划和进度信息。
|
||||
* 参考 OpenCode 的 plan_mode_respond 实现。
|
||||
*
|
||||
* 特性:
|
||||
* - response: 输出计划内容
|
||||
* - needs_more_exploration: 标记是否需要更多探索
|
||||
* - task_progress: 报告任务进度 (0-100)
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
|
||||
/**
|
||||
* 生成进度条字符串
|
||||
*/
|
||||
function generateProgressBar(progress: number): string {
|
||||
const total = 20;
|
||||
const filled = Math.round((progress / 100) * total);
|
||||
const empty = total - filled;
|
||||
return '[' + '='.repeat(filled) + ' '.repeat(empty) + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan Mode 响应工具
|
||||
*/
|
||||
export const planModeRespondTool: ToolWithMetadata = {
|
||||
name: 'plan_mode_respond',
|
||||
description: `Plan 模式专用响应工具。使用此工具输出结构化的计划和进度信息。
|
||||
|
||||
使用场景:
|
||||
- 完成探索后输出实现计划
|
||||
- 报告需要更多探索(设置 needs_more_exploration: true)
|
||||
- 更新任务进度
|
||||
|
||||
参数说明:
|
||||
- response (必需): 计划内容、分析结果或回复
|
||||
- needs_more_exploration (可选): 是否需要继续探索。设为 true 表示当前信息不足
|
||||
- task_progress (可选): 任务完成进度 (0-100)。0=刚开始,50=进行中,100=完成
|
||||
|
||||
示例:
|
||||
1. 输出初步分析:
|
||||
plan_mode_respond({
|
||||
response: "## 现状分析\\n代码结构如下...",
|
||||
needs_more_exploration: true,
|
||||
task_progress: 30
|
||||
})
|
||||
|
||||
2. 输出最终计划:
|
||||
plan_mode_respond({
|
||||
response: "## 实现方案\\n### 步骤 1: ...",
|
||||
needs_more_exploration: false,
|
||||
task_progress: 100
|
||||
})`,
|
||||
|
||||
metadata: {
|
||||
name: 'plan_mode_respond',
|
||||
category: 'agent',
|
||||
description: 'Plan 模式结构化响应工具',
|
||||
keywords: ['plan', 'respond', 'response', 'progress', '计划', '响应', '进度'],
|
||||
deferLoading: true, // 仅在 Plan 模式下加载
|
||||
},
|
||||
|
||||
parameters: {
|
||||
response: {
|
||||
type: 'string',
|
||||
description: '计划内容、分析结果或回复',
|
||||
required: true,
|
||||
},
|
||||
needs_more_exploration: {
|
||||
type: 'boolean',
|
||||
description: '是否需要更多探索。设为 true 表示当前信息不足,需要继续调研',
|
||||
required: false,
|
||||
},
|
||||
task_progress: {
|
||||
type: 'number',
|
||||
description: '任务完成进度 (0-100)。0=刚开始,50=进行中,100=完成',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
||||
const {
|
||||
response,
|
||||
needs_more_exploration = false,
|
||||
task_progress,
|
||||
} = params as {
|
||||
response: string;
|
||||
needs_more_exploration?: boolean;
|
||||
task_progress?: number;
|
||||
};
|
||||
|
||||
// 验证参数
|
||||
if (!response || typeof response !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'response 参数是必需的,且必须是字符串',
|
||||
};
|
||||
}
|
||||
|
||||
if (task_progress !== undefined) {
|
||||
if (typeof task_progress !== 'number' || task_progress < 0 || task_progress > 100) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'task_progress 必须是 0-100 之间的数字',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 构建结构化输出
|
||||
const output: string[] = [];
|
||||
|
||||
// 添加进度信息
|
||||
if (task_progress !== undefined) {
|
||||
const progressBar = generateProgressBar(task_progress);
|
||||
output.push(`[进度: ${task_progress}%] ${progressBar}`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// 添加响应内容
|
||||
output.push(response);
|
||||
|
||||
// 添加探索状态
|
||||
if (needs_more_exploration) {
|
||||
output.push('');
|
||||
output.push('---');
|
||||
output.push('[状态: 需要更多探索]');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output.join('\n'),
|
||||
metadata: {
|
||||
needs_more_exploration,
|
||||
task_progress,
|
||||
type: 'plan_response',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user