agent-muster 0.1.1 → 0.1.2
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/README.md +99 -54
- package/README.zh-CN.md +170 -0
- package/bin/agent-muster.js +0 -0
- package/dist/cli.js +16 -2
- package/dist/feishu-bridge.js +96 -0
- package/dist/feishu.js +232 -0
- package/dist/server.js +91 -3
- package/dist/sessions.js +23 -5
- package/dist/updates.js +40 -0
- package/package.json +10 -5
- package/web/dist/assets/index-5eptghP4.js +83 -0
- package/web/dist/assets/index-B-OcUiqP.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C9mqFMZx.css +0 -1
- package/web/dist/assets/index-DFo3V_i1.js +0 -83
package/dist/feishu.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { Client, Domain, EventDispatcher, LoggerLevel, WSClient, registerApp } from '@larksuiteoapi/node-sdk';
|
|
7
|
+
import QRCode from 'qrcode';
|
|
8
|
+
const directory = join(homedir(), '.agent-muster');
|
|
9
|
+
const dataFile = join(directory, 'feishu-apps.json');
|
|
10
|
+
export class FeishuRegistry extends EventEmitter {
|
|
11
|
+
apps = [];
|
|
12
|
+
clients = new Map();
|
|
13
|
+
registrations = new Map();
|
|
14
|
+
writeQueue = Promise.resolve();
|
|
15
|
+
async load() {
|
|
16
|
+
try {
|
|
17
|
+
this.apps = JSON.parse(await readFile(dataFile, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code !== 'ENOENT')
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
list() {
|
|
25
|
+
return this.apps.map(({ clientSecret, conversations, ...app }) => ({
|
|
26
|
+
...app, connectionStatus: this.clients.get(app.id)?.ws.getConnectionStatus().state,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
getBinding(id) {
|
|
30
|
+
const app = this.apps.find(item => item.id === id);
|
|
31
|
+
return app ? { project: app.project, agent: app.agent } : undefined;
|
|
32
|
+
}
|
|
33
|
+
getSession(id, chatId) {
|
|
34
|
+
return this.apps.find(item => item.id === id)?.conversations[chatId];
|
|
35
|
+
}
|
|
36
|
+
sessionBindings() {
|
|
37
|
+
return this.apps.flatMap(app => Object.values(app.conversations).map(sessionId => ({ sessionId, appId: app.id })));
|
|
38
|
+
}
|
|
39
|
+
async setSession(id, chatId, sessionId) {
|
|
40
|
+
this.getApp(id).conversations[chatId] = sessionId;
|
|
41
|
+
await this.persist();
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
const id = randomUUID();
|
|
45
|
+
const registration = { controller: new AbortController(), result: { status: 'WAITING' } };
|
|
46
|
+
this.registrations.set(id, registration);
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
void registerApp({
|
|
49
|
+
source: 'agent-muster', createOnly: true, signal: registration.controller.signal,
|
|
50
|
+
appPreset: { name: 'Agent Muster', desc: '通过飞书与本地编码 Agent 对话' },
|
|
51
|
+
addons: {
|
|
52
|
+
scopes: { tenant: ['im:message:send_as_bot', 'im:message.p2p_msg:readonly', 'im:message.group_at_msg:readonly'] },
|
|
53
|
+
events: { items: { tenant: ['im.message.receive_v1'] } },
|
|
54
|
+
},
|
|
55
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
56
|
+
registration.expiresAt = Date.now() + expireIn * 1000;
|
|
57
|
+
void QRCode.toString(url, { type: 'svg', margin: 1, width: 220 }).then(svg => {
|
|
58
|
+
resolve({ id, verificationUrl: url, qrCode: `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`, expiresAt: registration.expiresAt, interval: 2 });
|
|
59
|
+
}).catch(error => { registration.controller.abort(); reject(error); });
|
|
60
|
+
},
|
|
61
|
+
}).then(async (result) => {
|
|
62
|
+
if (result.user_info?.tenant_brand === 'lark')
|
|
63
|
+
throw new Error('请使用飞书账号创建应用');
|
|
64
|
+
const app = {
|
|
65
|
+
id: randomUUID(), clientId: result.client_id, clientSecret: result.client_secret,
|
|
66
|
+
name: 'Agent Muster', icon: null, project: null, agent: null, conversations: {},
|
|
67
|
+
};
|
|
68
|
+
this.apps.push(app);
|
|
69
|
+
try {
|
|
70
|
+
await this.persist();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.apps = this.apps.filter(item => item.id !== app.id);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
this.subscribe(app);
|
|
77
|
+
registration.result = { status: 'SUCCESS', appId: app.id };
|
|
78
|
+
}).catch((error) => {
|
|
79
|
+
registration.result = { status: error.code === 'expired_token' ? 'EXPIRED' : 'FAIL', reason: error.description || error.message };
|
|
80
|
+
if (!registration.expiresAt)
|
|
81
|
+
this.registrations.delete(id);
|
|
82
|
+
reject(error);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
poll(id) {
|
|
87
|
+
const registration = this.registrations.get(id);
|
|
88
|
+
if (!registration)
|
|
89
|
+
throw new Error('扫码会话不存在');
|
|
90
|
+
if (registration.result.status === 'WAITING' && registration.expiresAt && Date.now() >= registration.expiresAt) {
|
|
91
|
+
registration.result = { status: 'EXPIRED' };
|
|
92
|
+
registration.controller.abort();
|
|
93
|
+
}
|
|
94
|
+
return registration.result;
|
|
95
|
+
}
|
|
96
|
+
async cancelRegistration(id) {
|
|
97
|
+
const registration = this.registrations.get(id);
|
|
98
|
+
if (!registration)
|
|
99
|
+
return;
|
|
100
|
+
registration.controller.abort();
|
|
101
|
+
this.registrations.delete(id);
|
|
102
|
+
}
|
|
103
|
+
startSubscriptions() { for (const app of this.apps)
|
|
104
|
+
this.subscribe(app); }
|
|
105
|
+
shutdown() {
|
|
106
|
+
for (const registration of this.registrations.values())
|
|
107
|
+
registration.controller.abort();
|
|
108
|
+
for (const client of this.clients.values())
|
|
109
|
+
client.ws.close({ force: true });
|
|
110
|
+
this.clients.clear();
|
|
111
|
+
}
|
|
112
|
+
subscribe(app) {
|
|
113
|
+
if (this.clients.has(app.id))
|
|
114
|
+
return;
|
|
115
|
+
const config = { appId: app.clientId, appSecret: app.clientSecret, domain: Domain.Feishu, loggerLevel: LoggerLevel.warn };
|
|
116
|
+
const api = new Client(config);
|
|
117
|
+
const ws = new WSClient(config);
|
|
118
|
+
const eventDispatcher = new EventDispatcher({}).register({
|
|
119
|
+
'im.message.receive_v1': data => { this.emit('message', { appId: app.id, message: data }); },
|
|
120
|
+
});
|
|
121
|
+
this.clients.set(app.id, { api, ws });
|
|
122
|
+
void ws.start({ eventDispatcher }).catch(error => console.error(`飞书应用 ${app.clientId} 消息监听失败:`, error));
|
|
123
|
+
}
|
|
124
|
+
async send(id, chatId, content) {
|
|
125
|
+
const client = this.clients.get(id);
|
|
126
|
+
if (!client)
|
|
127
|
+
throw new Error('飞书应用消息监听未启动');
|
|
128
|
+
// 按 UTF-8 字节分段,避免超过飞书文本消息大小限制。
|
|
129
|
+
let chunk = '';
|
|
130
|
+
let size = 0;
|
|
131
|
+
const chunks = [];
|
|
132
|
+
for (const character of content) {
|
|
133
|
+
const bytes = Buffer.byteLength(character);
|
|
134
|
+
if (size + bytes > 20000) {
|
|
135
|
+
chunks.push(chunk);
|
|
136
|
+
chunk = '';
|
|
137
|
+
size = 0;
|
|
138
|
+
}
|
|
139
|
+
chunk += character;
|
|
140
|
+
size += bytes;
|
|
141
|
+
}
|
|
142
|
+
if (chunk)
|
|
143
|
+
chunks.push(chunk);
|
|
144
|
+
for (const text of chunks) {
|
|
145
|
+
const result = await client.api.im.message.create({
|
|
146
|
+
params: { receive_id_type: 'chat_id' },
|
|
147
|
+
data: { receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text }) },
|
|
148
|
+
});
|
|
149
|
+
if (result.code !== 0)
|
|
150
|
+
throw new Error(`飞书消息发送失败:${result.msg} (${result.code})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async sendToSession(id, sessionId, content) {
|
|
154
|
+
const chatId = Object.entries(this.getApp(id).conversations).find(([, value]) => value === sessionId)?.[0];
|
|
155
|
+
if (!chatId)
|
|
156
|
+
throw new Error('此对话尚无飞书发送目标');
|
|
157
|
+
await this.send(id, chatId, content);
|
|
158
|
+
}
|
|
159
|
+
async bind(id, binding, projects, agents) {
|
|
160
|
+
if (binding.project !== undefined && binding.project !== null && !projects.includes(binding.project))
|
|
161
|
+
throw new Error('项目不存在');
|
|
162
|
+
if (binding.agent !== undefined && binding.agent !== null && !agents.includes(binding.agent))
|
|
163
|
+
throw new Error('Agent 不可用');
|
|
164
|
+
const app = this.getApp(id);
|
|
165
|
+
if ((binding.project !== undefined && binding.project !== app.project) || (binding.agent !== undefined && binding.agent !== app.agent))
|
|
166
|
+
app.conversations = {};
|
|
167
|
+
if (binding.project !== undefined)
|
|
168
|
+
app.project = binding.project;
|
|
169
|
+
if (binding.agent !== undefined)
|
|
170
|
+
app.agent = binding.agent;
|
|
171
|
+
await this.persist();
|
|
172
|
+
return this.list().find(item => item.id === id);
|
|
173
|
+
}
|
|
174
|
+
async syncMetadata(id) {
|
|
175
|
+
const app = this.getApp(id);
|
|
176
|
+
const client = this.clients.get(id);
|
|
177
|
+
if (!client)
|
|
178
|
+
throw new Error('飞书应用消息监听未启动');
|
|
179
|
+
const result = await client.api.request({ url: '/open-apis/bot/v3/info', method: 'GET' });
|
|
180
|
+
if (result.code !== 0 || !result.bot)
|
|
181
|
+
throw new Error(`飞书应用信息获取失败:${result.msg}`);
|
|
182
|
+
app.name = result.bot.app_name;
|
|
183
|
+
app.icon = result.bot.avatar_url;
|
|
184
|
+
await this.persist();
|
|
185
|
+
return this.list().find(item => item.id === id);
|
|
186
|
+
}
|
|
187
|
+
async remove(id) {
|
|
188
|
+
this.getApp(id);
|
|
189
|
+
this.clients.get(id)?.ws.close({ force: true });
|
|
190
|
+
this.clients.delete(id);
|
|
191
|
+
this.apps = this.apps.filter(item => item.id !== id);
|
|
192
|
+
await this.persist();
|
|
193
|
+
}
|
|
194
|
+
async unbindSession(sessionId) {
|
|
195
|
+
let changed = false;
|
|
196
|
+
for (const app of this.apps) {
|
|
197
|
+
for (const [chatId, id] of Object.entries(app.conversations)) {
|
|
198
|
+
if (id !== sessionId)
|
|
199
|
+
continue;
|
|
200
|
+
delete app.conversations[chatId];
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (changed)
|
|
205
|
+
await this.persist();
|
|
206
|
+
}
|
|
207
|
+
async unbindProject(project) {
|
|
208
|
+
const apps = this.apps.filter(app => app.project === project);
|
|
209
|
+
for (const app of apps) {
|
|
210
|
+
app.project = null;
|
|
211
|
+
app.conversations = {};
|
|
212
|
+
}
|
|
213
|
+
if (apps.length)
|
|
214
|
+
await this.persist();
|
|
215
|
+
}
|
|
216
|
+
getApp(id) {
|
|
217
|
+
const app = this.apps.find(item => item.id === id);
|
|
218
|
+
if (!app)
|
|
219
|
+
throw new Error('飞书应用不存在');
|
|
220
|
+
return app;
|
|
221
|
+
}
|
|
222
|
+
persist() {
|
|
223
|
+
const snapshot = JSON.stringify(this.apps);
|
|
224
|
+
const write = this.writeQueue.then(async () => {
|
|
225
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
226
|
+
await writeFile(`${dataFile}.tmp`, snapshot, { mode: 0o600 });
|
|
227
|
+
await rename(`${dataFile}.tmp`, dataFile);
|
|
228
|
+
});
|
|
229
|
+
this.writeQueue = write.catch(() => { });
|
|
230
|
+
return write;
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -26,7 +26,7 @@ async function chooseProjectDirectory() {
|
|
|
26
26
|
throw error;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
export async function startServer(port, registry, projects, sessions, dingtalk) {
|
|
29
|
+
export async function startServer(port, registry, projects, sessions, dingtalk, feishu, dev = false) {
|
|
30
30
|
const app = express();
|
|
31
31
|
app.disable('x-powered-by');
|
|
32
32
|
app.use((request, response, next) => {
|
|
@@ -75,8 +75,10 @@ export async function startServer(port, registry, projects, sessions, dingtalk)
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
await dingtalk.unbindProject(path);
|
|
78
|
+
await feishu.unbindProject(path);
|
|
78
79
|
for (const session of projectSessions) {
|
|
79
80
|
await dingtalk.unbindSession(session.id);
|
|
81
|
+
await feishu.unbindSession(session.id);
|
|
80
82
|
await sessions.remove(session.id);
|
|
81
83
|
}
|
|
82
84
|
if (registered)
|
|
@@ -151,6 +153,61 @@ export async function startServer(port, registry, projects, sessions, dingtalk)
|
|
|
151
153
|
response.status(500).json({ error: error.message });
|
|
152
154
|
}
|
|
153
155
|
});
|
|
156
|
+
app.get('/api/feishu/apps', (_request, response) => response.json(feishu.list()));
|
|
157
|
+
app.post('/api/feishu/apps/:id/sync', async (request, response) => {
|
|
158
|
+
try {
|
|
159
|
+
response.json(await feishu.syncMetadata(request.params.id));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
response.status(502).json({ error: error.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
app.post('/api/feishu/registration', async (_request, response) => {
|
|
166
|
+
try {
|
|
167
|
+
response.status(201).json(await feishu.start());
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
response.status(502).json({ error: error.message });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
app.get('/api/feishu/registration/:id', async (request, response) => {
|
|
174
|
+
try {
|
|
175
|
+
response.json(await feishu.poll(request.params.id));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
response.status(502).json({ error: error.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
app.patch('/api/feishu/apps/:id', async (request, response) => {
|
|
182
|
+
try {
|
|
183
|
+
const { project, agent } = request.body || {};
|
|
184
|
+
if (project !== undefined && project !== null && typeof project !== 'string')
|
|
185
|
+
throw new Error('项目无效');
|
|
186
|
+
if (agent !== undefined && agent !== null && typeof agent !== 'string')
|
|
187
|
+
throw new Error('Agent 无效');
|
|
188
|
+
response.json(await feishu.bind(request.params.id, { project, agent }, projects.list(), registry.list().filter(item => item.installed).map(item => item.id)));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
response.status(400).json({ error: error.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
app.delete('/api/feishu/apps/:id', async (request, response) => {
|
|
195
|
+
try {
|
|
196
|
+
if (!feishu.getBinding(request.params.id)) {
|
|
197
|
+
response.status(404).json({ error: '飞书应用不存在' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await feishu.remove(request.params.id);
|
|
201
|
+
response.json({ id: request.params.id });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
response.status(500).json({ error: error.message });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
app.delete('/api/feishu/registration/:id', async (request, response) => {
|
|
208
|
+
await feishu.cancelRegistration(request.params.id);
|
|
209
|
+
response.status(204).end();
|
|
210
|
+
});
|
|
154
211
|
app.get('/api/sessions', (_request, response) => response.json(sessions.list()));
|
|
155
212
|
app.get('/api/sessions/:id', (request, response) => {
|
|
156
213
|
const session = sessions.get(request.params.id);
|
|
@@ -230,6 +287,24 @@ export async function startServer(port, registry, projects, sessions, dingtalk)
|
|
|
230
287
|
response.status(400).json({ error: error.message });
|
|
231
288
|
}
|
|
232
289
|
});
|
|
290
|
+
app.post('/api/sessions/:id/feishu/messages', async (request, response) => {
|
|
291
|
+
try {
|
|
292
|
+
const { content } = request.body || {};
|
|
293
|
+
if (typeof content !== 'string' || !content.trim())
|
|
294
|
+
throw new Error('请输入消息内容');
|
|
295
|
+
const session = sessions.get(request.params.id);
|
|
296
|
+
if (!session)
|
|
297
|
+
throw new Error('会话不存在');
|
|
298
|
+
if (!session.feishuAppId)
|
|
299
|
+
throw new Error('此对话未关联飞书应用');
|
|
300
|
+
await feishu.sendToSession(session.feishuAppId, session.id, content.trim());
|
|
301
|
+
response.json(sessions.recordFeishuMessage(session.id, content.trim()));
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error(`飞书会话 ${request.params.id} 消息发送失败:`, error);
|
|
305
|
+
response.status(400).json({ error: error.message });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
233
308
|
app.post('/api/sessions/:id/stop', (request, response) => {
|
|
234
309
|
try {
|
|
235
310
|
response.json(sessions.stop(request.params.id));
|
|
@@ -245,6 +320,7 @@ export async function startServer(port, registry, projects, sessions, dingtalk)
|
|
|
245
320
|
return;
|
|
246
321
|
}
|
|
247
322
|
await dingtalk.unbindSession(request.params.id);
|
|
323
|
+
await feishu.unbindSession(request.params.id);
|
|
248
324
|
await sessions.remove(request.params.id);
|
|
249
325
|
response.json({ id: request.params.id });
|
|
250
326
|
}
|
|
@@ -272,9 +348,21 @@ export async function startServer(port, registry, projects, sessions, dingtalk)
|
|
|
272
348
|
const heartbeat = setInterval(() => response.write(': keepalive\n\n'), 15000);
|
|
273
349
|
request.on('close', () => { clearInterval(heartbeat); sessions.off(session.id, listener); });
|
|
274
350
|
});
|
|
275
|
-
app.use(express.static(webRoot));
|
|
276
|
-
app.get('/{*path}', (_request, response) => response.sendFile(join(webRoot, 'index.html')));
|
|
277
351
|
const server = createServer(app);
|
|
352
|
+
if (dev) {
|
|
353
|
+
const { createServer: createViteServer } = await import('vite');
|
|
354
|
+
const vite = await createViteServer({
|
|
355
|
+
configFile: resolve(webRoot, '../vite.config.ts'),
|
|
356
|
+
root: resolve(webRoot, '..'),
|
|
357
|
+
server: { middlewareMode: true, hmr: { server } },
|
|
358
|
+
});
|
|
359
|
+
app.use(vite.middlewares);
|
|
360
|
+
server.once('close', () => { void vite.close(); });
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
app.use(express.static(webRoot));
|
|
364
|
+
app.get('/{*path}', (_request, response) => response.sendFile(join(webRoot, 'index.html')));
|
|
365
|
+
}
|
|
278
366
|
await new Promise((done, reject) => {
|
|
279
367
|
server.once('error', reject);
|
|
280
368
|
server.listen(port, '127.0.0.1', done);
|
package/dist/sessions.js
CHANGED
|
@@ -91,6 +91,13 @@ export class SessionManager extends EventEmitter {
|
|
|
91
91
|
session.dingtalkAppId = appId;
|
|
92
92
|
this.save(session);
|
|
93
93
|
}
|
|
94
|
+
linkFeishuSession(id, appId) {
|
|
95
|
+
const session = this.sessions.get(id);
|
|
96
|
+
if (!session || session.feishuAppId === appId)
|
|
97
|
+
return;
|
|
98
|
+
session.feishuAppId = appId;
|
|
99
|
+
this.save(session);
|
|
100
|
+
}
|
|
94
101
|
isRunning(id) {
|
|
95
102
|
return this.processes.has(id);
|
|
96
103
|
}
|
|
@@ -99,7 +106,7 @@ export class SessionManager extends EventEmitter {
|
|
|
99
106
|
this.writeQueue = this.writeQueue.then(() => writeSession(session.id, snapshot))
|
|
100
107
|
.catch(error => console.error('Failed to save session:', error));
|
|
101
108
|
}
|
|
102
|
-
append(session, type, text, detail, kind, comparison, dingtalkConversation) {
|
|
109
|
+
append(session, type, text, detail, kind, comparison, dingtalkConversation, feishuConversation) {
|
|
103
110
|
const event = {
|
|
104
111
|
id: (session.events.at(-1)?.id || 0) + 1,
|
|
105
112
|
type,
|
|
@@ -109,6 +116,7 @@ export class SessionManager extends EventEmitter {
|
|
|
109
116
|
...(comparison === undefined ? {} : { comparison }),
|
|
110
117
|
timestamp: Date.now(),
|
|
111
118
|
...(dingtalkConversation ? { dingtalkConversation } : {}),
|
|
119
|
+
...(feishuConversation ? { feishuConversation } : {}),
|
|
112
120
|
};
|
|
113
121
|
session.events.push(event);
|
|
114
122
|
if (session.events.length > 500)
|
|
@@ -128,7 +136,7 @@ export class SessionManager extends EventEmitter {
|
|
|
128
136
|
};
|
|
129
137
|
this.append(session, 'usage', JSON.stringify(session.usage));
|
|
130
138
|
}
|
|
131
|
-
async create(agent, cwd, prompt, model, dingtalkAppId, dingtalkConversation, images) {
|
|
139
|
+
async create(agent, cwd, prompt, model, dingtalkAppId, dingtalkConversation, images, feishu) {
|
|
132
140
|
if (!adapters[agent])
|
|
133
141
|
throw new Error('Unsupported agent');
|
|
134
142
|
if (!prompt.trim())
|
|
@@ -141,6 +149,7 @@ export class SessionManager extends EventEmitter {
|
|
|
141
149
|
id: `ses_${randomUUID()}`,
|
|
142
150
|
agent,
|
|
143
151
|
...(dingtalkAppId ? { dingtalkAppId } : {}),
|
|
152
|
+
...(feishu ? { feishuAppId: feishu.appId, feishuConversation: feishu.conversation } : {}),
|
|
144
153
|
...(dingtalkConversation ? { dingtalkConversation } : {}),
|
|
145
154
|
model,
|
|
146
155
|
cwd: resolve(cwd),
|
|
@@ -151,11 +160,11 @@ export class SessionManager extends EventEmitter {
|
|
|
151
160
|
events: [],
|
|
152
161
|
};
|
|
153
162
|
this.sessions.set(session.id, session);
|
|
154
|
-
this.append(session, 'message', session.prompt, undefined, undefined, undefined, dingtalkConversation);
|
|
163
|
+
this.append(session, 'message', session.prompt, undefined, undefined, undefined, dingtalkConversation, feishu?.conversation);
|
|
155
164
|
this.start(session, session.prompt, images);
|
|
156
165
|
return session;
|
|
157
166
|
}
|
|
158
|
-
send(id, message, model, dingtalkConversation, images) {
|
|
167
|
+
send(id, message, model, dingtalkConversation, images, feishuConversation) {
|
|
159
168
|
const session = this.sessions.get(id);
|
|
160
169
|
if (!session)
|
|
161
170
|
throw new Error('Session not found');
|
|
@@ -169,7 +178,9 @@ export class SessionManager extends EventEmitter {
|
|
|
169
178
|
session.model = model || undefined;
|
|
170
179
|
if (dingtalkConversation)
|
|
171
180
|
session.dingtalkConversation = dingtalkConversation;
|
|
172
|
-
|
|
181
|
+
if (feishuConversation)
|
|
182
|
+
session.feishuConversation = feishuConversation;
|
|
183
|
+
this.append(session, 'message', message.trim(), undefined, undefined, undefined, dingtalkConversation, feishuConversation);
|
|
173
184
|
this.start(session, message.trim(), images);
|
|
174
185
|
return session;
|
|
175
186
|
}
|
|
@@ -180,6 +191,13 @@ export class SessionManager extends EventEmitter {
|
|
|
180
191
|
this.append(session, 'dingtalk_message', content);
|
|
181
192
|
return session;
|
|
182
193
|
}
|
|
194
|
+
recordFeishuMessage(id, content) {
|
|
195
|
+
const session = this.sessions.get(id);
|
|
196
|
+
if (!session)
|
|
197
|
+
throw new Error('会话不存在');
|
|
198
|
+
this.append(session, 'feishu_message', content);
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
183
201
|
stop(id) {
|
|
184
202
|
const session = this.sessions.get(id);
|
|
185
203
|
if (!session)
|
package/dist/updates.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { gt, valid } from 'semver';
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
7
|
+
const shell = process.platform === 'win32';
|
|
8
|
+
async function getUpdate() {
|
|
9
|
+
const { name, version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
10
|
+
const { stdout } = await execute(npmCommand, ['view', `${name}@latest`, 'version', '--json', '--fetch-timeout=5000', '--fetch-retries=0'], { shell, timeout: 10000 });
|
|
11
|
+
const latest = JSON.parse(stdout);
|
|
12
|
+
if (typeof latest !== 'string' || !valid(latest))
|
|
13
|
+
throw new Error('npm 返回的版本号无效');
|
|
14
|
+
return { name, current: version, latest };
|
|
15
|
+
}
|
|
16
|
+
export async function checkForUpdates() {
|
|
17
|
+
const { name, current, latest } = await getUpdate();
|
|
18
|
+
if (!gt(latest, current))
|
|
19
|
+
return;
|
|
20
|
+
console.log(`\n发现新版本:${current} → ${latest}\n运行 ${name} update 更新全局安装,或运行 npx ${name}@latest 启动最新版本。\n`);
|
|
21
|
+
}
|
|
22
|
+
export async function updatePackage() {
|
|
23
|
+
const { name, current, latest } = await getUpdate();
|
|
24
|
+
if (!gt(latest, current)) {
|
|
25
|
+
console.log(`当前版本 ${current} 已是最新版本。`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`正在更新 ${name}:${current} → ${latest}`);
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(npmCommand, ['install', '--global', `${name}@${latest}`], { shell, stdio: 'inherit' });
|
|
31
|
+
child.once('error', reject);
|
|
32
|
+
child.once('close', (code, signal) => {
|
|
33
|
+
if (code === 0)
|
|
34
|
+
resolve();
|
|
35
|
+
else
|
|
36
|
+
reject(new Error(`npm 更新失败(${signal ? `信号:${signal}` : `退出码:${code}`})`));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
console.log(`已更新至 ${latest},请重新启动 Agent Muster。`);
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-muster",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A local web interface for managing coding agents, projects, sessions, and DingTalk integrations.",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "A local web interface for managing coding agents, projects, sessions, and DingTalk/Feishu integrations.",
|
|
5
5
|
"author": "sexyAmole",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://github.com/sexyAmole/agent-muster",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"coding-agent",
|
|
18
18
|
"codex",
|
|
19
19
|
"claude-code",
|
|
20
|
-
"dingtalk"
|
|
20
|
+
"dingtalk",
|
|
21
|
+
"feishu"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
23
24
|
"packageManager": "pnpm@10.33.4",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"agent-muster": "./bin/agent-muster.js"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
|
-
"dev": "
|
|
29
|
+
"dev": "tsx watch src/cli.ts web --dev --no-open",
|
|
29
30
|
"build": "tsc -p tsconfig.json && vite build --config web/vite.config.ts",
|
|
30
31
|
"prepack": "pnpm run build",
|
|
31
32
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p web/tsconfig.json --noEmit",
|
|
@@ -34,9 +35,11 @@
|
|
|
34
35
|
"files": [
|
|
35
36
|
"bin",
|
|
36
37
|
"dist",
|
|
37
|
-
"web/dist"
|
|
38
|
+
"web/dist",
|
|
39
|
+
"README.zh-CN.md"
|
|
38
40
|
],
|
|
39
41
|
"dependencies": {
|
|
42
|
+
"@larksuiteoapi/node-sdk": "^1.74.0",
|
|
40
43
|
"dingtalk-stream": "2.1.6-beta.1",
|
|
41
44
|
"express": "^5.1.0",
|
|
42
45
|
"qrcode": "^1.5.4",
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"react-dom": "^19.1.0",
|
|
45
48
|
"react-markdown": "^10.1.0",
|
|
46
49
|
"remark-gfm": "^4.0.1",
|
|
50
|
+
"semver": "^7.8.5",
|
|
47
51
|
"undici": "^8.11.2"
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
"@types/qrcode": "^1.5.6",
|
|
53
57
|
"@types/react": "^19.1.0",
|
|
54
58
|
"@types/react-dom": "^19.1.0",
|
|
59
|
+
"@types/semver": "^7.8.0",
|
|
55
60
|
"@vitejs/plugin-react": "^4.7.0",
|
|
56
61
|
"tsx": "^4.20.0",
|
|
57
62
|
"typescript": "^5.8.0",
|