lark-docx2md 0.5.1 → 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-Bqb4bqxh.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: {
@@ -93,11 +93,8 @@ function createClient(appId, appSecret, loggerLevel = LoggerLevel.warn) {
93
93
  await resp.writeFile(filename);
94
94
  return filename;
95
95
  } catch (error) {
96
- if ([
97
- 400,
98
- 401,
99
- 403
100
- ].includes(error.status)) throw new Error(`下载图片[${imgToken}]异常, 检查是否有接口 https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download 的权限。`);
96
+ if (error.status === 401) throw new Error(`下载图片[${imgToken}]异常, 检查是否有接口 https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download 的权限`);
97
+ if (error.status === 403) throw new Error(`下载图片[${imgToken}]异常, 应用的文档权限大于等于文档本身“谁可以创建副本、打印和下载”的权限`);
101
98
  throw error;
102
99
  }
103
100
  }
@@ -564,8 +561,10 @@ const tableParser = {
564
561
  const row = Math.floor(i / cols);
565
562
  const col = i % cols;
566
563
  if (rows[row]?.cells[col]) {
567
- rows[row].cells[col].rowSpan = m.row_span > 1 ? m.row_span : void 0;
568
- 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;
569
568
  }
570
569
  }
571
570
  const skipSet = /* @__PURE__ */ new Set();
@@ -2885,7 +2884,7 @@ var MdTransformer = class {
2885
2884
  error: `读取失败:${e.message}`
2886
2885
  });
2887
2886
  } finally {
2888
- await sleep(600);
2887
+ await sleep(300);
2889
2888
  }
2890
2889
  }
2891
2890
  map.set(raw, {
@@ -2997,6 +2996,33 @@ function extractHeadingText(block) {
2997
2996
  return body.elements.map((e) => e.text_run?.content ?? "").join("").trim();
2998
2997
  }
2999
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
+ /**
3000
3026
  * 创建标题过滤器,返回一个 pageHandler 兼容的回调和结果获取器。
3001
3027
  * 纯函数工厂,无副作用,易于测试。
3002
3028
  */
@@ -3006,6 +3032,7 @@ function createTitleFilter(options) {
3006
3032
  let matchedLevel = 0;
3007
3033
  const collected = [];
3008
3034
  const seenHeadings = [];
3035
+ const tracker = createHeadingTracker();
3009
3036
  function pageHandler(blocks) {
3010
3037
  for (const block of blocks) {
3011
3038
  if (block.block_type === 1) {
@@ -3014,16 +3041,12 @@ function createTitleFilter(options) {
3014
3041
  }
3015
3042
  switch (state) {
3016
3043
  case "scanning": {
3017
- const level = getHeadingLevel(block);
3018
- if (level !== null) {
3019
- const text = extractHeadingText(block) ?? "";
3020
- seenHeadings.push({
3021
- level,
3022
- text
3023
- });
3024
- if (text === targetTitle) {
3044
+ const info = tracker.push(block);
3045
+ if (info) {
3046
+ seenHeadings.push(info);
3047
+ if (info.text === targetTitle) {
3025
3048
  state = "collecting";
3026
- matchedLevel = level;
3049
+ matchedLevel = info.level;
3027
3050
  collected.push(block);
3028
3051
  }
3029
3052
  }
@@ -3055,6 +3078,81 @@ function createTitleFilter(options) {
3055
3078
  getResult
3056
3079
  };
3057
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
+ }
3058
3156
  //#endregion
3059
3157
  //#region src/converter.ts
3060
3158
  const logger = createLogger("converter");
@@ -3099,18 +3197,11 @@ async function convert(opts) {
3099
3197
  } else {
3100
3198
  const doc = await client.getDocxDocument(docToken);
3101
3199
  let blocks;
3102
- if (opts.filterTitle) {
3103
- const filter = createTitleFilter({ title: opts.filterTitle });
3200
+ const filter = createDocxFilter(opts);
3201
+ if (filter) {
3104
3202
  await client.getDocxBlocks(docToken, filter.pageHandler);
3105
3203
  const result = filter.getResult();
3106
- if (!result.matched) {
3107
- let msg = `No heading matched "${opts.filterTitle}". Please verify the heading text.`;
3108
- if (result.availableHeadings.length > 0) {
3109
- const list = result.availableHeadings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
3110
- msg += `\n\nAvailable headings in the document:\n\n${list}`;
3111
- }
3112
- throw new Error(msg);
3113
- }
3204
+ if (!result.matched) throw new Error(buildFilterErrorMessage(opts, result, opts.url, docToken));
3114
3205
  blocks = result.blocks;
3115
3206
  } else blocks = await client.getDocxBlocks(docToken);
3116
3207
  logger.info(`Fetched ${blocks.length} blocks`);
@@ -3135,7 +3226,70 @@ async function convert(opts) {
3135
3226
  filePath
3136
3227
  };
3137
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
+ }
3138
3292
  //#endregion
3139
- 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 };
3140
3294
 
3141
- //# sourceMappingURL=converter-Bqb4bqxh.js.map
3295
+ //# sourceMappingURL=converter-D1nCzDJF.js.map