lark-docx2md 0.5.2 → 0.5.3-beta.1
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 +21 -2
- package/dist/cli.js +81 -13
- package/dist/cli.js.map +1 -1
- package/dist/{converter-vuRwWoA4.js → converter-DLMAssSI.js} +156 -51
- package/dist/converter-DLMAssSI.js.map +1 -0
- package/dist/{converter-nwp8DCnk.d.ts → converter-YvAI8rgh.d.ts} +11 -5
- package/dist/converter-YvAI8rgh.d.ts.map +1 -0
- package/dist/converter.js +1 -1
- package/package.json +1 -1
- package/dist/converter-nwp8DCnk.d.ts.map +0 -1
- package/dist/converter-vuRwWoA4.js.map +0 -1
package/README.md
CHANGED
|
@@ -42,9 +42,10 @@ npx -y lark-docx2md@latest download <url>
|
|
|
42
42
|
| `--app-id <id>` | 飞书应用 App ID | `LARK_DOCX2MD_APP_ID` | — |
|
|
43
43
|
| `--app-secret <secret>` | 飞书应用 App Secret | `LARK_DOCX2MD_APP_SECRET` | — |
|
|
44
44
|
| `-o, --output <dir>` | 输出目录 | `LARK_DOCX2MD_OUTPUT` | `./larkDocx2mdOutput` |
|
|
45
|
-
| `--agent [mode]` | Agent 模式:日志 ERROR。不传值(或 `=
|
|
45
|
+
| `--agent [mode]` | Agent 模式:日志 ERROR。不传值(或 `=stdout`)为在线模式,Markdown 输出到 stdout;传 `local` 则落盘后输出引导 AI 读取的提示词 | `LARK_DOCX2MD_AGENT=stdout\|local` | `false` |
|
|
46
46
|
| `--image-mode <mode>` | 图片处理模式:`local`(下载到本地)或 `online`(24h 临时链接) | `LARK_DOCX2MD_IMAGE_MODE` | `local` |
|
|
47
47
|
| `--filter-title <title>` | 按标题过滤:仅转换匹配标题及其下级内容(匹配到同级或更高级标题时截止) | — | — |
|
|
48
|
+
| `--filter-title-block-id <id>` | 按 heading 块 id 精确过滤(无同名歧义),通常配合 `get-titles` 子命令获取;与 `--filter-title` 互斥 | — | — |
|
|
48
49
|
| `--wb-format <format>` | 画板输出格式:`base64`、`inline-svg`、`svg`、`yaml` | `LARK_DOCX2MD_WB_FORMAT` | `svg`(agent 下默认 `yaml`) |
|
|
49
50
|
| `--wb-bg <style>` | 画板 SVG 背景:`none`、`dot` 或颜色值如 `#fff` | `LARK_DOCX2MD_WB_BG` | `none` |
|
|
50
51
|
| `--wb-image-mode <mode>` | 画板图片模式:`online`、`base64` 或 `local` | `LARK_DOCX2MD_WB_IMAGE_MODE` | `local` |
|
|
@@ -54,7 +55,25 @@ npx -y lark-docx2md@latest download <url>
|
|
|
54
55
|
> - `--agent`(在线):强制 `--image-mode=online`、`--wb-image-mode=online`;`--wb-format` 默认 `yaml`,仅允许 `inline-svg` / `yaml`;转换完成后 Markdown 直接通过 stdout 输出。
|
|
55
56
|
> - `--agent local`:强制 `--image-mode=local`、`--wb-image-mode=local`(Markdown、图片、画板中的图片均落盘);`--wb-format` 默认 `yaml`,仅允许 `inline-svg` / `yaml`;stdout 输出引导 AI 读取文件的提示词(包含绝对路径)。
|
|
56
57
|
> - 非 agent 模式下 `--wb-format yaml` 时:`--wb-image-mode` 强制为 `online`。
|
|
57
|
-
> - `--filter-title
|
|
58
|
+
> - `--filter-title`:按标题文本精确匹配(忽略前后空格),收集该标题及其所有子级块,遇到同级或更高级标题时停止。同名标题取首个;未匹配时错误信息附全文标题 yaml 清单。
|
|
59
|
+
> - `--filter-title-block-id`:按 heading 块 id 严格相等匹配,适用于同名标题或脚本化场景;通常先用 `get-titles` 查出目标 `blockId` 再传入。与 `--filter-title` 互斥。
|
|
60
|
+
|
|
61
|
+
## 子命令:`get-titles`
|
|
62
|
+
|
|
63
|
+
列出 docx/wiki 文档全部标题(不支持 sheets),配合 `--filter-title-block-id` 使用可避开同名歧义。
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y lark-docx2md@latest get-titles --agent <url>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| 参数 | 说明 | 默认值 |
|
|
70
|
+
|-----------------------|------------------------------------------------------------------------------------------|--------|
|
|
71
|
+
| `<url>` | 飞书 wiki/docx URL | — |
|
|
72
|
+
| `--max-level <n>` | 仅输出 `level <= n` 的标题(1~9) | `9` |
|
|
73
|
+
| `--format <format>` | 输出格式:`yaml`(扁平) \| `yaml-tree`(嵌套) \| `json` \| `tree`(json 嵌套) \| `text`(缩进的 markdown 标题) | `yaml` |
|
|
74
|
+
| `--agent [mode]` | 同 `dl`,降低日志级别 | — |
|
|
75
|
+
|
|
76
|
+
输出包含 `blockId` / `level` / `text`,可被 AI 直接消费用于选择目标章节(嵌套关系由 `yaml-tree` / `tree` 格式天然表达)。
|
|
58
77
|
|
|
59
78
|
## 功能
|
|
60
79
|
|
package/dist/cli.js
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as setLogLevel, n as buildTitleTree, o as serializeYaml, r as getTitles, t as convert } from "./converter-DLMAssSI.js";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
3
5
|
import { Command } from "commander";
|
|
4
6
|
import { LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
5
7
|
//#region src/cli.ts
|
|
6
8
|
const program = new Command();
|
|
7
9
|
program.name("larkDocx2md").description("Download Lark/Feishu documents to markdown");
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 把 commander 解析出的 --agent 原值(undefined | true | string)与
|
|
12
|
+
* LARK_DOCX2MD_AGENT 环境变量统一收敛为精确的 AgentMode | false。
|
|
13
|
+
*
|
|
14
|
+
* 优先级:显式 --agent > 环境变量 > 默认 false。
|
|
15
|
+
* 非法取值返回错误信息字符串,由调用方决定如何上报。
|
|
16
|
+
*/
|
|
17
|
+
function resolveAgentMode(raw) {
|
|
18
|
+
if (raw === void 0) {
|
|
19
|
+
const env = process.env.LARK_DOCX2MD_AGENT;
|
|
20
|
+
if (env === "stdout" || env === "local") return { value: env };
|
|
21
|
+
if (env !== void 0 && env !== "") return {
|
|
22
|
+
value: false,
|
|
23
|
+
error: `Invalid LARK_DOCX2MD_AGENT="${env}", must be "stdout" or "local"`
|
|
24
|
+
};
|
|
25
|
+
return { value: false };
|
|
26
|
+
}
|
|
27
|
+
if (raw === true) return { value: "stdout" };
|
|
28
|
+
if (raw === "stdout" || raw === "local") return { value: raw };
|
|
29
|
+
return {
|
|
30
|
+
value: false,
|
|
31
|
+
error: `Invalid --agent value "${raw}", must be "stdout" or "local"`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
program.command("download").alias("dl").description("Download a wiki document to markdown").option("--app-id <id>", "Feishu app ID (or read from LARK_DOCX2MD_APP_ID)").option("--app-secret <secret>", "Feishu app secret (or read from LARK_DOCX2MD_APP_SECRET)").option("-o, --output <dir>", "Output directory (or LARK_DOCX2MD_OUTPUT)").option("--agent [mode]", "Enable agent mode: ERROR log level, and AI-oriented stdout. Modes: \"stdout\" (default, print markdown to stdout) or \"local\" (save markdown/images/whiteboards to disk and print a read-file prompt). Or LARK_DOCX2MD_AGENT=stdout|local").option("--wb-format <format>", "Whiteboard output format: \"base64\", \"inline-svg\", \"svg\", or \"yaml\" (or LARK_DOCX2MD_WB_FORMAT)").option("--wb-bg <style>", "Whiteboard SVG background: \"none\", \"dot\", or a color like \"#fff\" (or LARK_DOCX2MD_WB_BG)").option("--wb-image-mode <mode>", "Whiteboard image mode: \"online\", \"base64\", or \"local\" (or LARK_DOCX2MD_WB_IMAGE_MODE)").option("--image-mode <mode>", "Image handling mode: \"local\" or \"online\" (or LARK_DOCX2MD_IMAGE_MODE)").option("--filter-title <title>", "Only convert the section matching this heading title (single title, first match wins on duplicates)").option("--filter-title-block-id <id>", "Only convert the section whose heading block id matches (most precise; obtain from get-titles)").argument("<url>", "Feishu wiki document URL: https://*.feishu.cn/wiki/*").action(async (url, opts) => {
|
|
9
35
|
opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;
|
|
10
36
|
opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;
|
|
11
37
|
opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? "./larkDocx2mdOutput";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const agentEnabled = opts.agent ===
|
|
38
|
+
const agentResolved = resolveAgentMode(opts.agent);
|
|
39
|
+
if (agentResolved.error) {
|
|
40
|
+
program.error(agentResolved.error);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
opts.agent = agentResolved.value;
|
|
44
|
+
const agentEnabled = opts.agent === "stdout" || opts.agent === "local";
|
|
19
45
|
const agentLocal = opts.agent === "local";
|
|
46
|
+
const agentStdout = opts.agent === "stdout";
|
|
20
47
|
opts.imageMode = opts.imageMode ?? process.env.LARK_DOCX2MD_IMAGE_MODE ?? "local";
|
|
21
48
|
opts.wbFormat = opts.wbFormat ?? process.env.LARK_DOCX2MD_WB_FORMAT;
|
|
22
49
|
opts.wbBg = opts.wbBg ?? process.env.LARK_DOCX2MD_WB_BG ?? "none";
|
|
23
50
|
opts.wbImageMode = opts.wbImageMode ?? process.env.LARK_DOCX2MD_WB_IMAGE_MODE ?? "local";
|
|
24
|
-
if (!opts.wbFormat) opts.wbFormat =
|
|
51
|
+
if (!opts.wbFormat) if (agentLocal) opts.wbFormat = "inline-svg";
|
|
52
|
+
else if (agentStdout) opts.wbFormat = "yaml";
|
|
53
|
+
else opts.wbFormat = "svg";
|
|
25
54
|
if (agentEnabled) {
|
|
26
55
|
setLogLevel(LoggerLevel.error);
|
|
27
56
|
if (agentLocal) {
|
|
@@ -45,6 +74,7 @@ program.command("download").alias("dl").description("Download a wiki document to
|
|
|
45
74
|
"base64",
|
|
46
75
|
"local"
|
|
47
76
|
].includes(opts.wbImageMode)) program.error(`Invalid --wb-image-mode "${opts.wbImageMode}", must be "online", "base64", or "local"`);
|
|
77
|
+
if (opts.filterTitle && opts.filterTitleBlockId) program.error("--filter-title and --filter-title-block-id are mutually exclusive; choose one");
|
|
48
78
|
const appId = opts.appId;
|
|
49
79
|
const appSecret = opts.appSecret;
|
|
50
80
|
if (!appId || !appSecret) program.error("Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET");
|
|
@@ -57,11 +87,49 @@ program.command("download").alias("dl").description("Download a wiki document to
|
|
|
57
87
|
wbImageMode: opts.wbImageMode,
|
|
58
88
|
wbBg: opts.wbBg,
|
|
59
89
|
wbFormat: opts.wbFormat,
|
|
60
|
-
agent:
|
|
61
|
-
filterTitle: opts.filterTitle?.trim()
|
|
90
|
+
agent: opts.agent,
|
|
91
|
+
filterTitle: opts.filterTitle?.trim(),
|
|
92
|
+
filterTitleBlockId: opts.filterTitleBlockId?.trim()
|
|
62
93
|
});
|
|
63
94
|
if (agentLocal) process.stdout.write(`**The Feishu document has been downloaded to the following absolute path:**\n\n\`${result.filePath}\`\n\n**Read this file to access the full markdown content.**\n`);
|
|
64
|
-
else if (
|
|
95
|
+
else if (agentStdout) process.stdout.write(result.markdown);
|
|
96
|
+
});
|
|
97
|
+
program.command("get-titles").description("Print all headings (level 1~9) of a wiki/docx document as a nested yaml tree. Useful before --filter-title-block-id.").option("--app-id <id>", "Feishu app ID (or read from LARK_DOCX2MD_APP_ID)").option("--app-secret <secret>", "Feishu app secret (or read from LARK_DOCX2MD_APP_SECRET)").option("-o, --output <dir>", "Output directory used by --agent local (or LARK_DOCX2MD_OUTPUT)").option("--max-level <n>", "Only output headings whose level <= n (1~9)", "9").option("--agent [mode]", "Enable agent mode: ERROR log level, AI-oriented stdout. Modes: \"stdout\" (default, print titles to stdout) or \"local\" (save titles to disk and print a read-file prompt). Or LARK_DOCX2MD_AGENT=stdout|local").argument("<url>", "Feishu wiki/docx URL: https://*.feishu.cn/{wiki,docx,docs}/*").action(async (url, opts) => {
|
|
98
|
+
opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;
|
|
99
|
+
opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;
|
|
100
|
+
opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? "./larkDocx2mdOutput";
|
|
101
|
+
const agentResolved = resolveAgentMode(opts.agent);
|
|
102
|
+
if (agentResolved.error) {
|
|
103
|
+
program.error(agentResolved.error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
opts.agent = agentResolved.value;
|
|
107
|
+
const agentEnabled = opts.agent === "stdout" || opts.agent === "local";
|
|
108
|
+
const agentLocal = opts.agent === "local";
|
|
109
|
+
if (agentEnabled) setLogLevel(LoggerLevel.error);
|
|
110
|
+
const maxLevel = parseInt(opts.maxLevel ?? "9", 10);
|
|
111
|
+
if (!Number.isInteger(maxLevel) || maxLevel < 1 || maxLevel > 9) program.error(`Invalid --max-level "${opts.maxLevel}", must be an integer in [1, 9]`);
|
|
112
|
+
const appId = opts.appId;
|
|
113
|
+
const appSecret = opts.appSecret;
|
|
114
|
+
if (!appId || !appSecret) program.error("Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET");
|
|
115
|
+
const result = await getTitles({
|
|
116
|
+
appId,
|
|
117
|
+
appSecret,
|
|
118
|
+
url,
|
|
119
|
+
agent: opts.agent
|
|
120
|
+
});
|
|
121
|
+
const tree = buildTitleTree(result.titles.filter((t) => t.level <= maxLevel));
|
|
122
|
+
const content = serializeYaml({
|
|
123
|
+
url: result.url,
|
|
124
|
+
docToken: result.docToken,
|
|
125
|
+
titles: tree
|
|
126
|
+
});
|
|
127
|
+
if (agentLocal) {
|
|
128
|
+
fs.mkdirSync(opts.output, { recursive: true });
|
|
129
|
+
const filePath = path.resolve(opts.output, `${result.docToken}.titles.yaml`);
|
|
130
|
+
fs.writeFileSync(filePath, content);
|
|
131
|
+
process.stdout.write(`**The Feishu document titles have been downloaded to the following absolute path:**\n\n\`${filePath}\`\n\n**Read this file to access the full titles list.**\n`);
|
|
132
|
+
} else process.stdout.write(content);
|
|
65
133
|
});
|
|
66
134
|
program.parse();
|
|
67
135
|
//#endregion
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport { LoggerLevel } from '@larksuiteoapi/node-sdk';\nimport { convert } from './converter.js';\nimport { setLogLevel } from './logger.js';\nimport type { SvgBackground, WbFormat, WbImageMode } from './types.js';\n\nconst program = new Command();\nprogram.name('larkDocx2md').description('Download Lark/Feishu documents to markdown');\n\nprogram\n .command('download')\n .alias('dl')\n .description('Download a wiki document to markdown')\n .option('--app-id <id>', 'Feishu app ID (or read from LARK_DOCX2MD_APP_ID)')\n .option('--app-secret <secret>', 'Feishu app secret (or read from LARK_DOCX2MD_APP_SECRET)')\n .option('-o, --output <dir>', 'Output directory (or LARK_DOCX2MD_OUTPUT)')\n .option('--agent [mode]', 'Enable agent mode: ERROR log level, and AI-oriented stdout. Pass \"local\" to save markdown/images/whiteboards to disk and print a read-file prompt (or LARK_DOCX2MD_AGENT=true|local)')\n .option('--wb-format <format>', 'Whiteboard output format: \"base64\", \"inline-svg\", \"svg\", or \"yaml\" (or LARK_DOCX2MD_WB_FORMAT)')\n .option('--wb-bg <style>', 'Whiteboard SVG background: \"none\", \"dot\", or a color like \"#fff\" (or LARK_DOCX2MD_WB_BG)')\n .option('--wb-image-mode <mode>', 'Whiteboard image mode: \"online\", \"base64\", or \"local\" (or LARK_DOCX2MD_WB_IMAGE_MODE)')\n .option('--image-mode <mode>', 'Image handling mode: \"local\" or \"online\" (or LARK_DOCX2MD_IMAGE_MODE)')\n .option('--filter-title <title>', 'Only convert the section matching this heading title')\n .argument('<url>', 'Feishu wiki document URL: https://*.feishu.cn/wiki/*')\n .action(async (url: string, opts: { appId?: string; appSecret?: string; output?: string; agent?: boolean | string; imageMode?: string; wbImageMode?: string; wbBg?: SvgBackground; wbFormat?: string; filterTitle?: string }) => {\n // ─── 环境变量默认值(直接指定 > 环境变量 > 内置默认值)────────────────\n opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;\n opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;\n opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? './larkDocx2mdOutput';\n // 解析 --agent:可能为 undefined | true | 'local' | 其他字符串\n if (opts.agent === undefined) {\n const envAgent = process.env.LARK_DOCX2MD_AGENT;\n if (envAgent === 'true') opts.agent = true;\n else if (envAgent === 'local') opts.agent = 'local';\n else opts.agent = false;\n } else if (typeof opts.agent === 'string' && opts.agent !== 'local') {\n program.error(`Invalid --agent value \"${opts.agent}\", only \"local\" is supported (or omit the value)`);\n }\n const agentEnabled = opts.agent === true || opts.agent === 'local';\n const agentLocal = opts.agent === 'local';\n\n opts.imageMode = opts.imageMode ?? process.env.LARK_DOCX2MD_IMAGE_MODE ?? 'local';\n opts.wbFormat = opts.wbFormat ?? process.env.LARK_DOCX2MD_WB_FORMAT;\n opts.wbBg = opts.wbBg ?? process.env.LARK_DOCX2MD_WB_BG ?? 'none';\n opts.wbImageMode = opts.wbImageMode ?? process.env.LARK_DOCX2MD_WB_IMAGE_MODE ?? 'local';\n\n // 设置 wb-format 默认值:--agent local 默认 inline-svg(兼容本地画板图片),--agent(在线)默认 yaml,其余 svg\n if (!opts.wbFormat) {\n opts.wbFormat = agentEnabled ? 'yaml' : 'svg';\n }\n\n if (agentEnabled) {\n setLogLevel(LoggerLevel.error);\n if (agentLocal) {\n // --agent local:图片/画板图片均落盘\n opts.imageMode = 'local';\n opts.wbImageMode = 'local';\n } else {\n // --agent(在线):一律在线,且画板仅支持内嵌形式\n opts.imageMode = 'online';\n opts.wbImageMode = 'online';\n }\n if (!['inline-svg', 'yaml'].includes(opts.wbFormat)) {\n program.error(`Agent mode only supports \"inline-svg\" or \"yaml\" for --wb-format`);\n }\n } else {\n // yaml 格式图片仅支持 online\n if (opts.wbFormat === 'yaml') {\n opts.wbImageMode = 'online';\n }\n }\n\n if (opts.imageMode && !['local', 'online'].includes(opts.imageMode)) {\n program.error(`Invalid --image-mode \"${opts.imageMode}\", must be \"local\" or \"online\"`);\n }\n if (!['base64', 'inline-svg', 'svg', 'yaml'].includes(opts.wbFormat)) {\n program.error(`Invalid --wb-format \"${opts.wbFormat}\", must be \"base64\", \"inline-svg\", \"svg\", or \"yaml\"`);\n }\n if (!['online', 'base64', 'local'].includes(opts.wbImageMode)) {\n program.error(`Invalid --wb-image-mode \"${opts.wbImageMode}\", must be \"online\", \"base64\", or \"local\"`);\n }\n\n const appId = opts.appId!;\n const appSecret = opts.appSecret!;\n if (!appId || !appSecret) {\n program.error('Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET');\n }\n\n const result = await convert({\n appId,\n appSecret,\n url,\n output: opts.output,\n imageMode: opts.imageMode as 'local' | 'online',\n wbImageMode: opts.wbImageMode as WbImageMode,\n wbBg: opts.wbBg,\n wbFormat: opts.wbFormat as WbFormat,\n agent: agentLocal ? 'local' : (opts.agent === true),\n filterTitle: opts.filterTitle?.trim(),\n });\n\n if (agentLocal) {\n // 本地模式:输出引导 AI 读取文件的提示词(绝对路径)\n process.stdout.write(\n `**The Feishu document has been downloaded to the following absolute path:**\\n\\n` +\n `\\`${result.filePath}\\`\\n\\n` +\n `**Read this file to access the full markdown content.**\\n`,\n );\n } else if (opts.agent === true) {\n process.stdout.write(result.markdown);\n }\n });\n\nprogram.parse();\n"],"mappings":";;;;;AAOA,MAAM,UAAU,IAAI,SAAS;AAC7B,QAAQ,KAAK,cAAc,CAAC,YAAY,6CAA6C;AAErF,QACG,QAAQ,WAAW,CACnB,MAAM,KAAK,CACX,YAAY,uCAAuC,CACnD,OAAO,iBAAiB,mDAAmD,CAC3E,OAAO,yBAAyB,2DAA2D,CAC3F,OAAO,sBAAsB,4CAA4C,CACzE,OAAO,kBAAkB,yLAAuL,CAChN,OAAO,wBAAwB,yGAAiG,CAChI,OAAO,mBAAmB,iGAA2F,CACrH,OAAO,0BAA0B,8FAAwF,CACzH,OAAO,uBAAuB,4EAAwE,CACtG,OAAO,0BAA0B,uDAAuD,CACxF,SAAS,SAAS,uDAAuD,CACzE,OAAO,OAAO,KAAa,SAAqM;AAE/N,MAAK,QAAQ,KAAK,SAAS,QAAQ,IAAI;AACvC,MAAK,YAAY,KAAK,aAAa,QAAQ,IAAI;AAC/C,MAAK,SAAS,KAAK,UAAU,QAAQ,IAAI,uBAAuB;AAEhE,KAAI,KAAK,UAAU,KAAA,GAAW;EAC5B,MAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,aAAa,OAAQ,MAAK,QAAQ;WAC7B,aAAa,QAAS,MAAK,QAAQ;MACvC,MAAK,QAAQ;YACT,OAAO,KAAK,UAAU,YAAY,KAAK,UAAU,QAC1D,SAAQ,MAAM,0BAA0B,KAAK,MAAM,kDAAkD;CAEvG,MAAM,eAAe,KAAK,UAAU,QAAQ,KAAK,UAAU;CAC3D,MAAM,aAAa,KAAK,UAAU;AAElC,MAAK,YAAY,KAAK,aAAa,QAAQ,IAAI,2BAA2B;AAC1E,MAAK,WAAW,KAAK,YAAY,QAAQ,IAAI;AAC7C,MAAK,OAAO,KAAK,QAAQ,QAAQ,IAAI,sBAAsB;AAC3D,MAAK,cAAc,KAAK,eAAe,QAAQ,IAAI,8BAA8B;AAGjF,KAAI,CAAC,KAAK,SACR,MAAK,WAAW,eAAe,SAAS;AAG1C,KAAI,cAAc;AAChB,cAAY,YAAY,MAAM;AAC9B,MAAI,YAAY;AAEd,QAAK,YAAY;AACjB,QAAK,cAAc;SACd;AAEL,QAAK,YAAY;AACjB,QAAK,cAAc;;AAErB,MAAI,CAAC,CAAC,cAAc,OAAO,CAAC,SAAS,KAAK,SAAS,CACjD,SAAQ,MAAM,kEAAkE;YAI9E,KAAK,aAAa,OACpB,MAAK,cAAc;AAIvB,KAAI,KAAK,aAAa,CAAC,CAAC,SAAS,SAAS,CAAC,SAAS,KAAK,UAAU,CACjE,SAAQ,MAAM,yBAAyB,KAAK,UAAU,gCAAgC;AAExF,KAAI,CAAC;EAAC;EAAU;EAAc;EAAO;EAAO,CAAC,SAAS,KAAK,SAAS,CAClE,SAAQ,MAAM,wBAAwB,KAAK,SAAS,qDAAqD;AAE3G,KAAI,CAAC;EAAC;EAAU;EAAU;EAAQ,CAAC,SAAS,KAAK,YAAY,CAC3D,SAAQ,MAAM,4BAA4B,KAAK,YAAY,2CAA2C;CAGxG,MAAM,QAAQ,KAAK;CACnB,MAAM,YAAY,KAAK;AACvB,KAAI,CAAC,SAAS,CAAC,UACb,SAAQ,MAAM,qGAAqG;CAGrH,MAAM,SAAS,MAAM,QAAQ;EAC3B;EACA;EACA;EACA,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,UAAU,KAAK;EACf,OAAO,aAAa,UAAW,KAAK,UAAU;EAC9C,aAAa,KAAK,aAAa,MAAM;EACtC,CAAC;AAEF,KAAI,WAEF,SAAQ,OAAO,MACb,oFACK,OAAO,SAAS,iEAEtB;UACQ,KAAK,UAAU,KACxB,SAAQ,OAAO,MAAM,OAAO,SAAS;EAEvC;AAEJ,QAAQ,OAAO"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { Command } from 'commander';\nimport { LoggerLevel } from '@larksuiteoapi/node-sdk';\nimport { convert } from './converter.js';\nimport { buildTitleTree, getTitles } from './get-titles.js';\nimport { setLogLevel } from './logger.js';\nimport { serializeYaml } from './whiteboard/yaml/serialize.js';\nimport type { SvgBackground, WbFormat, WbImageMode, AgentMode } from './types.js';\n\nconst program = new Command();\nprogram.name('larkDocx2md').description('Download Lark/Feishu documents to markdown');\n\n/**\n * 把 commander 解析出的 --agent 原值(undefined | true | string)与\n * LARK_DOCX2MD_AGENT 环境变量统一收敛为精确的 AgentMode | false。\n *\n * 优先级:显式 --agent > 环境变量 > 默认 false。\n * 非法取值返回错误信息字符串,由调用方决定如何上报。\n */\nfunction resolveAgentMode (raw: boolean | string | undefined): { value: AgentMode | false; error?: string } {\n if (raw === undefined) {\n const env = process.env.LARK_DOCX2MD_AGENT;\n if (env === 'stdout' || env === 'local') return { value: env };\n if (env !== undefined && env !== '') {\n return { value: false, error: `Invalid LARK_DOCX2MD_AGENT=\"${env}\", must be \"stdout\" or \"local\"` };\n }\n return { value: false };\n }\n // --agent 不带值:默认 stdout\n if (raw === true) return { value: 'stdout' };\n if (raw === 'stdout' || raw === 'local') return { value: raw };\n return { value: false, error: `Invalid --agent value \"${raw}\", must be \"stdout\" or \"local\"` };\n}\n\nprogram\n .command('download')\n .alias('dl')\n .description('Download a wiki document to markdown')\n .option('--app-id <id>', 'Feishu app ID (or read from LARK_DOCX2MD_APP_ID)')\n .option('--app-secret <secret>', 'Feishu app secret (or read from LARK_DOCX2MD_APP_SECRET)')\n .option('-o, --output <dir>', 'Output directory (or LARK_DOCX2MD_OUTPUT)')\n .option('--agent [mode]', 'Enable agent mode: ERROR log level, and AI-oriented stdout. Modes: \"stdout\" (default, print markdown to stdout) or \"local\" (save markdown/images/whiteboards to disk and print a read-file prompt). Or LARK_DOCX2MD_AGENT=stdout|local')\n .option('--wb-format <format>', 'Whiteboard output format: \"base64\", \"inline-svg\", \"svg\", or \"yaml\" (or LARK_DOCX2MD_WB_FORMAT)')\n .option('--wb-bg <style>', 'Whiteboard SVG background: \"none\", \"dot\", or a color like \"#fff\" (or LARK_DOCX2MD_WB_BG)')\n .option('--wb-image-mode <mode>', 'Whiteboard image mode: \"online\", \"base64\", or \"local\" (or LARK_DOCX2MD_WB_IMAGE_MODE)')\n .option('--image-mode <mode>', 'Image handling mode: \"local\" or \"online\" (or LARK_DOCX2MD_IMAGE_MODE)')\n .option('--filter-title <title>', 'Only convert the section matching this heading title (single title, first match wins on duplicates)')\n .option('--filter-title-block-id <id>', 'Only convert the section whose heading block id matches (most precise; obtain from get-titles)')\n .argument('<url>', 'Feishu wiki document URL: https://*.feishu.cn/wiki/*')\n .action(async (url: string, opts: { appId?: string; appSecret?: string; output?: string; agent?: boolean | string; imageMode?: string; wbImageMode?: string; wbBg?: SvgBackground; wbFormat?: string; filterTitle?: string; filterTitleBlockId?: string }) => {\n // ─── 环境变量默认值(直接指定 > 环境变量 > 内置默认值)────────────────\n opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;\n opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;\n opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? './larkDocx2mdOutput';\n // 解析 --agent:可能为 undefined | true | 'stdout' | 'local' | 其他字符串\n const agentResolved = resolveAgentMode(opts.agent);\n if (agentResolved.error) {\n program.error(agentResolved.error);\n return;\n }\n opts.agent = agentResolved.value;\n const agentEnabled = opts.agent === 'stdout' || opts.agent === 'local';\n const agentLocal = opts.agent === 'local';\n const agentStdout = opts.agent === 'stdout';\n\n opts.imageMode = opts.imageMode ?? process.env.LARK_DOCX2MD_IMAGE_MODE ?? 'local';\n opts.wbFormat = opts.wbFormat ?? process.env.LARK_DOCX2MD_WB_FORMAT;\n opts.wbBg = opts.wbBg ?? process.env.LARK_DOCX2MD_WB_BG ?? 'none';\n opts.wbImageMode = opts.wbImageMode ?? process.env.LARK_DOCX2MD_WB_IMAGE_MODE ?? 'local';\n\n // 设置 wb-format 默认值:--agent local 默认 inline-svg(兼容本地画板图片),--agent stdout 默认 yaml,其余 svg\n if (!opts.wbFormat) {\n if (agentLocal) opts.wbFormat = 'inline-svg';\n else if (agentStdout) opts.wbFormat = 'yaml';\n else opts.wbFormat = 'svg';\n }\n\n if (agentEnabled) {\n setLogLevel(LoggerLevel.error);\n if (agentLocal) {\n // --agent local:图片/画板图片均落盘\n opts.imageMode = 'local';\n opts.wbImageMode = 'local';\n } else {\n // --agent stdout:一律在线,且画板仅支持内嵌形式\n opts.imageMode = 'online';\n opts.wbImageMode = 'online';\n }\n if (!['inline-svg', 'yaml'].includes(opts.wbFormat)) {\n program.error(`Agent mode only supports \"inline-svg\" or \"yaml\" for --wb-format`);\n }\n } else {\n // yaml 格式图片仅支持 online\n if (opts.wbFormat === 'yaml') {\n opts.wbImageMode = 'online';\n }\n }\n\n if (opts.imageMode && !['local', 'online'].includes(opts.imageMode)) {\n program.error(`Invalid --image-mode \"${opts.imageMode}\", must be \"local\" or \"online\"`);\n }\n if (!['base64', 'inline-svg', 'svg', 'yaml'].includes(opts.wbFormat)) {\n program.error(`Invalid --wb-format \"${opts.wbFormat}\", must be \"base64\", \"inline-svg\", \"svg\", or \"yaml\"`);\n }\n if (!['online', 'base64', 'local'].includes(opts.wbImageMode)) {\n program.error(`Invalid --wb-image-mode \"${opts.wbImageMode}\", must be \"online\", \"base64\", or \"local\"`);\n }\n if (opts.filterTitle && opts.filterTitleBlockId) {\n program.error('--filter-title and --filter-title-block-id are mutually exclusive; choose one');\n }\n\n const appId = opts.appId!;\n const appSecret = opts.appSecret!;\n if (!appId || !appSecret) {\n program.error('Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET');\n }\n\n const result = await convert({\n appId,\n appSecret,\n url,\n output: opts.output,\n imageMode: opts.imageMode as 'local' | 'online',\n wbImageMode: opts.wbImageMode as WbImageMode,\n wbBg: opts.wbBg,\n wbFormat: opts.wbFormat as WbFormat,\n agent: opts.agent as AgentMode | false,\n filterTitle: opts.filterTitle?.trim(),\n filterTitleBlockId: opts.filterTitleBlockId?.trim(),\n });\n\n if (agentLocal) {\n // 本地模式:输出引导 AI 读取文件的提示词(绝对路径)\n process.stdout.write(\n `**The Feishu document has been downloaded to the following absolute path:**\\n\\n` +\n `\\`${result.filePath}\\`\\n\\n` +\n `**Read this file to access the full markdown content.**\\n`,\n );\n } else if (agentStdout) {\n process.stdout.write(result.markdown);\n }\n });\n\nprogram\n .command('get-titles')\n .description('Print all headings (level 1~9) of a wiki/docx document as a nested yaml tree. Useful before --filter-title-block-id.')\n .option('--app-id <id>', 'Feishu app ID (or read from LARK_DOCX2MD_APP_ID)')\n .option('--app-secret <secret>', 'Feishu app secret (or read from LARK_DOCX2MD_APP_SECRET)')\n .option('-o, --output <dir>', 'Output directory used by --agent local (or LARK_DOCX2MD_OUTPUT)')\n .option('--max-level <n>', 'Only output headings whose level <= n (1~9)', '9')\n .option('--agent [mode]', 'Enable agent mode: ERROR log level, AI-oriented stdout. Modes: \"stdout\" (default, print titles to stdout) or \"local\" (save titles to disk and print a read-file prompt). Or LARK_DOCX2MD_AGENT=stdout|local')\n .argument('<url>', 'Feishu wiki/docx URL: https://*.feishu.cn/{wiki,docx,docs}/*')\n .action(async (url: string, opts: { appId?: string; appSecret?: string; output?: string; maxLevel?: string; agent?: boolean | string }) => {\n opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;\n opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;\n opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? './larkDocx2mdOutput';\n const agentResolved = resolveAgentMode(opts.agent);\n if (agentResolved.error) {\n program.error(agentResolved.error);\n return;\n }\n opts.agent = agentResolved.value;\n const agentEnabled = opts.agent === 'stdout' || opts.agent === 'local';\n const agentLocal = opts.agent === 'local';\n if (agentEnabled) setLogLevel(LoggerLevel.error);\n\n const maxLevel = parseInt(opts.maxLevel ?? '9', 10);\n if (!Number.isInteger(maxLevel) || maxLevel < 1 || maxLevel > 9) {\n program.error(`Invalid --max-level \"${opts.maxLevel}\", must be an integer in [1, 9]`);\n }\n\n const appId = opts.appId!;\n const appSecret = opts.appSecret!;\n if (!appId || !appSecret) {\n program.error('Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET');\n }\n\n const result = await getTitles({\n appId,\n appSecret,\n url,\n agent: opts.agent as AgentMode | false,\n });\n const filtered = result.titles.filter(t => t.level <= maxLevel);\n const tree = buildTitleTree(filtered);\n const content = serializeYaml({ url: result.url, docToken: result.docToken, titles: tree });\n\n if (agentLocal) {\n // 本地模式:落盘 + 输出引导 AI 读取文件的提示词(绝对路径)\n fs.mkdirSync(opts.output, { recursive: true });\n const filePath = path.resolve(opts.output, `${result.docToken}.titles.yaml`);\n fs.writeFileSync(filePath, content);\n process.stdout.write(\n `**The Feishu document titles have been downloaded to the following absolute path:**\\n\\n` +\n `\\`${filePath}\\`\\n\\n` +\n `**Read this file to access the full titles list.**\\n`,\n );\n } else {\n process.stdout.write(content);\n }\n });\n\nprogram.parse();\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import { LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
3
1
|
import * as fs from "node:fs";
|
|
4
2
|
import * as path from "node:path";
|
|
3
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
4
|
+
import { LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
5
5
|
//#region src/client.ts
|
|
6
6
|
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
7
|
const RATE_LIMIT_MAX_RETRIES = 3;
|
|
@@ -53,7 +53,7 @@ function createClient(appId, appSecret, loggerLevel = LoggerLevel.warn) {
|
|
|
53
53
|
const allBlocks = [];
|
|
54
54
|
let pageToken;
|
|
55
55
|
for (let i = 0;; i++) {
|
|
56
|
-
if (i > 0) await sleep$1(
|
|
56
|
+
if (i > 0) await sleep$1(50);
|
|
57
57
|
const data = await call("getDocxBlocks", () => client.docx.v1.documentBlock.list({
|
|
58
58
|
path: { document_id: docToken },
|
|
59
59
|
params: {
|
|
@@ -561,8 +561,10 @@ const tableParser = {
|
|
|
561
561
|
const row = Math.floor(i / cols);
|
|
562
562
|
const col = i % cols;
|
|
563
563
|
if (rows[row]?.cells[col]) {
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
const rowSpan = m.row_span ?? 1;
|
|
565
|
+
const colSpan = m.col_span ?? 1;
|
|
566
|
+
rows[row].cells[col].rowSpan = rowSpan > 1 ? rowSpan : void 0;
|
|
567
|
+
rows[row].cells[col].colSpan = colSpan > 1 ? colSpan : void 0;
|
|
566
568
|
}
|
|
567
569
|
}
|
|
568
570
|
const skipSet = /* @__PURE__ */ new Set();
|
|
@@ -2685,7 +2687,7 @@ function createLogger(module) {
|
|
|
2685
2687
|
}
|
|
2686
2688
|
//#endregion
|
|
2687
2689
|
//#region src/md-ast/transformer.ts
|
|
2688
|
-
const logger$
|
|
2690
|
+
const logger$2 = createLogger("transformer");
|
|
2689
2691
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2690
2692
|
var MdTransformer = class {
|
|
2691
2693
|
constructor(client, opts, sourceType = "docx") {
|
|
@@ -2701,14 +2703,14 @@ var MdTransformer = class {
|
|
|
2701
2703
|
const map = /* @__PURE__ */ new Map();
|
|
2702
2704
|
const uniqueTokens = [...new Set(tokens)];
|
|
2703
2705
|
if (uniqueTokens.length === 0) return map;
|
|
2704
|
-
if (this.opts.imageMode === "online" || this.opts.agent ===
|
|
2706
|
+
if (this.opts.imageMode === "online" || this.opts.agent === "stdout") for (let i = 0; i < uniqueTokens.length; i += 5) {
|
|
2705
2707
|
const batch = uniqueTokens.slice(i, i + 5);
|
|
2706
2708
|
const urlMap = await this.client.batchGetTmpDownloadUrl(batch);
|
|
2707
2709
|
for (const token of batch) {
|
|
2708
2710
|
const url = urlMap[token];
|
|
2709
2711
|
if (url) {
|
|
2710
2712
|
map.set(token, url);
|
|
2711
|
-
logger$
|
|
2713
|
+
logger$2.info("Resolved image URL:", token);
|
|
2712
2714
|
}
|
|
2713
2715
|
}
|
|
2714
2716
|
}
|
|
@@ -2718,7 +2720,7 @@ var MdTransformer = class {
|
|
|
2718
2720
|
let localPath = await this.client.downloadImage(token, imgDir);
|
|
2719
2721
|
localPath = path.relative(this.opts.output, localPath);
|
|
2720
2722
|
map.set(token, localPath);
|
|
2721
|
-
logger$
|
|
2723
|
+
logger$2.info("Downloaded image:", localPath);
|
|
2722
2724
|
}
|
|
2723
2725
|
}
|
|
2724
2726
|
return map;
|
|
@@ -2726,15 +2728,15 @@ var MdTransformer = class {
|
|
|
2726
2728
|
async resolveWhiteboards(tokens) {
|
|
2727
2729
|
const map = /* @__PURE__ */ new Map();
|
|
2728
2730
|
for (const token of tokens) try {
|
|
2729
|
-
logger$
|
|
2731
|
+
logger$2.info("Fetching whiteboard nodes:", token);
|
|
2730
2732
|
const wbNodes = await this.client.getWhiteboardNodes(token);
|
|
2731
|
-
logger$
|
|
2733
|
+
logger$2.info(`Whiteboard ${token}: ${wbNodes.length} nodes`);
|
|
2732
2734
|
let node;
|
|
2733
2735
|
if (this.opts.wbFormat === "yaml") node = await this.processWhiteboardYaml(token, wbNodes);
|
|
2734
2736
|
else node = await this.processWhiteboardSvg(token, wbNodes);
|
|
2735
2737
|
map.set(token, node);
|
|
2736
2738
|
} catch (e) {
|
|
2737
|
-
logger$
|
|
2739
|
+
logger$2.warn(`Failed to render whiteboard ${token}:`, e.message);
|
|
2738
2740
|
}
|
|
2739
2741
|
return map;
|
|
2740
2742
|
}
|
|
@@ -2754,7 +2756,7 @@ var MdTransformer = class {
|
|
|
2754
2756
|
const svgPath = path.join(svgDir, `${token}.svg`);
|
|
2755
2757
|
fs.writeFileSync(svgPath, svgContent);
|
|
2756
2758
|
const relPath = path.relative(this.opts.output, svgPath);
|
|
2757
|
-
logger$
|
|
2759
|
+
logger$2.info("Generated whiteboard SVG:", relPath);
|
|
2758
2760
|
return {
|
|
2759
2761
|
type: "image",
|
|
2760
2762
|
alt: `画板-${token}`,
|
|
@@ -2770,7 +2772,7 @@ var MdTransformer = class {
|
|
|
2770
2772
|
let resolvedYaml = yamlContent;
|
|
2771
2773
|
let effectiveMode = this.opts.wbImageMode;
|
|
2772
2774
|
if (effectiveMode === "base64") {
|
|
2773
|
-
logger$
|
|
2775
|
+
logger$2.warn("YAML mode does not support base64 image embedding, falling back to online mode");
|
|
2774
2776
|
effectiveMode = "online";
|
|
2775
2777
|
}
|
|
2776
2778
|
if (imageTokens.length > 0) resolvedYaml = await this.resolveYamlImages(resolvedYaml, imageTokens, effectiveMode);
|
|
@@ -2789,7 +2791,7 @@ var MdTransformer = class {
|
|
|
2789
2791
|
const onlineUrl = urlMap[token];
|
|
2790
2792
|
if (onlineUrl) {
|
|
2791
2793
|
svgContent = svgContent.split(`href="${token}"`).join(`href="${onlineUrl}"`);
|
|
2792
|
-
logger$
|
|
2794
|
+
logger$2.info("Replaced whiteboard image with online URL:", token);
|
|
2793
2795
|
}
|
|
2794
2796
|
}
|
|
2795
2797
|
}
|
|
@@ -2801,11 +2803,11 @@ var MdTransformer = class {
|
|
|
2801
2803
|
const buf = fs.readFileSync(localPath);
|
|
2802
2804
|
const dataUri = `data:${path.extname(localPath).slice(1) === "png" ? "image/png" : "image/jpeg"};base64,${buf.toString("base64")}`;
|
|
2803
2805
|
svgContent = svgContent.split(`href="${token}"`).join(`href="${dataUri}"`);
|
|
2804
|
-
logger$
|
|
2806
|
+
logger$2.info("Embedded whiteboard image as base64:", token);
|
|
2805
2807
|
} else {
|
|
2806
2808
|
const relPath = path.basename(localPath);
|
|
2807
2809
|
svgContent = svgContent.split(`href="${token}"`).join(`href="${relPath}"`);
|
|
2808
|
-
logger$
|
|
2810
|
+
logger$2.info("Replaced whiteboard image with local path:", relPath);
|
|
2809
2811
|
}
|
|
2810
2812
|
}
|
|
2811
2813
|
}
|
|
@@ -2819,7 +2821,7 @@ var MdTransformer = class {
|
|
|
2819
2821
|
const onlineUrl = urlMap[token];
|
|
2820
2822
|
if (onlineUrl) {
|
|
2821
2823
|
yamlContent = yamlContent.split(token).join(onlineUrl);
|
|
2822
|
-
logger$
|
|
2824
|
+
logger$2.info("Replaced YAML image token with online URL:", token);
|
|
2823
2825
|
}
|
|
2824
2826
|
}
|
|
2825
2827
|
}
|
|
@@ -2829,7 +2831,7 @@ var MdTransformer = class {
|
|
|
2829
2831
|
const localPath = await this.client.downloadImage(token, imgDir);
|
|
2830
2832
|
const relPath = path.relative(this.opts.output, localPath);
|
|
2831
2833
|
yamlContent = yamlContent.split(token).join(relPath);
|
|
2832
|
-
logger$
|
|
2834
|
+
logger$2.info("Replaced YAML image token with local path:", relPath);
|
|
2833
2835
|
}
|
|
2834
2836
|
}
|
|
2835
2837
|
return yamlContent;
|
|
@@ -2882,7 +2884,7 @@ var MdTransformer = class {
|
|
|
2882
2884
|
error: `读取失败:${e.message}`
|
|
2883
2885
|
});
|
|
2884
2886
|
} finally {
|
|
2885
|
-
await sleep(
|
|
2887
|
+
await sleep(300);
|
|
2886
2888
|
}
|
|
2887
2889
|
}
|
|
2888
2890
|
map.set(raw, {
|
|
@@ -2891,7 +2893,7 @@ var MdTransformer = class {
|
|
|
2891
2893
|
sheets: resolved
|
|
2892
2894
|
});
|
|
2893
2895
|
} catch (e) {
|
|
2894
|
-
logger$
|
|
2896
|
+
logger$2.warn(`Failed to resolve sheet ${raw}:`, e.message);
|
|
2895
2897
|
map.set(raw, {
|
|
2896
2898
|
type: "sheetResolved",
|
|
2897
2899
|
title: "",
|
|
@@ -2994,15 +2996,31 @@ function extractHeadingText(block) {
|
|
|
2994
2996
|
return body.elements.map((e) => e.text_run?.content ?? "").join("").trim();
|
|
2995
2997
|
}
|
|
2996
2998
|
/**
|
|
2997
|
-
*
|
|
2998
|
-
* 纯函数工厂,无副作用,易于测试。
|
|
2999
|
+
* 将 heading block 转为 HeadingInfo;非 heading 返回 null。纯函数,无状态。
|
|
2999
3000
|
*/
|
|
3000
|
-
function
|
|
3001
|
-
const
|
|
3001
|
+
function toHeadingInfo(block) {
|
|
3002
|
+
const level = getHeadingLevel(block);
|
|
3003
|
+
if (level === null) return null;
|
|
3004
|
+
return {
|
|
3005
|
+
blockId: block.block_id ?? "",
|
|
3006
|
+
level,
|
|
3007
|
+
text: extractHeadingText(block) ?? ""
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* 通用 heading 过滤器骨架:扫描 → 命中 → 收集 → 终止 状态机。
|
|
3012
|
+
* 调用方仅需提供 `match(block, info)` 谓词决定何时进入 collecting。
|
|
3013
|
+
*
|
|
3014
|
+
* 复用要点:
|
|
3015
|
+
* - page 节点(block_type=1)始终保留
|
|
3016
|
+
* - scanning 阶段把所有 heading 推入 availableHeadings
|
|
3017
|
+
* - collecting 阶段遇到同级或更高级标题终止
|
|
3018
|
+
*/
|
|
3019
|
+
function createHeadingMatchFilter(match) {
|
|
3002
3020
|
let state = "scanning";
|
|
3003
3021
|
let matchedLevel = 0;
|
|
3004
3022
|
const collected = [];
|
|
3005
|
-
const
|
|
3023
|
+
const seen = [];
|
|
3006
3024
|
function pageHandler(blocks) {
|
|
3007
3025
|
for (const block of blocks) {
|
|
3008
3026
|
if (block.block_type === 1) {
|
|
@@ -3011,16 +3029,12 @@ function createTitleFilter(options) {
|
|
|
3011
3029
|
}
|
|
3012
3030
|
switch (state) {
|
|
3013
3031
|
case "scanning": {
|
|
3014
|
-
const
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
level,
|
|
3019
|
-
text
|
|
3020
|
-
});
|
|
3021
|
-
if (text === targetTitle) {
|
|
3032
|
+
const info = toHeadingInfo(block);
|
|
3033
|
+
if (info) {
|
|
3034
|
+
seen.push(info);
|
|
3035
|
+
if (match(block, info)) {
|
|
3022
3036
|
state = "collecting";
|
|
3023
|
-
matchedLevel = level;
|
|
3037
|
+
matchedLevel = info.level;
|
|
3024
3038
|
collected.push(block);
|
|
3025
3039
|
}
|
|
3026
3040
|
}
|
|
@@ -3043,8 +3057,8 @@ function createTitleFilter(options) {
|
|
|
3043
3057
|
function getResult() {
|
|
3044
3058
|
return {
|
|
3045
3059
|
blocks: [...collected],
|
|
3046
|
-
matched: state
|
|
3047
|
-
availableHeadings: [...
|
|
3060
|
+
matched: state !== "scanning",
|
|
3061
|
+
availableHeadings: [...seen]
|
|
3048
3062
|
};
|
|
3049
3063
|
}
|
|
3050
3064
|
return {
|
|
@@ -3052,9 +3066,41 @@ function createTitleFilter(options) {
|
|
|
3052
3066
|
getResult
|
|
3053
3067
|
};
|
|
3054
3068
|
}
|
|
3069
|
+
/**
|
|
3070
|
+
* 按标题文本过滤(首个匹配生效;若有同名标题,请改用 createTitleBlockIdFilter)。
|
|
3071
|
+
*/
|
|
3072
|
+
function createTitleFilter(options) {
|
|
3073
|
+
const target = options.title.trim();
|
|
3074
|
+
return createHeadingMatchFilter((_block, info) => info.text === target);
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* 按 heading 块 id 过滤(最精确,不会受同名标题干扰)。
|
|
3078
|
+
* 仅匹配 block_type 为 heading(1~9)且 block_id 严格相等的块。
|
|
3079
|
+
*/
|
|
3080
|
+
function createTitleBlockIdFilter(options) {
|
|
3081
|
+
const target = options.blockId.trim();
|
|
3082
|
+
return createHeadingMatchFilter((block) => block.block_id === target);
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* 流式收集文档中所有标题,用于 get-titles 命令。
|
|
3086
|
+
* 返回的 pageHandler 始终返回 true,以遵循分页拉取全量文档。
|
|
3087
|
+
*/
|
|
3088
|
+
function createHeadingCollector() {
|
|
3089
|
+
const headings = [];
|
|
3090
|
+
function pageHandler(blocks) {
|
|
3091
|
+
for (const block of blocks) {
|
|
3092
|
+
const info = toHeadingInfo(block);
|
|
3093
|
+
if (info) headings.push(info);
|
|
3094
|
+
}
|
|
3095
|
+
return true;
|
|
3096
|
+
}
|
|
3097
|
+
return {
|
|
3098
|
+
pageHandler,
|
|
3099
|
+
getHeadings: () => [...headings]
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3055
3102
|
//#endregion
|
|
3056
|
-
//#region src/
|
|
3057
|
-
const logger = createLogger("converter");
|
|
3103
|
+
//#region src/url.ts
|
|
3058
3104
|
function parseWikiUrl(url) {
|
|
3059
3105
|
const m = url.match(/^https:\/\/[\w.-]+\/(docs|docx|wiki|sheets)\/([a-zA-Z0-9]+)/);
|
|
3060
3106
|
if (!m) throw new Error("Invalid feishu document URL");
|
|
@@ -3065,6 +3111,51 @@ function parseWikiUrl(url) {
|
|
|
3065
3111
|
sheetId
|
|
3066
3112
|
};
|
|
3067
3113
|
}
|
|
3114
|
+
//#endregion
|
|
3115
|
+
//#region src/get-titles.ts
|
|
3116
|
+
const logger$1 = createLogger("get-titles");
|
|
3117
|
+
/** 拉取 docx/wiki 文档中所有标题信息(扁平列表,不下载图片)。不支持电子表格。 */
|
|
3118
|
+
async function getTitles(opts) {
|
|
3119
|
+
const { docType, docToken: rawToken } = parseWikiUrl(opts.url);
|
|
3120
|
+
if (docType === "sheets") throw new Error("get-titles does not support spreadsheets, only docx/wiki documents are supported");
|
|
3121
|
+
const sdkLoggerLevel = opts.agent ? LoggerLevel.error : LoggerLevel.warn;
|
|
3122
|
+
const client = createClient(opts.appId, opts.appSecret, sdkLoggerLevel);
|
|
3123
|
+
let docToken = rawToken;
|
|
3124
|
+
if (docType === "wiki") {
|
|
3125
|
+
const node = await client.getWikiNodeInfo(docToken);
|
|
3126
|
+
docToken = node.obj_token;
|
|
3127
|
+
logger$1.info("Resolved wiki node:", node.obj_type, docToken);
|
|
3128
|
+
if (node.obj_type === "sheet") throw new Error("get-titles does not support spreadsheets (wiki node points to a spreadsheet), only docx/wiki documents are supported");
|
|
3129
|
+
}
|
|
3130
|
+
const collector = createHeadingCollector();
|
|
3131
|
+
await client.getDocxBlocks(docToken, collector.pageHandler);
|
|
3132
|
+
const titles = collector.getHeadings();
|
|
3133
|
+
logger$1.info(`Collected ${titles.length} headings`);
|
|
3134
|
+
return {
|
|
3135
|
+
url: opts.url,
|
|
3136
|
+
docToken,
|
|
3137
|
+
titles
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
/** 按 level 栈式回溯将扁平标题列表转为树(容忍跳级标题)。 */
|
|
3141
|
+
function buildTitleTree(titles) {
|
|
3142
|
+
const roots = [];
|
|
3143
|
+
const stack = [];
|
|
3144
|
+
for (const t of titles) {
|
|
3145
|
+
const node = { ...t };
|
|
3146
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) stack.pop();
|
|
3147
|
+
if (stack.length === 0) roots.push(node);
|
|
3148
|
+
else {
|
|
3149
|
+
const parent = stack[stack.length - 1];
|
|
3150
|
+
(parent.children ??= []).push(node);
|
|
3151
|
+
}
|
|
3152
|
+
stack.push(node);
|
|
3153
|
+
}
|
|
3154
|
+
return roots;
|
|
3155
|
+
}
|
|
3156
|
+
//#endregion
|
|
3157
|
+
//#region src/converter.ts
|
|
3158
|
+
const logger = createLogger("converter");
|
|
3068
3159
|
async function convert(opts) {
|
|
3069
3160
|
const { docType, docToken: rawToken, sheetId } = parseWikiUrl(opts.url);
|
|
3070
3161
|
logger.info("Captured document token:", rawToken, sheetId ? `sheetId: ${sheetId}` : "");
|
|
@@ -3096,18 +3187,11 @@ async function convert(opts) {
|
|
|
3096
3187
|
} else {
|
|
3097
3188
|
const doc = await client.getDocxDocument(docToken);
|
|
3098
3189
|
let blocks;
|
|
3099
|
-
|
|
3100
|
-
|
|
3190
|
+
const filter = createDocxFilter(opts);
|
|
3191
|
+
if (filter) {
|
|
3101
3192
|
await client.getDocxBlocks(docToken, filter.pageHandler);
|
|
3102
3193
|
const result = filter.getResult();
|
|
3103
|
-
if (!result.matched)
|
|
3104
|
-
let msg = `No heading matched "${opts.filterTitle}". Please verify the heading text.`;
|
|
3105
|
-
if (result.availableHeadings.length > 0) {
|
|
3106
|
-
const list = result.availableHeadings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
3107
|
-
msg += `\n\nAvailable headings in the document:\n\n${list}`;
|
|
3108
|
-
}
|
|
3109
|
-
throw new Error(msg);
|
|
3110
|
-
}
|
|
3194
|
+
if (!result.matched) throw new Error(buildFilterErrorMessage(opts, result, opts.url, docToken));
|
|
3111
3195
|
blocks = result.blocks;
|
|
3112
3196
|
} else blocks = await client.getDocxBlocks(docToken);
|
|
3113
3197
|
logger.info(`Fetched ${blocks.length} blocks`);
|
|
@@ -3132,7 +3216,28 @@ async function convert(opts) {
|
|
|
3132
3216
|
filePath
|
|
3133
3217
|
};
|
|
3134
3218
|
}
|
|
3219
|
+
/** 优先级:filterTitleBlockId > filterTitle。返回 null 表示不过滤。 */
|
|
3220
|
+
function createDocxFilter(opts) {
|
|
3221
|
+
if (opts.filterTitleBlockId) return createTitleBlockIdFilter({ blockId: opts.filterTitleBlockId });
|
|
3222
|
+
if (opts.filterTitle) return createTitleFilter({ title: opts.filterTitle });
|
|
3223
|
+
return null;
|
|
3224
|
+
}
|
|
3225
|
+
function buildFilterErrorMessage(opts, result, url, docToken) {
|
|
3226
|
+
let target;
|
|
3227
|
+
if (opts.filterTitleBlockId) target = `block id "${opts.filterTitleBlockId}"`;
|
|
3228
|
+
else target = `"${opts.filterTitle}"`;
|
|
3229
|
+
let msg = `No heading matched ${target}. Please verify the heading text/id.`;
|
|
3230
|
+
if (result.availableHeadings.length > 0) {
|
|
3231
|
+
const yaml = serializeYaml({
|
|
3232
|
+
url,
|
|
3233
|
+
docToken,
|
|
3234
|
+
titles: buildTitleTree(result.availableHeadings)
|
|
3235
|
+
});
|
|
3236
|
+
msg += `\n\nFull title list of the document:\n\n${yaml}`;
|
|
3237
|
+
}
|
|
3238
|
+
return msg;
|
|
3239
|
+
}
|
|
3135
3240
|
//#endregion
|
|
3136
|
-
export { parseWikiUrl as n,
|
|
3241
|
+
export { setLogLevel as a, parseWikiUrl as i, buildTitleTree as n, serializeYaml as o, getTitles as r, convert as t };
|
|
3137
3242
|
|
|
3138
|
-
//# sourceMappingURL=converter-
|
|
3243
|
+
//# sourceMappingURL=converter-DLMAssSI.js.map
|