refactor(core): 统一模板引擎到 src/template/ 目录
- 将 agent/prompt-template/ 目录合并到 src/template/
- 新增通用模板函数 renderTemplate、render
- 新增 Agent 特定函数 renderPromptTemplate、renderPrompt
- 新增 createToolDescriptionContext 支持工具描述模板变量
- 支持 ${GREP_TOOL_NAME} 等 Claude Code 风格变量
- 更新所有相关导入路径
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 通用模板引擎
|
||||
*
|
||||
* 提供动态模板渲染能力,支持:
|
||||
* - ${variable} 变量替换
|
||||
* - ${obj.prop} 嵌套属性访问
|
||||
* - ${condition ? "trueValue" : "falseValue"} 条件表达式
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { renderTemplate } from '@ai-assistant/core/template';
|
||||
*
|
||||
* const context = { name: 'World', count: 42 };
|
||||
* const result = renderTemplate('Hello ${name}, count is ${count}', context);
|
||||
* // => "Hello World, count is 42"
|
||||
* ```
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
TemplateContext,
|
||||
Template,
|
||||
RenderOptions,
|
||||
// Agent 特定类型
|
||||
PromptContext,
|
||||
PromptTemplate,
|
||||
ToolNameMapping,
|
||||
PlanModeContext,
|
||||
EnvContext,
|
||||
AgentContext,
|
||||
} from './types.js';
|
||||
|
||||
// 渲染器导出
|
||||
export {
|
||||
// 通用模板渲染
|
||||
renderTemplate,
|
||||
render,
|
||||
// Agent 特定函数
|
||||
renderPromptTemplate,
|
||||
renderPrompt,
|
||||
createDefaultContext,
|
||||
createPlanContext,
|
||||
createToolDescriptionContext,
|
||||
checkPlanFileExists,
|
||||
DEFAULT_TOOL_NAMES,
|
||||
} from './renderer.js';
|
||||
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 通用模板渲染器
|
||||
*
|
||||
* 支持的语法:
|
||||
* - ${variable} - 简单变量替换
|
||||
* - ${obj.prop} - 嵌套属性访问
|
||||
* - ${condition ? "trueValue" : "falseValue"} - 条件表达式
|
||||
*/
|
||||
|
||||
import type {
|
||||
TemplateContext,
|
||||
Template,
|
||||
RenderOptions,
|
||||
PromptContext,
|
||||
PromptTemplate,
|
||||
ToolNameMapping,
|
||||
} from './types.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 获取嵌套属性值
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, 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<string, unknown>)[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找三元运算符的各个部分
|
||||
* 处理嵌套引号的情况
|
||||
*/
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的表达式求值器
|
||||
* 支持简单的属性访问和三元运算符
|
||||
*/
|
||||
function evaluateExpression(
|
||||
expr: string,
|
||||
context: TemplateContext,
|
||||
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
|
||||
let value = getNestedValue(context, expr);
|
||||
|
||||
// 如果在主上下文中找不到,且是单层变量名,尝试直接访问
|
||||
// 这支持在 context 根级别定义的变量(如 GREP_TOOL_NAME)
|
||||
if (value === undefined && !expr.includes('.')) {
|
||||
value = context[expr];
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
if (options.throwOnUndefined) {
|
||||
throw new Error(`Undefined variable: ${expr}`);
|
||||
}
|
||||
if (options.debug) {
|
||||
console.warn(`[Template] Undefined variable: ${expr}`);
|
||||
}
|
||||
return options.undefinedValue ?? '';
|
||||
}
|
||||
|
||||
// 布尔值转换
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'true' : '';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板
|
||||
*
|
||||
* @param template 模板字符串
|
||||
* @param context 上下文变量
|
||||
* @param options 渲染选项
|
||||
* @returns 渲染后的字符串
|
||||
*/
|
||||
export function renderTemplate(
|
||||
template: string,
|
||||
context: TemplateContext,
|
||||
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(`[Template] Expression evaluation failed: ${expression}`, error);
|
||||
}
|
||||
result.push(`\${${expression}}`); // 保留原始文本
|
||||
}
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 Template 对象
|
||||
*/
|
||||
export function render(templateObj: Template, context: TemplateContext, options?: RenderOptions): string {
|
||||
return renderTemplate(templateObj.template, context, options);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Agent 特定函数(Prompt Template)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 渲染提示词模板(Agent 特定版本)
|
||||
* 将 custom 中的变量合并到顶层,以支持直接引用(如 ${GREP_TOOL_NAME})
|
||||
*/
|
||||
export function renderPromptTemplate(
|
||||
template: string,
|
||||
context: PromptContext,
|
||||
options: RenderOptions = {}
|
||||
): string {
|
||||
// 将 custom 中的变量合并到顶层上下文,这样 ${GREP_TOOL_NAME} 可以直接访问
|
||||
const flattenedContext: Record<string, unknown> = {
|
||||
...context,
|
||||
...context.custom,
|
||||
};
|
||||
return renderTemplate(template, flattenedContext, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染提示词模板对象(Agent 特定版本)
|
||||
*/
|
||||
export function renderPrompt(
|
||||
promptTemplate: PromptTemplate,
|
||||
context: PromptContext,
|
||||
options?: RenderOptions
|
||||
): string {
|
||||
// 将 custom 中的变量合并到顶层上下文
|
||||
const flattenedContext: Record<string, unknown> = {
|
||||
...context,
|
||||
...context.custom,
|
||||
};
|
||||
return render({ ...promptTemplate }, flattenedContext, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认工具名称映射
|
||||
*/
|
||||
export const DEFAULT_TOOL_NAMES: ToolNameMapping = {
|
||||
glob: 'glob',
|
||||
grep: 'grep',
|
||||
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>): 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将驼峰命名转换为大写下划线命名
|
||||
* 例如: askUserQuestion -> ASK_USER_QUESTION
|
||||
*/
|
||||
function camelToUpperSnake(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具描述渲染上下文
|
||||
*
|
||||
* 除了标准的 PromptContext 变量外,还会在 custom 中添加大写形式的工具名变量,
|
||||
* 以兼容 Claude Code 官方提示词格式(如 ${GREP_TOOL_NAME})。
|
||||
*
|
||||
* 支持的变量格式:
|
||||
* - ${tools.grep} - 小写驼峰形式(项目内部风格)
|
||||
* - ${GREP_TOOL_NAME} - 大写下划线形式(Claude Code 官方风格)
|
||||
*/
|
||||
export function createToolDescriptionContext(overrides?: Partial<PromptContext>): PromptContext {
|
||||
const base = createDefaultContext(overrides);
|
||||
|
||||
// 将 tools 映射转为大写变量放入 custom
|
||||
const toolVars: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(base.tools)) {
|
||||
// glob -> GLOB_TOOL_NAME
|
||||
// askUserQuestion -> ASK_USER_QUESTION_TOOL_NAME
|
||||
const upperKey = camelToUpperSnake(key) + '_TOOL_NAME';
|
||||
toolVars[upperKey] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
custom: { ...base.custom, ...toolVars },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 通用模板引擎 - 类型定义
|
||||
*
|
||||
* 支持动态变量替换和条件表达式的模板系统
|
||||
*/
|
||||
|
||||
/**
|
||||
* 模板上下文 - 运行时可用的变量
|
||||
*
|
||||
* 支持任意嵌套的键值对结构
|
||||
*/
|
||||
export type TemplateContext = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 模板定义
|
||||
*/
|
||||
export interface Template {
|
||||
/** 模板名称 */
|
||||
name: string;
|
||||
/** 模板描述 */
|
||||
description?: string;
|
||||
/** 模板内容(支持变量插值) */
|
||||
template: string;
|
||||
/** 所需变量列表(用于验证) */
|
||||
requiredVariables?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板渲染选项
|
||||
*/
|
||||
export interface RenderOptions {
|
||||
/** 遇到未定义变量时是否抛出错误 */
|
||||
throwOnUndefined?: boolean;
|
||||
/** 未定义变量的默认值 */
|
||||
undefinedValue?: string;
|
||||
/** 是否启用调试日志 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Agent 特定类型(Prompt Template)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 工具名称映射
|
||||
* 用于在模板中引用工具名称,便于重命名
|
||||
*/
|
||||
export interface ToolNameMapping {
|
||||
glob: string;
|
||||
grep: string;
|
||||
read: string;
|
||||
write: string;
|
||||
edit: string;
|
||||
bash: string;
|
||||
askUserQuestion: string;
|
||||
exitPlanMode: string;
|
||||
enterPlanMode: string;
|
||||
task: string;
|
||||
todoRead: string;
|
||||
todoWrite: string;
|
||||
webSearch: string;
|
||||
webExtract: string;
|
||||
gitStatus: string;
|
||||
gitDiff: string;
|
||||
gitLog: string;
|
||||
gitBranch: string;
|
||||
repoMap: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan 模式上下文
|
||||
*/
|
||||
export interface PlanModeContext {
|
||||
/** 是否处于 Plan 模式 */
|
||||
isActive: boolean;
|
||||
/** 计划文件是否存在 */
|
||||
planExists: boolean;
|
||||
/** 计划文件路径 */
|
||||
planFilePath: string;
|
||||
/** 允许写入的路径列表 */
|
||||
allowedWritePaths: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境信息上下文
|
||||
*/
|
||||
export interface EnvContext {
|
||||
/** 工作目录 */
|
||||
workdir: string;
|
||||
/** 是否是 Git 仓库 */
|
||||
isGitRepo: boolean;
|
||||
/** 操作系统平台 */
|
||||
platform: string;
|
||||
/** 当前日期 */
|
||||
date: string;
|
||||
/** 主目录 */
|
||||
homeDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 信息上下文
|
||||
*/
|
||||
export interface AgentContext {
|
||||
/** Agent 名称 */
|
||||
name: string;
|
||||
/** Agent 模式 */
|
||||
mode: 'primary' | 'subagent' | 'all' | 'internal';
|
||||
/** 是否是子代理 */
|
||||
isSubagent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示词模板上下文 - 运行时可用的变量
|
||||
*/
|
||||
export interface PromptContext {
|
||||
/** 工具名称映射 */
|
||||
tools: ToolNameMapping;
|
||||
|
||||
/** Plan 模式相关 */
|
||||
plan: PlanModeContext;
|
||||
|
||||
/** 环境信息 */
|
||||
env: EnvContext;
|
||||
|
||||
/** Agent 信息 */
|
||||
agent: AgentContext;
|
||||
|
||||
/** 自定义扩展变量 */
|
||||
custom?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示词模板定义(继承自通用 Template)
|
||||
*/
|
||||
export interface PromptTemplate extends Template {}
|
||||
Reference in New Issue
Block a user