lark-docx2md 0.1.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/LICENSE +15 -0
- package/README.md +115 -0
- package/dist/cli-CIsEcoQJ.d.ts +1 -0
- package/dist/cli.js +509 -0
- package/dist/cli.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 byte
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# larkDocx2md
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.org/package/lark-docx2md)
|
|
4
|
+
|
|
5
|
+
将飞书文档转换为 Markdown 文件的命令行工具。
|
|
6
|
+
|
|
7
|
+
> 支持的飞书文档链接格式:https://*.feishu.cn/wiki/*
|
|
8
|
+
|
|
9
|
+
## 使用
|
|
10
|
+
|
|
11
|
+
> 命令所需权限见下 ‘飞书自创应用需要的权限’
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y lark-docx2md download https://xxx.feishu.cn/wiki/xxx --app-id cli_xxx --app-secret xxxx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
或先设置环境变量(命令行参数可省略):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export LARK_DOCX2MD_APP_ID=<APP_ID>
|
|
21
|
+
export LARK_DOCX2MD_APP_SECRET=<APP_SECRET>
|
|
22
|
+
npx -y lark-docx2md download <url>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| 参数 | 说明 |
|
|
26
|
+
|-----------------------|-----------------------------------------------------------|
|
|
27
|
+
| `<url>` | 飞书文档链接 |
|
|
28
|
+
| `--app-id` | 飞书应用 App ID(可选;默认读取 `LARK_DOCX2MD_APP_ID`) |
|
|
29
|
+
| `--app-secret` | 飞书应用 App Secret(可选;默认读取 `LARK_DOCX2MD_APP_SECRET`) |
|
|
30
|
+
| `-o, --output <dir>` | 输出目录(默认:`./larkDocx2mdOutput`) |
|
|
31
|
+
| `--agent` | Agent 模式:日志等级为 ERROR,image-mod 为 `online`,Markdown 输出到标准流 |
|
|
32
|
+
| `--image-mode <mode>` | 图片处理模式:`local`(下载到本地,默认)或 `online`(使用 24h 临时在线链接) |
|
|
33
|
+
|
|
34
|
+
## 功能
|
|
35
|
+
|
|
36
|
+
- 支持飞书 Wiki 文档下载
|
|
37
|
+
- 转换 20+ 种块类型
|
|
38
|
+
- 输出标准 Markdown 文件
|
|
39
|
+
|
|
40
|
+
### 支持的内容块类型
|
|
41
|
+
|
|
42
|
+
| 块类型 | 说明 | Markdown 输出 |
|
|
43
|
+
|---------------------|---------|----------------------------|
|
|
44
|
+
| Page | 页面 | `# 标题` + 子块 |
|
|
45
|
+
| Text | 文本段落 | 纯文本 |
|
|
46
|
+
| Heading1 ~ Heading9 | 1-9 级标题 | `## ~ #########` |
|
|
47
|
+
| Bullet | 无序列表 | `- 内容`(支持嵌套) |
|
|
48
|
+
| Ordered | 有序列表 | `1. 内容`(自动计算序号) |
|
|
49
|
+
| Code | 代码块 | ` ```lang ``` `(支持 67 种语言) |
|
|
50
|
+
| Quote | 引用 | `> 内容` |
|
|
51
|
+
| Equation | 公式 | `$$ 公式 $$` |
|
|
52
|
+
| Todo | 待办事项 | `- [x]` / `- [ ]` |
|
|
53
|
+
| Callout | 高亮块 | `>[!TIP]` + 子块 |
|
|
54
|
+
| Divider | 分割线 | `---` |
|
|
55
|
+
| Image | 图片 | `` |
|
|
56
|
+
| Table / TableCell | 表格 | `<table>` HTML(支持合并单元格) |
|
|
57
|
+
| QuoteContainer | 引用容器 | `> 子块内容` |
|
|
58
|
+
| Grid / GridColumn | 分栏布局 | 展平为子块内容 |
|
|
59
|
+
|
|
60
|
+
### 支持的行内样式
|
|
61
|
+
|
|
62
|
+
| 样式 | Markdown 输出 |
|
|
63
|
+
|------|-------------|
|
|
64
|
+
| 加粗 | `**文本**` |
|
|
65
|
+
| 斜体 | `_文本_` |
|
|
66
|
+
| 删除线 | `~~文本~~` |
|
|
67
|
+
| 下划线 | `<u>文本</u>` |
|
|
68
|
+
| 行内代码 | `` `代码` `` |
|
|
69
|
+
| 链接 | `[文本](url)` |
|
|
70
|
+
| 行内公式 | `$公式$` |
|
|
71
|
+
| @用户 | 用户 ID |
|
|
72
|
+
| @文档 | `[标题](url)` |
|
|
73
|
+
|
|
74
|
+
> 未支持的块类型(如文件附件、视频、内嵌表格、画板等)会被静默忽略。
|
|
75
|
+
|
|
76
|
+
## 开发
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# 直接运行(无需构建)
|
|
80
|
+
pnpm dev download --app-id <APP_ID> --app-secret <APP_SECRET> <url>
|
|
81
|
+
|
|
82
|
+
# 或使用环境变量
|
|
83
|
+
LARK_DOCX2MD_APP_ID=<APP_ID> LARK_DOCX2MD_APP_SECRET=<APP_SECRET> pnpm dev download <url>
|
|
84
|
+
|
|
85
|
+
# 构建为 JS
|
|
86
|
+
pnpm build
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 飞书自创应用需要的权限
|
|
90
|
+
|
|
91
|
+
使用飞书开发平台的权限管理-批量导入/导出权限 导入下面的配置即可。
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"scopes": {
|
|
96
|
+
"tenant": [
|
|
97
|
+
"docs:document.media:download",
|
|
98
|
+
"docx:document:readonly",
|
|
99
|
+
"wiki:node:read"
|
|
100
|
+
],
|
|
101
|
+
"user": [
|
|
102
|
+
"docx:document:readonly",
|
|
103
|
+
"wiki:node:read"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
ISC
|
|
112
|
+
|
|
113
|
+
## 🙏 致谢
|
|
114
|
+
|
|
115
|
+
本项目开发过程中获得了 [LINUX DO](https://linux.do/latest) 社区佬友的帮助,本产品会在社区发布,感谢社区的支持。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
6
|
+
import { LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
7
|
+
//#region src/client.ts
|
|
8
|
+
function createClient(appId, appSecret, loggerLevel = LoggerLevel.warn) {
|
|
9
|
+
const client = new lark.Client({
|
|
10
|
+
appId,
|
|
11
|
+
appSecret,
|
|
12
|
+
loggerLevel
|
|
13
|
+
});
|
|
14
|
+
async function call(name, fn) {
|
|
15
|
+
let res;
|
|
16
|
+
try {
|
|
17
|
+
res = await fn();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
const error = e.response?.data?.error;
|
|
20
|
+
const code = e.response?.data?.code;
|
|
21
|
+
const msg = e.response?.data?.msg;
|
|
22
|
+
if (error) throw new Error(`${name} failed: [${code}] ${msg}: \n${JSON.stringify(error, null, 2)}`);
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
if (res.code !== 0) throw new Error(`${name} failed: [${res.code}] ${res.msg}`);
|
|
26
|
+
return res.data;
|
|
27
|
+
}
|
|
28
|
+
async function getWikiNodeInfo(token) {
|
|
29
|
+
return (await call("getWikiNodeInfo", () => client.wiki.v2.space.getNode({ params: { token } }))).node;
|
|
30
|
+
}
|
|
31
|
+
async function getDocxDocument(docToken) {
|
|
32
|
+
const doc = (await call("getDocxDocument", () => client.docx.v1.document.get({ path: { document_id: docToken } }))).document;
|
|
33
|
+
return {
|
|
34
|
+
documentId: doc.document_id,
|
|
35
|
+
title: doc.title
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function getDocxBlocks(docToken) {
|
|
39
|
+
const blocks = [];
|
|
40
|
+
let pageToken;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const data = await call("getDocxBlocks", () => client.docx.v1.documentBlock.list({
|
|
43
|
+
path: { document_id: docToken },
|
|
44
|
+
params: {
|
|
45
|
+
page_size: 500,
|
|
46
|
+
document_revision_id: -1,
|
|
47
|
+
page_token: pageToken
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
if (data.items) blocks.push(...data.items);
|
|
51
|
+
if (!data.has_more) break;
|
|
52
|
+
pageToken = data.page_token;
|
|
53
|
+
}
|
|
54
|
+
return blocks;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* @param fileTokens 一次最多可传递 5 个素材的 token
|
|
59
|
+
* @return {Record<string, string>} Record<token, downloadLink>
|
|
60
|
+
*/
|
|
61
|
+
async function batchGetTmpDownloadUrl(fileTokens) {
|
|
62
|
+
const list = (await call("batchGetTmpDownloadUrl", () => client.drive.v1.media.batchGetTmpDownloadUrl({ params: { file_tokens: fileTokens } }))).tmp_download_urls ?? [];
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const { file_token, tmp_download_url } of list) result[file_token] = tmp_download_url;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
async function downloadImage(imgToken, outDir) {
|
|
68
|
+
try {
|
|
69
|
+
const resp = await client.drive.v1.media.download({ path: { file_token: imgToken } });
|
|
70
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
71
|
+
const ext = (resp.headers?.["content-type"])?.includes("png") ? ".png" : ".jpg";
|
|
72
|
+
const filename = path.join(outDir, `${imgToken}${ext}`);
|
|
73
|
+
await resp.writeFile(filename);
|
|
74
|
+
return filename;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if ([
|
|
77
|
+
400,
|
|
78
|
+
401,
|
|
79
|
+
403
|
|
80
|
+
].includes(error.status)) throw new Error(`下载图片[${imgToken}]异常, 检查是否有接口 https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download 的权限。`);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
getWikiNodeInfo,
|
|
86
|
+
getDocxDocument,
|
|
87
|
+
getDocxBlocks,
|
|
88
|
+
downloadImage,
|
|
89
|
+
batchGetTmpDownloadUrl
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/parser.ts
|
|
94
|
+
const BlockType = {
|
|
95
|
+
Page: 1,
|
|
96
|
+
Text: 2,
|
|
97
|
+
Heading1: 3,
|
|
98
|
+
Heading2: 4,
|
|
99
|
+
Heading3: 5,
|
|
100
|
+
Heading4: 6,
|
|
101
|
+
Heading5: 7,
|
|
102
|
+
Heading6: 8,
|
|
103
|
+
Heading7: 9,
|
|
104
|
+
Heading8: 10,
|
|
105
|
+
Heading9: 11,
|
|
106
|
+
Bullet: 12,
|
|
107
|
+
Ordered: 13,
|
|
108
|
+
Code: 14,
|
|
109
|
+
Quote: 15,
|
|
110
|
+
Equation: 16,
|
|
111
|
+
Todo: 17,
|
|
112
|
+
Callout: 19,
|
|
113
|
+
Divider: 22,
|
|
114
|
+
Grid: 24,
|
|
115
|
+
GridColumn: 25,
|
|
116
|
+
Image: 27,
|
|
117
|
+
Table: 31,
|
|
118
|
+
TableCell: 32,
|
|
119
|
+
QuoteContainer: 34
|
|
120
|
+
};
|
|
121
|
+
const codeLangMap = {
|
|
122
|
+
1: "",
|
|
123
|
+
2: "abap",
|
|
124
|
+
3: "ada",
|
|
125
|
+
4: "apache",
|
|
126
|
+
5: "apex",
|
|
127
|
+
6: "assembly",
|
|
128
|
+
7: "bash",
|
|
129
|
+
8: "csharp",
|
|
130
|
+
9: "cpp",
|
|
131
|
+
10: "c",
|
|
132
|
+
11: "cobol",
|
|
133
|
+
12: "css",
|
|
134
|
+
13: "coffeescript",
|
|
135
|
+
14: "d",
|
|
136
|
+
15: "dart",
|
|
137
|
+
16: "delphi",
|
|
138
|
+
17: "django",
|
|
139
|
+
18: "dockerfile",
|
|
140
|
+
19: "erlang",
|
|
141
|
+
20: "fortran",
|
|
142
|
+
21: "foxpro",
|
|
143
|
+
22: "go",
|
|
144
|
+
23: "groovy",
|
|
145
|
+
24: "html",
|
|
146
|
+
25: "htmlbars",
|
|
147
|
+
26: "http",
|
|
148
|
+
27: "haskell",
|
|
149
|
+
28: "json",
|
|
150
|
+
29: "java",
|
|
151
|
+
30: "javascript",
|
|
152
|
+
31: "julia",
|
|
153
|
+
32: "kotlin",
|
|
154
|
+
33: "latex",
|
|
155
|
+
34: "lisp",
|
|
156
|
+
35: "logo",
|
|
157
|
+
36: "lua",
|
|
158
|
+
37: "matlab",
|
|
159
|
+
38: "makefile",
|
|
160
|
+
39: "markdown",
|
|
161
|
+
40: "nginx",
|
|
162
|
+
41: "objectivec",
|
|
163
|
+
42: "openedge-abl",
|
|
164
|
+
43: "php",
|
|
165
|
+
44: "perl",
|
|
166
|
+
45: "postscript",
|
|
167
|
+
46: "powershell",
|
|
168
|
+
47: "prolog",
|
|
169
|
+
48: "protobuf",
|
|
170
|
+
49: "python",
|
|
171
|
+
50: "r",
|
|
172
|
+
51: "rpg",
|
|
173
|
+
52: "ruby",
|
|
174
|
+
53: "rust",
|
|
175
|
+
54: "sas",
|
|
176
|
+
55: "scss",
|
|
177
|
+
56: "sql",
|
|
178
|
+
57: "scala",
|
|
179
|
+
58: "scheme",
|
|
180
|
+
59: "scratch",
|
|
181
|
+
60: "shell",
|
|
182
|
+
61: "swift",
|
|
183
|
+
62: "thrift",
|
|
184
|
+
63: "typescript",
|
|
185
|
+
64: "vbscript",
|
|
186
|
+
65: "vbnet",
|
|
187
|
+
66: "xml",
|
|
188
|
+
67: "yaml"
|
|
189
|
+
};
|
|
190
|
+
var Parser = class {
|
|
191
|
+
constructor() {
|
|
192
|
+
this.imgTokens = [];
|
|
193
|
+
this.blockMap = /* @__PURE__ */ new Map();
|
|
194
|
+
}
|
|
195
|
+
parseDocxContent(doc, blocks) {
|
|
196
|
+
for (const b of blocks) if (b.block_id) this.blockMap.set(b.block_id, b);
|
|
197
|
+
const entry = this.blockMap.get(doc.documentId);
|
|
198
|
+
if (!entry) return "";
|
|
199
|
+
return this.parseBlock(entry, 0);
|
|
200
|
+
}
|
|
201
|
+
parseBlock(b, indent) {
|
|
202
|
+
const prefix = " ".repeat(indent);
|
|
203
|
+
const bt = b.block_type;
|
|
204
|
+
if (bt === BlockType.Page) return this.parsePage(b);
|
|
205
|
+
if (bt === BlockType.Text) return prefix + this.parseText(b.text) + "\n";
|
|
206
|
+
if (bt >= BlockType.Heading1 && bt <= BlockType.Heading9) return prefix + this.parseHeading(b, bt - 2);
|
|
207
|
+
if (bt === BlockType.Bullet) return prefix + this.parseBullet(b, indent);
|
|
208
|
+
if (bt === BlockType.Ordered) return prefix + this.parseOrdered(b, indent);
|
|
209
|
+
if (bt === BlockType.Code) return prefix + this.parseCode(b);
|
|
210
|
+
if (bt === BlockType.Quote) return prefix + "> " + this.parseText(b.quote) + "\n";
|
|
211
|
+
if (bt === BlockType.Equation) return prefix + "$$\n" + this.parseText(b.equation) + "$$\n\n";
|
|
212
|
+
if (bt === BlockType.Todo) return prefix + this.parseTodo(b);
|
|
213
|
+
if (bt === BlockType.Callout) return this.parseCallout(b);
|
|
214
|
+
if (bt === BlockType.Divider) return prefix + "---\n\n";
|
|
215
|
+
if (bt === BlockType.Image) return prefix + this.parseImage(b) + "\n";
|
|
216
|
+
if (bt === BlockType.Table) return prefix + this.parseTable(b);
|
|
217
|
+
if (bt === BlockType.TableCell) return this.parseTableCell(b);
|
|
218
|
+
if (bt === BlockType.QuoteContainer) return this.parseQuoteContainer(b);
|
|
219
|
+
if (bt === BlockType.Grid) return this.parseGrid(b, indent);
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
parsePage(b) {
|
|
223
|
+
let s = "# " + this.parseText(b.page) + "\n";
|
|
224
|
+
for (const id of b.children ?? []) {
|
|
225
|
+
const child = this.blockMap.get(id);
|
|
226
|
+
if (child) s += this.parseBlock(child, 0) + "\n";
|
|
227
|
+
}
|
|
228
|
+
return s;
|
|
229
|
+
}
|
|
230
|
+
parseText(body) {
|
|
231
|
+
const inline = body.elements.length > 1;
|
|
232
|
+
return body.elements.map((e) => this.parseElement(e, inline)).join("") + "\n";
|
|
233
|
+
}
|
|
234
|
+
parseElement(e, inline) {
|
|
235
|
+
if (e.text_run) return this.parseTextRun(e.text_run);
|
|
236
|
+
if (e.mention_user) return e.mention_user.user_id;
|
|
237
|
+
if (e.mention_doc) {
|
|
238
|
+
const url = e.mention_doc.url ? decodeURIComponent(e.mention_doc.url) : "";
|
|
239
|
+
return `[${e.mention_doc.title ?? ""}](${url})`;
|
|
240
|
+
}
|
|
241
|
+
if (e.equation) {
|
|
242
|
+
const sym = inline ? "$" : "$$";
|
|
243
|
+
return sym + e.equation.content.replace(/\n$/, "") + sym;
|
|
244
|
+
}
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
parseTextRun(tr) {
|
|
248
|
+
const s = tr.text_element_style;
|
|
249
|
+
let pre = "", post = "";
|
|
250
|
+
if (s) {
|
|
251
|
+
if (s.bold) {
|
|
252
|
+
pre = "**";
|
|
253
|
+
post = "**";
|
|
254
|
+
} else if (s.italic) {
|
|
255
|
+
pre = "_";
|
|
256
|
+
post = "_";
|
|
257
|
+
} else if (s.strikethrough) {
|
|
258
|
+
pre = "~~";
|
|
259
|
+
post = "~~";
|
|
260
|
+
} else if (s.underline) {
|
|
261
|
+
pre = "<u>";
|
|
262
|
+
post = "</u>";
|
|
263
|
+
} else if (s.inline_code) {
|
|
264
|
+
pre = "`";
|
|
265
|
+
post = "`";
|
|
266
|
+
} else if (s.link) {
|
|
267
|
+
pre = "[";
|
|
268
|
+
post = `](${decodeURIComponent(s.link.url)})`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return pre + tr.content + post;
|
|
272
|
+
}
|
|
273
|
+
parseHeading(b, level) {
|
|
274
|
+
const body = b[`heading${level}`];
|
|
275
|
+
let s = "#".repeat(level) + " " + (body ? this.parseText(body) : "\n");
|
|
276
|
+
for (const id of b.children ?? []) {
|
|
277
|
+
const child = this.blockMap.get(id);
|
|
278
|
+
if (child) s += this.parseBlock(child, 0);
|
|
279
|
+
}
|
|
280
|
+
return s;
|
|
281
|
+
}
|
|
282
|
+
parseBullet(b, indent) {
|
|
283
|
+
let s = "- " + this.parseText(b.bullet);
|
|
284
|
+
for (const id of b.children ?? []) {
|
|
285
|
+
const child = this.blockMap.get(id);
|
|
286
|
+
if (child) s += this.parseBlock(child, indent + 1);
|
|
287
|
+
}
|
|
288
|
+
return s;
|
|
289
|
+
}
|
|
290
|
+
parseOrdered(b, indent) {
|
|
291
|
+
const parent = this.blockMap.get(b.parent_id);
|
|
292
|
+
let order = 1;
|
|
293
|
+
if (parent?.children) {
|
|
294
|
+
const idx = parent.children.indexOf(b.block_id);
|
|
295
|
+
for (let i = idx - 1; i >= 0; i--) if (this.blockMap.get(parent.children[i])?.block_type === BlockType.Ordered) order++;
|
|
296
|
+
else break;
|
|
297
|
+
}
|
|
298
|
+
let s = `${order}. ` + this.parseText(b.ordered);
|
|
299
|
+
for (const id of b.children ?? []) {
|
|
300
|
+
const child = this.blockMap.get(id);
|
|
301
|
+
if (child) s += this.parseBlock(child, indent + 1);
|
|
302
|
+
}
|
|
303
|
+
return s;
|
|
304
|
+
}
|
|
305
|
+
parseCode(b) {
|
|
306
|
+
const lang = codeLangMap[b.code?.style?.language ?? 1] ?? "";
|
|
307
|
+
const text = this.parseText(b.code).trim();
|
|
308
|
+
return "```" + lang + "\n" + text + "\n```\n";
|
|
309
|
+
}
|
|
310
|
+
parseTodo(b) {
|
|
311
|
+
return `- [${b.todo?.style?.done ? "x" : " "}] ` + this.parseText(b.todo) + "\n";
|
|
312
|
+
}
|
|
313
|
+
parseCallout(b) {
|
|
314
|
+
let s = ">[!TIP] \n";
|
|
315
|
+
for (const id of b.children ?? []) {
|
|
316
|
+
const child = this.blockMap.get(id);
|
|
317
|
+
if (child) s += this.parseBlock(child, 0);
|
|
318
|
+
}
|
|
319
|
+
return s;
|
|
320
|
+
}
|
|
321
|
+
parseImage(b) {
|
|
322
|
+
const token = b.image?.token;
|
|
323
|
+
if (token) {
|
|
324
|
+
this.imgTokens.push(token);
|
|
325
|
+
return `\n`;
|
|
326
|
+
}
|
|
327
|
+
return "";
|
|
328
|
+
}
|
|
329
|
+
parseTableCell(b) {
|
|
330
|
+
let s = "";
|
|
331
|
+
for (const id of b.children ?? []) {
|
|
332
|
+
const child = this.blockMap.get(id);
|
|
333
|
+
if (child) s += this.parseBlock(child, 0).replace(/\n/g, "") + "<br/>";
|
|
334
|
+
}
|
|
335
|
+
return s;
|
|
336
|
+
}
|
|
337
|
+
parseTable(b) {
|
|
338
|
+
const t = b.table;
|
|
339
|
+
const cols = t.property.column_size;
|
|
340
|
+
const rows = [];
|
|
341
|
+
const mergeInfos = t.property.merge_info ?? [];
|
|
342
|
+
for (let i = 0; i < (t.cells?.length ?? 0); i++) {
|
|
343
|
+
const cellId = t.cells[i];
|
|
344
|
+
const cell = this.blockMap.get(cellId);
|
|
345
|
+
const content = cell ? this.parseBlock(cell, 0).replace(/\n/g, "") : "";
|
|
346
|
+
const row = Math.floor(i / cols);
|
|
347
|
+
const col = i % cols;
|
|
348
|
+
if (!rows[row]) rows[row] = [];
|
|
349
|
+
rows[row][col] = content;
|
|
350
|
+
}
|
|
351
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
352
|
+
for (let i = 0; i < mergeInfos.length; i++) {
|
|
353
|
+
const m = mergeInfos[i];
|
|
354
|
+
const row = Math.floor(i / cols);
|
|
355
|
+
const col = i % cols;
|
|
356
|
+
mergeMap.set(`${row}-${col}`, {
|
|
357
|
+
rowSpan: m.row_span ?? 1,
|
|
358
|
+
colSpan: m.col_span ?? 1
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const processed = /* @__PURE__ */ new Set();
|
|
362
|
+
let buf = "<table>\n";
|
|
363
|
+
for (let r = 0; r < rows.length; r++) {
|
|
364
|
+
buf += "<tr>\n";
|
|
365
|
+
for (let c = 0; c < (rows[r]?.length ?? 0); c++) {
|
|
366
|
+
const key = `${r}-${c}`;
|
|
367
|
+
if (processed.has(key)) continue;
|
|
368
|
+
const merge = mergeMap.get(key);
|
|
369
|
+
let attrs = "";
|
|
370
|
+
if (merge) {
|
|
371
|
+
if (merge.rowSpan > 1) attrs += ` rowspan="${merge.rowSpan}"`;
|
|
372
|
+
if (merge.colSpan > 1) attrs += ` colspan="${merge.colSpan}"`;
|
|
373
|
+
for (let mr = r; mr < r + merge.rowSpan; mr++) for (let mc = c; mc < c + merge.colSpan; mc++) processed.add(`${mr}-${mc}`);
|
|
374
|
+
}
|
|
375
|
+
buf += `<td${attrs}>${rows[r][c] ?? ""}</td>`;
|
|
376
|
+
}
|
|
377
|
+
buf += "</tr>\n";
|
|
378
|
+
}
|
|
379
|
+
buf += "</table>\n";
|
|
380
|
+
return buf;
|
|
381
|
+
}
|
|
382
|
+
parseQuoteContainer(b) {
|
|
383
|
+
let s = "";
|
|
384
|
+
for (const id of b.children ?? []) {
|
|
385
|
+
const child = this.blockMap.get(id);
|
|
386
|
+
if (child) s += "> " + this.parseBlock(child, 0);
|
|
387
|
+
}
|
|
388
|
+
return s;
|
|
389
|
+
}
|
|
390
|
+
parseGrid(b, indent) {
|
|
391
|
+
let s = "";
|
|
392
|
+
for (const colId of b.children ?? []) {
|
|
393
|
+
const col = this.blockMap.get(colId);
|
|
394
|
+
if (!col) continue;
|
|
395
|
+
for (const id of col.children ?? []) {
|
|
396
|
+
const child = this.blockMap.get(id);
|
|
397
|
+
if (child) s += this.parseBlock(child, indent);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return s;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/logger.ts
|
|
405
|
+
const COLORS = {
|
|
406
|
+
[LoggerLevel.fatal]: "\x1B[35m",
|
|
407
|
+
[LoggerLevel.error]: "\x1B[31m",
|
|
408
|
+
[LoggerLevel.warn]: "\x1B[33m",
|
|
409
|
+
[LoggerLevel.info]: "\x1B[36m",
|
|
410
|
+
[LoggerLevel.debug]: "\x1B[32m",
|
|
411
|
+
[LoggerLevel.trace]: "\x1B[90m"
|
|
412
|
+
};
|
|
413
|
+
const RESET = "\x1B[0m";
|
|
414
|
+
const LEVEL_NAMES = [
|
|
415
|
+
"FATAL",
|
|
416
|
+
"ERROR",
|
|
417
|
+
"WARN",
|
|
418
|
+
"INFO",
|
|
419
|
+
"DEBUG",
|
|
420
|
+
"TRACE"
|
|
421
|
+
];
|
|
422
|
+
let minLogLevel = LoggerLevel.trace;
|
|
423
|
+
function log(level, module, ...args) {
|
|
424
|
+
if (level > minLogLevel) return;
|
|
425
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
426
|
+
const color = COLORS[level] ?? "";
|
|
427
|
+
const name = LEVEL_NAMES[level] ?? "INFO";
|
|
428
|
+
process.stderr.write(`${time} ${color}[${name}]${RESET} [${module}] ${args.map(String).join(" ")}\n`);
|
|
429
|
+
}
|
|
430
|
+
function setLogLevel(level) {
|
|
431
|
+
minLogLevel = level;
|
|
432
|
+
}
|
|
433
|
+
function createLogger(module) {
|
|
434
|
+
return {
|
|
435
|
+
fatal: (...args) => log(LoggerLevel.fatal, module, ...args),
|
|
436
|
+
error: (...args) => log(LoggerLevel.error, module, ...args),
|
|
437
|
+
warn: (...args) => log(LoggerLevel.warn, module, ...args),
|
|
438
|
+
info: (...args) => log(LoggerLevel.info, module, ...args),
|
|
439
|
+
debug: (...args) => log(LoggerLevel.debug, module, ...args),
|
|
440
|
+
trace: (...args) => log(LoggerLevel.trace, module, ...args)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/cli.ts
|
|
445
|
+
const logger = createLogger("cli");
|
|
446
|
+
function parseWikiUrl(url) {
|
|
447
|
+
const m = url.match(/^https:\/\/[\w.-]+\/(docs|docx|wiki)\/([a-zA-Z0-9]+)/);
|
|
448
|
+
if (!m) throw new Error("Invalid feishu document URL");
|
|
449
|
+
return {
|
|
450
|
+
docType: m[1],
|
|
451
|
+
docToken: m[2]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const program = new Command();
|
|
455
|
+
program.name("larkDocx2md").description("Download Lark/Feishu documents to markdown");
|
|
456
|
+
program.command("download").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", "./larkDocx2mdOutput").option("--agent", "Enable agent mode: ERROR log level, and AI prompt output").option("--image-mode <mode>", "Image handling mode: \"local\" (download) or \"online\" (temp URL)", "local").argument("<url>", "Feishu wiki document URL: https://*.feishu.cn/wiki/*").action(async (url, opts) => {
|
|
457
|
+
if (opts.agent) {
|
|
458
|
+
setLogLevel(LoggerLevel.error);
|
|
459
|
+
opts.imageMode = "online";
|
|
460
|
+
} else if (opts.imageMode && !["local", "online"].includes(opts.imageMode)) program.error(`Invalid --image-mode "${opts.imageMode}", must be "local" or "online"`);
|
|
461
|
+
const { docType, docToken: rawToken } = parseWikiUrl(url);
|
|
462
|
+
logger.info("Captured document token:", rawToken);
|
|
463
|
+
const appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID;
|
|
464
|
+
const appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET;
|
|
465
|
+
if (!appId || !appSecret) program.error("Missing credentials: pass --app-id/--app-secret or set LARK_DOCX2MD_APP_ID/LARK_DOCX2MD_APP_SECRET");
|
|
466
|
+
const client = createClient(appId, appSecret, opts.agent ? LoggerLevel.error : LoggerLevel.warn);
|
|
467
|
+
let docToken = rawToken;
|
|
468
|
+
if (docType === "wiki") {
|
|
469
|
+
docToken = (await client.getWikiNodeInfo(docToken)).obj_token;
|
|
470
|
+
logger.info("Resolved docx token:", docToken);
|
|
471
|
+
}
|
|
472
|
+
const doc = await client.getDocxDocument(docToken);
|
|
473
|
+
const blocks = await client.getDocxBlocks(docToken);
|
|
474
|
+
logger.info(`Fetched ${blocks.length} blocks`);
|
|
475
|
+
const parser = new Parser();
|
|
476
|
+
let markdown = parser.parseDocxContent(doc, blocks);
|
|
477
|
+
if (opts.imageMode === "online") for (let i = 0; i < parser.imgTokens.length; i += 5) {
|
|
478
|
+
const batch = parser.imgTokens.slice(i, i + 5);
|
|
479
|
+
const urlMap = await client.batchGetTmpDownloadUrl(batch);
|
|
480
|
+
for (const token of batch) {
|
|
481
|
+
const onlineUrl = urlMap[token];
|
|
482
|
+
if (onlineUrl) {
|
|
483
|
+
markdown = markdown.replace(`(${token})`, `(${onlineUrl})`);
|
|
484
|
+
logger.info("Replaced image with online URL:", token);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
const imgDir = path.join(opts.output, "static");
|
|
490
|
+
for (const imgToken of parser.imgTokens) {
|
|
491
|
+
let localPath = await client.downloadImage(imgToken, imgDir);
|
|
492
|
+
localPath = path.relative(opts.output, localPath);
|
|
493
|
+
markdown = markdown.replace(`(${imgToken})`, `(${localPath})`);
|
|
494
|
+
logger.info("Downloaded image:", localPath);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (opts.agent) process.stdout.write(markdown);
|
|
498
|
+
else {
|
|
499
|
+
fs.mkdirSync(opts.output, { recursive: true });
|
|
500
|
+
const mdPath = path.join(opts.output, `${docToken}.md`);
|
|
501
|
+
fs.writeFileSync(mdPath, markdown);
|
|
502
|
+
logger.info("Downloaded markdown file to", mdPath);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
program.parse();
|
|
506
|
+
//#endregion
|
|
507
|
+
export {};
|
|
508
|
+
|
|
509
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/client.ts","../src/parser.ts","../src/logger.ts","../src/cli.ts"],"sourcesContent":["import * as lark from '@larksuiteoapi/node-sdk';\nimport { LoggerLevel } from '@larksuiteoapi/node-sdk';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nexport type DocxBlock = NonNullable<NonNullable<\n Awaited<ReturnType<lark.Client['docx']['v1']['documentBlock']['list']>>['data']\n>['items']>[number];\n\nexport type TextBody = NonNullable<DocxBlock['text']>;\nexport type TextElement = TextBody['elements'][number];\n\nexport interface DocInfo {\n documentId: string;\n title: string;\n}\n\nexport function createClient (appId: string, appSecret: string, loggerLevel: LoggerLevel = LoggerLevel.warn) {\n const client = new lark.Client({ appId, appSecret, loggerLevel });\n\n async function call<T> (name: string, fn: () => Promise<{ code?: number; msg?: string; data?: T }>): Promise<T> {\n let res;\n try {\n res = await fn();\n } catch (e: any) {\n const error = e.response?.data?.error;\n const code = e.response?.data?.code;\n const msg = e.response?.data?.msg;\n if (error) {\n throw new Error(`${name} failed: [${code}] ${msg}: \\n${JSON.stringify(error, null, 2)}`);\n }\n throw e;\n }\n if (res.code !== 0) {\n throw new Error(`${name} failed: [${res.code}] ${res.msg}`);\n }\n return res.data!;\n }\n\n async function getWikiNodeInfo (token: string) {\n const data = await call('getWikiNodeInfo', () =>\n client.wiki.v2.space.getNode({ params: { token } }),\n );\n return data.node!;\n }\n\n async function getDocxDocument (docToken: string): Promise<DocInfo> {\n const data = await call('getDocxDocument', () =>\n client.docx.v1.document.get({ path: { document_id: docToken } }),\n );\n const doc = data.document!;\n return { documentId: doc.document_id!, title: doc.title! };\n }\n\n async function getDocxBlocks (docToken: string): Promise<DocxBlock[]> {\n const blocks: DocxBlock[] = [];\n let pageToken: string | undefined;\n for (; ;) {\n const data = await call('getDocxBlocks', () =>\n client.docx.v1.documentBlock.list({\n path: { document_id: docToken },\n params: { page_size: 500, document_revision_id: -1, page_token: pageToken },\n }),\n );\n if (data.items) blocks.push(...data.items);\n if (!data.has_more) break;\n pageToken = data.page_token;\n }\n return blocks;\n }\n\n /**\n *\n * @param fileTokens 一次最多可传递 5 个素材的 token\n * @return {Record<string, string>} Record<token, downloadLink>\n */\n async function batchGetTmpDownloadUrl (fileTokens: string[]): Promise<Record<string, string>> {\n const data = await call('batchGetTmpDownloadUrl', () =>\n client.drive.v1.media.batchGetTmpDownloadUrl({ params: { file_tokens: fileTokens } }),\n );\n const list = data.tmp_download_urls ?? [];\n const result: Record<string, string> = {};\n for (const { file_token, tmp_download_url } of list) {\n result[file_token] = tmp_download_url;\n }\n return result;\n }\n\n async function downloadImage (imgToken: string, outDir: string): Promise<string> {\n try {\n const resp = await client.drive.v1.media.download({ path: { file_token: imgToken } });\n fs.mkdirSync(outDir, { recursive: true });\n const ext = (resp.headers?.['content-type'] as string)?.includes('png') ? '.png' : '.jpg';\n const filename = path.join(outDir, `${imgToken}${ext}`);\n await resp.writeFile(filename);\n return filename;\n } catch (error: any) {\n if ([400, 401, 403].includes(error.status)) {\n throw new Error(`下载图片[${imgToken}]异常, 检查是否有接口 https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download 的权限。`);\n }\n throw error;\n }\n }\n\n return { getWikiNodeInfo, getDocxDocument, getDocxBlocks, downloadImage, batchGetTmpDownloadUrl };\n}\n\nexport type LarkClient = ReturnType<typeof createClient>;\n","import type { DocInfo, DocxBlock, TextBody, TextElement } from './client.js';\n\nconst BlockType = {\n Page: 1, Text: 2,\n Heading1: 3, Heading2: 4, Heading3: 5, Heading4: 6, Heading5: 7, Heading6: 8,\n Heading7: 9, Heading8: 10, Heading9: 11,\n Bullet: 12, Ordered: 13, Code: 14, Quote: 15, Equation: 16, Todo: 17,\n Callout: 19, Divider: 22, Grid: 24, GridColumn: 25, Image: 27,\n Table: 31, TableCell: 32, QuoteContainer: 34,\n} as const;\n\nconst codeLangMap: Record<number, string> = {\n 1: '', 2: 'abap', 3: 'ada', 4: 'apache', 5: 'apex', 6: 'assembly', 7: 'bash', 8: 'csharp',\n 9: 'cpp', 10: 'c', 11: 'cobol', 12: 'css', 13: 'coffeescript', 14: 'd', 15: 'dart',\n 16: 'delphi', 17: 'django', 18: 'dockerfile', 19: 'erlang', 20: 'fortran', 21: 'foxpro',\n 22: 'go', 23: 'groovy', 24: 'html', 25: 'htmlbars', 26: 'http', 27: 'haskell', 28: 'json',\n 29: 'java', 30: 'javascript', 31: 'julia', 32: 'kotlin', 33: 'latex', 34: 'lisp',\n 35: 'logo', 36: 'lua', 37: 'matlab', 38: 'makefile', 39: 'markdown', 40: 'nginx',\n 41: 'objectivec', 42: 'openedge-abl', 43: 'php', 44: 'perl', 45: 'postscript',\n 46: 'powershell', 47: 'prolog', 48: 'protobuf', 49: 'python', 50: 'r', 51: 'rpg',\n 52: 'ruby', 53: 'rust', 54: 'sas', 55: 'scss', 56: 'sql', 57: 'scala', 58: 'scheme',\n 59: 'scratch', 60: 'shell', 61: 'swift', 62: 'thrift', 63: 'typescript', 64: 'vbscript',\n 65: 'vbnet', 66: 'xml', 67: 'yaml',\n};\n\nexport class Parser {\n imgTokens: string[] = [];\n private blockMap = new Map<string, DocxBlock>();\n\n parseDocxContent (doc: DocInfo, blocks: DocxBlock[]): string {\n for (const b of blocks) {\n if (b.block_id) this.blockMap.set(b.block_id, b);\n }\n const entry = this.blockMap.get(doc.documentId);\n if (!entry) return '';\n return this.parseBlock(entry, 0);\n }\n\n private parseBlock (b: DocxBlock, indent: number): string {\n const prefix = '\\t'.repeat(indent);\n const bt = b.block_type;\n\n if (bt === BlockType.Page) return this.parsePage(b);\n if (bt === BlockType.Text) return prefix + this.parseText(b.text!) + '\\n';\n if (bt >= BlockType.Heading1 && bt <= BlockType.Heading9) return prefix + this.parseHeading(b, bt - 2);\n if (bt === BlockType.Bullet) return prefix + this.parseBullet(b, indent);\n if (bt === BlockType.Ordered) return prefix + this.parseOrdered(b, indent);\n if (bt === BlockType.Code) return prefix + this.parseCode(b);\n if (bt === BlockType.Quote) return prefix + '> ' + this.parseText(b.quote!) + '\\n';\n if (bt === BlockType.Equation) return prefix + '$$\\n' + this.parseText(b.equation!) + '$$\\n\\n';\n if (bt === BlockType.Todo) return prefix + this.parseTodo(b);\n if (bt === BlockType.Callout) return this.parseCallout(b);\n if (bt === BlockType.Divider) return prefix + '---\\n\\n';\n if (bt === BlockType.Image) return prefix + this.parseImage(b) + '\\n';\n if (bt === BlockType.Table) return prefix + this.parseTable(b);\n if (bt === BlockType.TableCell) return this.parseTableCell(b);\n if (bt === BlockType.QuoteContainer) return this.parseQuoteContainer(b);\n if (bt === BlockType.Grid) return this.parseGrid(b, indent);\n\n return '';\n }\n\n private parsePage (b: DocxBlock): string {\n let s = '# ' + this.parseText(b.page!) + '\\n';\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, 0) + '\\n';\n }\n return s;\n }\n\n private parseText (body: TextBody): string {\n const inline = body.elements.length > 1;\n return body.elements.map(e => this.parseElement(e, inline)).join('') + '\\n';\n }\n\n private parseElement (e: TextElement, inline: boolean): string {\n if (e.text_run) return this.parseTextRun(e.text_run);\n if (e.mention_user) return e.mention_user.user_id;\n if (e.mention_doc) {\n const url = e.mention_doc.url ? decodeURIComponent(e.mention_doc.url) : '';\n return `[${e.mention_doc.title ?? ''}](${url})`;\n }\n if (e.equation) {\n const sym = inline ? '$' : '$$';\n return sym + e.equation.content.replace(/\\n$/, '') + sym;\n }\n return '';\n }\n\n private parseTextRun (tr: NonNullable<TextElement['text_run']>): string {\n const s = tr.text_element_style;\n let pre = '', post = '';\n if (s) {\n if (s.bold) {\n pre = '**';\n post = '**';\n } else if (s.italic) {\n pre = '_';\n post = '_';\n } else if (s.strikethrough) {\n pre = '~~';\n post = '~~';\n } else if (s.underline) {\n pre = '<u>';\n post = '</u>';\n } else if (s.inline_code) {\n pre = '`';\n post = '`';\n } else if (s.link) {\n pre = '[';\n post = `](${decodeURIComponent(s.link.url)})`;\n }\n }\n return pre + tr.content + post;\n }\n\n private parseHeading (b: DocxBlock, level: number): string {\n const headingKey = `heading${level}` as keyof DocxBlock;\n const body = b[headingKey] as TextBody | undefined;\n let s = '#'.repeat(level) + ' ' + (body ? this.parseText(body) : '\\n');\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, 0);\n }\n return s;\n }\n\n private parseBullet (b: DocxBlock, indent: number): string {\n let s = '- ' + this.parseText(b.bullet!);\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, indent + 1);\n }\n return s;\n }\n\n private parseOrdered (b: DocxBlock, indent: number): string {\n const parent = this.blockMap.get(b.parent_id!);\n let order = 1;\n if (parent?.children) {\n const idx = parent.children.indexOf(b.block_id!);\n for (let i = idx - 1; i >= 0; i--) {\n const sib = this.blockMap.get(parent.children[i]!);\n if (sib?.block_type === BlockType.Ordered) order++;\n else break;\n }\n }\n let s = `${order}. ` + this.parseText(b.ordered!);\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, indent + 1);\n }\n return s;\n }\n\n private parseCode (b: DocxBlock): string {\n const lang = codeLangMap[b.code?.style?.language ?? 1] ?? '';\n const text = this.parseText(b.code!).trim();\n return '```' + lang + '\\n' + text + '\\n```\\n';\n }\n\n private parseTodo (b: DocxBlock): string {\n const checked = b.todo?.style?.done ? 'x' : ' ';\n return `- [${checked}] ` + this.parseText(b.todo!) + '\\n';\n }\n\n private parseCallout (b: DocxBlock): string {\n let s = '>[!TIP] \\n';\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, 0);\n }\n return s;\n }\n\n private parseImage (b: DocxBlock): string {\n const token = b.image?.token;\n if (token) {\n this.imgTokens.push(token);\n return `\\n`;\n }\n return '';\n }\n\n private parseTableCell (b: DocxBlock): string {\n let s = '';\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, 0).replace(/\\n/g, '') + '<br/>';\n }\n return s;\n }\n\n private parseTable (b: DocxBlock): string {\n const t = b.table!;\n const cols = t.property.column_size;\n const rows: string[][] = [];\n const mergeInfos = t.property.merge_info ?? [];\n\n for (let i = 0; i < (t.cells?.length ?? 0); i++) {\n const cellId = t.cells![i]!;\n const cell = this.blockMap.get(cellId);\n const content = cell ? this.parseBlock(cell, 0).replace(/\\n/g, '') : '';\n const row = Math.floor(i / cols);\n const col = i % cols;\n if (!rows[row]) rows[row] = [];\n rows[row]![col] = content;\n }\n\n const mergeMap = new Map<string, { rowSpan: number; colSpan: number }>();\n for (let i = 0; i < mergeInfos.length; i++) {\n const m = mergeInfos[i]!;\n const row = Math.floor(i / cols);\n const col = i % cols;\n mergeMap.set(`${row}-${col}`, { rowSpan: m.row_span ?? 1, colSpan: m.col_span ?? 1 });\n }\n\n const processed = new Set<string>();\n let buf = '<table>\\n';\n for (let r = 0; r < rows.length; r++) {\n buf += '<tr>\\n';\n for (let c = 0; c < (rows[r]?.length ?? 0); c++) {\n const key = `${r}-${c}`;\n if (processed.has(key)) continue;\n const merge = mergeMap.get(key);\n let attrs = '';\n if (merge) {\n if (merge.rowSpan > 1) attrs += ` rowspan=\"${merge.rowSpan}\"`;\n if (merge.colSpan > 1) attrs += ` colspan=\"${merge.colSpan}\"`;\n for (let mr = r; mr < r + merge.rowSpan; mr++) {\n for (let mc = c; mc < c + merge.colSpan; mc++) {\n processed.add(`${mr}-${mc}`);\n }\n }\n }\n buf += `<td${attrs}>${rows[r]![c] ?? ''}</td>`;\n }\n buf += '</tr>\\n';\n }\n buf += '</table>\\n';\n return buf;\n }\n\n private parseQuoteContainer (b: DocxBlock): string {\n let s = '';\n for (const id of b.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += '> ' + this.parseBlock(child, 0);\n }\n return s;\n }\n\n private parseGrid (b: DocxBlock, indent: number): string {\n let s = '';\n for (const colId of b.children ?? []) {\n const col = this.blockMap.get(colId);\n if (!col) continue;\n for (const id of col.children ?? []) {\n const child = this.blockMap.get(id);\n if (child) s += this.parseBlock(child, indent);\n }\n }\n return s;\n }\n}\n","import { LoggerLevel } from '@larksuiteoapi/node-sdk';\n\nconst COLORS: Record<number, string> = {\n [LoggerLevel.fatal]: '\\x1b[35m', // magenta\n [LoggerLevel.error]: '\\x1b[31m', // red\n [LoggerLevel.warn]: '\\x1b[33m', // yellow\n [LoggerLevel.info]: '\\x1b[36m', // cyan\n [LoggerLevel.debug]: '\\x1b[32m', // green\n [LoggerLevel.trace]: '\\x1b[90m', // gray\n};\nconst RESET = '\\x1b[0m';\nconst LEVEL_NAMES = ['FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'];\nlet minLogLevel = LoggerLevel.trace;\n\nfunction log (level: LoggerLevel, module: string, ...args: unknown[]) {\n if (level > minLogLevel) return;\n const time = new Date().toISOString();\n const color = COLORS[level] ?? '';\n const name = LEVEL_NAMES[level] ?? 'INFO';\n process.stderr.write(`${time} ${color}[${name}]${RESET} [${module}] ${args.map(String).join(' ')}\\n`);\n}\n\nexport function setLogLevel (level: LoggerLevel) {\n minLogLevel = level;\n}\n\nexport function createLogger (module: string) {\n return {\n fatal: (...args: unknown[]) => log(LoggerLevel.fatal, module, ...args),\n error: (...args: unknown[]) => log(LoggerLevel.error, module, ...args),\n warn: (...args: unknown[]) => log(LoggerLevel.warn, module, ...args),\n info: (...args: unknown[]) => log(LoggerLevel.info, module, ...args),\n debug: (...args: unknown[]) => log(LoggerLevel.debug, module, ...args),\n trace: (...args: unknown[]) => log(LoggerLevel.trace, module, ...args),\n };\n}\n","#!/usr/bin/env node\nimport { Command } from 'commander';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { LoggerLevel } from '@larksuiteoapi/node-sdk';\nimport { createClient } from './client.js';\nimport { Parser } from './parser.js';\nimport { createLogger, setLogLevel } from './logger.js';\n\nconst logger = createLogger('cli');\n\nfunction parseWikiUrl (url: string): { docType: string; docToken: string } {\n const m = url.match(/^https:\\/\\/[\\w.-]+\\/(docs|docx|wiki)\\/([a-zA-Z0-9]+)/);\n if (!m) throw new Error('Invalid feishu document URL');\n return { docType: m[1]!, docToken: m[2]! };\n}\n\nconst program = new Command();\nprogram.name('larkDocx2md').description('Download Lark/Feishu documents to markdown');\n\nprogram\n .command('download')\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', './larkDocx2mdOutput')\n .option('--agent', 'Enable agent mode: ERROR log level, and AI prompt output')\n .option('--image-mode <mode>', 'Image handling mode: \"local\" (download) or \"online\" (temp URL)', 'local')\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; imageMode: string }) => {\n if (opts.agent) {\n setLogLevel(LoggerLevel.error);\n opts.imageMode = 'online';\n } else if (opts.imageMode && !['local', 'online'].includes(opts.imageMode)) {\n program.error(`Invalid --image-mode \"${opts.imageMode}\", must be \"local\" or \"online\"`);\n }\n\n const { docType, docToken: rawToken } = parseWikiUrl(url);\n logger.info('Captured document token:', rawToken);\n\n const appId = opts.appId ?? process.env.LARK_DOCX2MD_APP_ID!;\n const appSecret = opts.appSecret ?? process.env.LARK_DOCX2MD_APP_SECRET!;\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 sdkLoggerLevel = opts.agent ? LoggerLevel.error : LoggerLevel.warn;\n const client = createClient(appId, appSecret, sdkLoggerLevel);\n let docToken = rawToken;\n\n if (docType === 'wiki') {\n const node = await client.getWikiNodeInfo(docToken);\n docToken = node.obj_token!;\n logger.info('Resolved docx token:', docToken);\n }\n\n const doc = await client.getDocxDocument(docToken);\n const blocks = await client.getDocxBlocks(docToken);\n logger.info(`Fetched ${blocks.length} blocks`);\n\n const parser = new Parser();\n let markdown = parser.parseDocxContent(doc, blocks);\n\n if (opts.imageMode === 'online') {\n // batch: max 5 tokens per request\n for (let i = 0; i < parser.imgTokens.length; i += 5) {\n const batch = parser.imgTokens.slice(i, i + 5);\n const urlMap = await client.batchGetTmpDownloadUrl(batch);\n for (const token of batch) {\n const onlineUrl = urlMap[token];\n if (onlineUrl) {\n markdown = markdown.replace(`(${token})`, `(${onlineUrl})`);\n logger.info('Replaced image with online URL:', token);\n }\n }\n }\n } else {\n const imgDir = path.join(opts.output, 'static');\n for (const imgToken of parser.imgTokens) {\n let localPath = await client.downloadImage(imgToken, imgDir);\n localPath = path.relative(opts.output, localPath);\n markdown = markdown.replace(`(${imgToken})`, `(${localPath})`);\n logger.info('Downloaded image:', localPath);\n }\n }\n\n if (opts.agent) {\n process.stdout.write(markdown);\n } else {\n fs.mkdirSync(opts.output, { recursive: true });\n const mdPath = path.join(opts.output, `${docToken}.md`);\n fs.writeFileSync(mdPath, markdown);\n logger.info('Downloaded markdown file to', mdPath);\n }\n });\n\nprogram.parse();\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lark-docx2md",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Convert Lark/Feishu documents to Markdown",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"larkDocx2md": "./dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"dev": "tsx src/cli.ts",
|
|
18
|
+
"prepack": "pnpm build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"lark",
|
|
22
|
+
"feishu",
|
|
23
|
+
"markdown",
|
|
24
|
+
"converter"
|
|
25
|
+
],
|
|
26
|
+
"author": "byte",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/Byte-n/larkDocx2md.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/Byte-n/larkDocx2md/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/Byte-n/larkDocx2md#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"packageManager": "pnpm@10.30.1",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@larksuiteoapi/node-sdk": "^1.60.0",
|
|
42
|
+
"commander": "^14.0.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.5.2",
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"tsdown": "^0.12.9",
|
|
48
|
+
"tsx": "^4.21.0",
|
|
49
|
+
"typescript": "^6.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|