pulse-coder-engine 0.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/new-engine-usage.ts +52 -0
- package/package.json +54 -0
- package/src/Engine.ts +150 -0
- package/src/ai/index.ts +116 -0
- package/src/built-in/index.ts +27 -0
- package/src/built-in/mcp-plugin/index.ts +104 -0
- package/src/built-in/skills-plugin/index.ts +223 -0
- package/src/built-in/sub-agent-plugin/index.ts +203 -0
- package/src/config/index.ts +35 -0
- package/src/context/index.ts +134 -0
- package/src/core/loop.ts +147 -0
- package/src/index.ts +17 -0
- package/src/plugin/EnginePlugin.ts +60 -0
- package/src/plugin/PluginManager.ts +426 -0
- package/src/plugin/UserConfigPlugin.ts +183 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/system.ts +126 -0
- package/src/shared/types.ts +50 -0
- package/src/tools/bash.ts +59 -0
- package/src/tools/clarify.ts +74 -0
- package/src/tools/edit.ts +79 -0
- package/src/tools/grep.ts +148 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/ls.ts +20 -0
- package/src/tools/read.ts +69 -0
- package/src/tools/tavily.ts +55 -0
- package/src/tools/utils.ts +16 -0
- package/src/tools/write.ts +42 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
package/src/core/loop.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ToolSet, type StepResult, type ModelMessage } from "ai";
|
|
2
|
+
import type { Context, ClarificationRequest, Tool } from "../shared/types";
|
|
3
|
+
import { streamTextAI } from "../ai";
|
|
4
|
+
import { maybeCompactContext } from "../context";
|
|
5
|
+
import {
|
|
6
|
+
MAX_COMPACTION_ATTEMPTS,
|
|
7
|
+
MAX_ERROR_COUNT,
|
|
8
|
+
MAX_STEPS
|
|
9
|
+
} from "../config/index.js";
|
|
10
|
+
|
|
11
|
+
export interface LoopOptions {
|
|
12
|
+
onText?: (delta: string) => void;
|
|
13
|
+
onToolCall?: (toolCall: any) => void;
|
|
14
|
+
onToolResult?: (toolResult: any) => void;
|
|
15
|
+
onStepFinish?: (step: StepResult<any>) => void;
|
|
16
|
+
onClarificationRequest?: (request: ClarificationRequest) => Promise<string>;
|
|
17
|
+
onCompacted?: (newMessages: ModelMessage[]) => void;
|
|
18
|
+
onResponse?: (messages: StepResult<ToolSet>['response']['messages']) => void;
|
|
19
|
+
abortSignal?: AbortSignal;
|
|
20
|
+
|
|
21
|
+
tools?: Record<string, Tool>; // 允许传入工具覆盖默认工具
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loop(context: Context, options?: LoopOptions): Promise<string> {
|
|
25
|
+
let errorCount = 0;
|
|
26
|
+
let totalSteps = 0;
|
|
27
|
+
let compactionAttempts = 0;
|
|
28
|
+
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
if (compactionAttempts < MAX_COMPACTION_ATTEMPTS) {
|
|
32
|
+
const { didCompact, newMessages } = await maybeCompactContext(context);
|
|
33
|
+
if (didCompact) {
|
|
34
|
+
compactionAttempts++;
|
|
35
|
+
|
|
36
|
+
if (newMessages) {
|
|
37
|
+
options?.onCompacted?.(newMessages)
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tools = options?.tools || {}; // 允许传入工具覆盖默认工具
|
|
44
|
+
|
|
45
|
+
// Prepare tool execution context
|
|
46
|
+
const toolExecutionContext = {
|
|
47
|
+
onClarificationRequest: options?.onClarificationRequest,
|
|
48
|
+
abortSignal: options?.abortSignal
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = streamTextAI(context.messages, tools, {
|
|
52
|
+
abortSignal: options?.abortSignal,
|
|
53
|
+
toolExecutionContext,
|
|
54
|
+
onStepFinish: (step) => {
|
|
55
|
+
options?.onStepFinish?.(step);
|
|
56
|
+
},
|
|
57
|
+
onChunk: ({ chunk }) => {
|
|
58
|
+
if (chunk.type === 'text-delta') {
|
|
59
|
+
options?.onText?.(chunk.text);
|
|
60
|
+
}
|
|
61
|
+
if (chunk.type === 'tool-call') {
|
|
62
|
+
options?.onToolCall?.(chunk);
|
|
63
|
+
}
|
|
64
|
+
if (chunk.type === 'tool-result') {
|
|
65
|
+
options?.onToolResult?.(chunk);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const [text, steps, finishReason] = await Promise.all([
|
|
71
|
+
result.text,
|
|
72
|
+
result.steps,
|
|
73
|
+
result.finishReason,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
totalSteps += steps.length;
|
|
77
|
+
|
|
78
|
+
for (const step of steps) {
|
|
79
|
+
if (step.response?.messages?.length) {
|
|
80
|
+
const messages = [...step.response.messages];
|
|
81
|
+
options?.onResponse?.(messages);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (finishReason === 'stop') {
|
|
86
|
+
return text || 'Task completed.';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (finishReason === 'length') {
|
|
90
|
+
if (compactionAttempts < MAX_COMPACTION_ATTEMPTS) {
|
|
91
|
+
const { didCompact, newMessages } = await maybeCompactContext(context, { force: true });
|
|
92
|
+
if (didCompact) {
|
|
93
|
+
compactionAttempts++;
|
|
94
|
+
if (newMessages) {
|
|
95
|
+
options?.onCompacted?.(newMessages)
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return text || 'Context limit reached.';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (finishReason === 'content-filter') {
|
|
104
|
+
return text || 'Content filtered.';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (finishReason === 'error') {
|
|
108
|
+
return text || 'Task failed.';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (finishReason === 'tool-calls') {
|
|
112
|
+
if (totalSteps >= MAX_STEPS) {
|
|
113
|
+
return text || 'Max steps reached, task may be incomplete.';
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return text || 'Task completed.';
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
if (options?.abortSignal?.aborted || error?.name === 'AbortError') {
|
|
121
|
+
return 'Request aborted.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
errorCount++;
|
|
125
|
+
if (errorCount >= MAX_ERROR_COUNT) {
|
|
126
|
+
return `Failed after ${errorCount} errors: ${error?.message ?? String(error)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isRetryableError(error)) {
|
|
130
|
+
const delay = Math.min(2000 * Math.pow(2, errorCount - 1), 30000);
|
|
131
|
+
await sleep(delay);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `Error: ${error?.message ?? String(error)}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isRetryableError(error: any): boolean {
|
|
141
|
+
const status = error?.status ?? error?.statusCode;
|
|
142
|
+
return status === 429 || status === 500 || status === 502 || status === 503;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sleep(ms: number): Promise<void> {
|
|
146
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
147
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Engine } from './Engine.js';
|
|
2
|
+
export { Engine as PulseAgent } from './Engine.js'; // 添加 PulseAgent 别名
|
|
3
|
+
|
|
4
|
+
// 插件系统导出
|
|
5
|
+
export type { EnginePlugin, EnginePluginContext } from './plugin/EnginePlugin.js';
|
|
6
|
+
export type { UserConfigPlugin, UserConfigPluginLoadOptions } from './plugin/UserConfigPlugin.js';
|
|
7
|
+
export { PluginManager } from './plugin/PluginManager.js';
|
|
8
|
+
|
|
9
|
+
// 内置插件导出
|
|
10
|
+
export { builtInPlugins, builtInMCPPlugin, builtInSkillsPlugin, BuiltInSkillRegistry } from './built-in/index.js';
|
|
11
|
+
|
|
12
|
+
// 原有导出保持不变
|
|
13
|
+
export * from './shared/types.js';
|
|
14
|
+
export { loop } from './core/loop.js';
|
|
15
|
+
export { streamTextAI } from './ai/index.js';
|
|
16
|
+
export { maybeCompactContext } from './context/index.js';
|
|
17
|
+
export * from './tools/index.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
import type { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
// 修复:确保类型正确导出
|
|
5
|
+
export interface EnginePlugin {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
dependencies?: string[];
|
|
9
|
+
|
|
10
|
+
// 生命周期钩子 - 添加类型标注
|
|
11
|
+
beforeInitialize?(context: EnginePluginContext): Promise<void>;
|
|
12
|
+
initialize(context: EnginePluginContext): Promise<void>;
|
|
13
|
+
afterInitialize?(context: EnginePluginContext): Promise<void>;
|
|
14
|
+
|
|
15
|
+
// 清理钩子
|
|
16
|
+
destroy?(context: EnginePluginContext): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EnginePluginContext {
|
|
20
|
+
registerTool(name: string, tool: Tool): void;
|
|
21
|
+
registerTools(tools: Record<string, Tool>): void;
|
|
22
|
+
getTool(name: string): Tool | undefined;
|
|
23
|
+
|
|
24
|
+
registerProtocol(name: string, handler: ProtocolHandler): void;
|
|
25
|
+
getProtocol(name: string): ProtocolHandler | undefined;
|
|
26
|
+
|
|
27
|
+
registerService<T>(name: string, service: T): void;
|
|
28
|
+
getService<T>(name: string): T | undefined;
|
|
29
|
+
|
|
30
|
+
getConfig<T>(key: string): T | undefined;
|
|
31
|
+
setConfig<T>(key: string, value: T): void;
|
|
32
|
+
|
|
33
|
+
events: EventEmitter;
|
|
34
|
+
|
|
35
|
+
logger: {
|
|
36
|
+
debug(message: string, meta?: any): void;
|
|
37
|
+
info(message: string, meta?: any): void;
|
|
38
|
+
warn(message: string, meta?: any): void;
|
|
39
|
+
error(message: string, error?: Error, meta?: any): void;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProtocolHandler {
|
|
44
|
+
name: string;
|
|
45
|
+
handle(message: any): Promise<any>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EnginePluginLoadOptions {
|
|
49
|
+
plugins?: EnginePlugin[];
|
|
50
|
+
dirs?: string[];
|
|
51
|
+
scan?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_ENGINE_PLUGIN_DIRS = [
|
|
55
|
+
'.pulse-coder/engine-plugins',
|
|
56
|
+
'.coder/engine-plugins',
|
|
57
|
+
'~/.pulse-coder/engine-plugins',
|
|
58
|
+
'~/.coder/engine-plugins',
|
|
59
|
+
'./plugins/engine'
|
|
60
|
+
];
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
import type { EnginePlugin, EnginePluginContext, EnginePluginLoadOptions } from './EnginePlugin.js';
|
|
7
|
+
import type { UserConfigPlugin, UserConfigPluginLoadOptions } from './UserConfigPlugin.js';
|
|
8
|
+
import { ConfigVariableResolver } from './UserConfigPlugin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 插件管理器 - 管理双轨插件系统
|
|
12
|
+
*/
|
|
13
|
+
export class PluginManager {
|
|
14
|
+
private enginePlugins = new Map<string, EnginePlugin>();
|
|
15
|
+
private userConfigPlugins: UserConfigPlugin[] = [];
|
|
16
|
+
private tools = new Map<string, any>();
|
|
17
|
+
private services = new Map<string, any>();
|
|
18
|
+
private protocols = new Map<string, any>();
|
|
19
|
+
private config = new Map<string, any>();
|
|
20
|
+
|
|
21
|
+
private events = new EventEmitter();
|
|
22
|
+
private logger = {
|
|
23
|
+
debug: (msg: string, meta?: any) => console.debug(`[PluginManager] ${msg}`, meta),
|
|
24
|
+
info: (msg: string, meta?: any) => console.info(`[PluginManager] ${msg}`, meta),
|
|
25
|
+
warn: (msg: string, meta?: any) => console.warn(`[PluginManager] ${msg}`, meta),
|
|
26
|
+
error: (msg: string, error?: Error, meta?: any) => console.error(`[PluginManager] ${msg}`, error, meta)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 初始化插件系统
|
|
31
|
+
*/
|
|
32
|
+
async initialize(options: {
|
|
33
|
+
enginePlugins?: EnginePluginLoadOptions;
|
|
34
|
+
userConfigPlugins?: UserConfigPluginLoadOptions;
|
|
35
|
+
} = {}): Promise<void> {
|
|
36
|
+
this.logger.info('Initializing plugin system...');
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// 1. 加载引擎插件(优先)
|
|
40
|
+
await this.loadEnginePlugins(options.enginePlugins);
|
|
41
|
+
|
|
42
|
+
// 2. 验证核心能力
|
|
43
|
+
await this.validateCoreCapabilities();
|
|
44
|
+
|
|
45
|
+
// 3. 加载用户配置插件
|
|
46
|
+
await this.loadUserConfigPlugins(options.userConfigPlugins);
|
|
47
|
+
|
|
48
|
+
this.logger.info('Plugin system initialized successfully');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.logger.error('Failed to initialize plugin system', error as Error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 加载引擎插件
|
|
57
|
+
*/
|
|
58
|
+
private async loadEnginePlugins(options: EnginePluginLoadOptions = {}): Promise<void> {
|
|
59
|
+
const plugins: EnginePlugin[] = [];
|
|
60
|
+
|
|
61
|
+
// 1. API传入的插件(最高优先级)
|
|
62
|
+
if (options.plugins) {
|
|
63
|
+
plugins.push(...options.plugins);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. 目录扫描的插件
|
|
67
|
+
if (options.scan !== false) {
|
|
68
|
+
const scanPaths = options.dirs || [
|
|
69
|
+
'.pulse-coder/engine-plugins',
|
|
70
|
+
'.coder/engine-plugins',
|
|
71
|
+
'~/.pulse-coder/engine-plugins',
|
|
72
|
+
'~/.coder/engine-plugins'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const dir of scanPaths) {
|
|
76
|
+
const scannedPlugins = await this.scanEnginePlugins(dir);
|
|
77
|
+
plugins.push(...scannedPlugins);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. 按依赖顺序初始化插件
|
|
82
|
+
const sortedPlugins = this.sortPluginsByDependencies(plugins);
|
|
83
|
+
|
|
84
|
+
for (const plugin of sortedPlugins) {
|
|
85
|
+
await this.initializeEnginePlugin(plugin);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 扫描引擎插件目录
|
|
91
|
+
*/
|
|
92
|
+
private async scanEnginePlugins(dir: string): Promise<EnginePlugin[]> {
|
|
93
|
+
const plugins: EnginePlugin[] = [];
|
|
94
|
+
const resolvedDir = this.resolvePath(dir);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const pattern = '**/*.plugin.{js,ts}';
|
|
98
|
+
const files = await glob(pattern, { cwd: resolvedDir, absolute: true });
|
|
99
|
+
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
try {
|
|
102
|
+
const plugin = await this.loadEnginePluginFile(file);
|
|
103
|
+
plugins.push(plugin);
|
|
104
|
+
this.logger.info(`Loaded engine plugin: ${plugin.name} from ${file}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logger.error(`Failed to load engine plugin from ${file}`, error as Error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// 目录不存在是正常现象
|
|
111
|
+
this.logger.debug(`Directory not found: ${resolvedDir}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return plugins;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 加载单个引擎插件文件
|
|
119
|
+
*/
|
|
120
|
+
private async loadEnginePluginFile(filePath: string): Promise<EnginePlugin> {
|
|
121
|
+
const plugin = await import(filePath);
|
|
122
|
+
|
|
123
|
+
// 支持 default export 或直接导出
|
|
124
|
+
const enginePlugin = plugin.default || plugin;
|
|
125
|
+
|
|
126
|
+
if (!enginePlugin.name || !enginePlugin.initialize) {
|
|
127
|
+
throw new Error(`Invalid engine plugin: ${filePath}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return enginePlugin;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 初始化单个引擎插件
|
|
135
|
+
*/
|
|
136
|
+
private async initializeEnginePlugin(plugin: EnginePlugin): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
// 检查依赖
|
|
139
|
+
if (plugin.dependencies) {
|
|
140
|
+
for (const dep of plugin.dependencies) {
|
|
141
|
+
if (!this.enginePlugins.has(dep)) {
|
|
142
|
+
throw new Error(`Dependency not found: ${dep} for plugin ${plugin.name}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const context: EnginePluginContext = {
|
|
148
|
+
registerTool: (name, tool) => {
|
|
149
|
+
this.tools.set(name, tool);
|
|
150
|
+
},
|
|
151
|
+
registerTools: (tools) => {
|
|
152
|
+
Object.entries(tools).forEach(([name, tool]) => {
|
|
153
|
+
this.tools.set(name, tool);
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
getTool: (name) => this.tools.get(name),
|
|
157
|
+
|
|
158
|
+
registerProtocol: (name, handler) => {
|
|
159
|
+
this.protocols.set(name, handler);
|
|
160
|
+
},
|
|
161
|
+
getProtocol: (name) => this.protocols.get(name),
|
|
162
|
+
|
|
163
|
+
registerService: (name, service) => {
|
|
164
|
+
this.services.set(name, service);
|
|
165
|
+
},
|
|
166
|
+
getService: (name) => this.services.get(name),
|
|
167
|
+
|
|
168
|
+
getConfig: (key) => this.config.get(key),
|
|
169
|
+
setConfig: (key, value) => this.config.set(key, value),
|
|
170
|
+
|
|
171
|
+
events: this.events,
|
|
172
|
+
logger: this.logger
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// 执行生命周期钩子
|
|
176
|
+
if (plugin.beforeInitialize) {
|
|
177
|
+
await plugin.beforeInitialize(context);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await plugin.initialize(context);
|
|
181
|
+
|
|
182
|
+
if (plugin.afterInitialize) {
|
|
183
|
+
await plugin.afterInitialize(context);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.enginePlugins.set(plugin.name, plugin);
|
|
187
|
+
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(`Failed to initialize engine plugin ${plugin.name}: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 加载用户配置插件
|
|
195
|
+
*/
|
|
196
|
+
private async loadUserConfigPlugins(options: UserConfigPluginLoadOptions = {}): Promise<void> {
|
|
197
|
+
const configs: UserConfigPlugin[] = [];
|
|
198
|
+
|
|
199
|
+
// 1. API传入的配置
|
|
200
|
+
if (options.configs) {
|
|
201
|
+
configs.push(...options.configs);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. 目录扫描的配置
|
|
205
|
+
if (options.scan !== false) {
|
|
206
|
+
const scanPaths = options.dirs || [
|
|
207
|
+
'.pulse-coder/config',
|
|
208
|
+
'.coder/config',
|
|
209
|
+
'~/.pulse-coder/config',
|
|
210
|
+
'~/.coder/config'
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const dir of scanPaths) {
|
|
214
|
+
const scannedConfigs = await this.scanUserConfigPlugins(dir);
|
|
215
|
+
configs.push(...scannedConfigs);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 3. 应用所有配置
|
|
220
|
+
for (const config of configs) {
|
|
221
|
+
await this.applyUserConfig(config);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 扫描用户配置插件目录
|
|
227
|
+
*/
|
|
228
|
+
private async scanUserConfigPlugins(dir: string): Promise<UserConfigPlugin[]> {
|
|
229
|
+
const configs: UserConfigPlugin[] = [];
|
|
230
|
+
const resolvedDir = this.resolvePath(dir);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const patterns = ['config.{json,yaml,yml}', '*.config.{json,yaml,yml}'];
|
|
234
|
+
|
|
235
|
+
for (const pattern of patterns) {
|
|
236
|
+
const files = await glob(pattern, { cwd: resolvedDir, absolute: true });
|
|
237
|
+
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
try {
|
|
240
|
+
const config = await this.loadUserConfigFile(file);
|
|
241
|
+
configs.push(config);
|
|
242
|
+
this.logger.info(`Loaded user config: ${config.name || file}`);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.logger.error(`Failed to load user config from ${file}`, error as Error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.logger.debug(`Directory not found: ${resolvedDir}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return configs;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 加载单个用户配置文件
|
|
257
|
+
*/
|
|
258
|
+
private async loadUserConfigFile(filePath: string): Promise<UserConfigPlugin> {
|
|
259
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
260
|
+
const ext = path.extname(filePath);
|
|
261
|
+
|
|
262
|
+
let config: UserConfigPlugin;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
if (ext === '.json') {
|
|
266
|
+
config = JSON.parse(content);
|
|
267
|
+
} else if (ext === '.yaml' || ext === '.yml') {
|
|
268
|
+
// 使用动态导入避免依赖问题
|
|
269
|
+
const YAML = await import('yaml');
|
|
270
|
+
config = YAML.parse(content);
|
|
271
|
+
} else {
|
|
272
|
+
throw new Error(`Unsupported config format: ${ext}`);
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(`Failed to parse config file ${filePath}: ${error}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 环境变量替换
|
|
279
|
+
const resolver = new ConfigVariableResolver();
|
|
280
|
+
config = resolver.resolveObject(config);
|
|
281
|
+
|
|
282
|
+
return config;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 应用用户配置
|
|
287
|
+
*/
|
|
288
|
+
private async applyUserConfig(config: UserConfigPlugin): Promise<void> {
|
|
289
|
+
try {
|
|
290
|
+
// 配置工具
|
|
291
|
+
if (config.tools) {
|
|
292
|
+
for (const [name, toolConfig] of Object.entries(config.tools)) {
|
|
293
|
+
if (toolConfig.enabled !== false) {
|
|
294
|
+
// 这里将根据配置创建具体工具实例
|
|
295
|
+
this.logger.debug(`Configured tool: ${name}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 配置MCP服务器
|
|
301
|
+
if (config.mcp?.servers) {
|
|
302
|
+
for (const server of config.mcp.servers) {
|
|
303
|
+
if (server.enabled !== false) {
|
|
304
|
+
this.logger.debug(`Configured MCP server: ${server.name}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 配置子代理
|
|
310
|
+
if (config.subAgents) {
|
|
311
|
+
for (const agent of config.subAgents) {
|
|
312
|
+
if (agent.enabled !== false) {
|
|
313
|
+
this.logger.debug(`Configured sub-agent: ${agent.name}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 配置技能(向后兼容)
|
|
319
|
+
if (config.skills) {
|
|
320
|
+
this.logger.debug(`Configured skills scanning`, config.skills);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.userConfigPlugins.push(config);
|
|
324
|
+
|
|
325
|
+
} catch (error) {
|
|
326
|
+
this.logger.error('Failed to apply user config', error as Error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 验证核心能力
|
|
332
|
+
*/
|
|
333
|
+
private async validateCoreCapabilities(): Promise<void> {
|
|
334
|
+
// 检查必需的核心插件是否已加载
|
|
335
|
+
const requiredCapabilities = [
|
|
336
|
+
'skill-registry' // 确保skill系统可用
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
for (const capability of requiredCapabilities) {
|
|
340
|
+
if (!this.enginePlugins.has(capability) && !this.enginePlugins.has(`@pulse-coder/engine-${capability}`)) {
|
|
341
|
+
this.logger.warn(`Missing core capability: ${capability}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 插件依赖排序
|
|
348
|
+
*/
|
|
349
|
+
private sortPluginsByDependencies(plugins: EnginePlugin[]): EnginePlugin[] {
|
|
350
|
+
const sorted: EnginePlugin[] = [];
|
|
351
|
+
const visited = new Set<string>();
|
|
352
|
+
const visiting = new Set<string>();
|
|
353
|
+
|
|
354
|
+
const visit = (plugin: EnginePlugin) => {
|
|
355
|
+
if (visited.has(plugin.name)) return;
|
|
356
|
+
if (visiting.has(plugin.name)) {
|
|
357
|
+
throw new Error(`Circular dependency detected: ${plugin.name}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
visiting.add(plugin.name);
|
|
361
|
+
|
|
362
|
+
if (plugin.dependencies) {
|
|
363
|
+
for (const dep of plugin.dependencies) {
|
|
364
|
+
const depPlugin = plugins.find(p => p.name === dep);
|
|
365
|
+
if (depPlugin) {
|
|
366
|
+
visit(depPlugin);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
visiting.delete(plugin.name);
|
|
372
|
+
visited.add(plugin.name);
|
|
373
|
+
sorted.push(plugin);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
for (const plugin of plugins) {
|
|
377
|
+
visit(plugin);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return sorted;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 工具获取
|
|
385
|
+
*/
|
|
386
|
+
getTools(): Record<string, any> {
|
|
387
|
+
return Object.fromEntries(this.tools);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 服务获取
|
|
392
|
+
*/
|
|
393
|
+
getService<T>(name: string): T | undefined {
|
|
394
|
+
return this.services.get(name);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 协议获取
|
|
399
|
+
*/
|
|
400
|
+
getProtocol(name: string): any {
|
|
401
|
+
return this.protocols.get(name);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 获取插件状态
|
|
406
|
+
*/
|
|
407
|
+
getStatus() {
|
|
408
|
+
return {
|
|
409
|
+
enginePlugins: Array.from(this.enginePlugins.keys()),
|
|
410
|
+
userConfigPlugins: this.userConfigPlugins.map(c => c.name || 'unnamed'),
|
|
411
|
+
tools: Array.from(this.tools.keys()),
|
|
412
|
+
services: Array.from(this.services.keys()),
|
|
413
|
+
protocols: Array.from(this.protocols.keys())
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 解析路径(支持 ~ 和相对路径)
|
|
419
|
+
*/
|
|
420
|
+
private resolvePath(dir: string): string {
|
|
421
|
+
if (dir.startsWith('~/')) {
|
|
422
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', dir.slice(2));
|
|
423
|
+
}
|
|
424
|
+
return path.resolve(dir);
|
|
425
|
+
}
|
|
426
|
+
}
|