agent-muster 0.1.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
@@ -0,0 +1,190 @@
1
+ function object(value) {
2
+ return value && typeof value === 'object' ? value : undefined;
3
+ }
4
+ function string(value) {
5
+ return typeof value === 'string' ? value : undefined;
6
+ }
7
+ function tokens(value) {
8
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
9
+ }
10
+ function hasTokens(usage, keys) {
11
+ return keys.some(key => typeof usage[key] === 'number');
12
+ }
13
+ function detail(value) {
14
+ if (value === undefined || value === null)
15
+ return undefined;
16
+ return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
17
+ }
18
+ function json(line) {
19
+ try {
20
+ return object(JSON.parse(line));
21
+ }
22
+ catch {
23
+ return { type: 'plain', text: line };
24
+ }
25
+ }
26
+ const codex = {
27
+ id: 'codex',
28
+ launch(prompt, externalSessionId, model, images) {
29
+ const imageArgs = images?.flatMap(image => ['--image', image.path]) || [];
30
+ return {
31
+ command: 'codex',
32
+ args: externalSessionId
33
+ ? ['exec', 'resume', ...imageArgs, '--json', '--skip-git-repo-check', ...(model ? ['--model', model] : []), externalSessionId, '-']
34
+ : ['exec', ...imageArgs, '--json', '--skip-git-repo-check', ...(model ? ['--model', model] : []), '-'],
35
+ prompt,
36
+ };
37
+ },
38
+ read(line) {
39
+ const event = json(line);
40
+ if (!event)
41
+ return {};
42
+ if (event.type === 'thread.started')
43
+ return { externalSessionId: string(event.thread_id) };
44
+ if (event.type === 'turn.completed') {
45
+ const usage = object(event.usage);
46
+ if (usage && hasTokens(usage, ['input_tokens', 'output_tokens']))
47
+ return { usage: { inputTokens: tokens(usage.input_tokens), outputTokens: tokens(usage.output_tokens) }, usageIsTotal: true };
48
+ }
49
+ if (event.type === 'item.completed') {
50
+ const item = object(event.item);
51
+ if (item?.type === 'agent_message')
52
+ return { text: string(item.text) };
53
+ if (item?.type === 'command_execution')
54
+ return { tools: [{ name: `$ ${string(item.command) || 'command'}`, detail: string(item.aggregated_output) }] };
55
+ if (item?.type === 'mcp_tool_call')
56
+ return { tools: [{ name: `${string(item.server) || 'MCP'} / ${string(item.tool) || 'tool'}`, detail: detail(item.result) }] };
57
+ if (item?.type === 'file_change') {
58
+ const changes = item.changes;
59
+ if (Array.isArray(changes))
60
+ return { changes: changes.flatMap(value => {
61
+ const change = object(value);
62
+ const path = string(change?.path);
63
+ return path ? [{ path, kind: string(change?.kind) || 'update', diff: string(change?.unified_diff) }] : [];
64
+ }) };
65
+ const entries = object(changes);
66
+ if (entries)
67
+ return { changes: Object.entries(entries).map(([path, value]) => {
68
+ const change = object(value);
69
+ return { path, kind: string(change?.type) || 'update', diff: string(change?.unified_diff) };
70
+ }) };
71
+ }
72
+ }
73
+ if (event.type === 'error')
74
+ return { error: string(event.message) || 'Codex error' };
75
+ if (event.type === 'plain')
76
+ return { text: string(event.text) };
77
+ return {};
78
+ },
79
+ };
80
+ const claude = {
81
+ id: 'claude',
82
+ launch(prompt, externalSessionId, model, images) {
83
+ return {
84
+ command: 'claude',
85
+ args: ['-p', '--verbose', '--output-format', 'stream-json', ...(images?.length ? ['--input-format', 'stream-json'] : []), ...(externalSessionId ? ['--resume', externalSessionId] : []), ...(model ? ['--model', model] : [])],
86
+ prompt: images?.length ? JSON.stringify({
87
+ type: 'user', message: { role: 'user', content: [
88
+ { type: 'text', text: prompt },
89
+ ...images.map(image => ({ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } })),
90
+ ] },
91
+ }) : prompt,
92
+ };
93
+ },
94
+ read(line) {
95
+ const event = json(line);
96
+ if (!event)
97
+ return {};
98
+ const externalSessionId = string(event.session_id);
99
+ if (event.type === 'assistant') {
100
+ const message = object(event.message);
101
+ const content = Array.isArray(message?.content) ? message.content : [];
102
+ const text = content.map(part => {
103
+ const block = object(part);
104
+ if (block?.type === 'text')
105
+ return string(block.text);
106
+ return undefined;
107
+ }).filter(Boolean).join('\n');
108
+ const tools = content.flatMap(part => {
109
+ const block = object(part);
110
+ return block?.type === 'tool_use' ? [{ name: string(block.name) || 'tool', detail: detail(block.input) }] : [];
111
+ });
112
+ const usage = object(message?.usage);
113
+ return { externalSessionId, text: text || undefined, tools, usage: usage && hasTokens(usage, ['input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens', 'output_tokens']) ? {
114
+ inputTokens: tokens(usage.input_tokens) + tokens(usage.cache_creation_input_tokens) + tokens(usage.cache_read_input_tokens),
115
+ outputTokens: tokens(usage.output_tokens),
116
+ } : undefined };
117
+ }
118
+ if (event.type === 'result') {
119
+ return { externalSessionId, error: event.is_error ? string(event.result) || 'Claude error' : undefined };
120
+ }
121
+ if (event.type === 'plain')
122
+ return { text: string(event.text) };
123
+ return { externalSessionId };
124
+ },
125
+ };
126
+ const pi = {
127
+ id: 'pi',
128
+ launch(prompt, externalSessionId, model, images) {
129
+ return {
130
+ command: 'pi',
131
+ args: ['--mode', 'json', ...(externalSessionId ? ['--session', externalSessionId] : []), ...(model ? ['--model', model] : []), ...(images?.map(image => `@${image.path}`) || [])],
132
+ prompt,
133
+ };
134
+ },
135
+ read(line) {
136
+ const event = json(line);
137
+ if (!event)
138
+ return {};
139
+ if (event.type === 'session')
140
+ return { externalSessionId: string(event.id) };
141
+ if (event.type === 'tool_execution_start')
142
+ return { tools: [{ name: string(event.toolName) || 'tool', detail: detail(event.args) }] };
143
+ if (event.type === 'message_end') {
144
+ const message = object(event.message);
145
+ if (message?.role !== 'assistant')
146
+ return {};
147
+ const content = Array.isArray(message.content) ? message.content : [];
148
+ const text = content.map(part => {
149
+ const block = object(part);
150
+ return block?.type === 'text' ? string(block.text) : undefined;
151
+ }).filter(Boolean).join('\n');
152
+ const usage = object(message.usage);
153
+ return { text: text || undefined, error: string(message.errorMessage), usage: usage && hasTokens(usage, ['input', 'cacheRead', 'cacheWrite', 'output']) ? {
154
+ inputTokens: tokens(usage.input) + tokens(usage.cacheRead) + tokens(usage.cacheWrite),
155
+ outputTokens: tokens(usage.output),
156
+ } : undefined };
157
+ }
158
+ return {};
159
+ },
160
+ };
161
+ const kimi = {
162
+ id: 'kimi',
163
+ launch(prompt, externalSessionId, model) {
164
+ return {
165
+ command: 'kimi',
166
+ args: ['--output-format', 'stream-json', ...(externalSessionId ? ['--session', externalSessionId] : []), ...(model ? ['--model', model] : []), '-p', prompt],
167
+ };
168
+ },
169
+ read(line) {
170
+ const event = json(line);
171
+ if (!event)
172
+ return {};
173
+ if (event.role === 'meta' && event.type === 'session.resume_hint')
174
+ return { externalSessionId: string(event.session_id) };
175
+ if (event.role === 'assistant') {
176
+ const calls = Array.isArray(event.tool_calls) ? event.tool_calls : [];
177
+ const tools = calls.map(call => {
178
+ const invocation = object(object(call)?.function);
179
+ return { name: string(invocation?.name) || 'tool', detail: detail(invocation?.arguments) };
180
+ });
181
+ const usage = object(event.usage);
182
+ return { text: string(event.content), tools, usage: usage && hasTokens(usage, ['prompt_tokens', 'completion_tokens']) ? {
183
+ inputTokens: tokens(usage.prompt_tokens),
184
+ outputTokens: tokens(usage.completion_tokens),
185
+ } : undefined };
186
+ }
187
+ return {};
188
+ },
189
+ };
190
+ export const adapters = { codex, claude, pi, kimi };
package/dist/agents.js ADDED
@@ -0,0 +1,141 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { execFile, spawn } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { createInterface } from 'node:readline';
8
+ const execFileAsync = promisify(execFile);
9
+ const definitions = [
10
+ { id: 'codex', name: 'Codex', command: 'codex' },
11
+ { id: 'claude', name: 'Claude Code', command: 'claude' },
12
+ { id: 'pi', name: 'Pi', command: 'pi' },
13
+ { id: 'kimi', name: 'Kimi', command: 'kimi' },
14
+ ];
15
+ async function executablePath(command) {
16
+ const pathValue = process.env.PATH;
17
+ if (!pathValue)
18
+ return undefined;
19
+ const extensions = process.platform === 'win32' ? ['.exe', '.cmd', '.bat'] : [''];
20
+ for (const directory of pathValue.split(delimiter)) {
21
+ if (!directory)
22
+ continue;
23
+ for (const extension of extensions) {
24
+ const path = join(directory, command + extension);
25
+ try {
26
+ await access(path, constants.X_OK);
27
+ return path;
28
+ }
29
+ catch { /* Continue scanning PATH. */ }
30
+ }
31
+ }
32
+ }
33
+ function contextWindow(value) {
34
+ const match = /^(\d+(?:\.\d+)?)([KM])?$/.exec(value);
35
+ return match ? Math.round(Number(match[1]) * (match[2] === 'M' ? 1_000_000 : match[2] === 'K' ? 1_000 : 1)) : undefined;
36
+ }
37
+ function codexModels() {
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn('codex', ['app-server'], { stdio: ['pipe', 'pipe', 'ignore'] });
40
+ const lines = createInterface({ input: child.stdout });
41
+ let finished = false;
42
+ const finish = (error, models) => {
43
+ if (finished)
44
+ return;
45
+ finished = true;
46
+ clearTimeout(timer);
47
+ lines.close();
48
+ child.kill();
49
+ if (error)
50
+ reject(error);
51
+ else if (models)
52
+ resolve(models);
53
+ };
54
+ const timer = setTimeout(() => finish(new Error('Codex model query timed out')), 15000);
55
+ child.on('error', error => finish(error));
56
+ child.stdin.on('error', error => finish(error));
57
+ child.on('exit', () => finish(new Error('Codex exited before returning models')));
58
+ lines.on('line', line => {
59
+ try {
60
+ const message = JSON.parse(line);
61
+ if (message.error) {
62
+ finish(new Error(message.error.message));
63
+ return;
64
+ }
65
+ if (message.id === 1) {
66
+ child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
67
+ child.stdin.write(`${JSON.stringify({ id: 2, method: 'model/list', params: { limit: 100, includeHidden: true } })}\n`);
68
+ }
69
+ if (message.id === 2 && message.result)
70
+ finish(undefined, message.result.data);
71
+ }
72
+ catch (error) {
73
+ finish(error);
74
+ }
75
+ });
76
+ child.stdin.write(`${JSON.stringify({ id: 1, method: 'initialize', params: { clientInfo: { name: 'agent_muster', version: '0.1.0' } } })}\n`);
77
+ });
78
+ }
79
+ async function availableModels(agent) {
80
+ if (agent === 'claude')
81
+ return ['sonnet', 'opus', 'haiku', 'fable'].map(id => ({ id, name: id }));
82
+ if (agent === 'codex') {
83
+ const models = await codexModels();
84
+ const catalog = JSON.parse(await readFile(join(homedir(), '.codex', 'models_cache.json'), 'utf8'));
85
+ return models.filter(model => model.model !== 'gpt-6.1-sol').map(model => ({ id: model.model, name: model.displayName, contextWindow: catalog.models.find(item => item.slug === model.model)?.context_window }));
86
+ }
87
+ if (agent === 'pi') {
88
+ const { stdout } = await execFileAsync('pi', ['--list-models'], { timeout: 5000 });
89
+ return stdout.trim().split(/\r?\n/).slice(1).flatMap(line => {
90
+ const [provider, model, limit] = line.trim().split(/\s+/);
91
+ if (!provider || !model)
92
+ return [];
93
+ return [{ id: `${provider}/${model}`, name: `${provider}/${model}`, contextWindow: contextWindow(limit) }];
94
+ });
95
+ }
96
+ if (agent === 'kimi') {
97
+ const { stdout } = await execFileAsync('kimi', ['provider', 'list', '--json'], { timeout: 5000 });
98
+ const catalog = JSON.parse(stdout);
99
+ return Object.entries(catalog.models).map(([id, model]) => ({ id, name: model.displayName || id, contextWindow: model.maxContextSize }));
100
+ }
101
+ return [];
102
+ }
103
+ async function configuredModel(agent) {
104
+ if (agent === 'codex') {
105
+ const config = await readFile(join(homedir(), '.codex', 'config.toml'), 'utf8');
106
+ return /^model\s*=\s*['"]([^'"]+)['"]/m.exec(config)?.[1];
107
+ }
108
+ if (agent === 'claude') {
109
+ const config = JSON.parse(await readFile(join(homedir(), '.claude', 'settings.json'), 'utf8'));
110
+ return config.model;
111
+ }
112
+ if (agent === 'pi') {
113
+ const config = JSON.parse(await readFile(join(homedir(), '.pi', 'agent', 'settings.json'), 'utf8'));
114
+ return config.defaultProvider && config.defaultModel ? `${config.defaultProvider}/${config.defaultModel}` : undefined;
115
+ }
116
+ if (agent === 'kimi') {
117
+ const { stdout } = await execFileAsync('kimi', ['provider', 'list'], { timeout: 5000 });
118
+ return /^Default model:\s*(\S+)/m.exec(stdout)?.[1];
119
+ }
120
+ }
121
+ export class AgentRegistry {
122
+ agents = new Map();
123
+ async scan() {
124
+ const agents = await Promise.all(definitions.map(async (definition) => {
125
+ const installed = Boolean(await executablePath(definition.command));
126
+ const [models, defaultModel] = installed ? await Promise.all([
127
+ availableModels(definition.id).catch(() => []),
128
+ configuredModel(definition.id).catch(() => undefined),
129
+ ]) : [[], undefined];
130
+ return { ...definition, installed, models, defaultModel };
131
+ }));
132
+ this.agents = new Map(agents.map(agent => [agent.id, agent]));
133
+ return agents;
134
+ }
135
+ list() {
136
+ return [...this.agents.values()];
137
+ }
138
+ get(id) {
139
+ return this.agents.get(id);
140
+ }
141
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,58 @@
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 { ProjectRegistry } from './projects.js';
6
+ import { SessionManager } from './sessions.js';
7
+ import { startServer } from './server.js';
8
+ function openBrowser(url) {
9
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
10
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
11
+ spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
12
+ }
13
+ async function main() {
14
+ const [command = 'web', ...options] = process.argv.slice(2);
15
+ const registry = new AgentRegistry();
16
+ await registry.scan();
17
+ const sessions = new SessionManager();
18
+ await sessions.load();
19
+ const projects = new ProjectRegistry();
20
+ await projects.load();
21
+ const dingtalk = new DingTalkRegistry();
22
+ await dingtalk.load();
23
+ if (command === 'agents') {
24
+ console.log('Installed Agents\n');
25
+ for (const agent of registry.list())
26
+ console.log(`${agent.name}\nCommand: ${agent.command}\nStatus: ${agent.installed ? 'Available' : 'Unavailable'}\n`);
27
+ return;
28
+ }
29
+ if (command === 'sessions') {
30
+ console.log('ID Agent Project Status');
31
+ for (const session of sessions.list()) {
32
+ console.log(`${session.id.padEnd(43)}${session.agent.padEnd(9)}${(session.cwd.split(/[\\/]/).at(-1) || '').padEnd(23)}${session.status}`);
33
+ }
34
+ return;
35
+ }
36
+ if (command !== 'web')
37
+ throw new Error(`Unknown command: ${command}`);
38
+ for (const { sessionId, appId } of dingtalk.sessionBindings())
39
+ sessions.linkDingTalkSession(sessionId, appId);
40
+ new DingTalkBridge(dingtalk, registry, sessions);
41
+ const portIndex = options.indexOf('--port');
42
+ const port = portIndex >= 0 ? Number(options[portIndex + 1]) : 17321;
43
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
44
+ throw new Error('Port must be 1–65535');
45
+ const server = await startServer(port, registry, projects, sessions, dingtalk);
46
+ dingtalk.startSubscriptions();
47
+ const url = `http://127.0.0.1:${port}`;
48
+ console.log(`Agent Muster started\n\nWeb: ${url}\n\nDetected Agents:`);
49
+ for (const agent of registry.list())
50
+ console.log(`${agent.installed ? '✓' : '✗'} ${agent.name}`);
51
+ console.log('\nPress Ctrl+C to stop');
52
+ if (!options.includes('--no-open'))
53
+ openBrowser(url);
54
+ const shutdown = () => { dingtalk.shutdown(); sessions.shutdown(); server.close(); server.closeAllConnections(); };
55
+ process.once('SIGINT', shutdown);
56
+ process.once('SIGTERM', shutdown);
57
+ }
58
+ main().catch(error => { console.error(error.message); process.exitCode = 1; });
@@ -0,0 +1,129 @@
1
+ const terminalStatuses = new Set(['completed', 'failed', 'stopped']);
2
+ function waitForCompletion(sessions, session) {
3
+ if (terminalStatuses.has(session.status) && !sessions.isRunning(session.id))
4
+ return Promise.resolve();
5
+ return new Promise(resolve => {
6
+ const check = () => {
7
+ if (!terminalStatuses.has(session.status) || sessions.isRunning(session.id))
8
+ return;
9
+ sessions.off(session.id, onEvent);
10
+ sessions.off(`${session.id}:idle`, check);
11
+ resolve();
12
+ };
13
+ const onEvent = (event) => { if (event.type === 'status')
14
+ check(); };
15
+ sessions.on(session.id, onEvent);
16
+ sessions.on(`${session.id}:idle`, check);
17
+ check();
18
+ });
19
+ }
20
+ export class DingTalkBridge {
21
+ dingtalk;
22
+ agents;
23
+ sessions;
24
+ queues = new Map();
25
+ seen = new Set();
26
+ constructor(dingtalk, agents, sessions) {
27
+ this.dingtalk = dingtalk;
28
+ this.agents = agents;
29
+ this.sessions = sessions;
30
+ dingtalk.on('message', event => this.enqueue(event));
31
+ }
32
+ enqueue({ appId, message }) {
33
+ if (!message.msgId || !message.conversationId || !['1', '2'].includes(message.conversationType))
34
+ return;
35
+ const messageKey = `${appId}:${message.msgId}`;
36
+ if (this.seen.has(messageKey))
37
+ return;
38
+ this.seen.add(messageKey);
39
+ if (this.seen.size > 1000)
40
+ this.seen.delete(this.seen.values().next().value);
41
+ const conversation = `${message.conversationType}:${message.conversationId}`;
42
+ const key = `${appId}:${conversation}`;
43
+ const task = (this.queues.get(key) || Promise.resolve())
44
+ .then(() => this.process(appId, conversation, message))
45
+ .catch(async (error) => {
46
+ console.error(`DingTalk message processing failed for ${appId} (message ${message.msgId}, conversation ${conversation}, type ${message.msgtype}):`, error);
47
+ try {
48
+ await this.dingtalk.reply(appId, message.sessionWebhook, 'Agent 执行失败,请在 Agent Muster 中查看会话日志。');
49
+ }
50
+ catch (replyError) {
51
+ console.error(`DingTalk error reply failed for ${appId}:`, replyError);
52
+ }
53
+ });
54
+ this.queues.set(key, task);
55
+ void task.finally(() => { if (this.queues.get(key) === task)
56
+ this.queues.delete(key); });
57
+ }
58
+ async process(appId, conversation, message) {
59
+ if (!['text', 'picture', 'richText'].includes(message.msgtype))
60
+ return;
61
+ if (message.msgtype === 'text' && !message.text.content.trim())
62
+ return;
63
+ const binding = this.dingtalk.getBinding(appId);
64
+ if (!binding?.project || !binding.agent) {
65
+ await this.dingtalk.reply(appId, message.sessionWebhook, '请先在 Agent Muster 中为此应用绑定项目和 Agent。');
66
+ return;
67
+ }
68
+ if (!this.agents.get(binding.agent)?.installed) {
69
+ await this.dingtalk.reply(appId, message.sessionWebhook, '绑定的 Agent 当前不可用。');
70
+ return;
71
+ }
72
+ await this.dingtalk.setConversationTarget(appId, conversation, message);
73
+ let content;
74
+ const images = [];
75
+ if (message.msgtype === 'text')
76
+ content = message.text.content.trim();
77
+ else {
78
+ const parts = message.msgtype === 'picture' ? [{ ...message.content, type: 'picture' }] : message.content.richText;
79
+ const promptParts = [];
80
+ for (const part of parts) {
81
+ if (part.type === 'picture') {
82
+ const downloadCode = part.downloadCode ?? part.pictureDownloadCode;
83
+ if (!downloadCode)
84
+ throw new Error('钉钉图片消息缺少下载码');
85
+ if (binding.agent === 'kimi')
86
+ throw new Error('当前 Kimi CLI 不支持图片附件输入');
87
+ images.push(await this.dingtalk.downloadImage(appId, message.robotCode, downloadCode));
88
+ }
89
+ else
90
+ promptParts.push(part.text);
91
+ }
92
+ content = promptParts.join('').trim();
93
+ if (!content && images.length)
94
+ content = '请查看这张图片。';
95
+ if (!content)
96
+ return;
97
+ }
98
+ const dingtalkConversation = {
99
+ type: message.conversationType === '2' ? 'group' : 'single',
100
+ ...(message.conversationType === '2' ? { groupName: message.conversationTitle } : {}),
101
+ senderName: message.senderNick,
102
+ senderStaffId: message.senderStaffId,
103
+ };
104
+ const prompt = message.conversationType === '2'
105
+ ? `${message.senderNick}(${message.senderStaffId})在群聊中说:\n${content}`
106
+ : content;
107
+ const previousId = this.dingtalk.getSession(appId, conversation);
108
+ let session = previousId ? this.sessions.get(previousId) : undefined;
109
+ if (session)
110
+ this.sessions.linkDingTalkSession(session.id, appId);
111
+ if (session && !terminalStatuses.has(session.status))
112
+ await waitForCompletion(this.sessions, session);
113
+ const lastEventId = session?.events.at(-1)?.id || 0;
114
+ if (session)
115
+ this.sessions.send(session.id, prompt, undefined, dingtalkConversation, images);
116
+ else {
117
+ session = await this.sessions.create(binding.agent, binding.project, prompt, undefined, appId, dingtalkConversation, images);
118
+ await this.dingtalk.setSession(appId, conversation, session.id);
119
+ }
120
+ await waitForCompletion(this.sessions, session);
121
+ if (session.status === 'failed') {
122
+ console.error(`DingTalk Agent execution failed for ${appId} (message ${message.msgId}, conversation ${conversation}, session ${session.id}):`, session.events.filter(event => event.id > lastEventId && event.type === 'error').map(event => event.text).join('\n'));
123
+ }
124
+ const reply = session.status === 'completed'
125
+ ? session.events.filter(event => event.id > lastEventId && event.type === 'output').at(-1)?.text.trim() || 'Agent 已执行完成。'
126
+ : 'Agent 执行失败,请在 Agent Muster 中查看会话日志。';
127
+ await this.dingtalk.reply(appId, message.sessionWebhook, reply);
128
+ }
129
+ }