feishu-docs-cli 0.1.0-beta.8 → 1.0.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.
Files changed (86) hide show
  1. package/README.md +70 -13
  2. package/README.zh.md +53 -13
  3. package/dist/auth.js +31 -17
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.js +16 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +4 -2
  8. package/dist/client.js +177 -61
  9. package/dist/client.js.map +1 -1
  10. package/dist/commands/authorize.d.ts +3 -0
  11. package/dist/commands/authorize.js +16 -34
  12. package/dist/commands/authorize.js.map +1 -1
  13. package/dist/commands/cp.d.ts +9 -0
  14. package/dist/commands/cp.js +70 -0
  15. package/dist/commands/cp.js.map +1 -0
  16. package/dist/commands/create.js +21 -7
  17. package/dist/commands/create.js.map +1 -1
  18. package/dist/commands/delete.js +53 -54
  19. package/dist/commands/delete.js.map +1 -1
  20. package/dist/commands/login.js +6 -2
  21. package/dist/commands/login.js.map +1 -1
  22. package/dist/commands/ls.js +38 -38
  23. package/dist/commands/ls.js.map +1 -1
  24. package/dist/commands/mkdir.d.ts +6 -0
  25. package/dist/commands/mkdir.js +49 -0
  26. package/dist/commands/mkdir.js.map +1 -0
  27. package/dist/commands/mv.d.ts +9 -0
  28. package/dist/commands/mv.js +72 -0
  29. package/dist/commands/mv.js.map +1 -0
  30. package/dist/commands/read.d.ts +1 -1
  31. package/dist/commands/read.js +17 -354
  32. package/dist/commands/read.js.map +1 -1
  33. package/dist/commands/search.js +57 -55
  34. package/dist/commands/search.js.map +1 -1
  35. package/dist/commands/share.d.ts +1 -1
  36. package/dist/commands/share.js +164 -91
  37. package/dist/commands/share.js.map +1 -1
  38. package/dist/commands/update.js +43 -60
  39. package/dist/commands/update.js.map +1 -1
  40. package/dist/commands/wiki.js +8 -20
  41. package/dist/commands/wiki.js.map +1 -1
  42. package/dist/parser/block-types.d.ts +0 -1
  43. package/dist/parser/block-types.js +0 -22
  44. package/dist/parser/block-types.js.map +1 -1
  45. package/dist/parser/blocks-to-md.d.ts +10 -18
  46. package/dist/parser/blocks-to-md.js +341 -450
  47. package/dist/parser/blocks-to-md.js.map +1 -1
  48. package/dist/scopes.d.ts +5 -47
  49. package/dist/scopes.js +9 -54
  50. package/dist/scopes.js.map +1 -1
  51. package/dist/services/block-writer.d.ts +3 -2
  52. package/dist/services/block-writer.js +29 -13
  53. package/dist/services/block-writer.js.map +1 -1
  54. package/dist/services/doc-blocks.d.ts +1 -1
  55. package/dist/services/doc-blocks.js +1 -1
  56. package/dist/services/doc-blocks.js.map +1 -1
  57. package/dist/services/doc-enrichment.d.ts +64 -0
  58. package/dist/services/doc-enrichment.js +397 -0
  59. package/dist/services/doc-enrichment.js.map +1 -0
  60. package/dist/services/image-download.d.ts +31 -0
  61. package/dist/services/image-download.js +127 -0
  62. package/dist/services/image-download.js.map +1 -0
  63. package/dist/services/markdown-convert.d.ts +20 -0
  64. package/dist/services/markdown-convert.js +55 -1
  65. package/dist/services/markdown-convert.js.map +1 -1
  66. package/dist/services/wiki-nodes.d.ts +1 -1
  67. package/dist/services/wiki-nodes.js +2 -3
  68. package/dist/services/wiki-nodes.js.map +1 -1
  69. package/dist/types/api-responses.d.ts +34 -0
  70. package/dist/types/api-responses.js +8 -0
  71. package/dist/types/api-responses.js.map +1 -0
  72. package/dist/types/index.d.ts +9 -17
  73. package/dist/utils/concurrency.d.ts +12 -0
  74. package/dist/utils/concurrency.js +37 -0
  75. package/dist/utils/concurrency.js.map +1 -0
  76. package/dist/utils/errors.d.ts +3 -1
  77. package/dist/utils/errors.js +11 -7
  78. package/dist/utils/errors.js.map +1 -1
  79. package/dist/utils/retry.d.ts +49 -0
  80. package/dist/utils/retry.js +70 -0
  81. package/dist/utils/retry.js.map +1 -0
  82. package/dist/utils/scope-prompt.d.ts +23 -18
  83. package/dist/utils/scope-prompt.js +62 -51
  84. package/dist/utils/scope-prompt.js.map +1 -1
  85. package/package.json +3 -1
  86. package/skills/feishu-docs/SKILL.md +37 -2
@@ -1,14 +1,6 @@
1
- /**
2
- * Convert Feishu document blocks to Markdown.
3
- *
4
- * Input: Block[] (flat array with parent_id + children)
5
- * Output: Markdown string
6
- */
7
- import { BlockType, isHeading, headingLevel, CODE_LANGUAGES, } from "./block-types.js";
1
+ /** Convert Feishu document blocks (flat array with parent_id + children) to Markdown string. */
2
+ import { BlockType, headingLevel, CODE_LANGUAGES, } from "./block-types.js";
8
3
  import { elementsToMarkdown } from "./text-elements.js";
9
- /**
10
- * Emoji ID to Unicode mapping for common callout emojis.
11
- */
12
4
  const EMOJI_MAP = {
13
5
  round_pushpin: "\u{1F4CD}",
14
6
  bulb: "\u{1F4A1}",
@@ -80,10 +72,7 @@ const EMOJI_MAP = {
80
72
  function emojiIdToUnicode(emojiId) {
81
73
  return EMOJI_MAP[emojiId] || `:${emojiId}:`;
82
74
  }
83
- /**
84
- * Build a tree from flat block array.
85
- * Each block gains a `_children` array of child block objects.
86
- */
75
+ /** Build a tree from flat block array. */
87
76
  function buildTree(blocks) {
88
77
  const map = new Map();
89
78
  for (const block of blocks) {
@@ -99,7 +88,6 @@ function buildTree(blocks) {
99
88
  root = node;
100
89
  }
101
90
  }
102
- // Sort children according to `children` order if available
103
91
  for (const node of map.values()) {
104
92
  if (node.children && node.children.length > 0) {
105
93
  const order = new Map(node.children.map((id, i) => [id, i]));
@@ -112,9 +100,7 @@ function buildTree(blocks) {
112
100
  }
113
101
  return root;
114
102
  }
115
- /**
116
- * Main entry: convert blocks to markdown string.
117
- */
103
+ /** Main entry: convert blocks to markdown string. */
118
104
  export function blocksToMarkdown(blocks, options = {}) {
119
105
  if (!blocks || blocks.length === 0)
120
106
  return "";
@@ -130,7 +116,6 @@ export function blocksToMarkdown(blocks, options = {}) {
130
116
  sheetDataMap: options.sheetDataMap || new Map(),
131
117
  warnings: [],
132
118
  };
133
- // Extract document title from root PAGE block
134
119
  if (root.block_type === BlockType.PAGE) {
135
120
  const titleText = getElements(root, "page", ctx);
136
121
  if (titleText) {
@@ -149,7 +134,6 @@ export function blocksToMarkdown(blocks, options = {}) {
149
134
  renderNode(child, lines, ctx, 0, {});
150
135
  }
151
136
  }
152
- // Emit warnings to stderr
153
137
  for (const w of ctx.warnings) {
154
138
  process.stderr.write(`feishu-docs: warning: ${w}\n`);
155
139
  }
@@ -158,9 +142,7 @@ export function blocksToMarkdown(blocks, options = {}) {
158
142
  .replace(/\n{3,}/g, "\n\n")
159
143
  .trim() + "\n");
160
144
  }
161
- /**
162
- * Render children with ordered list index tracking.
163
- */
145
+ /** Render children with ordered list index tracking. */
164
146
  function renderChildren(children, lines, ctx, depth) {
165
147
  const childState = { orderedIndex: 0 };
166
148
  for (const child of children) {
@@ -174,409 +156,344 @@ function renderChildren(children, lines, ctx, depth) {
174
156
  }
175
157
  }
176
158
  }
177
- /**
178
- * Render a single node and its children.
179
- */
159
+ /** Render a single node via dispatch table lookup. */
180
160
  function renderNode(node, lines, ctx, depth, state) {
181
- const type = node.block_type;
182
- const indent = " ".repeat(depth);
183
- if (type === BlockType.PAGE) {
184
- renderChildren(node._children, lines, ctx, depth);
185
- return;
186
- }
187
- if (type === BlockType.TEXT) {
188
- const text = getElements(node, "text", ctx);
189
- lines.push(indent + text);
190
- lines.push("");
191
- return;
192
- }
193
- if (isHeading(type)) {
194
- const level = headingLevel(type);
195
- const key = `heading${level}`;
196
- const text = getElements(node, key, ctx);
197
- const hashes = "#".repeat(Math.min(level, 6));
198
- lines.push(`${hashes} ${text}`);
199
- lines.push("");
200
- // Headings in Feishu can have children (folded/collapsible content)
201
- renderChildren(node._children, lines, ctx, depth);
202
- return;
203
- }
204
- if (type === BlockType.BULLET) {
205
- const text = getElements(node, "bullet", ctx);
206
- lines.push(`${indent}- ${text}`);
207
- renderChildren(node._children, lines, ctx, depth + 1);
208
- if (depth === 0)
209
- lines.push("");
210
- return;
211
- }
212
- if (type === BlockType.ORDERED) {
213
- const idx = (state.orderedIndex || 0) + 1;
214
- const text = getElements(node, "ordered", ctx);
215
- lines.push(`${indent}${idx}. ${text}`);
216
- renderChildren(node._children, lines, ctx, depth + 1);
217
- if (depth === 0)
218
- lines.push("");
219
- return;
220
- }
221
- if (type === BlockType.TODO) {
222
- const todo = (node.todo || {});
223
- const text = getElements(node, "todo", ctx);
224
- const check = todo.done ? "x" : " ";
225
- lines.push(`${indent}- [${check}] ${text}`);
226
- renderChildren(node._children, lines, ctx, depth + 1);
227
- if (depth === 0)
228
- lines.push("");
229
- return;
230
- }
231
- if (type === BlockType.CODE) {
232
- const codeData = (node.code || {});
233
- const langNum = codeData.style?.language;
234
- const lang = langNum !== undefined
235
- ? CODE_LANGUAGES[langNum] || ""
236
- : "";
237
- const text = getElements(node, "code", ctx);
238
- lines.push(`\`\`\`${lang}`);
239
- lines.push(text);
240
- lines.push("```");
241
- lines.push("");
242
- return;
243
- }
244
- if (type === BlockType.QUOTE) {
245
- const text = getElements(node, "quote", ctx);
246
- const quotedLines = text.split("\n").map((l) => `> ${l}`);
247
- lines.push(...quotedLines);
248
- for (const child of node._children) {
249
- const childLines = [];
250
- renderNode(child, childLines, ctx, 0, {});
251
- lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
252
- }
253
- lines.push("");
254
- return;
255
- }
256
- if (type === BlockType.QUOTE_CONTAINER) {
257
- for (const child of node._children) {
258
- const childLines = [];
259
- renderNode(child, childLines, ctx, 0, {});
260
- lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
261
- }
262
- lines.push("");
263
- return;
264
- }
265
- if (type === BlockType.EQUATION) {
266
- const eq = (node.equation || {});
267
- const content = (eq.content || "").trim();
268
- lines.push(`$$`);
269
- lines.push(content);
270
- lines.push(`$$`);
271
- lines.push("");
272
- return;
273
- }
274
- if (type === BlockType.DIVIDER) {
275
- lines.push("---");
276
- lines.push("");
277
- return;
278
- }
279
- if (type === BlockType.IMAGE) {
280
- const imageData = (node.image || {});
281
- const fileToken = imageData.token;
282
- const url = fileToken ? ctx.imageUrlMap.get(fileToken) || "" : "";
283
- const alt = imageData.alt || "";
284
- if (url) {
285
- lines.push(`![${alt}](${url})`);
286
- }
287
- else {
288
- lines.push(`![${alt}](${fileToken || ""})`);
289
- }
290
- lines.push("");
291
- return;
292
- }
293
- if (type === BlockType.TABLE) {
294
- renderTable(node, lines, ctx);
295
- lines.push("");
296
- return;
297
- }
298
- if (type === BlockType.CALLOUT) {
299
- const callout = (node.callout || {});
300
- const emoji = callout.emoji_id
301
- ? emojiIdToUnicode(callout.emoji_id) + " "
302
- : "";
303
- let isFirst = true;
304
- for (const child of node._children) {
305
- const childLines = [];
306
- renderNode(child, childLines, ctx, 0, {});
307
- const first = childLines.shift() || "";
308
- // Emoji only on the very first line of the callout
309
- const prefix = isFirst ? emoji : "";
310
- isFirst = false;
311
- lines.push(`> ${prefix}${first}`);
312
- for (const cl of childLines) {
313
- lines.push(cl ? `> ${cl}` : ">");
314
- }
315
- }
316
- lines.push("");
317
- return;
318
- }
319
- if (type === BlockType.DIAGRAM) {
320
- const text = getElements(node, "diagram", ctx);
321
- lines.push("```mermaid");
322
- lines.push(sanitizeMermaid(text));
323
- lines.push("```");
324
- lines.push("");
325
- return;
326
- }
327
- if (type === BlockType.IFRAME) {
328
- const iframe = (node.iframe || {});
329
- const url = iframe.component?.url || "";
330
- lines.push(`[嵌入](${url})`);
331
- lines.push("");
332
- return;
333
- }
334
- if (type === BlockType.GRID) {
335
- for (const child of node._children) {
336
- for (const grandchild of child._children) {
337
- renderNode(grandchild, lines, ctx, depth, {});
338
- }
339
- }
340
- return;
341
- }
342
- if (type === BlockType.GRID_COLUMN) {
343
- for (const child of node._children) {
344
- renderNode(child, lines, ctx, depth, {});
345
- }
346
- return;
161
+ const renderer = RENDERERS.get(node.block_type);
162
+ if (renderer) {
163
+ renderer(node, { lines, ctx, depth, state });
347
164
  }
348
- if (type === BlockType.TABLE_CELL) {
349
- // Handled by renderTable
350
- return;
351
- }
352
- if (type === BlockType.FILE) {
353
- const fileData = (node.file || {});
354
- const name = fileData.name || "文件";
355
- const token = fileData.token || "";
356
- const url = ctx.imageUrlMap.get(token) || "";
357
- lines.push(`[${name}](${url || token})`);
165
+ else {
166
+ ctx.warnings.push(`\u4E0D\u652F\u6301\u7684\u5185\u5BB9\u7C7B\u578B: ${node.block_type}`);
167
+ lines.push(`[\u4E0D\u652F\u6301\u7684\u5185\u5BB9\u7C7B\u578B: ${node.block_type}]`);
358
168
  lines.push("");
359
- return;
360
169
  }
361
- if (type === BlockType.ADDONS) {
362
- const addOns = node["add_ons"] ||
363
- {};
364
- // Try to extract Mermaid diagram from record
365
- try {
366
- const record = JSON.parse(addOns.record || "{}");
367
- if ((record.data &&
368
- typeof record.data === "string" &&
369
- record.data.includes("graph")) ||
370
- record.data?.includes("flowchart") ||
371
- record.data?.includes("sequenceDiagram") ||
372
- record.data?.includes("classDiagram") ||
373
- record.data?.includes("gantt") ||
374
- record.data?.includes("pie") ||
375
- record.data?.includes("erDiagram")) {
376
- lines.push("```mermaid");
377
- lines.push(sanitizeMermaid(record.data.trim()));
378
- lines.push("```");
379
- lines.push("");
380
- return;
381
- }
382
- }
383
- catch {
384
- // not parseable, skip
385
- }
386
- // TOC, Jira, OKR etc. — skip gracefully
387
- return;
388
- }
389
- // Bitable render as Markdown table if data available
390
- if (type === BlockType.BITABLE) {
391
- const token = node.bitable?.token || "";
392
- const data = ctx.bitableDataMap.get(token);
393
- if (data && data.fields.length > 0) {
394
- lines.push("| " +
395
- data.fields.map((f) => f.replace(/\|/g, "\\|")).join(" | ") +
396
- " |");
397
- lines.push("| " + data.fields.map(() => "---").join(" | ") + " |");
398
- for (const row of data.records) {
399
- lines.push("| " +
400
- row
401
- .map((c) => String(c).replace(/\|/g, "\\|").replace(/\n/g, " "))
402
- .join(" | ") +
403
- " |");
404
- }
405
- }
406
- else {
407
- lines.push(`[多维表格: ${token}]`);
408
- }
409
- lines.push("");
410
- return;
170
+ }
171
+ function renderPage(node, rctx) {
172
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth);
173
+ }
174
+ function renderText(node, rctx) {
175
+ const text = getElements(node, "text", rctx.ctx);
176
+ rctx.lines.push(" ".repeat(rctx.depth) + text);
177
+ rctx.lines.push("");
178
+ }
179
+ function renderHeading(node, rctx) {
180
+ const level = headingLevel(node.block_type);
181
+ const text = getElements(node, `heading${level}`, rctx.ctx);
182
+ rctx.lines.push(`${"#".repeat(Math.min(level, 6))} ${text}`);
183
+ rctx.lines.push("");
184
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth);
185
+ }
186
+ function renderBullet(node, rctx) {
187
+ const text = getElements(node, "bullet", rctx.ctx);
188
+ rctx.lines.push(`${" ".repeat(rctx.depth)}- ${text}`);
189
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth + 1);
190
+ if (rctx.depth === 0)
191
+ rctx.lines.push("");
192
+ }
193
+ function renderOrdered(node, rctx) {
194
+ const idx = (rctx.state.orderedIndex || 0) + 1;
195
+ const text = getElements(node, "ordered", rctx.ctx);
196
+ rctx.lines.push(`${" ".repeat(rctx.depth)}${idx}. ${text}`);
197
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth + 1);
198
+ if (rctx.depth === 0)
199
+ rctx.lines.push("");
200
+ }
201
+ function renderTodo(node, rctx) {
202
+ const todo = (node.todo || {});
203
+ const text = getElements(node, "todo", rctx.ctx);
204
+ const check = todo.done ? "x" : " ";
205
+ rctx.lines.push(`${" ".repeat(rctx.depth)}- [${check}] ${text}`);
206
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth + 1);
207
+ if (rctx.depth === 0)
208
+ rctx.lines.push("");
209
+ }
210
+ function renderCode(node, rctx) {
211
+ const codeData = (node.code || {});
212
+ const langNum = codeData.style?.language;
213
+ const lang = langNum !== undefined
214
+ ? CODE_LANGUAGES[langNum] || ""
215
+ : "";
216
+ const text = getElements(node, "code", rctx.ctx);
217
+ rctx.lines.push(`\`\`\`${lang}`, text, "```", "");
218
+ }
219
+ function renderQuote(node, rctx) {
220
+ const text = getElements(node, "quote", rctx.ctx);
221
+ rctx.lines.push(...text.split("\n").map((l) => `> ${l}`));
222
+ for (const child of node._children) {
223
+ const childLines = [];
224
+ renderNode(child, childLines, rctx.ctx, 0, {});
225
+ rctx.lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
226
+ }
227
+ rctx.lines.push("");
228
+ }
229
+ function renderQuoteContainer(node, rctx) {
230
+ for (const child of node._children) {
231
+ const childLines = [];
232
+ renderNode(child, childLines, rctx.ctx, 0, {});
233
+ rctx.lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
411
234
  }
412
- // Board — whiteboard/flowchart, rendered as image if available
413
- if (type === BlockType.BOARD) {
414
- const token = node.board
415
- ?.token || "";
416
- const imagePath = ctx.boardImageMap.get(token);
417
- if (imagePath) {
418
- lines.push(`![画板](${imagePath})`);
419
- }
420
- else {
421
- lines.push(`[画板: ${token}]`);
235
+ rctx.lines.push("");
236
+ }
237
+ function renderEquation(node, rctx) {
238
+ const eq = (node.equation || {});
239
+ rctx.lines.push("$$", (eq.content || "").trim(), "$$", "");
240
+ }
241
+ function renderDivider(_node, rctx) {
242
+ rctx.lines.push("---", "");
243
+ }
244
+ function renderImage(node, rctx) {
245
+ const imageData = (node.image || {});
246
+ const fileToken = imageData.token;
247
+ const url = fileToken ? rctx.ctx.imageUrlMap.get(fileToken) || "" : "";
248
+ const alt = imageData.alt || "";
249
+ rctx.lines.push(url ? `![${alt}](${url})` : `![${alt}](${fileToken || ""})`);
250
+ rctx.lines.push("");
251
+ }
252
+ function renderTableBlock(node, rctx) {
253
+ renderTable(node, rctx.lines, rctx.ctx);
254
+ rctx.lines.push("");
255
+ }
256
+ function renderCallout(node, rctx) {
257
+ const callout = (node.callout || {});
258
+ const emoji = callout.emoji_id
259
+ ? emojiIdToUnicode(callout.emoji_id) + " "
260
+ : "";
261
+ let isFirst = true;
262
+ for (const child of node._children) {
263
+ const childLines = [];
264
+ renderNode(child, childLines, rctx.ctx, 0, {});
265
+ const first = childLines.shift() || "";
266
+ const prefix = isFirst ? emoji : "";
267
+ isFirst = false;
268
+ rctx.lines.push(`> ${prefix}${first}`);
269
+ for (const cl of childLines) {
270
+ rctx.lines.push(cl ? `> ${cl}` : ">");
271
+ }
272
+ }
273
+ rctx.lines.push("");
274
+ }
275
+ function renderDiagram(node, rctx) {
276
+ const text = getElements(node, "diagram", rctx.ctx);
277
+ rctx.lines.push("```mermaid", sanitizeMermaid(text), "```", "");
278
+ }
279
+ function renderIframe(node, rctx) {
280
+ const url = (node.iframe || {}).component?.url ||
281
+ "";
282
+ rctx.lines.push(`[嵌入](${url})`, "");
283
+ }
284
+ function renderGrid(node, rctx) {
285
+ for (const child of node._children) {
286
+ for (const grandchild of child._children) {
287
+ renderNode(grandchild, rctx.lines, rctx.ctx, rctx.depth, {});
422
288
  }
423
- lines.push("");
424
- return;
425
289
  }
426
- // Sheet — render as Markdown table if data available
427
- if (type === BlockType.SHEET) {
428
- const token = node.sheet?.token || "";
429
- const data = ctx.sheetDataMap.get(token);
430
- if (data && data.fields.length > 0) {
431
- if (data.title) {
432
- lines.push(`**${data.title}**`);
433
- lines.push("");
434
- }
435
- lines.push("| " +
436
- data.fields.map((f) => f.replace(/\|/g, "\\|")).join(" | ") +
437
- " |");
438
- lines.push("| " + data.fields.map(() => "---").join(" | ") + " |");
439
- for (const row of data.records) {
440
- lines.push("| " +
441
- row
442
- .map((c) => String(c).replace(/\|/g, "\\|").replace(/\n/g, " "))
443
- .join(" | ") +
444
- " |");
445
- }
446
- }
447
- else {
448
- lines.push(`[电子表格: ${token}]`);
449
- }
450
- lines.push("");
451
- return;
290
+ }
291
+ function renderGridColumn(node, rctx) {
292
+ for (const child of node._children) {
293
+ renderNode(child, rctx.lines, rctx.ctx, rctx.depth, {});
452
294
  }
453
- // Task — render as TODO with metadata
454
- if (type === BlockType.TASK) {
455
- const task = (node.task || {});
456
- const taskId = task.task_id || "";
457
- const text = getElements(node, "task", ctx);
458
- const summary = text || task.summary || taskId || "未命名任务";
459
- const parts = [];
460
- if (task.assignees && task.assignees.length > 0) {
461
- const names = task.assignees.map((a) => `@${a.name || a.id || "?"}`);
462
- parts.push(names.join(", "));
463
- }
464
- if (task.due) {
465
- parts.push(`截止: ${task.due}`);
466
- }
467
- const meta = parts.length > 0 ? ` (${parts.join(", ")})` : "";
468
- const check = task.completed ? "x" : " ";
469
- lines.push(`${indent}- [${check}] ${summary}${meta}`);
470
- if (depth === 0)
471
- lines.push("");
472
- return;
295
+ }
296
+ function renderTableCell() {
297
+ /* Handled by renderTable */
298
+ }
299
+ function renderFile(node, rctx) {
300
+ const fileData = (node.file || {});
301
+ const name = fileData.name || "文件";
302
+ const token = fileData.token || "";
303
+ const url = rctx.ctx.imageUrlMap.get(token) || "";
304
+ rctx.lines.push(`[${name}](${url || token})`, "");
305
+ }
306
+ const MERMAID_KEYWORDS = [
307
+ "graph",
308
+ "flowchart",
309
+ "sequenceDiagram",
310
+ "classDiagram",
311
+ "gantt",
312
+ "pie",
313
+ "erDiagram",
314
+ ];
315
+ function renderAddons(node, rctx) {
316
+ const addOns = node["add_ons"] || {};
317
+ try {
318
+ const record = JSON.parse(addOns.record || "{}");
319
+ if (record.data &&
320
+ typeof record.data === "string" &&
321
+ MERMAID_KEYWORDS.some((k) => record.data.includes(k))) {
322
+ rctx.lines.push("```mermaid", sanitizeMermaid(record.data.trim()), "```", "");
323
+ return;
324
+ }
325
+ }
326
+ catch {
327
+ /* not parseable, skip */
473
328
  }
474
- // LinkPreview — degrade to hyperlink
475
- if (type === BlockType.LINK_PREVIEW) {
476
- const preview = node["link_preview"] || {};
477
- const url = preview.url || "";
478
- const title = preview.title || url || "链接";
479
- lines.push(`[${title}](${url})`);
480
- lines.push("");
481
- return;
329
+ }
330
+ function renderMdTable(fields, records, lines) {
331
+ lines.push("| " + fields.map((f) => f.replace(/\|/g, "\\|")).join(" | ") + " |");
332
+ lines.push("| " + fields.map(() => "---").join(" | ") + " |");
333
+ for (const row of records) {
334
+ lines.push("| " +
335
+ row
336
+ .map((c) => String(c).replace(/\|/g, "\\|").replace(/\n/g, " "))
337
+ .join(" | ") +
338
+ " |");
482
339
  }
483
- // JiraIssue — render as JIRA link
484
- if (type === BlockType.JIRA_ISSUE) {
485
- const jira = node["jira_issue"] || {};
486
- const key = jira.key || "";
487
- const url = jira.url || "";
488
- const summary = jira.summary || key || "JIRA Issue";
489
- if (url) {
490
- lines.push(`[JIRA: ${key || summary}](${url})`);
491
- }
492
- else {
493
- lines.push(`[JIRA: ${key || summary}]`);
494
- }
495
- lines.push("");
496
- return;
340
+ }
341
+ function renderBitable(node, rctx) {
342
+ const token = node.bitable?.token || "";
343
+ const data = rctx.ctx.bitableDataMap.get(token);
344
+ if (data && data.fields.length > 0) {
345
+ renderMdTable(data.fields, data.records, rctx.lines);
497
346
  }
498
- // WikiCatalog — render children or placeholder
499
- if (type === BlockType.WIKI_CATALOG) {
500
- if (node._children.length > 0) {
501
- renderChildren(node._children, lines, ctx, depth);
502
- }
503
- else {
504
- lines.push("[知识库目录]");
505
- lines.push("");
506
- }
507
- return;
347
+ else {
348
+ rctx.lines.push(`[多维表格: ${token}]`);
508
349
  }
509
- // SubPageList — render children or placeholder
510
- if (type === BlockType.SUB_PAGE_LIST) {
511
- if (node._children.length > 0) {
512
- renderChildren(node._children, lines, ctx, depth);
513
- }
514
- else {
515
- lines.push("[子页面列表]");
516
- lines.push("");
350
+ rctx.lines.push("");
351
+ }
352
+ function renderBoard(node, rctx) {
353
+ const token = node.board
354
+ ?.token || "";
355
+ const imagePath = rctx.ctx.boardImageMap.get(token);
356
+ rctx.lines.push(imagePath ? `![画板](${imagePath})` : `[画板: ${token}]`);
357
+ rctx.lines.push("");
358
+ }
359
+ function renderSheet(node, rctx) {
360
+ const token = node.sheet?.token || "";
361
+ const data = rctx.ctx.sheetDataMap.get(token);
362
+ if (data && data.fields.length > 0) {
363
+ if (data.title) {
364
+ rctx.lines.push(`**${data.title}**`, "");
517
365
  }
518
- return;
519
- }
520
- // Agenda blocks — meeting agenda rendering
521
- if (type === BlockType.AGENDA || type === BlockType.AGENDA_ITEM) {
522
- renderChildren(node._children, lines, ctx, depth);
523
- return;
366
+ renderMdTable(data.fields, data.records, rctx.lines);
524
367
  }
525
- if (type === BlockType.AGENDA_ITEM_TITLE) {
526
- const text = getElements(node, "agenda_item_title", ctx);
527
- lines.push(`${indent}**${text}**`);
528
- lines.push("");
529
- return;
530
- }
531
- if (type === BlockType.AGENDA_ITEM_CONTENT) {
532
- renderChildren(node._children, lines, ctx, depth);
533
- return;
534
- }
535
- // OKR blocks — complex business data, skip silently
536
- if (type === BlockType.OKR ||
537
- type === BlockType.OKR_OBJECTIVE ||
538
- type === BlockType.OKR_KEY_RESULT ||
539
- type === BlockType.OKR_PROGRESS) {
540
- return;
368
+ else {
369
+ rctx.lines.push(`[电子表格: ${token}]`);
541
370
  }
542
- // Synced blocks and AI template — no extractable content, skip silently
543
- if (type === BlockType.SOURCE_SYNCED ||
544
- type === BlockType.REFERENCE_SYNCED ||
545
- type === BlockType.AI_TEMPLATE) {
546
- return;
547
- }
548
- // Reference types show as label with token
549
- const refTypes = {
550
- [BlockType.MINDNOTE]: "思维笔记",
551
- [BlockType.VIEW]: "视图",
552
- [BlockType.CHAT_CARD]: "群消息卡片",
553
- [BlockType.ISV]: "三方块",
371
+ rctx.lines.push("");
372
+ }
373
+ function renderTask(node, rctx) {
374
+ const indent = " ".repeat(rctx.depth);
375
+ const task = (node.task || {});
376
+ const text = getElements(node, "task", rctx.ctx);
377
+ const summary = text || task.summary || task.task_id || "未命名任务";
378
+ const parts = [];
379
+ if (task.assignees && task.assignees.length > 0) {
380
+ parts.push(task.assignees.map((a) => `@${a.name || a.id || "?"}`).join(", "));
381
+ }
382
+ if (task.due)
383
+ parts.push(`截止: ${task.due}`);
384
+ const meta = parts.length > 0 ? ` (${parts.join(", ")})` : "";
385
+ rctx.lines.push(`${indent}- [${task.completed ? "x" : " "}] ${summary}${meta}`);
386
+ if (rctx.depth === 0)
387
+ rctx.lines.push("");
388
+ }
389
+ function renderLinkPreview(node, rctx) {
390
+ const preview = node["link_preview"] || {};
391
+ const url = preview.url || "";
392
+ rctx.lines.push(`[${preview.title || url || "链接"}](${url})`, "");
393
+ }
394
+ function renderJiraIssue(node, rctx) {
395
+ const jira = node["jira_issue"] || {};
396
+ const key = jira.key || "";
397
+ const url = jira.url || "";
398
+ const label = key || jira.summary || "JIRA Issue";
399
+ rctx.lines.push(url ? `[JIRA: ${label}](${url})` : `[JIRA: ${label}]`);
400
+ rctx.lines.push("");
401
+ }
402
+ function renderChildrenOrPlaceholder(placeholder) {
403
+ return (node, rctx) => {
404
+ if (node._children.length > 0)
405
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth);
406
+ else
407
+ rctx.lines.push(placeholder, "");
554
408
  };
555
- if (refTypes[type]) {
556
- const data = node[Object.keys(node).find((k) => typeof node[k] === "object" &&
557
- node[k] !== null &&
558
- node[k]
559
- ?.token) || ""] || {};
560
- const token = data.token || "";
561
- lines.push(`[${refTypes[type]}: ${token}]`);
562
- lines.push("");
563
- return;
564
- }
565
- // Unknown type
566
- ctx.warnings.push(`不支持的内容类型: ${type}`);
567
- lines.push(`[不支持的内容类型: ${type}]`);
568
- lines.push("");
569
- }
570
- /**
571
- * Get inline text from a block's elements.
572
- */
409
+ }
410
+ const renderWikiCatalog = renderChildrenOrPlaceholder("[知识库目录]");
411
+ const renderSubPageList = renderChildrenOrPlaceholder("[子页面列表]");
412
+ function renderDelegateChildren(node, rctx) {
413
+ renderChildren(node._children, rctx.lines, rctx.ctx, rctx.depth);
414
+ }
415
+ function renderAgendaItemTitle(node, rctx) {
416
+ const text = getElements(node, "agenda_item_title", rctx.ctx);
417
+ rctx.lines.push(`${" ".repeat(rctx.depth)}**${text}**`, "");
418
+ }
419
+ function renderNoop() {
420
+ /* OKR, synced blocks, AI template -- no content */
421
+ }
422
+ const REF_TYPE_LABELS = {
423
+ [BlockType.MINDNOTE]: "思维笔记",
424
+ [BlockType.VIEW]: "视图",
425
+ [BlockType.CHAT_CARD]: "群消息卡片",
426
+ [BlockType.ISV]: "三方块",
427
+ };
428
+ function renderRefType(node, rctx) {
429
+ const label = REF_TYPE_LABELS[node.block_type] || "未知引用";
430
+ const data = node[Object.keys(node).find((k) => typeof node[k] === "object" &&
431
+ node[k] !== null &&
432
+ node[k]
433
+ ?.token) || ""] || {};
434
+ rctx.lines.push(`[${label}: ${data.token || ""}]`, "");
435
+ }
436
+ /** Block type dispatch table. */
437
+ const RENDERERS = new Map([
438
+ [BlockType.PAGE, renderPage],
439
+ [BlockType.TEXT, renderText],
440
+ [BlockType.HEADING1, renderHeading],
441
+ [BlockType.HEADING2, renderHeading],
442
+ [BlockType.HEADING3, renderHeading],
443
+ [BlockType.HEADING4, renderHeading],
444
+ [BlockType.HEADING5, renderHeading],
445
+ [BlockType.HEADING6, renderHeading],
446
+ [BlockType.HEADING7, renderHeading],
447
+ [BlockType.HEADING8, renderHeading],
448
+ [BlockType.HEADING9, renderHeading],
449
+ [BlockType.BULLET, renderBullet],
450
+ [BlockType.ORDERED, renderOrdered],
451
+ [BlockType.CODE, renderCode],
452
+ [BlockType.QUOTE, renderQuote],
453
+ [BlockType.EQUATION, renderEquation],
454
+ [BlockType.TODO, renderTodo],
455
+ [BlockType.BITABLE, renderBitable],
456
+ [BlockType.CALLOUT, renderCallout],
457
+ [BlockType.CHAT_CARD, renderRefType],
458
+ [BlockType.DIAGRAM, renderDiagram],
459
+ [BlockType.DIVIDER, renderDivider],
460
+ [BlockType.FILE, renderFile],
461
+ [BlockType.GRID, renderGrid],
462
+ [BlockType.GRID_COLUMN, renderGridColumn],
463
+ [BlockType.IFRAME, renderIframe],
464
+ [BlockType.IMAGE, renderImage],
465
+ [BlockType.ISV, renderRefType],
466
+ [BlockType.MINDNOTE, renderRefType],
467
+ [BlockType.SHEET, renderSheet],
468
+ [BlockType.TABLE, renderTableBlock],
469
+ [BlockType.TABLE_CELL, renderTableCell],
470
+ [BlockType.VIEW, renderRefType],
471
+ [BlockType.QUOTE_CONTAINER, renderQuoteContainer],
472
+ [BlockType.TASK, renderTask],
473
+ [BlockType.OKR, renderNoop],
474
+ [BlockType.OKR_OBJECTIVE, renderNoop],
475
+ [BlockType.OKR_KEY_RESULT, renderNoop],
476
+ [BlockType.OKR_PROGRESS, renderNoop],
477
+ [BlockType.ADDONS, renderAddons],
478
+ [BlockType.JIRA_ISSUE, renderJiraIssue],
479
+ [BlockType.WIKI_CATALOG, renderWikiCatalog],
480
+ [BlockType.BOARD, renderBoard],
481
+ [BlockType.AGENDA, renderDelegateChildren],
482
+ [BlockType.AGENDA_ITEM, renderDelegateChildren],
483
+ [BlockType.AGENDA_ITEM_TITLE, renderAgendaItemTitle],
484
+ [BlockType.AGENDA_ITEM_CONTENT, renderDelegateChildren],
485
+ [BlockType.LINK_PREVIEW, renderLinkPreview],
486
+ [BlockType.SOURCE_SYNCED, renderNoop],
487
+ [BlockType.REFERENCE_SYNCED, renderNoop],
488
+ [BlockType.SUB_PAGE_LIST, renderSubPageList],
489
+ [BlockType.AI_TEMPLATE, renderNoop],
490
+ ]);
491
+ /** Get inline text from a block's elements. */
573
492
  function getElements(node, key, ctx) {
574
493
  const data = node[key] || {};
575
494
  return elementsToMarkdown(data.elements, ctx);
576
495
  }
577
- /**
578
- * Render a table block as Markdown table.
579
- */
496
+ /** Render a table block as Markdown table. */
580
497
  function renderTable(node, lines, ctx) {
581
498
  const tableData = (node.table || {});
582
499
  const property = tableData.property || {};
@@ -584,38 +501,26 @@ function renderTable(node, lines, ctx) {
584
501
  const colSize = property.column_size || 0;
585
502
  if (rowSize === 0 || colSize === 0)
586
503
  return;
587
- // Build 2D grid from table_cell children
588
504
  const cells = node._children;
589
505
  const grid = [];
590
506
  for (let r = 0; r < rowSize; r++) {
591
507
  const row = [];
592
508
  for (let c = 0; c < colSize; c++) {
593
- const cellIndex = r * colSize + c;
594
- const cell = cells[cellIndex];
595
- if (cell) {
596
- const text = cellToText(cell, ctx);
597
- row.push(text);
598
- }
599
- else {
600
- row.push("");
601
- }
509
+ const cell = cells[r * colSize + c];
510
+ row.push(cell ? cellToText(cell, ctx) : "");
602
511
  }
603
512
  grid.push(row);
604
513
  }
605
514
  if (grid.length === 0)
606
515
  return;
607
- // Header row
608
- lines.push("| " + grid[0].map((c) => c.replace(/\|/g, "\\|")).join(" | ") + " |");
609
- // Separator
516
+ const escape = (s) => s.replace(/\|/g, "\\|");
517
+ lines.push("| " + grid[0].map(escape).join(" | ") + " |");
610
518
  lines.push("| " + grid[0].map(() => "---").join(" | ") + " |");
611
- // Data rows
612
519
  for (let r = 1; r < grid.length; r++) {
613
- lines.push("| " + grid[r].map((c) => c.replace(/\|/g, "\\|")).join(" | ") + " |");
520
+ lines.push("| " + grid[r].map(escape).join(" | ") + " |");
614
521
  }
615
522
  }
616
- /**
617
- * Render a table_cell block content as inline text.
618
- */
523
+ /** Render a table_cell block content as inline text. */
619
524
  function cellToText(cell, ctx) {
620
525
  const parts = [];
621
526
  for (const child of cell._children) {
@@ -629,36 +534,22 @@ function cellToText(cell, ctx) {
629
534
  }
630
535
  return parts.join(" ").replace(/\n/g, " ");
631
536
  }
632
- /**
633
- * Sanitize Feishu mermaid content for standard mermaid compatibility.
634
- *
635
- * Feishu's mermaid renderer is more lenient than standard mermaid.
636
- * This function fixes common incompatibilities:
637
- * - Block labels (alt, else, loop, opt, rect, par, critical, break, note)
638
- * don't support <br/> in standard mermaid — replace with comma-space
639
- * - Arrow messages missing space after ':' — add the space
640
- */
537
+ /** Sanitize Feishu mermaid content for standard mermaid compatibility. */
641
538
  function sanitizeMermaid(content) {
642
- // Block-level keywords whose label text doesn't support <br/> in standard mermaid
643
- const blockKeywords = /^(\s*(?:alt|else|loop|opt|par|critical|break)\s+)(.+)$/;
539
+ const blockKw = /^(\s*(?:alt|else|loop|opt|par|critical|break)\s+)(.+)$/;
540
+ const arrowRe = /^(\s*\S+\s*-[-.)>x]+[+-]?\s*\S+\s*):(\S)/;
644
541
  return content
645
542
  .split("\n")
646
543
  .map((line) => {
647
- // Fix block labels: replace <br/> and <br> with comma-space,
648
- // and replace parentheses with full-width versions to avoid parser confusion
649
- const blockMatch = line.match(blockKeywords);
650
- if (blockMatch) {
651
- const cleaned = blockMatch[2]
652
- .replace(/<br\s*\/?>/gi, ", ")
653
- .replace(/\(/g, "\uFF08")
654
- .replace(/\)/g, "\uFF09");
655
- return blockMatch[1] + cleaned;
656
- }
657
- // Fix missing space after ':' in arrow messages (e.g., A->>B:text → A->>B: text)
658
- const arrowMatch = line.match(/^(\s*\S+\s*-[-.)>x]+[+-]?\s*\S+\s*):(\S)/);
659
- if (arrowMatch) {
660
- return line.replace(/^(\s*\S+\s*-[-.)>x]+[+-]?\s*\S+\s*):(\S)/, "$1: $2");
661
- }
544
+ const bm = line.match(blockKw);
545
+ if (bm)
546
+ return (bm[1] +
547
+ bm[2]
548
+ .replace(/<br\s*\/?>/gi, ", ")
549
+ .replace(/\(/g, "\uFF08")
550
+ .replace(/\)/g, "\uFF09"));
551
+ if (line.match(arrowRe))
552
+ return line.replace(arrowRe, "$1: $2");
662
553
  return line;
663
554
  })
664
555
  .join("\n");