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/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
- this.append(session, 'message', message.trim(), undefined, undefined, undefined, dingtalkConversation);
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)
@@ -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.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": "pnpm run build && tsx watch src/cli.ts web --no-open",
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",