agent-muster 0.1.2 → 0.1.4
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/dist/{adapters.js → agents/adapters.js} +164 -1
- package/dist/agents/management.js +111 -0
- package/dist/{agents.js → agents/registry.js} +52 -2
- package/dist/cli.js +8 -8
- package/dist/{dingtalk-bridge.js → integrations/dingtalk/bridge.js} +2 -0
- package/dist/{dingtalk.js → integrations/dingtalk/registry.js} +1 -1
- package/dist/{feishu.js → integrations/feishu/registry.js} +9 -4
- package/dist/{server.js → server/index.js} +54 -11
- package/dist/{sessions.js → sessions/manager.js} +26 -5
- package/package.json +1 -1
- package/web/dist/assets/index-011NfKCC.js +84 -0
- package/web/dist/assets/index-hevu5YqJ.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-5eptghP4.js +0 -83
- package/web/dist/assets/index-B-OcUiqP.css +0 -1
- /package/dist/{feishu-bridge.js → integrations/feishu/bridge.js} +0 -0
- /package/dist/{projects.js → projects/registry.js} +0 -0
|
@@ -187,4 +187,167 @@ const kimi = {
|
|
|
187
187
|
return {};
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
|
-
|
|
190
|
+
const gemini = {
|
|
191
|
+
id: 'gemini',
|
|
192
|
+
launch(prompt, externalSessionId, model, images) {
|
|
193
|
+
return {
|
|
194
|
+
command: 'gemini',
|
|
195
|
+
args: ['--output-format', 'stream-json', '--approval-mode', 'auto_edit', '--skip-trust', ...(externalSessionId ? ['--resume', externalSessionId] : []), ...(model ? ['--model', model] : []), '-p', [prompt, ...(images?.map(image => `@${image.path}`) || [])].join('\n')],
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
read(line) {
|
|
199
|
+
const event = json(line);
|
|
200
|
+
if (!event)
|
|
201
|
+
return {};
|
|
202
|
+
if (event.type === 'init')
|
|
203
|
+
return { externalSessionId: string(event.session_id) };
|
|
204
|
+
if (event.type === 'message' && event.role === 'assistant')
|
|
205
|
+
return { text: string(event.content) };
|
|
206
|
+
if (event.type === 'tool_use')
|
|
207
|
+
return { tools: [{ name: string(event.tool_name) || 'tool', detail: detail(event.parameters) }] };
|
|
208
|
+
if (event.type === 'error' && event.severity === 'error')
|
|
209
|
+
return { error: string(event.message) };
|
|
210
|
+
if (event.type === 'result') {
|
|
211
|
+
const stats = object(event.stats);
|
|
212
|
+
return {
|
|
213
|
+
error: event.status === 'error' ? string(object(event.error)?.message) || 'Gemini 执行失败' : undefined,
|
|
214
|
+
usage: stats && hasTokens(stats, ['input_tokens', 'output_tokens']) ? {
|
|
215
|
+
inputTokens: tokens(stats.input_tokens), outputTokens: tokens(stats.output_tokens),
|
|
216
|
+
} : undefined,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {};
|
|
220
|
+
},
|
|
221
|
+
createReader() {
|
|
222
|
+
let text = '';
|
|
223
|
+
const edits = new Map();
|
|
224
|
+
return line => {
|
|
225
|
+
const event = json(line);
|
|
226
|
+
const result = gemini.read(line);
|
|
227
|
+
if (event?.type === 'message' && event.role === 'assistant') {
|
|
228
|
+
text += result.text || '';
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
if (event?.type === 'tool_use') {
|
|
232
|
+
const path = string(object(event.parameters)?.file_path);
|
|
233
|
+
const id = string(event.tool_id);
|
|
234
|
+
if (path && id && (event.tool_name === 'write_file' || event.tool_name === 'replace')) {
|
|
235
|
+
edits.set(id, { path, kind: 'update' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (event?.type === 'tool_result') {
|
|
239
|
+
const id = string(event.tool_id);
|
|
240
|
+
const change = id ? edits.get(id) : undefined;
|
|
241
|
+
if (id)
|
|
242
|
+
edits.delete(id);
|
|
243
|
+
if (change && event.status === 'success')
|
|
244
|
+
result.changes = [change];
|
|
245
|
+
}
|
|
246
|
+
if (event?.type === 'tool_use' || event?.type === 'result' || (event?.type === 'error' && event.severity === 'error')) {
|
|
247
|
+
result.text = text || undefined;
|
|
248
|
+
text = '';
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const opencode = {
|
|
255
|
+
id: 'opencode',
|
|
256
|
+
launch(prompt, externalSessionId, model, images) {
|
|
257
|
+
return {
|
|
258
|
+
command: 'opencode',
|
|
259
|
+
args: ['run', '--format', 'json', ...(externalSessionId ? ['--session', externalSessionId] : []), ...(model ? ['--model', model] : []), ...(images?.flatMap(image => ['--file', image.path]) || [])],
|
|
260
|
+
prompt,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
readError(line) {
|
|
264
|
+
if (/^ProviderModelNotFoundError:/.test(line.trim())) {
|
|
265
|
+
return 'OpenCode 配置的模型不存在,请选择可用模型,或修改 OpenCode 的默认模型配置。';
|
|
266
|
+
}
|
|
267
|
+
if (/^\w*Error:/.test(line.trim()))
|
|
268
|
+
return line;
|
|
269
|
+
},
|
|
270
|
+
read(line) {
|
|
271
|
+
const event = json(line);
|
|
272
|
+
if (!event)
|
|
273
|
+
return {};
|
|
274
|
+
const externalSessionId = string(event.sessionID);
|
|
275
|
+
const part = object(event.part);
|
|
276
|
+
if (event.type === 'text')
|
|
277
|
+
return { externalSessionId, text: string(part?.text) };
|
|
278
|
+
if (event.type === 'tool_use') {
|
|
279
|
+
const state = object(part?.state);
|
|
280
|
+
const input = object(state?.input);
|
|
281
|
+
const path = string(input?.filePath);
|
|
282
|
+
const metadata = object(state?.metadata);
|
|
283
|
+
const filediff = object(metadata?.filediff);
|
|
284
|
+
const changes = [];
|
|
285
|
+
if (state?.status === 'completed') {
|
|
286
|
+
if (path && (part?.tool === 'write' || part?.tool === 'edit'))
|
|
287
|
+
changes.push({ path, kind: 'update', diff: string(filediff?.diff) });
|
|
288
|
+
if (part?.tool === 'apply_patch' && Array.isArray(metadata?.files)) {
|
|
289
|
+
for (const value of metadata.files) {
|
|
290
|
+
const file = object(value);
|
|
291
|
+
const path = string(file?.filePath);
|
|
292
|
+
if (path)
|
|
293
|
+
changes.push({ path, kind: string(file?.type) || 'update', diff: string(file?.patch) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { externalSessionId, tools: [{ name: string(part?.tool) || 'tool', detail: detail(state) }], changes };
|
|
298
|
+
}
|
|
299
|
+
if (event.type === 'step_finish') {
|
|
300
|
+
const usage = object(part?.tokens);
|
|
301
|
+
const cache = object(usage?.cache);
|
|
302
|
+
return { externalSessionId, usage: usage && hasTokens(usage, ['input', 'output']) ? {
|
|
303
|
+
inputTokens: tokens(usage.input) + tokens(cache?.read) + tokens(cache?.write),
|
|
304
|
+
outputTokens: tokens(usage.output) + tokens(usage.reasoning),
|
|
305
|
+
} : undefined };
|
|
306
|
+
}
|
|
307
|
+
if (event.type === 'error')
|
|
308
|
+
return { externalSessionId, error: string(object(object(event.error)?.data)?.message) || string(object(event.error)?.name) || 'OpenCode 执行失败' };
|
|
309
|
+
return { externalSessionId };
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
const cursor = {
|
|
313
|
+
id: 'cursor',
|
|
314
|
+
launch(prompt, externalSessionId, model, images) {
|
|
315
|
+
if (images?.length)
|
|
316
|
+
throw new Error('当前 Cursor CLI 接入不支持图片附件输入');
|
|
317
|
+
return {
|
|
318
|
+
command: 'cursor-agent',
|
|
319
|
+
args: ['--print', '--force', '--output-format', 'stream-json', ...(externalSessionId ? ['--resume', externalSessionId] : []), ...(model ? ['--model', model] : []), '--', prompt],
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
read(line) {
|
|
323
|
+
const event = json(line);
|
|
324
|
+
if (!event)
|
|
325
|
+
return {};
|
|
326
|
+
const externalSessionId = string(event.session_id);
|
|
327
|
+
if (event.type === 'assistant') {
|
|
328
|
+
const message = object(event.message);
|
|
329
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
330
|
+
const text = content.flatMap(value => {
|
|
331
|
+
const block = object(value);
|
|
332
|
+
return block?.type === 'text' && typeof block.text === 'string' ? [block.text] : [];
|
|
333
|
+
}).join('');
|
|
334
|
+
return { externalSessionId, text: text || undefined };
|
|
335
|
+
}
|
|
336
|
+
if (event.type === 'tool_call' && event.subtype === 'completed') {
|
|
337
|
+
const calls = object(event.tool_call);
|
|
338
|
+
const changes = [];
|
|
339
|
+
const tools = Object.entries(calls || {}).map(([name, value]) => {
|
|
340
|
+
const call = object(value);
|
|
341
|
+
const path = string(object(call?.args)?.path);
|
|
342
|
+
if (path && name === 'writeToolCall' && object(object(call?.result)?.success))
|
|
343
|
+
changes.push({ path, kind: 'update' });
|
|
344
|
+
return { name, detail: detail(call) };
|
|
345
|
+
});
|
|
346
|
+
return { externalSessionId, tools, changes };
|
|
347
|
+
}
|
|
348
|
+
if (event.type === 'result')
|
|
349
|
+
return { externalSessionId, error: event.is_error ? string(event.result) || 'Cursor 执行失败' : undefined };
|
|
350
|
+
return { externalSessionId };
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
export const adapters = { codex, claude, pi, kimi, gemini, opencode, cursor };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { executablePath } from './registry.js';
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
9
|
+
const packages = {
|
|
10
|
+
codex: '@openai/codex',
|
|
11
|
+
claude: '@anthropic-ai/claude-code',
|
|
12
|
+
pi: '@earendil-works/pi-coding-agent',
|
|
13
|
+
kimi: '@moonshot-ai/kimi-code',
|
|
14
|
+
gemini: '@google/gemini-cli',
|
|
15
|
+
opencode: 'opencode-ai',
|
|
16
|
+
};
|
|
17
|
+
const cursorDirectory = join(homedir(), '.local', 'share', 'cursor-agent');
|
|
18
|
+
export class AgentManagement {
|
|
19
|
+
registry;
|
|
20
|
+
busy = false;
|
|
21
|
+
constructor(registry) {
|
|
22
|
+
this.registry = registry;
|
|
23
|
+
}
|
|
24
|
+
get operating() { return this.busy; }
|
|
25
|
+
async list() {
|
|
26
|
+
const { stdout } = await execute(npmCommand, ['root', '--global'], { shell: process.platform === 'win32', timeout: 10000 });
|
|
27
|
+
const root = resolve(stdout.trim());
|
|
28
|
+
return Promise.all(this.registry.list().map(async (agent) => {
|
|
29
|
+
const path = await executablePath(agent.command);
|
|
30
|
+
if (!path) {
|
|
31
|
+
const supported = agent.id !== 'cursor' || process.platform === 'darwin' || process.platform === 'linux';
|
|
32
|
+
return { ...agent, management: supported ? agent.id === 'cursor' ? 'cursor' : 'npm' : undefined, managementError: supported ? undefined : 'Cursor CLI 仅支持 macOS、Linux 和 WSL' };
|
|
33
|
+
}
|
|
34
|
+
const target = await realpath(path);
|
|
35
|
+
if (agent.id === 'cursor' && target.startsWith(cursorDirectory + sep) && path === join(homedir(), '.local', 'bin', 'cursor-agent')) {
|
|
36
|
+
return { ...agent, path, management: 'cursor' };
|
|
37
|
+
}
|
|
38
|
+
const packageName = packages[agent.id];
|
|
39
|
+
if (packageName && target.startsWith(join(root, packageName) + sep))
|
|
40
|
+
return { ...agent, path, management: 'npm' };
|
|
41
|
+
return { ...agent, path, managementError: '当前安装不由本面板支持的安装器管理,请使用原安装方式更新或卸载' };
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async refresh() {
|
|
45
|
+
if (this.busy)
|
|
46
|
+
throw new Error('正在执行 Agent 操作,请稍后重试');
|
|
47
|
+
this.busy = true;
|
|
48
|
+
try {
|
|
49
|
+
await this.registry.scan();
|
|
50
|
+
return await this.list();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.busy = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async run(id, action) {
|
|
57
|
+
if (this.busy)
|
|
58
|
+
throw new Error('正在执行 Agent 操作,请稍后重试');
|
|
59
|
+
this.busy = true;
|
|
60
|
+
try {
|
|
61
|
+
const agent = (await this.list()).find(item => item.id === id);
|
|
62
|
+
if (!agent)
|
|
63
|
+
throw new Error('Agent 不存在');
|
|
64
|
+
if (!agent.management)
|
|
65
|
+
throw new Error(agent.managementError);
|
|
66
|
+
if (action === 'install' && agent.installed)
|
|
67
|
+
throw new Error('Agent 已安装');
|
|
68
|
+
if (action !== 'install' && !agent.installed)
|
|
69
|
+
throw new Error('Agent 尚未安装');
|
|
70
|
+
let output;
|
|
71
|
+
if (agent.management === 'npm') {
|
|
72
|
+
const args = action === 'uninstall' ? ['uninstall', '--global', packages[id]] : ['install', '--global', `${packages[id]}@latest`];
|
|
73
|
+
const result = await execute(npmCommand, args, { shell: process.platform === 'win32', timeout: 300000, maxBuffer: 5_000_000 });
|
|
74
|
+
output = result.stdout + result.stderr;
|
|
75
|
+
}
|
|
76
|
+
else if (action === 'uninstall') {
|
|
77
|
+
const bin = join(homedir(), '.local', 'bin');
|
|
78
|
+
const alias = await executablePath('agent');
|
|
79
|
+
if (alias === join(bin, 'agent') && (await realpath(alias)).startsWith(cursorDirectory + sep))
|
|
80
|
+
await rm(alias);
|
|
81
|
+
await rm(join(bin, 'cursor-agent'));
|
|
82
|
+
await rm(cursorDirectory, { recursive: true });
|
|
83
|
+
output = 'Cursor CLI 已卸载';
|
|
84
|
+
}
|
|
85
|
+
else if (action === 'update') {
|
|
86
|
+
const result = await execute(agent.path, ['update'], { timeout: 300000, maxBuffer: 5_000_000 });
|
|
87
|
+
output = result.stdout + result.stderr;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (!process.env.PATH?.split(delimiter).includes(join(homedir(), '.local', 'bin')))
|
|
91
|
+
throw new Error('请先将 ~/.local/bin 加入 PATH 并重新启动 Agent Muster');
|
|
92
|
+
const result = await execute('bash', ['-o', 'pipefail', '-c', 'curl -fsSL https://cursor.com/install | bash'], { timeout: 300000, maxBuffer: 5_000_000 });
|
|
93
|
+
output = result.stdout + result.stderr;
|
|
94
|
+
}
|
|
95
|
+
await this.registry.scan();
|
|
96
|
+
const agents = await this.list();
|
|
97
|
+
const updated = agents.find(item => item.id === id);
|
|
98
|
+
if (updated.installed !== (action !== 'uninstall'))
|
|
99
|
+
throw new Error(action === 'uninstall' ? '卸载后仍检测到 Agent,请检查 PATH 中的其他安装' : '安装后未检测到 Agent,请检查安装输出和 PATH');
|
|
100
|
+
return { agents, output: output.replace(/\u001b\[[0-9;]*m/g, '').trim() };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
await this.registry.scan();
|
|
104
|
+
const failure = error;
|
|
105
|
+
throw new Error(failure.stderr?.trim() || failure.message);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.busy = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -11,8 +11,11 @@ const definitions = [
|
|
|
11
11
|
{ id: 'claude', name: 'Claude Code', command: 'claude' },
|
|
12
12
|
{ id: 'pi', name: 'Pi', command: 'pi' },
|
|
13
13
|
{ id: 'kimi', name: 'Kimi', command: 'kimi' },
|
|
14
|
+
{ id: 'gemini', name: 'Gemini CLI', command: 'gemini' },
|
|
15
|
+
{ id: 'opencode', name: 'OpenCode', command: 'opencode' },
|
|
16
|
+
{ id: 'cursor', name: 'Cursor', command: 'cursor-agent' },
|
|
14
17
|
];
|
|
15
|
-
async function executablePath(command) {
|
|
18
|
+
export async function executablePath(command) {
|
|
16
19
|
const pathValue = process.env.PATH;
|
|
17
20
|
if (!pathValue)
|
|
18
21
|
return undefined;
|
|
@@ -98,6 +101,25 @@ async function availableModels(agent) {
|
|
|
98
101
|
const catalog = JSON.parse(stdout);
|
|
99
102
|
return Object.entries(catalog.models).map(([id, model]) => ({ id, name: model.displayName || id, contextWindow: model.maxContextSize }));
|
|
100
103
|
}
|
|
104
|
+
if (agent === 'gemini') {
|
|
105
|
+
return ['auto-gemini-3', 'auto-gemini-2.5', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-3.5-flash', 'gemini-3.1-flash-lite'].map(id => ({ id, name: id }));
|
|
106
|
+
}
|
|
107
|
+
if (agent === 'opencode') {
|
|
108
|
+
const { stdout } = await execFileAsync('opencode', ['models', '--verbose'], { timeout: 10000, maxBuffer: 5_000_000 });
|
|
109
|
+
return stdout.trim().split(/\r?\n(?=[\w.-]+\/[^\s]+\r?$)/m).map(entry => {
|
|
110
|
+
const end = entry.indexOf('\n');
|
|
111
|
+
const id = entry.slice(0, end).trim();
|
|
112
|
+
const model = JSON.parse(entry.slice(end + 1));
|
|
113
|
+
return { id, name: `${model.name} (${id})`, contextWindow: model.limit.context };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (agent === 'cursor') {
|
|
117
|
+
const { stdout } = await execFileAsync('cursor-agent', ['--list-models'], { timeout: 10000 });
|
|
118
|
+
return stdout.replace(/\u001b\[[0-9;]*m/g, '').split(/\r?\n/).flatMap(line => {
|
|
119
|
+
const match = /^\s*([\w.-]+)\s+-\s+(.+?)(?:\s+\((?:current|default)\))?\s*$/.exec(line);
|
|
120
|
+
return match ? [{ id: match[1], name: match[2] }] : [];
|
|
121
|
+
});
|
|
122
|
+
}
|
|
101
123
|
return [];
|
|
102
124
|
}
|
|
103
125
|
async function configuredModel(agent) {
|
|
@@ -117,17 +139,45 @@ async function configuredModel(agent) {
|
|
|
117
139
|
const { stdout } = await execFileAsync('kimi', ['provider', 'list'], { timeout: 5000 });
|
|
118
140
|
return /^Default model:\s*(\S+)/m.exec(stdout)?.[1];
|
|
119
141
|
}
|
|
142
|
+
if (agent === 'gemini') {
|
|
143
|
+
if (process.env.GEMINI_MODEL)
|
|
144
|
+
return process.env.GEMINI_MODEL;
|
|
145
|
+
const config = JSON.parse(await readFile(join(homedir(), '.gemini', 'settings.json'), 'utf8'));
|
|
146
|
+
return config.model?.name;
|
|
147
|
+
}
|
|
148
|
+
if (agent === 'opencode') {
|
|
149
|
+
const { stdout } = await execFileAsync('opencode', ['debug', 'config'], { timeout: 10000 });
|
|
150
|
+
return JSON.parse(stdout).model;
|
|
151
|
+
}
|
|
152
|
+
if (agent === 'cursor') {
|
|
153
|
+
const directory = process.env.CURSOR_CONFIG_DIR || (process.env.XDG_CONFIG_HOME && process.platform !== 'darwin' && process.platform !== 'win32' ? join(process.env.XDG_CONFIG_HOME, 'cursor') : join(homedir(), '.cursor'));
|
|
154
|
+
const config = JSON.parse(await readFile(join(directory, 'cli-config.json'), 'utf8'));
|
|
155
|
+
return config.model?.displayModelId;
|
|
156
|
+
}
|
|
120
157
|
}
|
|
121
158
|
export class AgentRegistry {
|
|
122
159
|
agents = new Map();
|
|
123
160
|
async scan() {
|
|
124
161
|
const agents = await Promise.all(definitions.map(async (definition) => {
|
|
125
162
|
const installed = Boolean(await executablePath(definition.command));
|
|
163
|
+
let version;
|
|
164
|
+
let versionError;
|
|
165
|
+
if (installed) {
|
|
166
|
+
try {
|
|
167
|
+
const { stdout } = await execFileAsync(definition.command, ['--version'], { shell: process.platform === 'win32', timeout: 10000 });
|
|
168
|
+
version = stdout.replace(/\u001b\[[0-9;]*m/g, '').trim();
|
|
169
|
+
if (!version)
|
|
170
|
+
versionError = '命令未返回版本信息';
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
versionError = error.message;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
126
176
|
const [models, defaultModel] = installed ? await Promise.all([
|
|
127
177
|
availableModels(definition.id).catch(() => []),
|
|
128
178
|
configuredModel(definition.id).catch(() => undefined),
|
|
129
179
|
]) : [[], undefined];
|
|
130
|
-
return { ...definition, installed, models, defaultModel };
|
|
180
|
+
return { ...definition, installed, version, versionError, models, defaultModel };
|
|
131
181
|
}));
|
|
132
182
|
this.agents = new Map(agents.map(agent => [agent.id, agent]));
|
|
133
183
|
return agents;
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { AgentRegistry } from './agents.js';
|
|
3
|
-
import { DingTalkBridge } from './dingtalk
|
|
4
|
-
import { DingTalkRegistry } from './dingtalk.js';
|
|
5
|
-
import { FeishuRegistry } from './feishu.js';
|
|
6
|
-
import { FeishuBridge } from './feishu
|
|
7
|
-
import { ProjectRegistry } from './projects.js';
|
|
8
|
-
import { SessionManager } from './sessions.js';
|
|
9
|
-
import { startServer } from './server.js';
|
|
2
|
+
import { AgentRegistry } from './agents/registry.js';
|
|
3
|
+
import { DingTalkBridge } from './integrations/dingtalk/bridge.js';
|
|
4
|
+
import { DingTalkRegistry } from './integrations/dingtalk/registry.js';
|
|
5
|
+
import { FeishuRegistry } from './integrations/feishu/registry.js';
|
|
6
|
+
import { FeishuBridge } from './integrations/feishu/bridge.js';
|
|
7
|
+
import { ProjectRegistry } from './projects/registry.js';
|
|
8
|
+
import { SessionManager } from './sessions/manager.js';
|
|
9
|
+
import { startServer } from './server/index.js';
|
|
10
10
|
import { checkForUpdates, updatePackage } from './updates.js';
|
|
11
11
|
function openBrowser(url) {
|
|
12
12
|
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
@@ -84,6 +84,8 @@ export class DingTalkBridge {
|
|
|
84
84
|
throw new Error('钉钉图片消息缺少下载码');
|
|
85
85
|
if (binding.agent === 'kimi')
|
|
86
86
|
throw new Error('当前 Kimi CLI 不支持图片附件输入');
|
|
87
|
+
if (binding.agent === 'cursor')
|
|
88
|
+
throw new Error('当前 Cursor CLI 接入不支持图片附件输入');
|
|
87
89
|
images.push(await this.dingtalk.downloadImage(appId, message.robotCode, downloadCode));
|
|
88
90
|
}
|
|
89
91
|
else
|
|
@@ -138,7 +138,7 @@ export class DingTalkRegistry extends EventEmitter {
|
|
|
138
138
|
const response = await fetch(`${openApiBase}${path}`, {
|
|
139
139
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'x-acs-dingtalk-access-token': token },
|
|
140
140
|
body: JSON.stringify({
|
|
141
|
-
robotCode: target.robotCode, msgKey: '
|
|
141
|
+
robotCode: target.robotCode, msgKey: 'sampleMarkdown', msgParam: JSON.stringify({ title: 'Agent 执行结果', text: content }),
|
|
142
142
|
...(target.type === '1' ? { userIds: [target.userId] } : { openConversationId: target.openConversationId }),
|
|
143
143
|
}),
|
|
144
144
|
redirect: 'error', signal: AbortSignal.timeout(10000), dispatcher,
|
|
@@ -121,11 +121,11 @@ export class FeishuRegistry extends EventEmitter {
|
|
|
121
121
|
this.clients.set(app.id, { api, ws });
|
|
122
122
|
void ws.start({ eventDispatcher }).catch(error => console.error(`飞书应用 ${app.clientId} 消息监听失败:`, error));
|
|
123
123
|
}
|
|
124
|
-
async send(id, chatId, content) {
|
|
124
|
+
async send(id, chatId, content, format = 'text') {
|
|
125
125
|
const client = this.clients.get(id);
|
|
126
126
|
if (!client)
|
|
127
127
|
throw new Error('飞书应用消息监听未启动');
|
|
128
|
-
// 按 UTF-8
|
|
128
|
+
// 按 UTF-8 字节分段,避免超过飞书消息大小限制。
|
|
129
129
|
let chunk = '';
|
|
130
130
|
let size = 0;
|
|
131
131
|
const chunks = [];
|
|
@@ -144,7 +144,12 @@ export class FeishuRegistry extends EventEmitter {
|
|
|
144
144
|
for (const text of chunks) {
|
|
145
145
|
const result = await client.api.im.message.create({
|
|
146
146
|
params: { receive_id_type: 'chat_id' },
|
|
147
|
-
data: {
|
|
147
|
+
data: {
|
|
148
|
+
receive_id: chatId, msg_type: format === 'markdown' ? 'interactive' : 'text',
|
|
149
|
+
content: JSON.stringify(format === 'markdown'
|
|
150
|
+
? { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text }] } }
|
|
151
|
+
: { text }),
|
|
152
|
+
},
|
|
148
153
|
});
|
|
149
154
|
if (result.code !== 0)
|
|
150
155
|
throw new Error(`飞书消息发送失败:${result.msg} (${result.code})`);
|
|
@@ -154,7 +159,7 @@ export class FeishuRegistry extends EventEmitter {
|
|
|
154
159
|
const chatId = Object.entries(this.getApp(id).conversations).find(([, value]) => value === sessionId)?.[0];
|
|
155
160
|
if (!chatId)
|
|
156
161
|
throw new Error('此对话尚无飞书发送目标');
|
|
157
|
-
await this.send(id, chatId, content);
|
|
162
|
+
await this.send(id, chatId, content, 'markdown');
|
|
158
163
|
}
|
|
159
164
|
async bind(id, binding, projects, agents) {
|
|
160
165
|
if (binding.project !== undefined && binding.project !== null && !projects.includes(binding.project))
|
|
@@ -6,7 +6,8 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
|
-
|
|
9
|
+
import { AgentManagement } from '../agents/management.js';
|
|
10
|
+
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../web/dist');
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
async function chooseProjectDirectory() {
|
|
12
13
|
const platform = process.platform;
|
|
@@ -45,6 +46,36 @@ export async function startServer(port, registry, projects, sessions, dingtalk,
|
|
|
45
46
|
});
|
|
46
47
|
app.use('/api', express.json({ limit: '128kb' }));
|
|
47
48
|
app.get('/api/agents', (_request, response) => response.json(registry.list()));
|
|
49
|
+
const agentManagement = new AgentManagement(registry);
|
|
50
|
+
app.get('/api/agents/management', async (_request, response) => {
|
|
51
|
+
try {
|
|
52
|
+
response.json(await agentManagement.list());
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
response.status(500).json({ error: error.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
app.post('/api/agents/refresh', async (_request, response) => {
|
|
59
|
+
try {
|
|
60
|
+
response.json(await agentManagement.refresh());
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
response.status(400).json({ error: error.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.post('/api/agents/:id/actions', async (request, response) => {
|
|
67
|
+
try {
|
|
68
|
+
const { action } = request.body || {};
|
|
69
|
+
if (action !== 'install' && action !== 'update' && action !== 'uninstall')
|
|
70
|
+
throw new Error('Agent 操作无效');
|
|
71
|
+
if (sessions.list().some(session => session.agent === request.params.id && (session.status === 'starting' || session.status === 'running')))
|
|
72
|
+
throw new Error('此 Agent 有正在运行的会话,请先停止会话');
|
|
73
|
+
response.json(await agentManagement.run(request.params.id, action));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
response.status(400).json({ error: error.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
48
79
|
app.get('/api/projects', (_request, response) => response.json(projects.list()));
|
|
49
80
|
app.post('/api/projects', async (request, response) => {
|
|
50
81
|
try {
|
|
@@ -241,6 +272,8 @@ export async function startServer(port, registry, projects, sessions, dingtalk,
|
|
|
241
272
|
app.post('/api/sessions', async (request, response) => {
|
|
242
273
|
try {
|
|
243
274
|
const { agent, cwd, prompt, model } = request.body || {};
|
|
275
|
+
if (agentManagement.operating)
|
|
276
|
+
throw new Error('正在执行 Agent 操作,请完成后再创建会话');
|
|
244
277
|
if (typeof agent !== 'string' || !registry.get(agent)?.installed)
|
|
245
278
|
throw new Error('Agent is not installed');
|
|
246
279
|
if (typeof cwd !== 'string' || typeof prompt !== 'string')
|
|
@@ -256,11 +289,15 @@ export async function startServer(port, registry, projects, sessions, dingtalk,
|
|
|
256
289
|
app.post('/api/sessions/:id/messages', (request, response) => {
|
|
257
290
|
try {
|
|
258
291
|
const { content, model } = request.body || {};
|
|
292
|
+
if (agentManagement.operating)
|
|
293
|
+
throw new Error('正在执行 Agent 操作,请完成后再发送消息');
|
|
259
294
|
if (typeof content !== 'string')
|
|
260
295
|
throw new Error('Message is required');
|
|
261
296
|
const session = sessions.get(request.params.id);
|
|
262
297
|
if (!session)
|
|
263
298
|
throw new Error('Session not found');
|
|
299
|
+
if (!registry.get(session.agent)?.installed)
|
|
300
|
+
throw new Error('此 Agent 已卸载,请先重新安装');
|
|
264
301
|
if (model !== undefined && (typeof model !== 'string' || (model && !registry.get(session.agent)?.models.some(item => item.id === model))))
|
|
265
302
|
throw new Error('Invalid model');
|
|
266
303
|
response.json(sessions.send(request.params.id, content, model));
|
|
@@ -271,16 +308,19 @@ export async function startServer(port, registry, projects, sessions, dingtalk,
|
|
|
271
308
|
});
|
|
272
309
|
app.post('/api/sessions/:id/dingtalk/messages', async (request, response) => {
|
|
273
310
|
try {
|
|
274
|
-
const {
|
|
275
|
-
if (typeof content !== 'string' || !content.trim())
|
|
276
|
-
throw new Error('Message is required');
|
|
311
|
+
const { eventId } = request.body || {};
|
|
277
312
|
const session = sessions.get(request.params.id);
|
|
278
313
|
if (!session)
|
|
279
314
|
throw new Error('Session not found');
|
|
280
315
|
if (!session.dingtalkAppId)
|
|
281
316
|
throw new Error('此对话未关联钉钉应用');
|
|
282
|
-
|
|
283
|
-
|
|
317
|
+
const event = session.events.find(item => item.id === eventId && item.type === 'output');
|
|
318
|
+
if (!event?.text.trim())
|
|
319
|
+
throw new Error('助手结果不存在或内容为空');
|
|
320
|
+
if (event.pushedToIm)
|
|
321
|
+
throw new Error('此结果已推送到 IM');
|
|
322
|
+
await dingtalk.sendToSession(session.dingtalkAppId, session.id, event.text.trim());
|
|
323
|
+
response.json(sessions.markImPushed(session.id, event.id));
|
|
284
324
|
}
|
|
285
325
|
catch (error) {
|
|
286
326
|
console.error(`DingTalk message sending failed for session ${request.params.id}:`, error);
|
|
@@ -289,16 +329,19 @@ export async function startServer(port, registry, projects, sessions, dingtalk,
|
|
|
289
329
|
});
|
|
290
330
|
app.post('/api/sessions/:id/feishu/messages', async (request, response) => {
|
|
291
331
|
try {
|
|
292
|
-
const {
|
|
293
|
-
if (typeof content !== 'string' || !content.trim())
|
|
294
|
-
throw new Error('请输入消息内容');
|
|
332
|
+
const { eventId } = request.body || {};
|
|
295
333
|
const session = sessions.get(request.params.id);
|
|
296
334
|
if (!session)
|
|
297
335
|
throw new Error('会话不存在');
|
|
298
336
|
if (!session.feishuAppId)
|
|
299
337
|
throw new Error('此对话未关联飞书应用');
|
|
300
|
-
|
|
301
|
-
|
|
338
|
+
const event = session.events.find(item => item.id === eventId && item.type === 'output');
|
|
339
|
+
if (!event?.text.trim())
|
|
340
|
+
throw new Error('助手结果不存在或内容为空');
|
|
341
|
+
if (event.pushedToIm)
|
|
342
|
+
throw new Error('此结果已推送到 IM');
|
|
343
|
+
await feishu.sendToSession(session.feishuAppId, session.id, event.text.trim());
|
|
344
|
+
response.json(sessions.markImPushed(session.id, event.id));
|
|
302
345
|
}
|
|
303
346
|
catch (error) {
|
|
304
347
|
console.error(`飞书会话 ${request.params.id} 消息发送失败:`, error);
|
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, isAbsolute, relative, resolve } from 'node:path';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { adapters } from '
|
|
7
|
+
import { adapters } from '../agents/adapters.js';
|
|
8
8
|
const sessionDirectory = join(homedir(), '.agent-muster', 'sessions');
|
|
9
9
|
const legacyDataFile = join(homedir(), '.agent-muster', 'sessions.json');
|
|
10
10
|
function sessionFile(id) {
|
|
@@ -198,6 +198,18 @@ export class SessionManager extends EventEmitter {
|
|
|
198
198
|
this.append(session, 'feishu_message', content);
|
|
199
199
|
return session;
|
|
200
200
|
}
|
|
201
|
+
markImPushed(id, eventId) {
|
|
202
|
+
const session = this.sessions.get(id);
|
|
203
|
+
if (!session)
|
|
204
|
+
throw new Error('会话不存在');
|
|
205
|
+
const event = session.events.find(item => item.id === eventId && item.type === 'output');
|
|
206
|
+
if (!event)
|
|
207
|
+
throw new Error('助手结果不存在');
|
|
208
|
+
event.pushedToIm = true;
|
|
209
|
+
this.emit(session.id, event);
|
|
210
|
+
this.save(session);
|
|
211
|
+
return session;
|
|
212
|
+
}
|
|
201
213
|
stop(id) {
|
|
202
214
|
const session = this.sessions.get(id);
|
|
203
215
|
if (!session)
|
|
@@ -232,6 +244,7 @@ export class SessionManager extends EventEmitter {
|
|
|
232
244
|
start(session, prompt, images) {
|
|
233
245
|
const adapter = adapters[session.agent];
|
|
234
246
|
const launch = adapter.launch(prompt, session.externalSessionId, session.model, images);
|
|
247
|
+
const read = adapter.createReader?.() || adapter.read;
|
|
235
248
|
const child = spawn(launch.command, launch.args, {
|
|
236
249
|
cwd: session.cwd,
|
|
237
250
|
stdio: 'pipe',
|
|
@@ -246,15 +259,19 @@ export class SessionManager extends EventEmitter {
|
|
|
246
259
|
let stderrPending = '';
|
|
247
260
|
let failed = false;
|
|
248
261
|
const recordStderr = (line) => {
|
|
249
|
-
if (line.trim())
|
|
250
|
-
|
|
262
|
+
if (!line.trim())
|
|
263
|
+
return;
|
|
264
|
+
const error = adapter.readError?.(line);
|
|
265
|
+
if (error)
|
|
266
|
+
failed = true;
|
|
267
|
+
this.append(session, isWarning(line) ? 'warning' : 'error', error || line);
|
|
251
268
|
};
|
|
252
269
|
child.stdout.on('data', (chunk) => {
|
|
253
270
|
pending += chunk.toString('utf8');
|
|
254
271
|
const lines = pending.split('\n');
|
|
255
272
|
pending = lines.pop() || '';
|
|
256
273
|
for (const line of lines) {
|
|
257
|
-
const result =
|
|
274
|
+
const result = read(line.trim());
|
|
258
275
|
if (result.externalSessionId) {
|
|
259
276
|
session.externalSessionId = result.externalSessionId;
|
|
260
277
|
this.save(session);
|
|
@@ -293,7 +310,11 @@ export class SessionManager extends EventEmitter {
|
|
|
293
310
|
child.on('close', code => {
|
|
294
311
|
recordStderr(stderrPending);
|
|
295
312
|
if (pending.trim()) {
|
|
296
|
-
const result =
|
|
313
|
+
const result = read(pending.trim());
|
|
314
|
+
if (result.externalSessionId) {
|
|
315
|
+
session.externalSessionId = result.externalSessionId;
|
|
316
|
+
this.save(session);
|
|
317
|
+
}
|
|
297
318
|
if (result.text)
|
|
298
319
|
this.append(session, 'output', result.text);
|
|
299
320
|
if (result.usage)
|
package/package.json
CHANGED