larkcc 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.
package/dist/feishu.js ADDED
@@ -0,0 +1,964 @@
1
+ import * as lark from "@larksuiteoapi/node-sdk";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ const DOC_REGISTRY_DIR = path.join(os.homedir(), ".larkcc");
6
+ function getDocRegistryPath(profile) {
7
+ if (!profile || profile === "default") {
8
+ return path.join(DOC_REGISTRY_DIR, "doc-registry.json");
9
+ }
10
+ return path.join(DOC_REGISTRY_DIR, `doc-registry-${profile}.json`);
11
+ }
12
+ function loadDocRegistry(profile) {
13
+ try {
14
+ const filePath = getDocRegistryPath(profile);
15
+ if (fs.existsSync(filePath)) {
16
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
17
+ }
18
+ }
19
+ catch {
20
+ // 忽略错误
21
+ }
22
+ return [];
23
+ }
24
+ function saveDocRegistry(profile, records) {
25
+ if (!fs.existsSync(DOC_REGISTRY_DIR)) {
26
+ fs.mkdirSync(DOC_REGISTRY_DIR, { recursive: true });
27
+ }
28
+ const filePath = getDocRegistryPath(profile);
29
+ fs.writeFileSync(filePath, JSON.stringify(records, null, 2), "utf8");
30
+ }
31
+ function registerDocument(docId, profile) {
32
+ const records = loadDocRegistry(profile);
33
+ records.push({
34
+ id: docId,
35
+ createdAt: Date.now(),
36
+ });
37
+ saveDocRegistry(profile, records);
38
+ }
39
+ function getOldestDocuments(profile, keepCount) {
40
+ const records = loadDocRegistry(profile);
41
+ // 按创建时间升序排列(最旧的在前)
42
+ const sortedDocs = [...records].sort((a, b) => a.createdAt - b.createdAt);
43
+ // 返回需要删除的文档(超出保留数量的)
44
+ if (sortedDocs.length <= keepCount) {
45
+ return [];
46
+ }
47
+ return sortedDocs.slice(0, sortedDocs.length - keepCount);
48
+ }
49
+ function removeDocumentRecord(docId, profile) {
50
+ const records = loadDocRegistry(profile);
51
+ const filtered = records.filter(r => r.id !== docId);
52
+ saveDocRegistry(profile, filtered);
53
+ }
54
+ export function createLarkClient(appId, appSecret) {
55
+ return new lark.Client({ appId, appSecret });
56
+ }
57
+ export function createWSClient(appId, appSecret) {
58
+ return new lark.WSClient({ appId, appSecret });
59
+ }
60
+ // ── 消息发送 ─────────────────────────────────────────────────
61
+ export async function sendText(client, chatId, text) {
62
+ const res = await client.im.message.create({
63
+ params: { receive_id_type: "chat_id" },
64
+ data: {
65
+ receive_id: chatId,
66
+ msg_type: "text",
67
+ content: JSON.stringify({ text }),
68
+ },
69
+ });
70
+ return res.data?.message_id ?? "";
71
+ }
72
+ export async function replyText(client, chatId, rootMsgId, text) {
73
+ const card = buildMarkdownCard(text);
74
+ const res = await client.im.message.reply({
75
+ path: { message_id: rootMsgId },
76
+ data: { content: JSON.stringify(card), msg_type: "interactive", reply_in_thread: false },
77
+ });
78
+ return res.data?.message_id ?? "";
79
+ }
80
+ export async function updateText(client, msgId, text) {
81
+ const card = buildMarkdownCard(text);
82
+ await client.im.message.patch({
83
+ path: { message_id: msgId },
84
+ data: { content: JSON.stringify(card) },
85
+ });
86
+ }
87
+ // 最终回复卡片,超长分段发送,卡片失败 fallback 到普通文本
88
+ const CHUNK_SIZE = 2800;
89
+ function splitMarkdown(text, size) {
90
+ if (text.length <= size)
91
+ return [text];
92
+ const chunks = [];
93
+ let remaining = text;
94
+ while (remaining.length > 0) {
95
+ if (remaining.length <= size) {
96
+ chunks.push(remaining);
97
+ break;
98
+ }
99
+ // 尝试在换行处分割
100
+ let cut = remaining.lastIndexOf("\n", size);
101
+ if (cut < size * 0.5)
102
+ cut = remaining.lastIndexOf(" ", size);
103
+ if (cut < size * 0.5)
104
+ cut = size;
105
+ chunks.push(remaining.slice(0, cut).trim());
106
+ remaining = remaining.slice(cut).trim();
107
+ }
108
+ return chunks;
109
+ }
110
+ export async function replyFinalCard(client, chatId, rootMsgId, markdown, context) {
111
+ const threshold = context?.overflow.mode === "document"
112
+ ? context.overflow.document.threshold
113
+ : context?.overflow.chunk.threshold ?? CHUNK_SIZE;
114
+ // 不超限,直接发送
115
+ if (markdown.length <= threshold) {
116
+ await sendMessageChunk(client, rootMsgId, markdown);
117
+ return;
118
+ }
119
+ // 超限处理
120
+ if (context?.overflow.mode === "document") {
121
+ // 写入云文档
122
+ await replyWithDocument(client, chatId, rootMsgId, markdown, context);
123
+ }
124
+ else {
125
+ // 分片发送
126
+ const chunks = splitMarkdown(markdown, context?.overflow.chunk.threshold ?? CHUNK_SIZE);
127
+ for (let i = 0; i < chunks.length; i++) {
128
+ const content = chunks.length > 1
129
+ ? `**(${i + 1}/${chunks.length})**\n${chunks[i]}`
130
+ : chunks[i];
131
+ await sendMessageChunk(client, rootMsgId, content);
132
+ }
133
+ }
134
+ }
135
+ async function sendMessageChunk(client, rootMsgId, content) {
136
+ try {
137
+ const card = buildMarkdownCard(content);
138
+ await client.im.message.reply({
139
+ path: { message_id: rootMsgId },
140
+ data: { content: JSON.stringify(card), msg_type: "interactive", reply_in_thread: false },
141
+ });
142
+ }
143
+ catch {
144
+ // 卡片失败 fallback 到普通文本
145
+ await client.im.message.reply({
146
+ path: { message_id: rootMsgId },
147
+ data: { content: JSON.stringify({ text: content }), msg_type: "text", reply_in_thread: false },
148
+ });
149
+ }
150
+ }
151
+ async function replyWithDocument(client, chatId, rootMsgId, markdown, context) {
152
+ try {
153
+ // 获取 tenant_access_token
154
+ const token = await getTenantAccessToken(context.appId, context.appSecret);
155
+ // 构建文档标题
156
+ const now = new Date();
157
+ const datetime = now.toISOString().replace("T", " ").slice(0, 19);
158
+ const title = context.overflow.document.title_template
159
+ .replace("{profile}", context.profile)
160
+ .replace("{cwd}", context.cwd)
161
+ .replace("{session_id}", context.sessionId ?? "")
162
+ .replace("{datetime}", datetime)
163
+ .replace("{date}", datetime.slice(0, 10));
164
+ // 获取用户原始消息内容
165
+ let originalMessage = "";
166
+ try {
167
+ const msgRes = await client.im.message.get({
168
+ path: { message_id: rootMsgId },
169
+ });
170
+ const msgData = msgRes.data;
171
+ // 提取消息文本内容
172
+ if (msgData?.items?.[0]?.body?.content) {
173
+ const content = JSON.parse(msgData.items[0].body.content);
174
+ originalMessage = content.text || "";
175
+ }
176
+ }
177
+ catch {
178
+ // 获取失败时忽略
179
+ }
180
+ // 清理旧文档(如果启用)
181
+ let cleanupResult = null;
182
+ const cleanupConfig = context.overflow.document.cleanup;
183
+ if (cleanupConfig?.enabled) {
184
+ cleanupResult = await cleanupOldDocuments(token, cleanupConfig.max_docs, context.profile);
185
+ }
186
+ // 构建文档元信息
187
+ const meta = {
188
+ cwd: context.cwd,
189
+ profile: context.profile,
190
+ sessionId: context.sessionId ?? "",
191
+ datetime: datetime,
192
+ };
193
+ // 创建文档(在应用云空间)
194
+ const { docUrl, docId } = await createOverflowDocument(token, title, markdown, originalMessage, meta);
195
+ // 注册新文档到本地记录
196
+ registerDocument(docId, context.profile);
197
+ // 构建回复消息
198
+ let replyMsg = `📝 内容较长,已写入云文档:${docUrl}`;
199
+ if (cleanupConfig?.notify && cleanupResult && (cleanupResult.deleted > 0 || cleanupResult.failed > 0)) {
200
+ if (cleanupResult.failed > 0) {
201
+ replyMsg += `\n🗑️ 已清理 ${cleanupResult.deleted} 个旧文档,${cleanupResult.failed} 个删除失败`;
202
+ }
203
+ else {
204
+ replyMsg += `\n🗑️ 已清理 ${cleanupResult.deleted} 个旧文档(保留最近 ${cleanupConfig.max_docs} 个)`;
205
+ }
206
+ }
207
+ // 回复文档链接
208
+ await sendMessageChunk(client, rootMsgId, replyMsg);
209
+ }
210
+ catch (error) {
211
+ // 写文档失败,回退到分片发送
212
+ console.error("Failed to create document:", error);
213
+ await sendMessageChunk(client, rootMsgId, `❌ 写入云文档失败:${error},回退到分片发送`);
214
+ const chunks = splitMarkdown(markdown, CHUNK_SIZE);
215
+ for (let i = 0; i < chunks.length; i++) {
216
+ const content = `**(${i + 1}/${chunks.length})**\n${chunks[i]}`;
217
+ await sendMessageChunk(client, rootMsgId, content);
218
+ }
219
+ }
220
+ }
221
+ export async function sendToolCard(client, chatId, rootMsgId, label, detail, status = "running") {
222
+ const statusIcon = status === "running" ? "⏳ 进行中..." : status === "done" ? "✅ 完成" : "❌ 失败";
223
+ const content = `${label}\n\`${detail}\`\n${statusIcon}`;
224
+ const card = buildMarkdownCard(content);
225
+ const res = await client.im.message.reply({
226
+ path: { message_id: rootMsgId },
227
+ data: { content: JSON.stringify(card), msg_type: "interactive", reply_in_thread: false },
228
+ });
229
+ return res.data?.message_id ?? "";
230
+ }
231
+ export async function updateToolCard(client, msgId, label, detail, _resultPreview) {
232
+ const content = `${label}\n\`${detail}\`\n✅ 完成`;
233
+ const card = buildMarkdownCard(content);
234
+ await client.im.message.patch({
235
+ path: { message_id: msgId },
236
+ data: { content: JSON.stringify(card) },
237
+ });
238
+ }
239
+ // ── 图片下载 ─────────────────────────────────────────────────
240
+ export async function downloadImage(client, messageId, imageKey) {
241
+ try {
242
+ console.error(`[IMAGE] Downloading image: ${imageKey}`);
243
+ const res = await client.im.messageResource.get({
244
+ path: { message_id: messageId, file_key: imageKey },
245
+ params: { type: "image" },
246
+ });
247
+ const stream = res.getReadableStream();
248
+ const chunks = [];
249
+ await new Promise((resolve, reject) => {
250
+ stream.on("data", (chunk) => {
251
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
252
+ });
253
+ stream.on("end", resolve);
254
+ stream.on("error", reject);
255
+ });
256
+ const buf = Buffer.concat(chunks);
257
+ const base64 = buf.toString("base64");
258
+ const header = buf.slice(0, 4).toString("hex");
259
+ let mediaType = "image/jpeg";
260
+ if (header.startsWith("89504e47"))
261
+ mediaType = "image/png";
262
+ else if (header.startsWith("47494638"))
263
+ mediaType = "image/gif";
264
+ else if (header.startsWith("52494646"))
265
+ mediaType = "image/webp";
266
+ const sizeKB = Math.round(buf.length / 1024);
267
+ console.error(`[IMAGE] Downloaded: ${mediaType}, ${sizeKB}KB, base64=${base64.length}chars`);
268
+ return { base64, mediaType };
269
+ }
270
+ catch (e) {
271
+ console.error(`[IMAGE] Download failed:`, e);
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * 下载飞书消息中的文件到本地临时目录
277
+ */
278
+ export async function downloadFile(client, messageId, fileKey, tempDir, filename) {
279
+ try {
280
+ console.error(`[FILE] Downloading file: ${fileKey}, name: ${filename}`);
281
+ // 确保临时目录存在
282
+ if (!fs.existsSync(tempDir)) {
283
+ fs.mkdirSync(tempDir, { recursive: true });
284
+ }
285
+ // 下载文件
286
+ const res = await client.im.messageResource.get({
287
+ path: { message_id: messageId, file_key: fileKey },
288
+ params: { type: "file" },
289
+ });
290
+ const stream = res.getReadableStream();
291
+ const chunks = [];
292
+ await new Promise((resolve, reject) => {
293
+ stream.on("data", (chunk) => {
294
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
295
+ });
296
+ stream.on("end", resolve);
297
+ stream.on("error", reject);
298
+ });
299
+ const buf = Buffer.concat(chunks);
300
+ // 生成安全的文件名(添加时间戳避免冲突)
301
+ const timestamp = Date.now();
302
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
303
+ const uniqueName = `${timestamp}_${safeName}`;
304
+ const filepath = path.join(tempDir, uniqueName);
305
+ // 写入文件
306
+ fs.writeFileSync(filepath, buf);
307
+ // 检测 MIME 类型
308
+ const mimeTypes = {
309
+ // 文档
310
+ "pdf": "application/pdf",
311
+ "doc": "application/msword",
312
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
313
+ "xls": "application/vnd.ms-excel",
314
+ "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
315
+ "ppt": "application/vnd.ms-powerpoint",
316
+ "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
317
+ // 文本
318
+ "txt": "text/plain",
319
+ "md": "text/markdown",
320
+ "csv": "text/csv",
321
+ "json": "application/json",
322
+ "xml": "application/xml",
323
+ "yaml": "application/x-yaml",
324
+ "yml": "application/x-yaml",
325
+ // 代码
326
+ "js": "application/javascript",
327
+ "ts": "application/typescript",
328
+ "py": "text/x-python",
329
+ "java": "text/x-java",
330
+ "go": "text/x-go",
331
+ "rs": "text/x-rust",
332
+ "c": "text/x-c",
333
+ "cpp": "text/x-c++",
334
+ "h": "text/x-c",
335
+ "hpp": "text/x-c++",
336
+ "sh": "application/x-sh",
337
+ "bash": "application/x-sh",
338
+ // 压缩
339
+ "zip": "application/zip",
340
+ "tar": "application/x-tar",
341
+ "gz": "application/gzip",
342
+ "rar": "application/vnd.rar",
343
+ "7z": "application/x-7z-compressed",
344
+ // 其他
345
+ "html": "text/html",
346
+ "css": "text/css",
347
+ };
348
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
349
+ const mime_type = mimeTypes[ext] || "application/octet-stream";
350
+ const sizeKB = Math.round(buf.length / 1024);
351
+ console.error(`[FILE] Downloaded: ${mime_type}, ${sizeKB}KB, saved to ${filepath}`);
352
+ return {
353
+ filepath,
354
+ filename,
355
+ size: buf.length,
356
+ mime_type,
357
+ file_key: fileKey,
358
+ };
359
+ }
360
+ catch (e) {
361
+ console.error(`[FILE] Download failed:`, e);
362
+ return null;
363
+ }
364
+ }
365
+ // ── 卡片构建 ─────────────────────────────────────────────────
366
+ function buildMarkdownCard(markdown) {
367
+ return {
368
+ schema: "2.0",
369
+ body: { elements: [{ tag: "markdown", content: markdown }] },
370
+ };
371
+ }
372
+ /**
373
+ * 安全解析 JSON 响应
374
+ */
375
+ async function safeJsonParse(res, context) {
376
+ const text = await res.text();
377
+ if (!res.ok) {
378
+ throw new Error(`${context} failed (${res.status}): ${text.slice(0, 200)}`);
379
+ }
380
+ try {
381
+ return JSON.parse(text);
382
+ }
383
+ catch {
384
+ throw new Error(`${context} returned non-JSON: ${text.slice(0, 200)}`);
385
+ }
386
+ }
387
+ // 飞书文档块类型常量(通过 API 测试验证)
388
+ // 注意:飞书 API 的 block_type 与文档类型是一一对应的,不是用属性区分
389
+ const BlockType = {
390
+ PAGE: 1, // 页面
391
+ TEXT: 2, // 文本
392
+ HEADING1: 3, // 一级标题
393
+ HEADING2: 4, // 二级标题
394
+ HEADING3: 5, // 三级标题
395
+ BULLET: 12, // 无序列表
396
+ ORDERED: 13, // 有序列表
397
+ CODE: 14, // 代码块
398
+ QUOTE: 15, // 引用块
399
+ DIVIDER: 22, // 分割线
400
+ };
401
+ // 飞书官方语言ID对照表(ID范围1-75)
402
+ // 参考:https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/files/guide/create-document/create-new-document/create-new-document-overview
403
+ const LanguageMap = {
404
+ // 1 - PlainText
405
+ "": 1, "text": 1, "plain": 1, "txt": 1,
406
+ // 2 - ABAP
407
+ "abap": 2,
408
+ // 3 - Ada
409
+ "ada": 3,
410
+ // 4 - Apache
411
+ "apache": 4, "apacheconf": 4,
412
+ // 5 - Apex
413
+ "apex": 5,
414
+ // 6 - Assembly
415
+ "assembly": 6, "asm": 6,
416
+ // 7 - Bash
417
+ "bash": 7, "sh": 7, "shell": 7, "zsh": 7,
418
+ // 8 - CSharp
419
+ "csharp": 8, "cs": 8, "c#": 8,
420
+ // 9 - C++
421
+ "cpp": 9, "c++": 9, "cplusplus": 9, "cc": 9, "cxx": 9,
422
+ // 10 - C
423
+ "c": 10,
424
+ // 11 - COBOL
425
+ "cobol": 11,
426
+ // 12 - CSS
427
+ "css": 12,
428
+ // 13 - CoffeeScript
429
+ "coffeescript": 13, "coffee": 13,
430
+ // 14 - D
431
+ "d": 14,
432
+ // 15 - Dart
433
+ "dart": 15,
434
+ // 16 - Delphi
435
+ "delphi": 16, "pas": 16, "pascal": 16,
436
+ // 17 - Django
437
+ "django": 17, "jinja2": 17,
438
+ // 18 - Dockerfile
439
+ "dockerfile": 18, "docker": 18,
440
+ // 19 - Erlang
441
+ "erlang": 19, "erl": 19,
442
+ // 20 - Fortran
443
+ "fortran": 20, "f90": 20,
444
+ // 21 - FoxPro
445
+ "foxpro": 21, "dbf": 21,
446
+ // 22 - Go
447
+ "go": 22, "golang": 22,
448
+ // 23 - Groovy
449
+ "groovy": 23, "gradle": 23,
450
+ // 24 - HTML
451
+ "html": 24, "htm": 24,
452
+ // 25 - HTMLBars
453
+ "htmlbars": 25, "handlebars": 25, "hbs": 25,
454
+ // 26 - HTTP
455
+ "http": 26, "https": 26,
456
+ // 27 - Haskell
457
+ "haskell": 27, "hs": 27,
458
+ // 28 - JSON
459
+ "json": 28,
460
+ // 29 - Java
461
+ "java": 29,
462
+ // 30 - JavaScript
463
+ "javascript": 30, "js": 30, "jsx": 30,
464
+ // 31 - Julia
465
+ "julia": 31,
466
+ // 32 - Kotlin
467
+ "kotlin": 32, "kt": 32, "kts": 32,
468
+ // 33 - LaTeX
469
+ "latex": 33, "tex": 33,
470
+ // 34 - Lisp
471
+ "lisp": 34, "elisp": 34, "clisp": 34,
472
+ // 35 - Logo
473
+ "logo": 35,
474
+ // 36 - Lua
475
+ "lua": 36,
476
+ // 37 - MATLAB
477
+ "matlab": 37,
478
+ // 38 - Makefile
479
+ "makefile": 38, "make": 38, "mk": 38,
480
+ // 39 - Markdown
481
+ "markdown": 39, "md": 39,
482
+ // 40 - Nginx
483
+ "nginx": 40, "nginxconf": 40,
484
+ // 41 - Objective-C
485
+ "objc": 41, "objective-c": 41, "oc": 41,
486
+ // 42 - OpenEdgeABL
487
+ "openedge": 42, "abl": 42,
488
+ // 43 - PHP
489
+ "php": 43,
490
+ // 44 - Perl
491
+ "perl": 44, "pl": 44,
492
+ // 45 - PostScript
493
+ "postscript": 45, "ps": 45,
494
+ // 46 - PowerShell
495
+ "powershell": 46, "ps1": 46, "pwsh": 46,
496
+ // 47 - Prolog
497
+ "prolog": 47,
498
+ // 48 - ProtoBuf
499
+ "protobuf": 48, "proto": 48, "pb": 48,
500
+ // 49 - Python
501
+ "python": 49, "py": 49,
502
+ // 50 - R
503
+ "r": 50,
504
+ // 51 - RPG
505
+ "rpg": 51,
506
+ // 52 - Ruby
507
+ "ruby": 52, "rb": 52,
508
+ // 53 - Rust
509
+ "rust": 53, "rs": 53,
510
+ // 54 - SAS
511
+ "sas": 54,
512
+ // 55 - SCSS
513
+ "scss": 55, "sass": 55,
514
+ // 56 - SQL
515
+ "sql": 56, "mysql": 56, "postgresql": 56, "pgsql": 56,
516
+ // 57 - Scala
517
+ "scala": 57,
518
+ // 58 - Scheme
519
+ "scheme": 58,
520
+ // 59 - Scratch
521
+ "scratch": 59,
522
+ // 60 - Shell (shell 别名已归入 Bash 7)
523
+ // 61 - Swift
524
+ "swift": 61,
525
+ // 62 - Thrift
526
+ "thrift": 62,
527
+ // 63 - TypeScript
528
+ "typescript": 63, "ts": 63, "tsx": 63,
529
+ // 64 - VBScript
530
+ "vbscript": 64, "vbs": 64,
531
+ // 65 - Visual Basic
532
+ "vb": 65, "visual basic": 65, "vbnet": 65,
533
+ // 66 - XML
534
+ "xml": 66,
535
+ // 67 - YAML
536
+ "yaml": 67, "yml": 67,
537
+ // 68 - CMake
538
+ "cmake": 68,
539
+ // 69 - Diff
540
+ "diff": 69, "patch": 69,
541
+ // 70 - Gherkin
542
+ "gherkin": 70, "cucumber": 70, "feature": 70,
543
+ // 71 - GraphQL
544
+ "graphql": 71, "gql": 71,
545
+ // 72 - OpenGL Shading Language
546
+ "glsl": 72, "opengl": 72,
547
+ // 73 - Properties
548
+ "properties": 73, "ini": 73, "conf": 73,
549
+ // 74 - Solidity
550
+ "solidity": 74, "sol": 74,
551
+ // 75 - TOML
552
+ "toml": 75, "tml": 75,
553
+ };
554
+ /**
555
+ * 解析内联 Markdown 文本,支持粗体、斜体、行内代码、链接等
556
+ * 返回飞书文本元素数组
557
+ */
558
+ function parseInlineText(text) {
559
+ const elements = [];
560
+ let remaining = text;
561
+ // 正则表达式匹配各种内联元素(按优先级排序)
562
+ while (remaining.length > 0) {
563
+ // 链接 [text](url)
564
+ const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
565
+ if (linkMatch) {
566
+ elements.push({
567
+ text_run: {
568
+ content: linkMatch[1],
569
+ text_element_style: { link: { url: linkMatch[2] } },
570
+ },
571
+ });
572
+ remaining = remaining.slice(linkMatch[0].length);
573
+ continue;
574
+ }
575
+ // 粗体 **text**
576
+ const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
577
+ if (boldMatch) {
578
+ elements.push({
579
+ text_run: {
580
+ content: boldMatch[1],
581
+ text_element_style: { bold: true },
582
+ },
583
+ });
584
+ remaining = remaining.slice(boldMatch[0].length);
585
+ continue;
586
+ }
587
+ // 行内代码 `code`
588
+ const codeMatch = remaining.match(/^`([^`]+)`/);
589
+ if (codeMatch) {
590
+ elements.push({
591
+ text_run: {
592
+ content: codeMatch[1],
593
+ text_element_style: { inline_code: true },
594
+ },
595
+ });
596
+ remaining = remaining.slice(codeMatch[0].length);
597
+ continue;
598
+ }
599
+ // 斜体 *text* 或 _text_(避免与粗体混淆)
600
+ const italicMatch = remaining.match(/^(?<!\*)\*([^*]+)\*(?!\*)|^(?<!_)_([^_]+)_(?!_)/);
601
+ if (italicMatch) {
602
+ const content = italicMatch[1] || italicMatch[2];
603
+ elements.push({
604
+ text_run: {
605
+ content,
606
+ text_element_style: { italic: true },
607
+ },
608
+ });
609
+ remaining = remaining.slice(italicMatch[0].length);
610
+ continue;
611
+ }
612
+ // 删除线 ~~text~~
613
+ const strikeMatch = remaining.match(/^~~([^~]+)~~/);
614
+ if (strikeMatch) {
615
+ elements.push({
616
+ text_run: {
617
+ content: strikeMatch[1],
618
+ text_element_style: { strikethrough: true },
619
+ },
620
+ });
621
+ remaining = remaining.slice(strikeMatch[0].length);
622
+ continue;
623
+ }
624
+ // 查找下一个特殊字符的位置
625
+ const nextSpecial = remaining.search(/[*_`~\[]/);
626
+ if (nextSpecial === -1) {
627
+ // 没有更多特殊字符,添加剩余文本
628
+ if (remaining) {
629
+ elements.push({ text_run: { content: remaining } });
630
+ }
631
+ break;
632
+ }
633
+ else if (nextSpecial > 0) {
634
+ // 添加特殊字符前的普通文本
635
+ elements.push({ text_run: { content: remaining.slice(0, nextSpecial) } });
636
+ remaining = remaining.slice(nextSpecial);
637
+ }
638
+ else {
639
+ // 特殊字符在开头但无法匹配任何模式,作为普通字符处理
640
+ elements.push({ text_run: { content: remaining[0] } });
641
+ remaining = remaining.slice(1);
642
+ }
643
+ }
644
+ return elements.length > 0 ? elements : [{ text_run: { content: "" } }];
645
+ }
646
+ /**
647
+ * 将 Markdown 文本转换为飞书文档块
648
+ * 支持标题、代码块、列表、引用和文本
649
+ */
650
+ function markdownToBlocks(markdown, originalMessage, meta) {
651
+ const blocks = [];
652
+ // 处理 markdown 内容
653
+ const lines = markdown.split("\n");
654
+ let currentPara = [];
655
+ let inCodeBlock = false;
656
+ let codeContent = [];
657
+ let codeLang = "";
658
+ const flushPara = () => {
659
+ if (currentPara.length > 0) {
660
+ const content = currentPara.join("\n").trim();
661
+ if (content) {
662
+ // 解析内联 Markdown 格式
663
+ const elements = parseInlineText(content);
664
+ blocks.push({
665
+ block_type: BlockType.TEXT,
666
+ text: { elements },
667
+ });
668
+ }
669
+ currentPara = [];
670
+ }
671
+ };
672
+ // 处理列表项
673
+ const flushListItem = (item, isOrdered, _number) => {
674
+ const trimmed = item.trim();
675
+ if (!trimmed)
676
+ return;
677
+ const blockType = isOrdered ? BlockType.ORDERED : BlockType.BULLET;
678
+ const property = isOrdered ? "ordered" : "bullet";
679
+ // 解析内联 Markdown 格式
680
+ const elements = parseInlineText(trimmed);
681
+ blocks.push({
682
+ block_type: blockType,
683
+ [property]: { elements },
684
+ });
685
+ };
686
+ for (let i = 0; i < lines.length; i++) {
687
+ const line = lines[i];
688
+ // 代码块处理
689
+ if (line.startsWith("```")) {
690
+ if (!inCodeBlock) {
691
+ flushPara();
692
+ inCodeBlock = true;
693
+ codeLang = line.slice(3).trim().toLowerCase();
694
+ codeContent = [];
695
+ }
696
+ else {
697
+ // 代码块结束,创建原生代码块
698
+ const code = codeContent.join("\n");
699
+ const langId = LanguageMap[codeLang] ?? 1;
700
+ blocks.push({
701
+ block_type: BlockType.CODE,
702
+ code: {
703
+ style: { language: langId, wrap: true },
704
+ elements: [{ text_run: { content: code } }],
705
+ },
706
+ });
707
+ inCodeBlock = false;
708
+ codeContent = [];
709
+ codeLang = "";
710
+ }
711
+ continue;
712
+ }
713
+ if (inCodeBlock) {
714
+ codeContent.push(line);
715
+ continue;
716
+ }
717
+ // 引用块处理 (> 开头)
718
+ if (line.startsWith("> ")) {
719
+ flushPara();
720
+ const quoteContent = line.slice(2).trim();
721
+ if (quoteContent) {
722
+ const elements = parseInlineText(quoteContent);
723
+ blocks.push({
724
+ block_type: BlockType.QUOTE,
725
+ quote: { elements },
726
+ });
727
+ }
728
+ continue;
729
+ }
730
+ // 无序列表处理 (- 或 * 开头)
731
+ const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/);
732
+ if (bulletMatch) {
733
+ flushPara();
734
+ flushListItem(bulletMatch[2], false);
735
+ continue;
736
+ }
737
+ // 有序列表处理 (数字. 开头)
738
+ const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
739
+ if (orderedMatch) {
740
+ flushPara();
741
+ flushListItem(orderedMatch[3], true, parseInt(orderedMatch[2]));
742
+ continue;
743
+ }
744
+ // 标题处理(每种标题使用不同的 block_type)
745
+ if (line.startsWith("### ")) {
746
+ flushPara();
747
+ const headingContent = line.slice(4);
748
+ const elements = parseInlineText(headingContent);
749
+ blocks.push({
750
+ block_type: BlockType.HEADING3,
751
+ heading3: { elements },
752
+ });
753
+ }
754
+ else if (line.startsWith("## ")) {
755
+ flushPara();
756
+ const headingContent = line.slice(3);
757
+ const elements = parseInlineText(headingContent);
758
+ blocks.push({
759
+ block_type: BlockType.HEADING2,
760
+ heading2: { elements },
761
+ });
762
+ }
763
+ else if (line.startsWith("# ")) {
764
+ flushPara();
765
+ const headingContent = line.slice(2);
766
+ const elements = parseInlineText(headingContent);
767
+ blocks.push({
768
+ block_type: BlockType.HEADING1,
769
+ heading1: { elements },
770
+ });
771
+ }
772
+ else if (line.trim() === "---" || line.trim() === "***" || line.trim() === "___") {
773
+ // 分割线
774
+ flushPara();
775
+ blocks.push({ block_type: BlockType.DIVIDER, divider: {} });
776
+ }
777
+ else {
778
+ currentPara.push(line);
779
+ }
780
+ }
781
+ // 处理未结束的代码块
782
+ if (inCodeBlock && codeContent.length > 0) {
783
+ const code = codeContent.join("\n");
784
+ const langId = LanguageMap[codeLang] ?? 0;
785
+ blocks.push({
786
+ block_type: BlockType.CODE,
787
+ code: {
788
+ style: { language: langId, wrap: true },
789
+ elements: [{ text_run: { content: code } }],
790
+ },
791
+ });
792
+ }
793
+ // 添加最后一个段落
794
+ flushPara();
795
+ // 构建文档头部结构:引用块 → 分割线 → 元信息 → 分割线 → 正文
796
+ // 1. 引用块(显示用户的原始消息)
797
+ const quoteBlock = {
798
+ block_type: BlockType.QUOTE,
799
+ quote: {
800
+ elements: [{ text_run: { content: originalMessage || "(原消息内容获取失败)" } }],
801
+ },
802
+ };
803
+ // 2. 分割线
804
+ const divider1 = { block_type: BlockType.DIVIDER, divider: {} };
805
+ // 3. 元信息块
806
+ const metaLines = [
807
+ `📁 工作目录: ${meta.cwd}`,
808
+ `🤖 机器人: ${meta.profile}`,
809
+ `🔗 会话ID: ${meta.sessionId}`,
810
+ `📅 时间: ${meta.datetime}`,
811
+ ];
812
+ const metaBlock = {
813
+ block_type: BlockType.TEXT,
814
+ text: {
815
+ elements: metaLines.map(line => ({
816
+ text_run: { content: line },
817
+ })),
818
+ },
819
+ };
820
+ // 4. 分割线
821
+ const divider2 = { block_type: BlockType.DIVIDER, divider: {} };
822
+ // 在正文前插入头部结构
823
+ const header = [quoteBlock, divider1, metaBlock, divider2];
824
+ // 如果第一个块是标题,在标题后面插入头部
825
+ // 否则在最开头插入
826
+ const firstBlock = blocks[0];
827
+ if (firstBlock && (firstBlock.block_type === BlockType.HEADING1 ||
828
+ firstBlock.block_type === BlockType.HEADING2 ||
829
+ firstBlock.block_type === BlockType.HEADING3)) {
830
+ blocks.splice(1, 0, ...header);
831
+ }
832
+ else {
833
+ blocks.unshift(...header);
834
+ }
835
+ return blocks;
836
+ }
837
+ /**
838
+ * 创建云文档并写入内容(在应用云空间)
839
+ */
840
+ export async function createOverflowDocument(token, title, markdown, originalMessage, meta) {
841
+ // 1. 将 markdown 转换为文档块
842
+ const blocks = markdownToBlocks(markdown, originalMessage, meta);
843
+ // 2. 创建文档(不指定 folder_token,创建在应用云空间)
844
+ const createRes = await fetch("https://open.feishu.cn/open-apis/docx/v1/documents", {
845
+ method: "POST",
846
+ headers: {
847
+ "Authorization": `Bearer ${token}`,
848
+ "Content-Type": "application/json",
849
+ },
850
+ body: JSON.stringify({ title }),
851
+ });
852
+ const createData = await safeJsonParse(createRes, "Create document");
853
+ const docId = createData.data?.document?.document_id;
854
+ if (!docId) {
855
+ throw new Error("Failed to create document");
856
+ }
857
+ // 3. 写入内容到文档(分批写入,每批最多 50 个块)
858
+ const BATCH_SIZE = 50;
859
+ for (let i = 0; i < blocks.length; i += BATCH_SIZE) {
860
+ const batch = blocks.slice(i, i + BATCH_SIZE);
861
+ // 第一批用 index: 0 插入开头,后续用 index: -1 追加到末尾
862
+ const index = i === 0 ? 0 : -1;
863
+ const updateRes = await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docId}/blocks/${docId}/children`, {
864
+ method: "POST",
865
+ headers: {
866
+ "Authorization": `Bearer ${token}`,
867
+ "Content-Type": "application/json",
868
+ },
869
+ body: JSON.stringify({
870
+ children: batch,
871
+ index,
872
+ }),
873
+ });
874
+ const updateData = await safeJsonParse(updateRes, "Write content");
875
+ if (updateData.code !== 0) {
876
+ throw new Error(`Write content failed (${updateData.code}): ${updateData.msg}`);
877
+ }
878
+ }
879
+ // 返回文档链接和 ID
880
+ return {
881
+ docUrl: `https://feishu.cn/docx/${docId}`,
882
+ docId,
883
+ };
884
+ }
885
+ /**
886
+ * 获取 tenant_access_token
887
+ * 直接使用 appId 和 appSecret 获取
888
+ */
889
+ let cachedToken = null;
890
+ async function getTenantAccessToken(appId, appSecret) {
891
+ // 检查缓存
892
+ if (cachedToken && cachedToken.expiresAt > Date.now()) {
893
+ return cachedToken.token;
894
+ }
895
+ const res = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
896
+ method: "POST",
897
+ headers: { "Content-Type": "application/json" },
898
+ body: JSON.stringify({
899
+ app_id: appId,
900
+ app_secret: appSecret,
901
+ }),
902
+ });
903
+ const data = await safeJsonParse(res, "Get access token");
904
+ if (!data.tenant_access_token) {
905
+ throw new Error(`Failed to get tenant access token: ${data.msg ?? JSON.stringify(data)}`);
906
+ }
907
+ // 缓存 token(提前 5 分钟过期)
908
+ cachedToken = {
909
+ token: data.tenant_access_token,
910
+ expiresAt: Date.now() + ((data.expire ?? 7200) - 300) * 1000,
911
+ };
912
+ return cachedToken.token;
913
+ }
914
+ /**
915
+ * 构建飞书消息链接
916
+ */
917
+ function buildMessageLink(chatId, messageId) {
918
+ return `https://feishu.cn/client/chat/open?openChatId=${chatId}&openMessageId=${messageId}`;
919
+ }
920
+ /**
921
+ * 清理旧文档
922
+ * 使用本地注册表追踪文档,按创建时间排序,删除超出数量的旧文档
923
+ * 注意:docx API 没有 DELETE 接口,需要使用 drive API 删除
924
+ */
925
+ async function cleanupOldDocuments(token, maxDocs, profile) {
926
+ const result = { deleted: 0, failed: 0 };
927
+ try {
928
+ // 获取需要删除的文档(最旧的)
929
+ const toDelete = getOldestDocuments(profile, maxDocs);
930
+ for (const doc of toDelete) {
931
+ try {
932
+ // 使用 drive API 删除文档(docx 文档也是 drive 中的一种文件)
933
+ const deleteRes = await fetch(`https://open.feishu.cn/open-apis/drive/v1/files/${doc.id}?type=file`, {
934
+ method: "DELETE",
935
+ headers: {
936
+ "Authorization": `Bearer ${token}`,
937
+ },
938
+ });
939
+ const deleteData = await safeJsonParse(deleteRes, "Delete document");
940
+ if (deleteData.code === 0) {
941
+ result.deleted++;
942
+ // 从注册表中移除
943
+ removeDocumentRecord(doc.id, profile);
944
+ }
945
+ else {
946
+ result.failed++;
947
+ console.error(`Failed to delete document ${doc.id}:`, deleteData);
948
+ // 即使删除失败也尝试从注册表移除(可能是文档已被手动删除)
949
+ removeDocumentRecord(doc.id, profile);
950
+ }
951
+ }
952
+ catch {
953
+ result.failed++;
954
+ // 从注册表移除无效记录
955
+ removeDocumentRecord(doc.id, profile);
956
+ }
957
+ }
958
+ }
959
+ catch (error) {
960
+ console.error("Cleanup error:", error);
961
+ }
962
+ return result;
963
+ }
964
+ //# sourceMappingURL=feishu.js.map