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,
|
AgentExecutionResult,
|
||||||
ImageData,
|
ImageData,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { checkBashPermission } from './permission-merger.js';
|
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
|
||||||
import { getProviderRegistry } from '../provider/index.js';
|
import { getProviderRegistry } from '../provider/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,6 +261,17 @@ export class AgentExecutor {
|
|||||||
if (action === 'deny') {
|
if (action === 'deny') {
|
||||||
return { allowed: false, reason: `${operation} 操作被禁止` };
|
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,
|
global: AgentFilePermission | undefined,
|
||||||
agent: AgentFilePermission | undefined
|
agent: AgentFilePermission | undefined
|
||||||
): AgentFilePermission {
|
): AgentFilePermission {
|
||||||
|
// allowedWritePaths: Agent 优先,否则用 global,不继承 system
|
||||||
|
const allowedWritePaths = agent?.allowedWritePaths ?? global?.allowedWritePaths;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
read: mergeAction(system?.read, global?.read, agent?.read),
|
read: mergeAction(system?.read, global?.read, agent?.read),
|
||||||
write: mergeAction(system?.write, global?.write, agent?.write),
|
write: mergeAction(system?.write, global?.write, agent?.write),
|
||||||
@@ -92,6 +95,7 @@ function mergeFilePermission(
|
|||||||
global?.sensitivePaths,
|
global?.sensitivePaths,
|
||||||
agent?.sensitivePaths
|
agent?.sensitivePaths
|
||||||
),
|
),
|
||||||
|
allowedWritePaths,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,3 +223,46 @@ export function checkFilePathPermission(
|
|||||||
|
|
||||||
return null;
|
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;
|
delete?: PermissionAction;
|
||||||
/** 敏感路径规则 */
|
/** 敏感路径规则 */
|
||||||
sensitivePaths?: PermissionRule[];
|
sensitivePaths?: PermissionRule[];
|
||||||
|
/**
|
||||||
|
* 允许写入的路径列表(支持通配符和 ~ 展开)
|
||||||
|
* 当 write='allow' 时,如果设置此项则只允许写入这些路径
|
||||||
|
* 写入其他路径会被拒绝
|
||||||
|
* 示例: ['~/.ai-terminal-assistant/plan/*']
|
||||||
|
*/
|
||||||
|
allowedWritePaths?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ import {
|
|||||||
} from './checkpoint/index.js';
|
} from './checkpoint/index.js';
|
||||||
|
|
||||||
// Plan 工具
|
// Plan 工具
|
||||||
import { planModeRespondTool } from './plan/index.js';
|
import {
|
||||||
|
askUserQuestionTool,
|
||||||
|
enterPlanModeTool,
|
||||||
|
exitPlanModeTool,
|
||||||
|
} from './plan/index.js';
|
||||||
|
|
||||||
// 所有工具列表(用于注册)
|
// 所有工具列表(用于注册)
|
||||||
const allToolsWithMetadata: ToolWithMetadata[] = [
|
const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||||
@@ -116,8 +120,10 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
|||||||
checkpointDiffTool,
|
checkpointDiffTool,
|
||||||
checkpointRestoreTool,
|
checkpointRestoreTool,
|
||||||
|
|
||||||
// Plan 工具 (deferLoading: true - 仅 Plan 模式)
|
// Plan 工具
|
||||||
planModeRespondTool,
|
enterPlanModeTool, // deferLoading: false - Act 模式可用
|
||||||
|
exitPlanModeTool, // deferLoading: true - 仅 Plan 模式
|
||||||
|
askUserQuestionTool, // deferLoading: false - 通用
|
||||||
];
|
];
|
||||||
|
|
||||||
// 注册所有工具到 registry
|
// 注册所有工具到 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 模式专用工具集合
|
* 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