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.
@@ -187,4 +187,167 @@ const kimi = {
187
187
  return {};
188
188
  },
189
189
  };
190
- export const adapters = { codex, claude, pi, kimi };
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-bridge.js';
4
- import { DingTalkRegistry } from './dingtalk.js';
5
- import { FeishuRegistry } from './feishu.js';
6
- import { FeishuBridge } from './feishu-bridge.js';
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: 'sampleText', msgParam: JSON.stringify({ content }),
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: { receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text }) },
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
- const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../web/dist');
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 { content } = request.body || {};
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
- await dingtalk.sendToSession(session.dingtalkAppId, session.id, content.trim());
283
- response.json(sessions.recordDingTalkMessage(session.id, content.trim()));
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 { content } = request.body || {};
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
- await feishu.sendToSession(session.feishuAppId, session.id, content.trim());
301
- response.json(sessions.recordFeishuMessage(session.id, content.trim()));
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 './adapters.js';
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
- this.append(session, isWarning(line) ? 'warning' : 'error', line);
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 = adapter.read(line.trim());
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 = adapter.read(pending.trim());
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-muster",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A local web interface for managing coding agents, projects, sessions, and DingTalk/Feishu integrations.",
5
5
  "author": "sexyAmole",
6
6
  "license": "UNLICENSED",