/** * 提示词模板渲染器 * * 支持的语法: * - ${variable} - 简单变量替换 * - ${obj.prop} - 嵌套属性访问 * - ${condition ? "trueValue" : "falseValue"} - 条件表达式 */ import type { PromptContext, PromptTemplate, RenderOptions, ToolNameMapping } from './types.js'; import * as fs from 'fs'; import * as path from 'path'; /** * 获取嵌套属性值 */ function getNestedValue(obj: Record, pathStr: string): unknown { const parts = pathStr.split('.'); let current: unknown = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } if (typeof current === 'object') { current = (current as Record)[part]; } else { return undefined; } } return current; } /** * 安全的表达式求值器 * 支持简单的属性访问和三元运算符 */ function evaluateExpression( expr: string, context: PromptContext, options: RenderOptions = {} ): string { // 移除首尾空格 expr = expr.trim(); // 处理三元运算符: condition ? trueValue : falseValue // 需要处理嵌套的情况,使用更精确的匹配 const ternaryMatch = findTernaryOperator(expr); if (ternaryMatch) { const { condition, trueValue, falseValue } = ternaryMatch; const conditionResult = evaluateExpression(condition, context, options); // 将结果转换为布尔值 const isTruthy = conditionResult !== '' && conditionResult !== 'false' && conditionResult !== 'undefined' && conditionResult !== 'null' && conditionResult !== '0'; return isTruthy ? evaluateExpression(trueValue, context, options) : evaluateExpression(falseValue, context, options); } // 处理字符串字面量: "string" 或 'string' 或 `string` const stringMatch = expr.match(/^["'`]([\s\S]*)["'`]$/); if (stringMatch) { // 递归处理字符串中的变量插值 return renderTemplate(stringMatch[1], context, options); } // 处理布尔字面量 if (expr === 'true') return 'true'; if (expr === 'false') return ''; // 处理属性访问: obj.prop.subprop const value = getNestedValue(context as unknown as Record, expr); if (value === undefined) { if (options.throwOnUndefined) { throw new Error(`Undefined variable: ${expr}`); } if (options.debug) { console.warn(`[PromptTemplate] Undefined variable: ${expr}`); } return options.undefinedValue ?? ''; } // 布尔值转换 if (typeof value === 'boolean') { return value ? 'true' : ''; } return String(value); } /** * 查找三元运算符的各个部分 * 处理嵌套引号的情况 */ function findTernaryOperator( expr: string ): { condition: string; trueValue: string; falseValue: string } | null { let depth = 0; let inString: string | null = null; let questionPos = -1; let colonPos = -1; for (let i = 0; i < expr.length; i++) { const char = expr[i]; const prevChar = i > 0 ? expr[i - 1] : ''; // 处理字符串边界 if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') { if (inString === null) { inString = char; } else if (inString === char) { inString = null; } continue; } // 如果在字符串中,跳过 if (inString !== null) continue; // 处理括号深度 if (char === '(' || char === '[' || char === '{') { depth++; } else if (char === ')' || char === ']' || char === '}') { depth--; } // 只在顶层寻找 ? 和 : if (depth === 0) { if (char === '?' && questionPos === -1) { questionPos = i; } else if (char === ':' && questionPos !== -1 && colonPos === -1) { colonPos = i; } } } if (questionPos === -1 || colonPos === -1) { return null; } return { condition: expr.substring(0, questionPos).trim(), trueValue: expr.substring(questionPos + 1, colonPos).trim(), falseValue: expr.substring(colonPos + 1).trim(), }; } /** * 渲染提示词模板 * * @param template 模板字符串 * @param context 上下文变量 * @param options 渲染选项 * @returns 渲染后的字符串 */ export function renderTemplate( template: string, context: PromptContext, options: RenderOptions = {} ): string { // 匹配 ${...} 模式,支持嵌套括号和字符串 const result: string[] = []; let i = 0; while (i < template.length) { // 查找 ${ const start = template.indexOf('${', i); if (start === -1) { result.push(template.substring(i)); break; } // 添加 ${ 之前的文本 result.push(template.substring(i, start)); // 查找匹配的 } let depth = 1; let j = start + 2; let inString: string | null = null; while (j < template.length && depth > 0) { const char = template[j]; const prevChar = j > 0 ? template[j - 1] : ''; // 处理字符串边界 if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') { if (inString === null) { inString = char; } else if (inString === char) { inString = null; } } else if (inString === null) { if (char === '{') depth++; else if (char === '}') depth--; } j++; } if (depth !== 0) { // 未找到匹配的 },保留原文 result.push(template.substring(start, j)); } else { // 提取表达式并求值 const expression = template.substring(start + 2, j - 1); try { const value = evaluateExpression(expression, context, options); result.push(value); } catch (error) { if (options.debug) { console.warn(`[PromptTemplate] Expression evaluation failed: ${expression}`, error); } result.push(`\${${expression}}`); // 保留原始文本 } } i = j; } return result.join(''); } /** * 渲染 PromptTemplate 对象 */ export function render( promptTemplate: PromptTemplate, context: PromptContext, options?: RenderOptions ): string { return renderTemplate(promptTemplate.template, context, options); } /** * 默认工具名称映射 */ export const DEFAULT_TOOL_NAMES: ToolNameMapping = { glob: 'glob', grep: 'grep_content', read: 'read_file', write: 'write_file', edit: 'edit_file', bash: 'bash', askUserQuestion: 'ask_user_question', exitPlanMode: 'exit_plan_mode', enterPlanMode: 'enter_plan_mode', task: 'task', todoRead: 'todoread', todoWrite: 'todowrite', webSearch: 'web_search', webExtract: 'web_extract', gitStatus: 'git_status', gitDiff: 'git_diff', gitLog: 'git_log', gitBranch: 'git_branch', repoMap: 'repo_map', }; /** * 创建默认的提示词上下文 */ export function createDefaultContext(overrides?: Partial): PromptContext { const homeDir = process.env.HOME || process.env.USERPROFILE || ''; const workdir = process.cwd(); return { tools: { ...DEFAULT_TOOL_NAMES, ...overrides?.tools, }, plan: { isActive: false, planExists: false, planFilePath: path.join(homeDir, '.ai-terminal-assistant', 'plan', 'plan.md'), allowedWritePaths: [path.join(homeDir, '.ai-terminal-assistant', 'plan', '*')], ...overrides?.plan, }, env: { workdir, isGitRepo: fs.existsSync(path.join(workdir, '.git')), platform: process.platform, date: new Date().toISOString().split('T')[0], homeDir, ...overrides?.env, }, agent: { name: 'build', mode: 'primary', isSubagent: false, ...overrides?.agent, }, custom: overrides?.custom, }; } /** * 检查计划文件是否存在 */ export function checkPlanFileExists(planFilePath: string): boolean { try { fs.accessSync(planFilePath); return true; } catch { return false; } } /** * 创建 Plan 模式上下文 */ export function createPlanContext(options: { planFilePath?: string; workdir?: string; isSubagent?: boolean; }): PromptContext { const homeDir = process.env.HOME || process.env.USERPROFILE || ''; const planPath = options.planFilePath || path.join(homeDir, '.ai-terminal-assistant', 'plan', 'plan.md'); const workdir = options.workdir || process.cwd(); return createDefaultContext({ plan: { isActive: true, planExists: checkPlanFileExists(planPath), planFilePath: planPath, allowedWritePaths: [path.join(homeDir, '.ai-terminal-assistant', 'plan', '*')], }, env: { workdir, isGitRepo: fs.existsSync(path.join(workdir, '.git')), platform: process.platform, date: new Date().toISOString().split('T')[0], homeDir, }, agent: { name: 'plan', mode: options.isSubagent ? 'subagent' : 'primary', isSubagent: options.isSubagent || false, }, }); }