pigpig-agent 1.0.0
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/.skills/code-review/SKILL.md +42 -0
- package/dist/agent/loop-detection.js +135 -0
- package/dist/agent/loop.js +123 -0
- package/dist/agent/retry.js +39 -0
- package/dist/agents/registry.js +59 -0
- package/dist/agents/spawn.js +124 -0
- package/dist/agents/types.js +5 -0
- package/dist/commands/agent.js +33 -0
- package/dist/commands/context.js +27 -0
- package/dist/commands/cron.js +38 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/dream.js +36 -0
- package/dist/commands/index.js +10 -0
- package/dist/commands/memory.js +40 -0
- package/dist/commands/plugin.js +73 -0
- package/dist/commands/rag.js +25 -0
- package/dist/commands/security.js +44 -0
- package/dist/commands/skill.js +84 -0
- package/dist/config/init.js +69 -0
- package/dist/config/loader.js +52 -0
- package/dist/config/schema.js +55 -0
- package/dist/context/compressor.js +165 -0
- package/dist/context/defense.js +201 -0
- package/dist/context/prompt-builder.js +56 -0
- package/dist/context/prompt-pipes.js +14 -0
- package/dist/context/tool-result-output.js +25 -0
- package/dist/context/view.js +185 -0
- package/dist/cron/parser.js +27 -0
- package/dist/cron/service.js +211 -0
- package/dist/cron/store.js +53 -0
- package/dist/cron/types.js +1 -0
- package/dist/index.js +9 -0
- package/dist/main.js +270 -0
- package/dist/memory/store.js +175 -0
- package/dist/memory/validator.js +62 -0
- package/dist/mock-model.js +534 -0
- package/dist/plugins/manager.js +97 -0
- package/dist/plugins/supabase-plugin.js +111 -0
- package/dist/plugins/types.js +1 -0
- package/dist/rag/chunker.js +54 -0
- package/dist/rag/embedder.js +53 -0
- package/dist/rag/search.js +123 -0
- package/dist/rag/sqlite-store.js +195 -0
- package/dist/rag/store.js +29 -0
- package/dist/security/bash-classifier.js +39 -0
- package/dist/security/hook.js +53 -0
- package/dist/security/roles.js +16 -0
- package/dist/session/store.js +60 -0
- package/dist/skills/loader.js +100 -0
- package/dist/tools/cron-tools.js +94 -0
- package/dist/tools/file-tools.js +115 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/mcp-client.js +100 -0
- package/dist/tools/memory-tools.js +81 -0
- package/dist/tools/rag-tools.js +54 -0
- package/dist/tools/registry.js +270 -0
- package/dist/tools/search-tools.js +116 -0
- package/dist/tools/shell-tools.js +39 -0
- package/dist/tools/spawn-tools.js +35 -0
- package/dist/tools/tool-search.js +17 -0
- package/dist/tools/web-search.js +150 -0
- package/dist/usage/tracker.js +83 -0
- package/package.json +55 -0
- package/readme.md +131 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const SESSION_DIR = '.sessions';
|
|
4
|
+
const DEFAULT_SESSION = 'default';
|
|
5
|
+
export class SessionStore {
|
|
6
|
+
dir = '';
|
|
7
|
+
sessionId = '';
|
|
8
|
+
constructor(sessionId = DEFAULT_SESSION) {
|
|
9
|
+
this.sessionId = sessionId;
|
|
10
|
+
this.dir = SESSION_DIR;
|
|
11
|
+
if (!existsSync(this.dir)) {
|
|
12
|
+
mkdirSync(this.dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
get filePath() {
|
|
16
|
+
return join(this.dir, `${this.sessionId}.jsonl`); // ./sessions/default.jsonl
|
|
17
|
+
}
|
|
18
|
+
// 追加消息到会话文件
|
|
19
|
+
append(message) {
|
|
20
|
+
const entry = {
|
|
21
|
+
type: 'message',
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
message,
|
|
24
|
+
};
|
|
25
|
+
appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
// 追加多个消息到会话文件
|
|
28
|
+
appendAll(messages) {
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
this.append(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 从会话文件加载消息
|
|
34
|
+
load() {
|
|
35
|
+
if (!existsSync(this.filePath))
|
|
36
|
+
return [];
|
|
37
|
+
const content = readFileSync(this.filePath, 'utf-8').trim();
|
|
38
|
+
if (!content)
|
|
39
|
+
return [];
|
|
40
|
+
const messages = [];
|
|
41
|
+
for (const line of content.split('\n')) {
|
|
42
|
+
if (!line.trim())
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const entry = JSON.parse(line);
|
|
46
|
+
if (entry.type === 'message') {
|
|
47
|
+
messages.push(entry.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// skip malformed lines
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return messages;
|
|
55
|
+
}
|
|
56
|
+
// 检查会话文件是否存在
|
|
57
|
+
exists() {
|
|
58
|
+
return existsSync(this.filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const SKILLS_DIR = '.skills';
|
|
5
|
+
const SKILL_FILE = 'SKILL.md';
|
|
6
|
+
const BUILTIN_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../.skills');
|
|
7
|
+
export class SkillLoader {
|
|
8
|
+
baseDir = '';
|
|
9
|
+
skills = new Map();
|
|
10
|
+
constructor(baseDir = '.') {
|
|
11
|
+
this.baseDir = baseDir;
|
|
12
|
+
}
|
|
13
|
+
load() {
|
|
14
|
+
this.skills.clear();
|
|
15
|
+
const dirs = [
|
|
16
|
+
path.join(this.baseDir, SKILLS_DIR), // 'xxx/xxx/.skills'
|
|
17
|
+
BUILTIN_SKILLS_DIR,
|
|
18
|
+
];
|
|
19
|
+
for (const skillsDir of dirs) {
|
|
20
|
+
this.loadFrom(skillsDir);
|
|
21
|
+
}
|
|
22
|
+
return this.list(); // 得到所有的Skill
|
|
23
|
+
}
|
|
24
|
+
loadFrom(skillsDir) {
|
|
25
|
+
if (!fs.existsSync(skillsDir))
|
|
26
|
+
return;
|
|
27
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isDirectory())
|
|
30
|
+
continue;
|
|
31
|
+
const skillFile = path.join(skillsDir, entry.name, SKILL_FILE); // 'xxx/xxx/.skills/<skill-name>/SKILL.md'
|
|
32
|
+
if (!fs.existsSync(skillFile))
|
|
33
|
+
continue;
|
|
34
|
+
const raw = fs.readFileSync(skillFile, 'utf-8');
|
|
35
|
+
const parsed = this.parseFrontmatter(raw); // 解析Skill的Frontmatter
|
|
36
|
+
if (!parsed)
|
|
37
|
+
continue;
|
|
38
|
+
if (this.skills.has(entry.name))
|
|
39
|
+
continue;
|
|
40
|
+
this.skills.set(entry.name, {
|
|
41
|
+
name: entry.name,
|
|
42
|
+
description: parsed.description,
|
|
43
|
+
whenToUse: parsed.whenToUse,
|
|
44
|
+
content: parsed.content,
|
|
45
|
+
dirPath: path.join(skillsDir, entry.name), // 'xxx/xxx/.skills/<skill-name>'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
list() {
|
|
50
|
+
return Array.from(this.skills.values());
|
|
51
|
+
}
|
|
52
|
+
get(name) {
|
|
53
|
+
return this.skills.get(name);
|
|
54
|
+
}
|
|
55
|
+
buildPromptSection(activeSkills) {
|
|
56
|
+
if (this.skills.size === 0)
|
|
57
|
+
return null;
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (const name of activeSkills) {
|
|
60
|
+
const skill = this.skills.get(name);
|
|
61
|
+
if (!skill)
|
|
62
|
+
continue;
|
|
63
|
+
lines.push(`[激活的Skill:${skill.name}]`);
|
|
64
|
+
lines.push(skill.content);
|
|
65
|
+
lines.push('');
|
|
66
|
+
}
|
|
67
|
+
const available = this.list()
|
|
68
|
+
.filter(s => !activeSkills.has(s.name)) // 过滤出未激活的Skill
|
|
69
|
+
.map(s => {
|
|
70
|
+
const hint = s.whenToUse ? `(适用场景为:${s.whenToUse})` : '';
|
|
71
|
+
return ` / ${s.name} - ${s.description}${hint}`; // '/ <skill-name> - <skill-description> (适用场景为:<when-to-use>)'
|
|
72
|
+
});
|
|
73
|
+
if (available.length > 0) {
|
|
74
|
+
lines.push(`可用的 Skills (输入 /skill load <name> 激活):`);
|
|
75
|
+
lines.push(...available);
|
|
76
|
+
}
|
|
77
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
78
|
+
}
|
|
79
|
+
parseFrontmatter(raw) {
|
|
80
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
81
|
+
if (!match)
|
|
82
|
+
return { description: '', content: raw };
|
|
83
|
+
const meta = {};
|
|
84
|
+
for (const line of match[1].split(/\r?\n/)) { // ['name: <skill-name>', 'description: <skill-description>']
|
|
85
|
+
const idx = line.indexOf(':');
|
|
86
|
+
if (idx > 0) {
|
|
87
|
+
const key = line.slice(0, idx).trim();
|
|
88
|
+
let value = line.slice(idx + 1).trim();
|
|
89
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
90
|
+
value = value.slice(1, -1);
|
|
91
|
+
meta[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
description: meta.description || '',
|
|
96
|
+
whenToUse: meta.whenToUse || undefined,
|
|
97
|
+
content: match[2].trim()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function createCronTool(cronService) {
|
|
2
|
+
return {
|
|
3
|
+
name: 'cron_manage',
|
|
4
|
+
description: '管理定时任务。支持创建、删除、查看、立即执行定时任务。',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
action: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
enum: ['list', 'add', 'remove', 'run', 'enable', 'disable', 'logs'],
|
|
11
|
+
description: '操作类型',
|
|
12
|
+
},
|
|
13
|
+
id: { type: 'string', description: '任务 ID(add/remove/run/enable/disable 时必填)' },
|
|
14
|
+
name: { type: 'string', description: '任务名称(add 时必填)' },
|
|
15
|
+
schedule: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: '调度表达式:cron("*/5 * * * *")、间隔("every 30s")、一次性(ISO 时间戳)',
|
|
18
|
+
},
|
|
19
|
+
prompt: { type: 'string', description: '任务执行时发送给 Agent 的 prompt(add 时必填)' },
|
|
20
|
+
},
|
|
21
|
+
required: ['action'],
|
|
22
|
+
},
|
|
23
|
+
isConcurrencySafe: false,
|
|
24
|
+
isReadOnly: false,
|
|
25
|
+
execute: async (input) => {
|
|
26
|
+
switch (input.action) {
|
|
27
|
+
case 'list': {
|
|
28
|
+
const jobs = cronService.list();
|
|
29
|
+
if (jobs.length === 0)
|
|
30
|
+
return '当前没有定时任务';
|
|
31
|
+
return jobs.map(j => {
|
|
32
|
+
const last = j.lastRun
|
|
33
|
+
? ` | 上次: ${j.lastRun.status} @ ${j.lastRun.finishedAt}`
|
|
34
|
+
: '';
|
|
35
|
+
return `[${j.status}] ${j.config.id} — ${j.config.name}\n 调度: ${j.config.schedule}${last}`;
|
|
36
|
+
}).join('\n\n');
|
|
37
|
+
}
|
|
38
|
+
case 'add': {
|
|
39
|
+
if (!input.id || !input.name || !input.schedule || !input.prompt) {
|
|
40
|
+
return '添加任务需要: id, name, schedule, prompt';
|
|
41
|
+
}
|
|
42
|
+
const scheduleType = input.schedule.startsWith('every') ? 'interval'
|
|
43
|
+
: /^\d{4}-/.test(input.schedule) ? 'once'
|
|
44
|
+
: 'cron';
|
|
45
|
+
const config = {
|
|
46
|
+
id: input.id,
|
|
47
|
+
name: input.name,
|
|
48
|
+
schedule: input.schedule,
|
|
49
|
+
scheduleType,
|
|
50
|
+
enabled: true,
|
|
51
|
+
payload: { type: 'agent', prompt: input.prompt },
|
|
52
|
+
source: 'runtime',
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
cronService.add(config);
|
|
56
|
+
return `✓ 任务 "${input.name}" 已创建,调度: ${input.schedule}`;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return `✗ 创建失败: ${err.message}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
case 'remove': {
|
|
63
|
+
if (!input.id)
|
|
64
|
+
return '需要指定任务 id';
|
|
65
|
+
const removed = cronService.remove(input.id);
|
|
66
|
+
return removed ? `✓ 任务 ${input.id} 已删除` : `✗ 任务 ${input.id} 不存在`;
|
|
67
|
+
}
|
|
68
|
+
case 'run': {
|
|
69
|
+
if (!input.id)
|
|
70
|
+
return '需要指定任务 id';
|
|
71
|
+
return cronService.runNow(input.id);
|
|
72
|
+
}
|
|
73
|
+
case 'enable': {
|
|
74
|
+
if (!input.id)
|
|
75
|
+
return '需要指定任务 id';
|
|
76
|
+
return cronService.enable(input.id) ? `✓ 已启用` : `✗ 任务不存在`;
|
|
77
|
+
}
|
|
78
|
+
case 'disable': {
|
|
79
|
+
if (!input.id)
|
|
80
|
+
return '需要指定任务 id';
|
|
81
|
+
return cronService.disable(input.id) ? `✓ 已禁用` : `✗ 任务不存在`;
|
|
82
|
+
}
|
|
83
|
+
case 'logs': {
|
|
84
|
+
const logs = cronService.getRecentLogs(input.id, 5);
|
|
85
|
+
if (logs.length === 0)
|
|
86
|
+
return '暂无执行记录';
|
|
87
|
+
return logs.map(l => `[${l.status}] ${l.jobId} @ ${l.startedAt}\n ${l.output?.slice(0, 100) || l.error || ''}`).join('\n\n');
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
return `未知操作: ${input.action}`;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
|
+
export const readFileTool = {
|
|
4
|
+
name: 'read_file',
|
|
5
|
+
description: '读取指定路径的文件内容',
|
|
6
|
+
parameters: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
path: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: '文件路径'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
required: ['path'],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
},
|
|
17
|
+
isConcurrencySafe: true,
|
|
18
|
+
isReadOnly: true,
|
|
19
|
+
maxResultChars: 500,
|
|
20
|
+
execute: async ({ path }) => {
|
|
21
|
+
return readFileSync(resolve(path), 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const writeFileTool = {
|
|
25
|
+
name: 'write_file',
|
|
26
|
+
description: '写入内容到指定路径的文件',
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
path: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: '文件路径'
|
|
33
|
+
},
|
|
34
|
+
content: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: '要写入的内容'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ['path', 'content'],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
isConcurrencySafe: false, // 写入操作不能并发
|
|
43
|
+
isReadOnly: false,
|
|
44
|
+
execute: async ({ path, content }) => {
|
|
45
|
+
writeFileSync(resolve(path), content, 'utf-8');
|
|
46
|
+
return `已写入 ${content.length} 个字符到 ${path}`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export const listDirectoryTool = {
|
|
50
|
+
name: 'list_directory',
|
|
51
|
+
description: '列出指定目录下的文件和子目录',
|
|
52
|
+
parameters: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
path: { type: 'string', description: '目录路径,默认为当前目录' },
|
|
56
|
+
},
|
|
57
|
+
required: [],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
isConcurrencySafe: true,
|
|
61
|
+
isReadOnly: true,
|
|
62
|
+
execute: async ({ path = '.' }) => {
|
|
63
|
+
const resolved = resolve(path);
|
|
64
|
+
const entries = readdirSync(resolved);
|
|
65
|
+
return entries.map(name => {
|
|
66
|
+
try {
|
|
67
|
+
const stat = statSync(join(resolved, name));
|
|
68
|
+
return `${stat.isDirectory() ? '[DIR]' : '[FILE]'} ${name}`;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return `[?] ${name}`;
|
|
72
|
+
}
|
|
73
|
+
}).join('\n');
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
// 编辑文件
|
|
77
|
+
export const editFileTool = {
|
|
78
|
+
name: 'edit_file',
|
|
79
|
+
description: '精确的替换文件中的指定内容,用 old_string 定位要替换的内容,用 new_string 替换它。不是全量覆写,只是改你指定的部分',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
path: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: '文件路径'
|
|
86
|
+
},
|
|
87
|
+
old_string: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: '要被替换的原始文本(必须精确匹配)'
|
|
90
|
+
},
|
|
91
|
+
new_string: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: '替换后的新文本'
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
required: ['path', 'old_string', 'new_string'],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
},
|
|
99
|
+
isConcurrencySafe: false, // 编辑操作不能并发
|
|
100
|
+
isReadOnly: false,
|
|
101
|
+
execute: async ({ path, old_string, new_string }) => {
|
|
102
|
+
const resolved = resolve(path);
|
|
103
|
+
if (!existsSync(resolved))
|
|
104
|
+
return `文件不存在: ${path}`;
|
|
105
|
+
const content = readFileSync(resolved, 'utf-8');
|
|
106
|
+
const count = content.split(old_string).length - 1;
|
|
107
|
+
if (count === 0)
|
|
108
|
+
return `未找到匹配的内容。请检查 old_string 是否与文件中的文本完全一致(包括空格和换行符)`;
|
|
109
|
+
if (count > 1)
|
|
110
|
+
return `找到 ${count} 个匹配项。请提供更多上下文信息,让 old_string 唯一`;
|
|
111
|
+
const updated = content.replace(old_string, new_string);
|
|
112
|
+
writeFileSync(resolved, updated, 'utf-8');
|
|
113
|
+
return `已替换 ${path} 中的内容 (${old_string} 为 ${new_string} 字符)`;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileTool, writeFileTool, listDirectoryTool, editFileTool } from './file-tools.js';
|
|
2
|
+
import { globTool, grepTool } from './search-tools.js';
|
|
3
|
+
import { bashTool } from './shell-tools.js';
|
|
4
|
+
import { pickSearchTool, webFetchTool } from './web-search.js';
|
|
5
|
+
export const allTools = [
|
|
6
|
+
readFileTool,
|
|
7
|
+
writeFileTool,
|
|
8
|
+
listDirectoryTool,
|
|
9
|
+
editFileTool,
|
|
10
|
+
globTool,
|
|
11
|
+
grepTool,
|
|
12
|
+
bashTool,
|
|
13
|
+
pickSearchTool(),
|
|
14
|
+
webFetchTool,
|
|
15
|
+
];
|
|
16
|
+
// 核心工具
|
|
17
|
+
export { readFileTool, writeFileTool, listDirectoryTool, editFileTool, globTool, grepTool, bashTool, };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
export class MCPClient {
|
|
4
|
+
command;
|
|
5
|
+
args;
|
|
6
|
+
env;
|
|
7
|
+
process = null;
|
|
8
|
+
requestId = 0;
|
|
9
|
+
pending = new Map();
|
|
10
|
+
serverName = '';
|
|
11
|
+
rl = null;
|
|
12
|
+
constructor(command, args, env) {
|
|
13
|
+
this.command = command;
|
|
14
|
+
this.args = args;
|
|
15
|
+
this.env = env;
|
|
16
|
+
this.serverName = args[args.length - 1]?.replace(/^@.*\//, '') || 'mcp-server';
|
|
17
|
+
}
|
|
18
|
+
async connect() {
|
|
19
|
+
// Windows 上命令是 .cmd/.exe 文件,spawn 无法直接启动(报 ENOENT/EINVAL),
|
|
20
|
+
// 需要显式用 cmd.exe /c 包装;Linux/macOS 直接 spawn 即可。
|
|
21
|
+
const isWindows = process.platform === 'win32';
|
|
22
|
+
const command = isWindows ? 'cmd.exe' : this.command;
|
|
23
|
+
const args = isWindows ? ['/c', this.command, ...this.args] : this.args;
|
|
24
|
+
this.process = spawn(command, args, {
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'], // 三个pipe:指的是标准输入、标准输出、标准错误输出都通过管道传递
|
|
26
|
+
env: { ...process.env, ...this.env }, // 主进程环境变量传给子进程,用于访问环境变量
|
|
27
|
+
});
|
|
28
|
+
this.process.on('error', (error) => {
|
|
29
|
+
console.error(` [MCP] 进程启动失败:${error.message}`);
|
|
30
|
+
// 启动失败时立刻拒绝所有待处理请求,避免它们一直等到超时
|
|
31
|
+
for (const p of this.pending.values()) {
|
|
32
|
+
p.reject(new Error(`MCP 进程启动失败: ${error.message}`));
|
|
33
|
+
}
|
|
34
|
+
this.pending.clear();
|
|
35
|
+
});
|
|
36
|
+
this.process.stderr?.on('data', () => { });
|
|
37
|
+
this.rl = createInterface({
|
|
38
|
+
input: this.process.stdout,
|
|
39
|
+
});
|
|
40
|
+
this.rl.on('line', (line) => {
|
|
41
|
+
try {
|
|
42
|
+
const msg = JSON.parse(line);
|
|
43
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
44
|
+
const p = this.pending.get(msg.id);
|
|
45
|
+
this.pending.delete(msg.id);
|
|
46
|
+
if (msg.error) {
|
|
47
|
+
p?.reject(new Error(` [MCP] 错误:${msg.error.code}: ${msg.error.message}`));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
p?.resolve(msg.result);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(` [MCP] 解析错误:${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
await this.send('initialize', {
|
|
59
|
+
protocolVersion: '2025-11-25',
|
|
60
|
+
capabilities: {},
|
|
61
|
+
clientInfo: { name: 'super-agent', version: '0.5.0' },
|
|
62
|
+
});
|
|
63
|
+
this.process.stdin.write(JSON.stringify({
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
method: 'notifications/initialized',
|
|
66
|
+
}) + '\n');
|
|
67
|
+
}
|
|
68
|
+
send(method, params) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const id = ++this.requestId;
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
this.pending.delete(id);
|
|
73
|
+
reject(new Error(`MCP request timeout: ${method}`));
|
|
74
|
+
}, 15000);
|
|
75
|
+
this.pending.set(id, {
|
|
76
|
+
resolve: (v) => { clearTimeout(timeout); resolve(v); },
|
|
77
|
+
reject: (e) => { clearTimeout(timeout); reject(e); },
|
|
78
|
+
});
|
|
79
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
80
|
+
this.process.stdin.write(msg + '\n');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async listTools() {
|
|
84
|
+
const result = await this.send('tools/list', {});
|
|
85
|
+
return result.tools || [];
|
|
86
|
+
}
|
|
87
|
+
async callTool(name, args) {
|
|
88
|
+
const result = await this.send('tools/call', { name, arguments: args });
|
|
89
|
+
const texts = (result.content || [])
|
|
90
|
+
.filter(c => c.type === 'text' && c.text)
|
|
91
|
+
.map(c => c.text);
|
|
92
|
+
return texts.join('\n') || '(无返回内容)';
|
|
93
|
+
}
|
|
94
|
+
async close() {
|
|
95
|
+
if (this.rl)
|
|
96
|
+
this.rl.close();
|
|
97
|
+
if (this.process)
|
|
98
|
+
this.process.kill();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function createMemoryTool(memoryStore) {
|
|
2
|
+
return {
|
|
3
|
+
name: 'memory',
|
|
4
|
+
description: '管理跨会话记忆。action: save | list | search | read | delete | lint。read/delete 需要 filename(如 project_xxx.md);save 同名自动覆盖;lint 结果自带内容预览,不需要逐条 read',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
action: { type: 'string', enum: ['save', 'list', 'search', 'read', 'delete', 'lint'] },
|
|
9
|
+
name: { type: 'string', description: '记忆名称(save 时必填)' },
|
|
10
|
+
description: { type: 'string', description: '一句话描述(save 时必填)' },
|
|
11
|
+
type: { type: 'string', enum: ['user', 'feedback', 'project', 'reference'], description: '记忆类型(save 时必填)' },
|
|
12
|
+
content: { type: 'string', description: '记忆内容(save 时必填)' },
|
|
13
|
+
query: { type: 'string', description: '搜索关键词(search 时必填)' },
|
|
14
|
+
filename: { type: 'string', description: '文件名(read/delete 时必填)' },
|
|
15
|
+
},
|
|
16
|
+
required: ['action'],
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
},
|
|
19
|
+
isConcurrencySafe: false,
|
|
20
|
+
isReadOnly: false,
|
|
21
|
+
execute: async (args) => {
|
|
22
|
+
switch (args.action) {
|
|
23
|
+
case 'save': {
|
|
24
|
+
if (!args.name || !args.type || !args.content) {
|
|
25
|
+
return '保存失败:需要 name、type、content 参数';
|
|
26
|
+
}
|
|
27
|
+
const filename = memoryStore.save({
|
|
28
|
+
name: args.name,
|
|
29
|
+
description: args.description || args.name,
|
|
30
|
+
type: args.type,
|
|
31
|
+
content: args.content,
|
|
32
|
+
});
|
|
33
|
+
return `已保存到记忆: ${filename}`;
|
|
34
|
+
}
|
|
35
|
+
case 'list': {
|
|
36
|
+
const entries = memoryStore.list();
|
|
37
|
+
if (entries.length === 0)
|
|
38
|
+
return '当前没有存储任何记忆。';
|
|
39
|
+
return `记忆列表(共 ${entries.length} 条记忆):\n` +
|
|
40
|
+
entries.map(e => ` [${e.type}] ${e.name} — ${e.description}`).join('\n');
|
|
41
|
+
}
|
|
42
|
+
case 'search': {
|
|
43
|
+
const results = memoryStore.search(args.query || '');
|
|
44
|
+
if (results.length === 0)
|
|
45
|
+
return `没有找到与 "${args.query}" 相关的记忆。`;
|
|
46
|
+
return `搜索结果(${results.length} 条匹配):\n` +
|
|
47
|
+
results.map(e => ` [${e.type}] ${e.name} — ${e.description}`).join('\n');
|
|
48
|
+
}
|
|
49
|
+
case 'read': {
|
|
50
|
+
if (!args.filename)
|
|
51
|
+
return '读取失败:需要 filename 参数';
|
|
52
|
+
return memoryStore.loadFile(args.filename) ?? `文件不存在: ${args.filename}`;
|
|
53
|
+
}
|
|
54
|
+
case 'delete': {
|
|
55
|
+
if (!args.filename)
|
|
56
|
+
return '删除失败:需要 filename 参数';
|
|
57
|
+
return memoryStore.delete(args.filename) ? `已删除: ${args.filename}` : `文件不存在: ${args.filename}`;
|
|
58
|
+
}
|
|
59
|
+
case 'lint': {
|
|
60
|
+
const reports = memoryStore.lint();
|
|
61
|
+
if (reports.length === 0)
|
|
62
|
+
return '记忆库健康,没有发现问题。';
|
|
63
|
+
const lines = [`记忆库 lint 报告(${reports.length} 条有问题):`, ''];
|
|
64
|
+
for (const r of reports) {
|
|
65
|
+
const fname = r.entry.filePath.split('/').pop();
|
|
66
|
+
const preview = r.entry.content.slice(0, 100).replace(/\n/g, ' ');
|
|
67
|
+
lines.push(`📁 ${fname} [${r.entry.type}] ${r.entry.name}`);
|
|
68
|
+
lines.push(` 内容预览: ${preview}${r.entry.content.length > 100 ? '...' : ''}`);
|
|
69
|
+
for (const issue of r.issues)
|
|
70
|
+
lines.push(` • ${issue.kind}: ${issue.message}`);
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
lines.push('提示: 基于以上报告直接操作即可(delete 删除、save 覆盖更新),不需要逐条 read。');
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return `未知操作: ${args.action}`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { chunkDocument } from '../rag/chunker.js';
|
|
3
|
+
import { embed } from '../rag/embedder.js';
|
|
4
|
+
export function createRagTools(vectorStore, embedFn) {
|
|
5
|
+
const ragIngestTool = {
|
|
6
|
+
name: 'rag_ingest',
|
|
7
|
+
description: '将文档导入知识库。path 为文件路径,内容会被分块、向量化后存储。',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: { path: { type: 'string', description: '文档路径' } },
|
|
11
|
+
required: ['path'],
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
},
|
|
14
|
+
isConcurrencySafe: false,
|
|
15
|
+
isReadOnly: false,
|
|
16
|
+
execute: async ({ path }) => {
|
|
17
|
+
try {
|
|
18
|
+
const text = fs.readFileSync(path, 'utf-8');
|
|
19
|
+
const chunks = chunkDocument(path, text);
|
|
20
|
+
const embeddings = await embed(embedFn, chunks.map(c => c.text));
|
|
21
|
+
vectorStore.addBatch(chunks.map((c, i) => ({ chunk: c, embedding: embeddings[i] })));
|
|
22
|
+
return `已导入 ${chunks.length} 个文档片段(来源: ${path})。知识库共 ${vectorStore.size()} 个片段。`;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return `导入失败: ${e.message}`;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const ragSearchTool = {
|
|
30
|
+
name: 'rag_search',
|
|
31
|
+
description: '从知识库中搜索相关信息。返回最相关的文档片段。',
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
query: { type: 'string', description: '搜索查询' },
|
|
36
|
+
top_k: { type: 'number', description: '返回结果数量(默认 5)' },
|
|
37
|
+
},
|
|
38
|
+
required: ['query'],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
isConcurrencySafe: true,
|
|
42
|
+
isReadOnly: true,
|
|
43
|
+
execute: async ({ query, top_k }) => {
|
|
44
|
+
if (vectorStore.size() === 0)
|
|
45
|
+
return '知识库为空,请先使用 rag_ingest 导入文档。';
|
|
46
|
+
// const results = await hybridSearch(vectorStore, embedFn, query, top_k || 5);
|
|
47
|
+
const results = await vectorStore.hybridSearch(embedFn, query, top_k || 5);
|
|
48
|
+
if (results.length === 0)
|
|
49
|
+
return `没有找到与 "${query}" 相关的内容。`;
|
|
50
|
+
return results.map((r, i) => `[${i + 1}] 来源: ${r.chunk.source} | 综合分: ${r.score.toFixed(3)} (向量: ${r.vectorScore.toFixed(2)}, 关键词: ${r.keywordScore.toFixed(2)})\n${r.chunk.text.slice(0, 500)}`).join('\n\n---\n\n');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return [ragIngestTool, ragSearchTool];
|
|
54
|
+
}
|