@leviyuan/lodestar 0.1.0 → 2.0.14
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 +76 -67
- package/cli.ts +176 -0
- package/config.ts +135 -0
- package/daemon.ts +1080 -144
- package/email-worker.ts +534 -0
- package/env-bootstrap.ts +7 -0
- package/feishu-mcp.ts +482 -0
- package/package.json +36 -37
- package/runtime-api.ts +569 -0
- package/scripts/runtime-thread.sh +91 -0
- package/status-dashboard.ts +733 -0
- package/src/cardkit.ts +0 -215
- package/src/cards.ts +0 -304
- package/src/claude-process.ts +0 -301
- package/src/config.ts +0 -83
- package/src/feishu.ts +0 -365
- package/src/instructions.ts +0 -22
- package/src/log.ts +0 -11
- package/src/paths.ts +0 -41
- package/src/session.ts +0 -447
package/feishu-mcp.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Feishu MCP Server for DeepSeek TUI.
|
|
4
|
+
*
|
|
5
|
+
* Stateless design: every tool receives a `chat_id` parameter. No file-based
|
|
6
|
+
* message queues, no Claude-specific channel notifications. DeepSeek loads
|
|
7
|
+
* this server as a global MCP tool provider via ~/.deepseek/mcp.json.
|
|
8
|
+
*
|
|
9
|
+
* Tools: feishu_reply / feishu_react / feishu_send_file / feishu_fetch_history
|
|
10
|
+
*
|
|
11
|
+
* State paths migrated to ~/.deepseek/lodestar/.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
16
|
+
import {
|
|
17
|
+
ListToolsRequestSchema,
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
import * as lark from '@larksuiteoapi/node-sdk'
|
|
22
|
+
import {
|
|
23
|
+
readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync,
|
|
24
|
+
realpathSync, chmodSync, existsSync,
|
|
25
|
+
} from 'fs'
|
|
26
|
+
import { homedir } from 'os'
|
|
27
|
+
import { join, extname, basename, sep } from 'path'
|
|
28
|
+
|
|
29
|
+
// ── State paths ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const STATE_DIR = join(homedir(), '.deepseek', 'lodestar')
|
|
32
|
+
|
|
33
|
+
const APP_ID = process.env.FEISHU_APP_ID
|
|
34
|
+
const APP_SECRET = process.env.FEISHU_APP_SECRET
|
|
35
|
+
|
|
36
|
+
if (!APP_ID || !APP_SECRET) {
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
`feishu mcp: FEISHU_APP_ID and FEISHU_APP_SECRET required\n` +
|
|
39
|
+
` set in ~/.deepseek/lodestar.toml under [feishu]\n`,
|
|
40
|
+
)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Retry helper ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3, baseDelayMs = 500): Promise<T> {
|
|
47
|
+
for (let attempt = 1; ; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
return await fn()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (attempt >= maxAttempts) throw err
|
|
52
|
+
const delay = baseDelayMs * attempt
|
|
53
|
+
process.stderr.write(`feishu mcp: retry ${attempt}/${maxAttempts} after ${delay}ms: ${err}\n`)
|
|
54
|
+
await new Promise(r => setTimeout(r, delay))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Lark client ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const client = new lark.Client({
|
|
62
|
+
appId: APP_ID,
|
|
63
|
+
appSecret: APP_SECRET,
|
|
64
|
+
disableTokenCache: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const MAX_TEXT_LENGTH = 4000
|
|
70
|
+
const MAX_CARD_CONTENT_LENGTH = 28000
|
|
71
|
+
const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024
|
|
72
|
+
|
|
73
|
+
function hasMarkdown(text: string): boolean {
|
|
74
|
+
return /(?:^#{1,6}\s|^[-*]\s|^\d+\.\s|```|\*\*.+\*\*|`[^`]+`|^\|.+\|)/m.test(text)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function markdownToCard(text: string): string {
|
|
78
|
+
const elements: any[] = []
|
|
79
|
+
const parts = chunk(text, MAX_CARD_CONTENT_LENGTH)
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
elements.push({
|
|
82
|
+
tag: 'markdown',
|
|
83
|
+
content: part,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
schema: '2.0',
|
|
88
|
+
config: { wide_screen_mode: true },
|
|
89
|
+
body: { elements },
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function chunk(text: string, limit: number): string[] {
|
|
94
|
+
if (text.length <= limit) return [text]
|
|
95
|
+
const out: string[] = []
|
|
96
|
+
let rest = text
|
|
97
|
+
while (rest.length > limit) {
|
|
98
|
+
const para = rest.lastIndexOf('\n\n', limit)
|
|
99
|
+
const line = rest.lastIndexOf('\n', limit)
|
|
100
|
+
const space = rest.lastIndexOf(' ', limit)
|
|
101
|
+
const cut = para > limit / 2 ? para : line > limit / 2 ? line : space > 0 ? space : limit
|
|
102
|
+
out.push(rest.slice(0, cut))
|
|
103
|
+
rest = rest.slice(cut).replace(/^\n+/, '')
|
|
104
|
+
}
|
|
105
|
+
if (rest) out.push(rest)
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertSendable(f: string): void {
|
|
110
|
+
let real: string, stateReal: string
|
|
111
|
+
try {
|
|
112
|
+
real = realpathSync(f)
|
|
113
|
+
stateReal = realpathSync(STATE_DIR)
|
|
114
|
+
} catch { return }
|
|
115
|
+
if (real.startsWith(stateReal + sep)) {
|
|
116
|
+
throw new Error(`refusing to send channel state: ${f}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'])
|
|
121
|
+
|
|
122
|
+
// ── Feishu API helpers ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function getTenantToken(): Promise<string> {
|
|
125
|
+
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET }),
|
|
129
|
+
})
|
|
130
|
+
const json = await res.json() as any
|
|
131
|
+
return json.tenant_access_token ?? ''
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function uploadImage(filePath: string): Promise<string> {
|
|
135
|
+
const token = await getTenantToken()
|
|
136
|
+
const buf = readFileSync(filePath)
|
|
137
|
+
const form = new FormData()
|
|
138
|
+
form.append('image_type', 'message')
|
|
139
|
+
form.append('image', new File([buf], basename(filePath)))
|
|
140
|
+
const res = await fetch('https://open.feishu.cn/open-apis/im/v1/images', {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
143
|
+
body: form,
|
|
144
|
+
})
|
|
145
|
+
const json = await res.json() as any
|
|
146
|
+
return json?.data?.image_key ?? ''
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function uploadFile(filePath: string): Promise<string> {
|
|
150
|
+
const token = await getTenantToken()
|
|
151
|
+
const buf = readFileSync(filePath)
|
|
152
|
+
const form = new FormData()
|
|
153
|
+
form.append('file_type', 'stream')
|
|
154
|
+
form.append('file_name', basename(filePath))
|
|
155
|
+
form.append('file', new File([buf], basename(filePath)))
|
|
156
|
+
const res = await fetch('https://open.feishu.cn/open-apis/im/v1/files', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
159
|
+
body: form,
|
|
160
|
+
})
|
|
161
|
+
const json = await res.json() as any
|
|
162
|
+
return json?.data?.file_key ?? ''
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const mcp = new Server(
|
|
168
|
+
{ name: 'feishu', version: '1.0.0' },
|
|
169
|
+
{
|
|
170
|
+
capabilities: {
|
|
171
|
+
tools: {},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
179
|
+
tools: [
|
|
180
|
+
{
|
|
181
|
+
name: 'feishu_reply',
|
|
182
|
+
description:
|
|
183
|
+
'Send a reply to the Feishu group. Text with markdown formatting is rendered as an interactive card. ' +
|
|
184
|
+
'Optionally attach files (images are sent inline, others as file attachments).',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
chat_id: { type: 'string', description: 'The Feishu chat ID to send to.' },
|
|
189
|
+
text: { type: 'string', description: 'The message text (supports markdown).' },
|
|
190
|
+
reply_to: { type: 'string', description: 'Message ID to reply to (optional).' },
|
|
191
|
+
files: {
|
|
192
|
+
type: 'array',
|
|
193
|
+
items: { type: 'string' },
|
|
194
|
+
description: 'Absolute file paths to attach (optional).',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ['chat_id', 'text'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'feishu_react',
|
|
202
|
+
description:
|
|
203
|
+
'Add or remove a reaction (emoji) on a Feishu message. Used for status indication (e.g. 👌 OK, ✅ done).',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
chat_id: { type: 'string', description: 'The Feishu chat ID (for context, not required by API).' },
|
|
208
|
+
message_id: { type: 'string', description: 'The message ID to react to.' },
|
|
209
|
+
emoji_type: { type: 'string', description: 'Emoji type name (e.g. OK, CheckMark, CrossMark, HEART).' },
|
|
210
|
+
action: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
enum: ['add', 'remove'],
|
|
213
|
+
description: 'Whether to add or remove the reaction.',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
required: ['message_id', 'emoji_type', 'action'],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'feishu_send_file',
|
|
221
|
+
description:
|
|
222
|
+
'Upload and send a file or image to the Feishu group. Images are sent as inline images, other files as attachments.',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
chat_id: { type: 'string', description: 'The Feishu chat ID to send to.' },
|
|
227
|
+
file_path: { type: 'string', description: 'Absolute path to the file to send.' },
|
|
228
|
+
},
|
|
229
|
+
required: ['chat_id', 'file_path'],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'feishu_fetch_history',
|
|
234
|
+
description:
|
|
235
|
+
'Fetch recent messages from a Feishu group chat. Useful for understanding conversation context.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
chat_id: { type: 'string', description: 'The Feishu chat ID to fetch history from.' },
|
|
240
|
+
limit: {
|
|
241
|
+
type: 'number',
|
|
242
|
+
description: 'Maximum number of messages to fetch (default 20, max 50).',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ['chat_id'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
// ── Tool handlers ────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
254
|
+
const args = (req.params.arguments ?? {}) as Record<string, unknown>
|
|
255
|
+
try {
|
|
256
|
+
switch (req.params.name) {
|
|
257
|
+
case 'feishu_reply': {
|
|
258
|
+
const chat_id = args.chat_id as string
|
|
259
|
+
if (!chat_id) throw new Error('chat_id is required')
|
|
260
|
+
const text = args.text as string
|
|
261
|
+
const files = (args.files as string[] | undefined) ?? []
|
|
262
|
+
const reply_to = args.reply_to as string | undefined
|
|
263
|
+
|
|
264
|
+
for (const f of files) {
|
|
265
|
+
assertSendable(f)
|
|
266
|
+
const st = statSync(f)
|
|
267
|
+
if (st.size > MAX_ATTACHMENT_BYTES) {
|
|
268
|
+
throw new Error(`file too large: ${f} (${(st.size / 1024 / 1024).toFixed(1)}MB, max 50MB)`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const chunks = chunk(text, MAX_TEXT_LENGTH)
|
|
273
|
+
const sentIds: string[] = []
|
|
274
|
+
|
|
275
|
+
const useCard = hasMarkdown(text)
|
|
276
|
+
process.stderr.write(`feishu mcp: reply useCard=${useCard} textLen=${text.length}\n`)
|
|
277
|
+
if (useCard) {
|
|
278
|
+
const cardParts = chunk(text, MAX_CARD_CONTENT_LENGTH)
|
|
279
|
+
for (const part of cardParts) {
|
|
280
|
+
const card = markdownToCard(part)
|
|
281
|
+
const res = await withRetry(() => client.im.message.create({
|
|
282
|
+
params: { receive_id_type: 'chat_id' },
|
|
283
|
+
data: {
|
|
284
|
+
receive_id: chat_id,
|
|
285
|
+
msg_type: 'interactive',
|
|
286
|
+
content: card,
|
|
287
|
+
...(reply_to ? { root_id: reply_to } : {}),
|
|
288
|
+
},
|
|
289
|
+
}))
|
|
290
|
+
if (res.data?.message_id) sentIds.push(res.data.message_id)
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
for (const c of chunks) {
|
|
294
|
+
const res = await withRetry(() => client.im.message.create({
|
|
295
|
+
params: { receive_id_type: 'chat_id' },
|
|
296
|
+
data: {
|
|
297
|
+
receive_id: chat_id,
|
|
298
|
+
msg_type: 'text',
|
|
299
|
+
content: JSON.stringify({ text: c }),
|
|
300
|
+
...(reply_to ? { root_id: reply_to } : {}),
|
|
301
|
+
},
|
|
302
|
+
}))
|
|
303
|
+
if (res.data?.message_id) sentIds.push(res.data.message_id)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const f of files) {
|
|
308
|
+
const ext = extname(f).toLowerCase()
|
|
309
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
310
|
+
const imageKey = await uploadImage(f)
|
|
311
|
+
if (imageKey) {
|
|
312
|
+
const res = await withRetry(() => client.im.message.create({
|
|
313
|
+
params: { receive_id_type: 'chat_id' },
|
|
314
|
+
data: {
|
|
315
|
+
receive_id: chat_id,
|
|
316
|
+
msg_type: 'image',
|
|
317
|
+
content: JSON.stringify({ image_key: imageKey }),
|
|
318
|
+
...(reply_to ? { root_id: reply_to } : {}),
|
|
319
|
+
},
|
|
320
|
+
}))
|
|
321
|
+
if (res.data?.message_id) sentIds.push(res.data.message_id)
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const fileKey = await uploadFile(f)
|
|
325
|
+
if (fileKey) {
|
|
326
|
+
const res = await withRetry(() => client.im.message.create({
|
|
327
|
+
params: { receive_id_type: 'chat_id' },
|
|
328
|
+
data: {
|
|
329
|
+
receive_id: chat_id,
|
|
330
|
+
msg_type: 'file',
|
|
331
|
+
content: JSON.stringify({ file_key: fileKey }),
|
|
332
|
+
...(reply_to ? { root_id: reply_to } : {}),
|
|
333
|
+
},
|
|
334
|
+
}))
|
|
335
|
+
if (res.data?.message_id) sentIds.push(res.data.message_id)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = sentIds.length === 1
|
|
341
|
+
? `sent (id: ${sentIds[0]})`
|
|
342
|
+
: `sent ${sentIds.length} parts (ids: ${sentIds.join(', ')})`
|
|
343
|
+
return { content: [{ type: 'text', text: result }] }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case 'feishu_react': {
|
|
347
|
+
const message_id = args.message_id as string
|
|
348
|
+
const emoji_type = args.emoji_type as string
|
|
349
|
+
const action = (args.action as string) || 'add'
|
|
350
|
+
|
|
351
|
+
if (!message_id) throw new Error('message_id is required')
|
|
352
|
+
if (!emoji_type) throw new Error('emoji_type is required')
|
|
353
|
+
|
|
354
|
+
if (action === 'add') {
|
|
355
|
+
const res: any = await client.im.messageReaction.create({
|
|
356
|
+
path: { message_id },
|
|
357
|
+
data: { reaction_type: { emoji_type } },
|
|
358
|
+
})
|
|
359
|
+
const rid = res?.data?.reaction_id
|
|
360
|
+
return { content: [{ type: 'text', text: `added reaction ${emoji_type} (reaction_id: ${rid})` }] }
|
|
361
|
+
} else {
|
|
362
|
+
// For remove, we need reaction_id — but that requires knowing it first.
|
|
363
|
+
// In practice, daemon handles reactions directly. This tool exists for
|
|
364
|
+
// DeepSeek to add reactions when useful.
|
|
365
|
+
return { content: [{ type: 'text', text: `remove not yet supported (daemon handles react lifecycle)` }], isError: true }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'feishu_send_file': {
|
|
370
|
+
const chat_id = args.chat_id as string
|
|
371
|
+
const file_path = args.file_path as string
|
|
372
|
+
|
|
373
|
+
if (!chat_id) throw new Error('chat_id is required')
|
|
374
|
+
if (!file_path) throw new Error('file_path is required')
|
|
375
|
+
assertSendable(file_path)
|
|
376
|
+
|
|
377
|
+
const st = statSync(file_path)
|
|
378
|
+
if (st.size > MAX_ATTACHMENT_BYTES) {
|
|
379
|
+
throw new Error(`file too large: ${file_path} (${(st.size / 1024 / 1024).toFixed(1)}MB, max 50MB)`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const ext = extname(file_path).toLowerCase()
|
|
383
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
384
|
+
const imageKey = await uploadImage(file_path)
|
|
385
|
+
if (!imageKey) throw new Error('image upload failed')
|
|
386
|
+
const res = await withRetry(() => client.im.message.create({
|
|
387
|
+
params: { receive_id_type: 'chat_id' },
|
|
388
|
+
data: {
|
|
389
|
+
receive_id: chat_id,
|
|
390
|
+
msg_type: 'image',
|
|
391
|
+
content: JSON.stringify({ image_key: imageKey }),
|
|
392
|
+
},
|
|
393
|
+
}))
|
|
394
|
+
return { content: [{ type: 'text', text: `image sent (id: ${res.data?.message_id})` }] }
|
|
395
|
+
} else {
|
|
396
|
+
const fileKey = await uploadFile(file_path)
|
|
397
|
+
if (!fileKey) throw new Error('file upload failed')
|
|
398
|
+
const res = await withRetry(() => client.im.message.create({
|
|
399
|
+
params: { receive_id_type: 'chat_id' },
|
|
400
|
+
data: {
|
|
401
|
+
receive_id: chat_id,
|
|
402
|
+
msg_type: 'file',
|
|
403
|
+
content: JSON.stringify({ file_key: fileKey }),
|
|
404
|
+
},
|
|
405
|
+
}))
|
|
406
|
+
return { content: [{ type: 'text', text: `file sent (id: ${res.data?.message_id})` }] }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'feishu_fetch_history': {
|
|
411
|
+
const chat_id = args.chat_id as string
|
|
412
|
+
if (!chat_id) throw new Error('chat_id is required')
|
|
413
|
+
const limit = Math.min((args.limit as number) || 20, 50)
|
|
414
|
+
|
|
415
|
+
const res = await client.im.message.list({
|
|
416
|
+
params: {
|
|
417
|
+
receive_id_type: 'chat_id',
|
|
418
|
+
receive_id: chat_id,
|
|
419
|
+
page_size: limit,
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const messages = (res.data?.items ?? []).map((m: any) => {
|
|
424
|
+
let content = ''
|
|
425
|
+
try {
|
|
426
|
+
const obj = JSON.parse(m.body?.content ?? '{}')
|
|
427
|
+
content = obj.text ?? JSON.stringify(obj)
|
|
428
|
+
} catch { content = m.body?.content ?? '' }
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
message_id: m.message_id,
|
|
432
|
+
sender: m.sender?.id ?? 'unknown',
|
|
433
|
+
content,
|
|
434
|
+
create_time: m.create_time,
|
|
435
|
+
msg_type: m.msg_type,
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
default:
|
|
445
|
+
return { content: [{ type: 'text', text: `unknown tool: ${req.params.name}` }], isError: true }
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
449
|
+
return { content: [{ type: 'text', text: `${req.params.name} failed: ${msg}` }], isError: true }
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// ── Boot ─────────────────────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
process.on('unhandledRejection', err => {
|
|
456
|
+
process.stderr.write(`feishu mcp: unhandled rejection: ${err}\n`)
|
|
457
|
+
})
|
|
458
|
+
process.on('uncaughtException', err => {
|
|
459
|
+
process.stderr.write(`feishu mcp: uncaught exception: ${err}\n`)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
async function main() {
|
|
463
|
+
await mcp.connect(new StdioServerTransport())
|
|
464
|
+
process.stderr.write('feishu mcp: ready\n')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let shuttingDown = false
|
|
468
|
+
function shutdown(): void {
|
|
469
|
+
if (shuttingDown) return
|
|
470
|
+
shuttingDown = true
|
|
471
|
+
process.stderr.write('feishu mcp: shutting down\n')
|
|
472
|
+
setTimeout(() => process.exit(0), 2000)
|
|
473
|
+
}
|
|
474
|
+
process.stdin.on('end', shutdown)
|
|
475
|
+
process.stdin.on('close', shutdown)
|
|
476
|
+
process.on('SIGTERM', shutdown)
|
|
477
|
+
process.on('SIGINT', shutdown)
|
|
478
|
+
|
|
479
|
+
main().catch(err => {
|
|
480
|
+
process.stderr.write(`feishu mcp: fatal: ${err}\n`)
|
|
481
|
+
process.exit(1)
|
|
482
|
+
})
|
package/package.json
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leviyuan/lodestar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"access": "public"
|
|
6
|
-
},
|
|
7
|
-
"description": "Lodestar (夜航星) — IM-native frontend for Claude Code via Feishu Card Kit streaming",
|
|
8
|
-
"type": "module",
|
|
3
|
+
"version": "2.0.14",
|
|
4
|
+
"description": "夜航星 (Lodestar) — DeepSeek TUI 飞书群协作通道",
|
|
9
5
|
"license": "MIT",
|
|
10
|
-
"
|
|
11
|
-
"homepage": "https://github.com/leviyuan/lodestar#readme",
|
|
12
|
-
"bugs": "https://github.com/leviyuan/lodestar/issues",
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/leviyuan/lodestar.git"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"feishu",
|
|
19
|
-
"lark",
|
|
20
|
-
"claude-code",
|
|
21
|
-
"claude",
|
|
22
|
-
"ai",
|
|
23
|
-
"agent",
|
|
24
|
-
"im",
|
|
25
|
-
"chat",
|
|
26
|
-
"card-kit",
|
|
27
|
-
"streaming",
|
|
28
|
-
"lodestar"
|
|
29
|
-
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"start": "bun daemon.ts"
|
|
32
|
-
},
|
|
6
|
+
"type": "module",
|
|
33
7
|
"bin": {
|
|
34
|
-
"lodestar
|
|
8
|
+
"lodestar": "./cli.ts"
|
|
35
9
|
},
|
|
36
10
|
"files": [
|
|
11
|
+
"cli.ts",
|
|
12
|
+
"config.ts",
|
|
37
13
|
"daemon.ts",
|
|
38
|
-
"
|
|
14
|
+
"feishu-mcp.ts",
|
|
15
|
+
"runtime-api.ts",
|
|
16
|
+
"email-worker.ts",
|
|
17
|
+
"status-dashboard.ts",
|
|
18
|
+
"env-bootstrap.ts",
|
|
19
|
+
"scripts/runtime-thread.sh",
|
|
20
|
+
"package.json",
|
|
39
21
|
"README.md",
|
|
40
22
|
"LICENSE"
|
|
41
23
|
],
|
|
42
|
-
"
|
|
43
|
-
"
|
|
24
|
+
"scripts": {
|
|
25
|
+
"postinstall": "bun cli.ts configure --silent 2>/dev/null || true"
|
|
44
26
|
},
|
|
45
27
|
"dependencies": {
|
|
46
|
-
"@larksuiteoapi/node-sdk": "^1.44.0"
|
|
28
|
+
"@larksuiteoapi/node-sdk": "^1.44.0",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"imapflow": "^1.0.0",
|
|
31
|
+
"mailparser": "^3.9.8",
|
|
32
|
+
"nodemailer": "^6.9.0",
|
|
33
|
+
"zod": "^3.23.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"bun": ">=1.0.0"
|
|
47
37
|
},
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
38
|
+
"keywords": [
|
|
39
|
+
"feishu",
|
|
40
|
+
"lark",
|
|
41
|
+
"deepseek",
|
|
42
|
+
"ai",
|
|
43
|
+
"collaboration",
|
|
44
|
+
"夜航星",
|
|
45
|
+
"lodestar"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/leviyuan/lodestar.git"
|
|
51
50
|
}
|
|
52
51
|
}
|