lark-docx2md 0.5.2 → 0.5.3-beta.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/README.md CHANGED
@@ -45,6 +45,7 @@ npx -y lark-docx2md@latest download <url>
45
45
  | `--agent [mode]` | Agent 模式:日志 ERROR。不传值(或 `=true`)为在线模式,Markdown 输出到 stdout;传 `local` 则落盘后输出引导 AI 读取的提示词 | `LARK_DOCX2MD_AGENT=true\|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` / `index` / `level` / `text` / `path`,可被 AI 直接消费用于选择目标章节。
58
77
 
59
78
  ## 功能
60
79
 
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { r as setLogLevel, t as convert } from "./converter-vuRwWoA4.js";
2
+ import { i as getTitles, n as convert, o as setLogLevel, r as formatTitlesAsText, s as serializeYaml, t as buildTitleTree } from "./converter-D1nCzDJF.js";
3
3
  import { Command } from "commander";
4
4
  import { LoggerLevel } from "@larksuiteoapi/node-sdk";
5
5
  //#region src/cli.ts
6
6
  const program = new Command();
7
7
  program.name("larkDocx2md").description("Download Lark/Feishu documents to markdown");
8
- 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. Pass \"local\" to save markdown/images/whiteboards to disk and print a read-file prompt (or LARK_DOCX2MD_AGENT=true|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").argument("<url>", "Feishu wiki document URL: https://*.feishu.cn/wiki/*").action(async (url, opts) => {
8
+ 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. Pass \"local\" to save markdown/images/whiteboards to disk and print a read-file prompt (or LARK_DOCX2MD_AGENT=true|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
9
  opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;
10
10
  opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;
11
11
  opts.output = opts.output ?? process.env.LARK_DOCX2MD_OUTPUT ?? "./larkDocx2mdOutput";
@@ -45,6 +45,7 @@ program.command("download").alias("dl").description("Download a wiki document to
45
45
  "base64",
46
46
  "local"
47
47
  ].includes(opts.wbImageMode)) program.error(`Invalid --wb-image-mode "${opts.wbImageMode}", must be "online", "base64", or "local"`);
48
+ if (opts.filterTitle && opts.filterTitleBlockId) program.error("--filter-title and --filter-title-block-id are mutually exclusive; choose one");
48
49
  const appId = opts.appId;
49
50
  const appSecret = opts.appSecret;
50
51
  if (!appId || !appSecret) program.error("Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET");
@@ -58,11 +59,68 @@ program.command("download").alias("dl").description("Download a wiki document to
58
59
  wbBg: opts.wbBg,
59
60
  wbFormat: opts.wbFormat,
60
61
  agent: agentLocal ? "local" : opts.agent === true,
61
- filterTitle: opts.filterTitle?.trim()
62
+ filterTitle: opts.filterTitle?.trim(),
63
+ filterTitleBlockId: opts.filterTitleBlockId?.trim()
62
64
  });
63
65
  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
66
  else if (opts.agent === true) process.stdout.write(result.markdown);
65
67
  });
68
+ program.command("get-titles").description("Print all headings (level 1~9) of a wiki/docx document. 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("--max-level <n>", "Only output headings whose level <= n (1~9)", "9").option("--format <format>", "Output format: \"yaml\" (flat, default) | \"yaml-tree\" (nested) | \"json\" (flat) | \"tree\" (json nested) | \"text\" (indented markdown headings)", "yaml").option("--agent [mode]", "Enable agent mode: ERROR log level, AI-oriented stdout (or LARK_DOCX2MD_AGENT=true|local)").argument("<url>", "Feishu wiki/docx URL: https://*.feishu.cn/{wiki,docx,docs}/*").action(async (url, opts) => {
69
+ opts.appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;
70
+ opts.appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;
71
+ if (opts.agent === void 0) {
72
+ const envAgent = process.env.LARK_DOCX2MD_AGENT;
73
+ if (envAgent === "true") opts.agent = true;
74
+ else if (envAgent === "local") opts.agent = "local";
75
+ else opts.agent = false;
76
+ } else if (typeof opts.agent === "string" && opts.agent !== "local") program.error(`Invalid --agent value "${opts.agent}", only "local" is supported (or omit the value)`);
77
+ if (opts.agent === true || opts.agent === "local") setLogLevel(LoggerLevel.error);
78
+ const maxLevel = parseInt(opts.maxLevel ?? "9", 10);
79
+ if (!Number.isInteger(maxLevel) || maxLevel < 1 || maxLevel > 9) program.error(`Invalid --max-level "${opts.maxLevel}", must be an integer in [1, 9]`);
80
+ const format = opts.format ?? "yaml";
81
+ if (![
82
+ "yaml",
83
+ "yaml-tree",
84
+ "json",
85
+ "tree",
86
+ "text"
87
+ ].includes(format)) program.error(`Invalid --format "${format}", must be "yaml" | "yaml-tree" | "json" | "tree" | "text"`);
88
+ const appId = opts.appId;
89
+ const appSecret = opts.appSecret;
90
+ if (!appId || !appSecret) program.error("Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET");
91
+ const result = await getTitles({
92
+ appId,
93
+ appSecret,
94
+ url,
95
+ agent: opts.agent === "local" ? "local" : opts.agent === true
96
+ });
97
+ const filtered = result.titles.filter((t) => t.level <= maxLevel);
98
+ if (format === "yaml") process.stdout.write(serializeYaml({
99
+ url: result.url,
100
+ docToken: result.docToken,
101
+ titles: filtered
102
+ }));
103
+ else if (format === "yaml-tree") {
104
+ const tree = buildTitleTree(filtered);
105
+ process.stdout.write(serializeYaml({
106
+ url: result.url,
107
+ docToken: result.docToken,
108
+ titles: tree
109
+ }));
110
+ } else if (format === "json") process.stdout.write(JSON.stringify({
111
+ url: result.url,
112
+ docToken: result.docToken,
113
+ titles: filtered
114
+ }, null, 2) + "\n");
115
+ else if (format === "tree") {
116
+ const tree = buildTitleTree(filtered);
117
+ process.stdout.write(JSON.stringify({
118
+ url: result.url,
119
+ docToken: result.docToken,
120
+ titles: tree
121
+ }, null, 2) + "\n");
122
+ } else process.stdout.write(formatTitlesAsText(filtered) + "\n");
123
+ });
66
124
  program.parse();
67
125
  //#endregion
68
126
  export {};
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 { Command } from 'commander';\nimport { LoggerLevel } from '@larksuiteoapi/node-sdk';\nimport { buildTitleTree, convert, formatTitlesAsText, getTitles } from './converter.js';\nimport { setLogLevel } from './logger.js';\nimport { serializeYaml } from './whiteboard/yaml/serialize.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 (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 | '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 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: agentLocal ? 'local' : (opts.agent === true),\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 (opts.agent === true) {\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. 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('--max-level <n>', 'Only output headings whose level <= n (1~9)', '9')\n .option('--format <format>', 'Output format: \"yaml\" (flat, default) | \"yaml-tree\" (nested) | \"json\" (flat) | \"tree\" (json nested) | \"text\" (indented markdown headings)', 'yaml')\n .option('--agent [mode]', 'Enable agent mode: ERROR log level, AI-oriented stdout (or LARK_DOCX2MD_AGENT=true|local)')\n .argument('<url>', 'Feishu wiki/docx URL: https://*.feishu.cn/{wiki,docx,docs}/*')\n .action(async (url: string, opts: { appId?: string; appSecret?: string; maxLevel?: string; format?: 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 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 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 const format = opts.format ?? 'yaml';\n if (!['yaml', 'yaml-tree', 'json', 'tree', 'text'].includes(format)) {\n program.error(`Invalid --format \"${format}\", must be \"yaml\" | \"yaml-tree\" | \"json\" | \"tree\" | \"text\"`);\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 === 'local' ? 'local' : (opts.agent === true),\n });\n const filtered = result.titles.filter(t => t.level <= maxLevel);\n\n if (format === 'yaml') {\n process.stdout.write(serializeYaml({ url: result.url, docToken: result.docToken, titles: filtered }));\n } else if (format === 'yaml-tree') {\n const tree = buildTitleTree(filtered);\n process.stdout.write(serializeYaml({ url: result.url, docToken: result.docToken, titles: tree }));\n } else if (format === 'json') {\n process.stdout.write(JSON.stringify({ url: result.url, docToken: result.docToken, titles: filtered }, null, 2) + '\\n');\n } else if (format === 'tree') {\n const tree = buildTitleTree(filtered);\n process.stdout.write(JSON.stringify({ url: result.url, docToken: result.docToken, titles: tree }, null, 2) + '\\n');\n } else {\n process.stdout.write(formatTitlesAsText(filtered) + '\\n');\n }\n });\n\nprogram.parse();\n"],"mappings":"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"}
@@ -17,6 +17,7 @@ interface ConvertOptions {
17
17
  wbFormat: WbFormat;
18
18
  agent?: boolean | 'local';
19
19
  filterTitle?: string;
20
+ filterTitleBlockId?: string;
20
21
  }
21
22
  interface ConvertResult {
22
23
  markdown: string;
@@ -25,6 +26,15 @@ interface ConvertResult {
25
26
  }
26
27
  //# sourceMappingURL=types.d.ts.map
27
28
  //#endregion
29
+ //#region src/title-filter.d.ts
30
+ interface HeadingInfo {
31
+ blockId: string;
32
+ index: number;
33
+ level: number;
34
+ text: string;
35
+ path: string[];
36
+ }
37
+ //#endregion
28
38
  //#region src/converter.d.ts
29
39
  declare function parseWikiUrl(url: string): {
30
40
  docType: string;
@@ -32,8 +42,25 @@ declare function parseWikiUrl(url: string): {
32
42
  sheetId?: string;
33
43
  };
34
44
  declare function convert(opts: ConvertOptions): Promise<ConvertResult>;
45
+ interface GetTitlesOptions {
46
+ appId: string;
47
+ appSecret: string;
48
+ url: string;
49
+ agent?: boolean | 'local';
50
+ }
51
+ interface GetTitlesResult {
52
+ url: string;
53
+ docToken: string;
54
+ titles: HeadingInfo[];
55
+ }
56
+ declare function getTitles(opts: GetTitlesOptions): Promise<GetTitlesResult>;
57
+ interface TitleTreeNode extends HeadingInfo {
58
+ children?: TitleTreeNode[];
59
+ }
60
+ declare function buildTitleTree(titles: HeadingInfo[]): TitleTreeNode[];
61
+ declare function formatTitlesAsText(titles: HeadingInfo[]): string;
35
62
  //# sourceMappingURL=converter.d.ts.map
36
63
 
37
64
  //#endregion
38
- export { convert, parseWikiUrl };
39
- //# sourceMappingURL=converter-nwp8DCnk.d.ts.map
65
+ export { GetTitlesOptions, GetTitlesResult, TitleTreeNode, buildTitleTree, convert, formatTitlesAsText, getTitles, parseWikiUrl };
66
+ //# sourceMappingURL=converter-Cb2tYM7d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter-Cb2tYM7d.d.ts","names":[],"sources":["../src/types.ts","../src/title-filter.ts","../src/converter.ts"],"mappings":";;;;AAsCa,KAhBD,SAAA,GAgBC,OAAA,GAAA,QAAA;AACE,KAdH,WAAA,GAcG,QAAA,GAAA,QAAA,GAAA,OAAA;AACP,KAZI,QAAA,GAYJ,QAAA,GAAA,YAAA,GAAA,KAAA,GAAA,MAAA;AACI,KAVA,aAAA,GAUA,MAAA,GAAA,KAAA,GAAA,CAAA,MAAA,GAAA,CAAA,CAAA,CAAA;AAAQ,UARH,cAAA,CAQG;EAaH,KAAA,EAAA,MAAA;;;;EC/BA,SAAA,EDeJ,SCfe;eDgBb;QACP;YACI;EElBI,KAAA,CAAA,EAAA,OAAY,GAAA,OAAA;EAUN,WAAO,CAAA,EAAA,MAAA;EAAA,kBAAA,CAAA,EAAA,MAAA;;AAAiC,UFqB7C,aAAA,CErB6C;UAAR,EAAA,MAAA;EAAO,QAAA,EAAA,MAAA;EAsG5C,QAAA,CAAA,EAAA,MAAA;AAQjB;AAOA;;;AFhHa,UCfI,WAAA,CDeJ;SACE,EAAA,MAAA;OACP,EAAA,MAAA;OACI,EAAA,MAAA;EAAQ,IAAA,EAAA,MAAA;EAaH,IAAA,EAAA,MAAA,EAAA;;;;iBE/BD,YAAA;EFDJ,OAAA,EAAA,MAAS;EAGT,QAAA,EAAA,MAAW;EAGX,OAAA,CAAA,EAAA,MAAQ;AAGpB,CAAA;AAEiB,iBEAK,OAAA,CFAS,IAAA,EEAM,cFAN,CAAA,EEAuB,OFAvB,CEA+B,aFA/B,CAAA;AAAA,UEsGd,gBAAA,CFtGc;OAKlB,EAAA,MAAA;WACE,EAAA,MAAA;KACP,EAAA,MAAA;OACI,CAAA,EAAA,OAAA,GAAA,OAAA;;AAaK,UEyFA,eAAA,CFzFa;;;UE4FpB;AD3HV;iBC+HsB,SAAA,OAAiB,mBAAmB,QAAQ;UAwBjD,aAAA,SAAsB;aAC1B;AAxJb;AAUsB,iBAkJN,cAAA,CAlJa,MAAA,EAkJW,WAlJX,EAAA,CAAA,EAkJ2B,aAlJ3B,EAAA;AAAA,iBAoKb,kBAAA,CApKa,MAAA,EAoKe,WApKf,EAAA,CAAA,EAAA,MAAA"}
@@ -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(100);
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
- rows[row].cells[col].rowSpan = m.row_span > 1 ? m.row_span : void 0;
565
- rows[row].cells[col].colSpan = m.col_span > 1 ? m.col_span : void 0;
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();
@@ -2882,7 +2884,7 @@ var MdTransformer = class {
2882
2884
  error: `读取失败:${e.message}`
2883
2885
  });
2884
2886
  } finally {
2885
- await sleep(600);
2887
+ await sleep(300);
2886
2888
  }
2887
2889
  }
2888
2890
  map.set(raw, {
@@ -2994,6 +2996,33 @@ function extractHeadingText(block) {
2994
2996
  return body.elements.map((e) => e.text_run?.content ?? "").join("").trim();
2995
2997
  }
2996
2998
  /**
2999
+ * 内部工具:维护标题祖先栈与 1-based 序号,将一个 heading block 转为 HeadingInfo。
3000
+ * 传入非 heading block 返回 null,不改变内部状态。
3001
+ */
3002
+ function createHeadingTracker() {
3003
+ const stack = [];
3004
+ let counter = 0;
3005
+ function push(block) {
3006
+ const level = getHeadingLevel(block);
3007
+ if (level === null) return null;
3008
+ const text = extractHeadingText(block) ?? "";
3009
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop();
3010
+ stack.push({
3011
+ level,
3012
+ text
3013
+ });
3014
+ counter += 1;
3015
+ return {
3016
+ blockId: block.block_id ?? "",
3017
+ index: counter,
3018
+ level,
3019
+ text,
3020
+ path: stack.map((s) => s.text)
3021
+ };
3022
+ }
3023
+ return { push };
3024
+ }
3025
+ /**
2997
3026
  * 创建标题过滤器,返回一个 pageHandler 兼容的回调和结果获取器。
2998
3027
  * 纯函数工厂,无副作用,易于测试。
2999
3028
  */
@@ -3003,6 +3032,7 @@ function createTitleFilter(options) {
3003
3032
  let matchedLevel = 0;
3004
3033
  const collected = [];
3005
3034
  const seenHeadings = [];
3035
+ const tracker = createHeadingTracker();
3006
3036
  function pageHandler(blocks) {
3007
3037
  for (const block of blocks) {
3008
3038
  if (block.block_type === 1) {
@@ -3011,16 +3041,12 @@ function createTitleFilter(options) {
3011
3041
  }
3012
3042
  switch (state) {
3013
3043
  case "scanning": {
3014
- const level = getHeadingLevel(block);
3015
- if (level !== null) {
3016
- const text = extractHeadingText(block) ?? "";
3017
- seenHeadings.push({
3018
- level,
3019
- text
3020
- });
3021
- if (text === targetTitle) {
3044
+ const info = tracker.push(block);
3045
+ if (info) {
3046
+ seenHeadings.push(info);
3047
+ if (info.text === targetTitle) {
3022
3048
  state = "collecting";
3023
- matchedLevel = level;
3049
+ matchedLevel = info.level;
3024
3050
  collected.push(block);
3025
3051
  }
3026
3052
  }
@@ -3052,6 +3078,81 @@ function createTitleFilter(options) {
3052
3078
  getResult
3053
3079
  };
3054
3080
  }
3081
+ /**
3082
+ * 按 heading 块 id 过滤(最精确,不会受同名标题干扰)。
3083
+ * 仅匹配 block_type 为 heading(1~9)且 block_id 严格相等的块。
3084
+ */
3085
+ function createTitleBlockIdFilter(options) {
3086
+ const target = options.blockId.trim();
3087
+ let state = "scanning";
3088
+ let matchedLevel = 0;
3089
+ const collected = [];
3090
+ const seen = [];
3091
+ const tracker = createHeadingTracker();
3092
+ function pageHandler(blocks) {
3093
+ for (const block of blocks) {
3094
+ if (block.block_type === 1) {
3095
+ collected.push(block);
3096
+ continue;
3097
+ }
3098
+ switch (state) {
3099
+ case "scanning": {
3100
+ const info = tracker.push(block);
3101
+ if (info) {
3102
+ seen.push(info);
3103
+ if (block.block_id === target) {
3104
+ state = "collecting";
3105
+ matchedLevel = info.level;
3106
+ collected.push(block);
3107
+ }
3108
+ }
3109
+ break;
3110
+ }
3111
+ case "collecting": {
3112
+ const level = getHeadingLevel(block);
3113
+ if (level !== null && level <= matchedLevel) {
3114
+ state = "done";
3115
+ return false;
3116
+ }
3117
+ collected.push(block);
3118
+ break;
3119
+ }
3120
+ case "done": return false;
3121
+ }
3122
+ }
3123
+ return state !== "done";
3124
+ }
3125
+ function getResult() {
3126
+ return {
3127
+ blocks: [...collected],
3128
+ matched: state === "collecting" || state === "done",
3129
+ availableHeadings: [...seen]
3130
+ };
3131
+ }
3132
+ return {
3133
+ pageHandler,
3134
+ getResult
3135
+ };
3136
+ }
3137
+ /**
3138
+ * 流式收集文档中所有标题,用于 get-titles 命令。
3139
+ * 返回的 pageHandler 始终返回 true,以遵循分页拉取全量文档。
3140
+ */
3141
+ function createHeadingCollector() {
3142
+ const headings = [];
3143
+ const tracker = createHeadingTracker();
3144
+ function pageHandler(blocks) {
3145
+ for (const block of blocks) {
3146
+ const info = tracker.push(block);
3147
+ if (info) headings.push(info);
3148
+ }
3149
+ return true;
3150
+ }
3151
+ return {
3152
+ pageHandler,
3153
+ getHeadings: () => [...headings]
3154
+ };
3155
+ }
3055
3156
  //#endregion
3056
3157
  //#region src/converter.ts
3057
3158
  const logger = createLogger("converter");
@@ -3096,18 +3197,11 @@ async function convert(opts) {
3096
3197
  } else {
3097
3198
  const doc = await client.getDocxDocument(docToken);
3098
3199
  let blocks;
3099
- if (opts.filterTitle) {
3100
- const filter = createTitleFilter({ title: opts.filterTitle });
3200
+ const filter = createDocxFilter(opts);
3201
+ if (filter) {
3101
3202
  await client.getDocxBlocks(docToken, filter.pageHandler);
3102
3203
  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
- }
3204
+ if (!result.matched) throw new Error(buildFilterErrorMessage(opts, result, opts.url, docToken));
3111
3205
  blocks = result.blocks;
3112
3206
  } else blocks = await client.getDocxBlocks(docToken);
3113
3207
  logger.info(`Fetched ${blocks.length} blocks`);
@@ -3132,7 +3226,70 @@ async function convert(opts) {
3132
3226
  filePath
3133
3227
  };
3134
3228
  }
3229
+ /** 优先级:filterTitleBlockId > filterTitle。返回 null 表示不过滤。 */
3230
+ function createDocxFilter(opts) {
3231
+ if (opts.filterTitleBlockId) return createTitleBlockIdFilter({ blockId: opts.filterTitleBlockId });
3232
+ if (opts.filterTitle) return createTitleFilter({ title: opts.filterTitle });
3233
+ return null;
3234
+ }
3235
+ function buildFilterErrorMessage(opts, result, url, docToken) {
3236
+ let target;
3237
+ if (opts.filterTitleBlockId) target = `block id "${opts.filterTitleBlockId}"`;
3238
+ else target = `"${opts.filterTitle}"`;
3239
+ let msg = `No heading matched ${target}. Please verify the heading text/id.`;
3240
+ if (result.availableHeadings.length > 0) {
3241
+ const yaml = serializeYaml({
3242
+ url,
3243
+ docToken,
3244
+ titles: result.availableHeadings
3245
+ });
3246
+ msg += `\n\nFull title list of the document (yaml, same shape as \`get-titles\`):\n\n${yaml}`;
3247
+ }
3248
+ return msg;
3249
+ }
3250
+ /** 拉取 docx/wiki 文档中所有标题信息(扁平列表,不下载图片)。 */
3251
+ async function getTitles(opts) {
3252
+ const { docType, docToken: rawToken } = parseWikiUrl(opts.url);
3253
+ if (docType === "sheets") throw new Error("get-titles only supports docx/wiki documents, not spreadsheets");
3254
+ const sdkLoggerLevel = opts.agent ? LoggerLevel.error : LoggerLevel.warn;
3255
+ const client = createClient(opts.appId, opts.appSecret, sdkLoggerLevel);
3256
+ let docToken = rawToken;
3257
+ if (docType === "wiki") {
3258
+ const node = await client.getWikiNodeInfo(docToken);
3259
+ docToken = node.obj_token;
3260
+ logger.info("Resolved wiki node:", node.obj_type, docToken);
3261
+ }
3262
+ const collector = createHeadingCollector();
3263
+ await client.getDocxBlocks(docToken, collector.pageHandler);
3264
+ const titles = collector.getHeadings();
3265
+ logger.info(`Collected ${titles.length} headings`);
3266
+ return {
3267
+ url: opts.url,
3268
+ docToken,
3269
+ titles
3270
+ };
3271
+ }
3272
+ /** 按 level 栈式回溯将扁平标题列表转为树(容忍跳级标题)。 */
3273
+ function buildTitleTree(titles) {
3274
+ const roots = [];
3275
+ const stack = [];
3276
+ for (const t of titles) {
3277
+ const node = { ...t };
3278
+ while (stack.length > 0 && stack[stack.length - 1].level >= node.level) stack.pop();
3279
+ if (stack.length === 0) roots.push(node);
3280
+ else {
3281
+ const parent = stack[stack.length - 1];
3282
+ (parent.children ??= []).push(node);
3283
+ }
3284
+ stack.push(node);
3285
+ }
3286
+ return roots;
3287
+ }
3288
+ /** 按级别以 markdown heading 风格输出人读文本(包含缩进)。 */
3289
+ function formatTitlesAsText(titles) {
3290
+ return titles.map((t) => `${" ".repeat(Math.max(0, t.level - 1))}${"#".repeat(t.level)} ${t.text}`).join("\n");
3291
+ }
3135
3292
  //#endregion
3136
- export { parseWikiUrl as n, setLogLevel as r, convert as t };
3293
+ export { parseWikiUrl as a, getTitles as i, convert as n, setLogLevel as o, formatTitlesAsText as r, serializeYaml as s, buildTitleTree as t };
3137
3294
 
3138
- //# sourceMappingURL=converter-vuRwWoA4.js.map
3295
+ //# sourceMappingURL=converter-D1nCzDJF.js.map