@md-lark-converter/core 1.0.0 → 1.2.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.
@@ -0,0 +1,28 @@
1
+ import { ClipboardData } from './index.mjs';
2
+
3
+ interface FetchOptions {
4
+ cookie?: string;
5
+ verbose?: boolean;
6
+ }
7
+ /**
8
+ * Parse a Feishu document URL.
9
+ * Supports:
10
+ * https://xxx.feishu.cn/wiki/TOKEN
11
+ * https://xxx.feishu.cn/docx/TOKEN
12
+ */
13
+ declare function parseFeishuUrl(url: string): {
14
+ origin: string;
15
+ token: string;
16
+ type: 'wiki' | 'docx';
17
+ };
18
+ /**
19
+ * Resolve the cookie string from various sources.
20
+ * Priority: --cookie flag > FEISHU_COOKIE env > config file
21
+ */
22
+ declare function resolveCookie(flagCookie?: string): string;
23
+ /**
24
+ * Fetch a Feishu document and return it as ClipboardData.
25
+ */
26
+ declare function fetchFeishuDocument(url: string, options?: FetchOptions): Promise<ClipboardData>;
27
+
28
+ export { fetchFeishuDocument, parseFeishuUrl, resolveCookie };
@@ -0,0 +1,28 @@
1
+ import { ClipboardData } from './index.js';
2
+
3
+ interface FetchOptions {
4
+ cookie?: string;
5
+ verbose?: boolean;
6
+ }
7
+ /**
8
+ * Parse a Feishu document URL.
9
+ * Supports:
10
+ * https://xxx.feishu.cn/wiki/TOKEN
11
+ * https://xxx.feishu.cn/docx/TOKEN
12
+ */
13
+ declare function parseFeishuUrl(url: string): {
14
+ origin: string;
15
+ token: string;
16
+ type: 'wiki' | 'docx';
17
+ };
18
+ /**
19
+ * Resolve the cookie string from various sources.
20
+ * Priority: --cookie flag > FEISHU_COOKIE env > config file
21
+ */
22
+ declare function resolveCookie(flagCookie?: string): string;
23
+ /**
24
+ * Fetch a Feishu document and return it as ClipboardData.
25
+ */
26
+ declare function fetchFeishuDocument(url: string, options?: FetchOptions): Promise<ClipboardData>;
27
+
28
+ export { fetchFeishuDocument, parseFeishuUrl, resolveCookie };
package/dist/feishu.js ADDED
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/feishuFetcher.ts
21
+ var feishuFetcher_exports = {};
22
+ __export(feishuFetcher_exports, {
23
+ fetchFeishuDocument: () => fetchFeishuDocument,
24
+ parseFeishuUrl: () => parseFeishuUrl,
25
+ resolveCookie: () => resolveCookie
26
+ });
27
+ module.exports = __toCommonJS(feishuFetcher_exports);
28
+ var import_fs = require("fs");
29
+ var import_os = require("os");
30
+ var import_path = require("path");
31
+ var CONFIG_PATH = (0, import_path.join)((0, import_os.homedir)(), ".md-lark-converter.json");
32
+ function parseFeishuUrl(url) {
33
+ const parsed = new URL(url);
34
+ const pathParts = parsed.pathname.split("/").filter(Boolean);
35
+ if (pathParts.length < 2) {
36
+ throw new Error(`Invalid Feishu URL: ${url}`);
37
+ }
38
+ const docType = pathParts[0];
39
+ if (docType !== "wiki" && docType !== "docx") {
40
+ throw new Error(`Unsupported document type: ${docType}. Expected 'wiki' or 'docx'.`);
41
+ }
42
+ return {
43
+ origin: parsed.origin,
44
+ token: pathParts[1],
45
+ type: docType
46
+ };
47
+ }
48
+ async function resolveWikiToken(origin, wikiToken, cookie, csrfToken) {
49
+ const url = `${origin}/space/api/wiki/v2/tree/get_node/?wiki_token=${wikiToken}`;
50
+ const resp = await fetch(url, {
51
+ headers: {
52
+ "Cookie": cookie,
53
+ "X-CSRFToken": csrfToken
54
+ }
55
+ });
56
+ if (!resp.ok) {
57
+ throw new Error(`Failed to resolve wiki token: HTTP ${resp.status}`);
58
+ }
59
+ const body = await resp.json();
60
+ if (body.code !== 0) {
61
+ throw new Error(`Failed to resolve wiki token: ${body.msg}`);
62
+ }
63
+ return body.data.obj_token;
64
+ }
65
+ async function fetchClientVars(origin, objToken, cookie, csrfToken, verbose) {
66
+ const allBlocks = {};
67
+ let allSequence = [];
68
+ let docId = "";
69
+ let cursors = [];
70
+ let isFirst = true;
71
+ do {
72
+ const params = new URLSearchParams({
73
+ id: objToken,
74
+ mode: "7",
75
+ limit: "239"
76
+ });
77
+ if (!isFirst && cursors.length > 0) {
78
+ params.set("cursor", cursors[0]);
79
+ }
80
+ const url = `${origin}/space/api/docx/pages/client_vars?${params}`;
81
+ if (verbose) console.log(`Fetching: ${url}`);
82
+ const resp = await fetch(url, {
83
+ headers: {
84
+ "Cookie": cookie,
85
+ "X-CSRFToken": csrfToken,
86
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
87
+ }
88
+ });
89
+ if (!resp.ok) {
90
+ throw new Error(`API request failed: HTTP ${resp.status}`);
91
+ }
92
+ const body = await resp.json();
93
+ if (body.code !== 0) {
94
+ throw new Error(`API error: ${body.msg} (code: ${body.code})`);
95
+ }
96
+ const data = body.data;
97
+ Object.assign(allBlocks, data.block_map);
98
+ if (data.block_sequence) {
99
+ allSequence = allSequence.concat(data.block_sequence);
100
+ }
101
+ if (isFirst) {
102
+ docId = data.id;
103
+ }
104
+ if (verbose) {
105
+ const count = Object.keys(data.block_map).length;
106
+ console.log(` Got ${count} blocks, has_more: ${data.has_more}`);
107
+ }
108
+ cursors = data.next_cursors || [];
109
+ isFirst = false;
110
+ } while (cursors.length > 0);
111
+ return {
112
+ block_map: allBlocks,
113
+ block_sequence: allSequence.length > 0 ? allSequence : Object.keys(allBlocks),
114
+ id: docId,
115
+ has_more: false,
116
+ next_cursors: []
117
+ };
118
+ }
119
+ function resolveCookie(flagCookie) {
120
+ if (flagCookie) return flagCookie;
121
+ const envCookie = process.env.FEISHU_COOKIE;
122
+ if (envCookie) return envCookie;
123
+ if ((0, import_fs.existsSync)(CONFIG_PATH)) {
124
+ try {
125
+ const config = JSON.parse((0, import_fs.readFileSync)(CONFIG_PATH, "utf-8"));
126
+ if (config.cookie) return config.cookie;
127
+ } catch {
128
+ }
129
+ }
130
+ throw new Error(
131
+ `No cookie provided. Use one of:
132
+ --cookie "your_cookie_string"
133
+ FEISHU_COOKIE environment variable
134
+ ${CONFIG_PATH} with {"cookie": "..."}`
135
+ );
136
+ }
137
+ function extractCsrfToken(cookie) {
138
+ const match = cookie.match(/_csrf_token=([^;]+)/);
139
+ return match ? match[1] : "";
140
+ }
141
+ function blockMapToClipboardData(data) {
142
+ const recordMap = {};
143
+ for (const [id, entry] of Object.entries(data.block_map)) {
144
+ recordMap[id] = {
145
+ id: entry.id,
146
+ snapshot: entry.data
147
+ };
148
+ }
149
+ return {
150
+ isCut: false,
151
+ rootId: data.id,
152
+ parentId: "",
153
+ blockIds: [],
154
+ recordIds: data.block_sequence || Object.keys(data.block_map),
155
+ recordMap,
156
+ payloadMap: {},
157
+ selection: [],
158
+ extra: {
159
+ channel: "",
160
+ pasteRandomId: "",
161
+ mention_page_title: {},
162
+ external_mention_url: {},
163
+ isEqualBlockSelection: false
164
+ },
165
+ isKeepQuoteContainer: false,
166
+ pasteFlag: ""
167
+ };
168
+ }
169
+ async function fetchFeishuDocument(url, options = {}) {
170
+ const { verbose = false } = options;
171
+ const cookie = resolveCookie(options.cookie);
172
+ const csrfToken = extractCsrfToken(cookie);
173
+ const { origin, token, type } = parseFeishuUrl(url);
174
+ if (verbose) console.log(`Document type: ${type}, token: ${token}`);
175
+ let objToken = token;
176
+ if (type === "wiki") {
177
+ if (verbose) console.log("Resolving wiki token...");
178
+ objToken = await resolveWikiToken(origin, token, cookie, csrfToken);
179
+ if (verbose) console.log(`Resolved to obj_token: ${objToken}`);
180
+ }
181
+ const data = await fetchClientVars(origin, objToken, cookie, csrfToken, verbose);
182
+ const blockCount = Object.keys(data.block_map).length;
183
+ if (verbose) console.log(`Total blocks: ${blockCount}`);
184
+ if (blockCount === 0) {
185
+ throw new Error("No blocks found. The document may be empty or inaccessible.");
186
+ }
187
+ return blockMapToClipboardData(data);
188
+ }
189
+ // Annotate the CommonJS export names for ESM import in node:
190
+ 0 && (module.exports = {
191
+ fetchFeishuDocument,
192
+ parseFeishuUrl,
193
+ resolveCookie
194
+ });
195
+ //# sourceMappingURL=feishu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/feishuFetcher.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\nimport { homedir } from 'os';\nimport { join } from 'path';\nimport type { ClipboardData } from './index.js';\n\nconst CONFIG_PATH = join(homedir(), '.md-lark-converter.json');\n\ninterface FetchOptions {\n cookie?: string;\n verbose?: boolean;\n}\n\ninterface BlockMapEntry {\n id: string;\n version: number;\n data: Record<string, unknown>;\n}\n\ninterface ClientVarResponse {\n code: number;\n msg: string;\n data: {\n block_map: Record<string, BlockMapEntry>;\n block_sequence?: string[];\n id: string;\n has_more: boolean;\n next_cursors: string[];\n cursor?: string;\n meta_map?: Record<string, unknown>;\n editor_map?: Record<string, unknown>;\n [key: string]: unknown;\n };\n}\n\n/**\n * Parse a Feishu document URL.\n * Supports:\n * https://xxx.feishu.cn/wiki/TOKEN\n * https://xxx.feishu.cn/docx/TOKEN\n */\nexport function parseFeishuUrl(url: string): { origin: string; token: string; type: 'wiki' | 'docx' } {\n const parsed = new URL(url);\n const pathParts = parsed.pathname.split('/').filter(Boolean);\n\n if (pathParts.length < 2) {\n throw new Error(`Invalid Feishu URL: ${url}`);\n }\n\n const docType = pathParts[0] as 'wiki' | 'docx';\n if (docType !== 'wiki' && docType !== 'docx') {\n throw new Error(`Unsupported document type: ${docType}. Expected 'wiki' or 'docx'.`);\n }\n\n return {\n origin: parsed.origin,\n token: pathParts[1],\n type: docType,\n };\n}\n\n/**\n * Resolve a wiki token to its underlying docx obj_token.\n */\nasync function resolveWikiToken(\n origin: string,\n wikiToken: string,\n cookie: string,\n csrfToken: string,\n): Promise<string> {\n const url = `${origin}/space/api/wiki/v2/tree/get_node/?wiki_token=${wikiToken}`;\n const resp = await fetch(url, {\n headers: {\n 'Cookie': cookie,\n 'X-CSRFToken': csrfToken,\n },\n });\n\n if (!resp.ok) {\n throw new Error(`Failed to resolve wiki token: HTTP ${resp.status}`);\n }\n\n const body = await resp.json() as any;\n if (body.code !== 0) {\n throw new Error(`Failed to resolve wiki token: ${body.msg}`);\n }\n\n return body.data.obj_token;\n}\n\n/**\n * Fetch document blocks via the clientvar HTTP API.\n * Handles pagination if the document is large.\n */\nasync function fetchClientVars(\n origin: string,\n objToken: string,\n cookie: string,\n csrfToken: string,\n verbose: boolean,\n): Promise<ClientVarResponse['data']> {\n const allBlocks: Record<string, BlockMapEntry> = {};\n let allSequence: string[] = [];\n let docId = '';\n let cursors: string[] = [];\n let isFirst = true;\n\n do {\n const params = new URLSearchParams({\n id: objToken,\n mode: '7',\n limit: '239',\n });\n\n if (!isFirst && cursors.length > 0) {\n params.set('cursor', cursors[0]);\n }\n\n const url = `${origin}/space/api/docx/pages/client_vars?${params}`;\n if (verbose) console.log(`Fetching: ${url}`);\n\n const resp = await fetch(url, {\n headers: {\n 'Cookie': cookie,\n 'X-CSRFToken': csrfToken,\n 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',\n },\n });\n\n if (!resp.ok) {\n throw new Error(`API request failed: HTTP ${resp.status}`);\n }\n\n const body = await resp.json() as ClientVarResponse;\n if (body.code !== 0) {\n throw new Error(`API error: ${body.msg} (code: ${body.code})`);\n }\n\n const data = body.data;\n Object.assign(allBlocks, data.block_map);\n\n if (data.block_sequence) {\n allSequence = allSequence.concat(data.block_sequence);\n }\n\n if (isFirst) {\n docId = data.id;\n }\n\n if (verbose) {\n const count = Object.keys(data.block_map).length;\n console.log(` Got ${count} blocks, has_more: ${data.has_more}`);\n }\n\n cursors = data.next_cursors || [];\n isFirst = false;\n } while (cursors.length > 0);\n\n return {\n block_map: allBlocks,\n block_sequence: allSequence.length > 0 ? allSequence : Object.keys(allBlocks),\n id: docId,\n has_more: false,\n next_cursors: [],\n };\n}\n\n/**\n * Resolve the cookie string from various sources.\n * Priority: --cookie flag > FEISHU_COOKIE env > config file\n */\nexport function resolveCookie(flagCookie?: string): string {\n if (flagCookie) return flagCookie;\n\n const envCookie = process.env.FEISHU_COOKIE;\n if (envCookie) return envCookie;\n\n if (existsSync(CONFIG_PATH)) {\n try {\n const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));\n if (config.cookie) return config.cookie;\n } catch {\n // ignore parse errors\n }\n }\n\n throw new Error(\n 'No cookie provided. Use one of:\\n' +\n ' --cookie \"your_cookie_string\"\\n' +\n ' FEISHU_COOKIE environment variable\\n' +\n ` ${CONFIG_PATH} with {\"cookie\": \"...\"}`\n );\n}\n\n/**\n * Extract the _csrf_token from a cookie string.\n */\nfunction extractCsrfToken(cookie: string): string {\n const match = cookie.match(/_csrf_token=([^;]+)/);\n return match ? match[1] : '';\n}\n\n/**\n * Convert clientvar block_map to ClipboardData format for larkToMarkdown.\n */\nfunction blockMapToClipboardData(data: ClientVarResponse['data']): ClipboardData {\n const recordMap: Record<string, { id: string; snapshot: Record<string, unknown> }> = {};\n\n for (const [id, entry] of Object.entries(data.block_map)) {\n recordMap[id] = {\n id: entry.id,\n snapshot: entry.data,\n };\n }\n\n return {\n isCut: false,\n rootId: data.id,\n parentId: '',\n blockIds: [],\n recordIds: data.block_sequence || Object.keys(data.block_map),\n recordMap: recordMap as any,\n payloadMap: {},\n selection: [],\n extra: {\n channel: '',\n pasteRandomId: '',\n mention_page_title: {},\n external_mention_url: {},\n isEqualBlockSelection: false,\n },\n isKeepQuoteContainer: false,\n pasteFlag: '',\n };\n}\n\n/**\n * Fetch a Feishu document and return it as ClipboardData.\n */\nexport async function fetchFeishuDocument(\n url: string,\n options: FetchOptions = {}\n): Promise<ClipboardData> {\n const { verbose = false } = options;\n\n const cookie = resolveCookie(options.cookie);\n const csrfToken = extractCsrfToken(cookie);\n const { origin, token, type } = parseFeishuUrl(url);\n\n if (verbose) console.log(`Document type: ${type}, token: ${token}`);\n\n // Resolve wiki token to obj_token if needed\n let objToken = token;\n if (type === 'wiki') {\n if (verbose) console.log('Resolving wiki token...');\n objToken = await resolveWikiToken(origin, token, cookie, csrfToken);\n if (verbose) console.log(`Resolved to obj_token: ${objToken}`);\n }\n\n // Fetch document blocks\n const data = await fetchClientVars(origin, objToken, cookie, csrfToken, verbose);\n\n const blockCount = Object.keys(data.block_map).length;\n if (verbose) console.log(`Total blocks: ${blockCount}`);\n\n if (blockCount === 0) {\n throw new Error('No blocks found. The document may be empty or inaccessible.');\n }\n\n return blockMapToClipboardData(data);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,168 @@
1
+ // src/feishuFetcher.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var CONFIG_PATH = join(homedir(), ".md-lark-converter.json");
6
+ function parseFeishuUrl(url) {
7
+ const parsed = new URL(url);
8
+ const pathParts = parsed.pathname.split("/").filter(Boolean);
9
+ if (pathParts.length < 2) {
10
+ throw new Error(`Invalid Feishu URL: ${url}`);
11
+ }
12
+ const docType = pathParts[0];
13
+ if (docType !== "wiki" && docType !== "docx") {
14
+ throw new Error(`Unsupported document type: ${docType}. Expected 'wiki' or 'docx'.`);
15
+ }
16
+ return {
17
+ origin: parsed.origin,
18
+ token: pathParts[1],
19
+ type: docType
20
+ };
21
+ }
22
+ async function resolveWikiToken(origin, wikiToken, cookie, csrfToken) {
23
+ const url = `${origin}/space/api/wiki/v2/tree/get_node/?wiki_token=${wikiToken}`;
24
+ const resp = await fetch(url, {
25
+ headers: {
26
+ "Cookie": cookie,
27
+ "X-CSRFToken": csrfToken
28
+ }
29
+ });
30
+ if (!resp.ok) {
31
+ throw new Error(`Failed to resolve wiki token: HTTP ${resp.status}`);
32
+ }
33
+ const body = await resp.json();
34
+ if (body.code !== 0) {
35
+ throw new Error(`Failed to resolve wiki token: ${body.msg}`);
36
+ }
37
+ return body.data.obj_token;
38
+ }
39
+ async function fetchClientVars(origin, objToken, cookie, csrfToken, verbose) {
40
+ const allBlocks = {};
41
+ let allSequence = [];
42
+ let docId = "";
43
+ let cursors = [];
44
+ let isFirst = true;
45
+ do {
46
+ const params = new URLSearchParams({
47
+ id: objToken,
48
+ mode: "7",
49
+ limit: "239"
50
+ });
51
+ if (!isFirst && cursors.length > 0) {
52
+ params.set("cursor", cursors[0]);
53
+ }
54
+ const url = `${origin}/space/api/docx/pages/client_vars?${params}`;
55
+ if (verbose) console.log(`Fetching: ${url}`);
56
+ const resp = await fetch(url, {
57
+ headers: {
58
+ "Cookie": cookie,
59
+ "X-CSRFToken": csrfToken,
60
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
61
+ }
62
+ });
63
+ if (!resp.ok) {
64
+ throw new Error(`API request failed: HTTP ${resp.status}`);
65
+ }
66
+ const body = await resp.json();
67
+ if (body.code !== 0) {
68
+ throw new Error(`API error: ${body.msg} (code: ${body.code})`);
69
+ }
70
+ const data = body.data;
71
+ Object.assign(allBlocks, data.block_map);
72
+ if (data.block_sequence) {
73
+ allSequence = allSequence.concat(data.block_sequence);
74
+ }
75
+ if (isFirst) {
76
+ docId = data.id;
77
+ }
78
+ if (verbose) {
79
+ const count = Object.keys(data.block_map).length;
80
+ console.log(` Got ${count} blocks, has_more: ${data.has_more}`);
81
+ }
82
+ cursors = data.next_cursors || [];
83
+ isFirst = false;
84
+ } while (cursors.length > 0);
85
+ return {
86
+ block_map: allBlocks,
87
+ block_sequence: allSequence.length > 0 ? allSequence : Object.keys(allBlocks),
88
+ id: docId,
89
+ has_more: false,
90
+ next_cursors: []
91
+ };
92
+ }
93
+ function resolveCookie(flagCookie) {
94
+ if (flagCookie) return flagCookie;
95
+ const envCookie = process.env.FEISHU_COOKIE;
96
+ if (envCookie) return envCookie;
97
+ if (existsSync(CONFIG_PATH)) {
98
+ try {
99
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
100
+ if (config.cookie) return config.cookie;
101
+ } catch {
102
+ }
103
+ }
104
+ throw new Error(
105
+ `No cookie provided. Use one of:
106
+ --cookie "your_cookie_string"
107
+ FEISHU_COOKIE environment variable
108
+ ${CONFIG_PATH} with {"cookie": "..."}`
109
+ );
110
+ }
111
+ function extractCsrfToken(cookie) {
112
+ const match = cookie.match(/_csrf_token=([^;]+)/);
113
+ return match ? match[1] : "";
114
+ }
115
+ function blockMapToClipboardData(data) {
116
+ const recordMap = {};
117
+ for (const [id, entry] of Object.entries(data.block_map)) {
118
+ recordMap[id] = {
119
+ id: entry.id,
120
+ snapshot: entry.data
121
+ };
122
+ }
123
+ return {
124
+ isCut: false,
125
+ rootId: data.id,
126
+ parentId: "",
127
+ blockIds: [],
128
+ recordIds: data.block_sequence || Object.keys(data.block_map),
129
+ recordMap,
130
+ payloadMap: {},
131
+ selection: [],
132
+ extra: {
133
+ channel: "",
134
+ pasteRandomId: "",
135
+ mention_page_title: {},
136
+ external_mention_url: {},
137
+ isEqualBlockSelection: false
138
+ },
139
+ isKeepQuoteContainer: false,
140
+ pasteFlag: ""
141
+ };
142
+ }
143
+ async function fetchFeishuDocument(url, options = {}) {
144
+ const { verbose = false } = options;
145
+ const cookie = resolveCookie(options.cookie);
146
+ const csrfToken = extractCsrfToken(cookie);
147
+ const { origin, token, type } = parseFeishuUrl(url);
148
+ if (verbose) console.log(`Document type: ${type}, token: ${token}`);
149
+ let objToken = token;
150
+ if (type === "wiki") {
151
+ if (verbose) console.log("Resolving wiki token...");
152
+ objToken = await resolveWikiToken(origin, token, cookie, csrfToken);
153
+ if (verbose) console.log(`Resolved to obj_token: ${objToken}`);
154
+ }
155
+ const data = await fetchClientVars(origin, objToken, cookie, csrfToken, verbose);
156
+ const blockCount = Object.keys(data.block_map).length;
157
+ if (verbose) console.log(`Total blocks: ${blockCount}`);
158
+ if (blockCount === 0) {
159
+ throw new Error("No blocks found. The document may be empty or inaccessible.");
160
+ }
161
+ return blockMapToClipboardData(data);
162
+ }
163
+ export {
164
+ fetchFeishuDocument,
165
+ parseFeishuUrl,
166
+ resolveCookie
167
+ };
168
+ //# sourceMappingURL=feishu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/feishuFetcher.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\nimport { homedir } from 'os';\nimport { join } from 'path';\nimport type { ClipboardData } from './index.js';\n\nconst CONFIG_PATH = join(homedir(), '.md-lark-converter.json');\n\ninterface FetchOptions {\n cookie?: string;\n verbose?: boolean;\n}\n\ninterface BlockMapEntry {\n id: string;\n version: number;\n data: Record<string, unknown>;\n}\n\ninterface ClientVarResponse {\n code: number;\n msg: string;\n data: {\n block_map: Record<string, BlockMapEntry>;\n block_sequence?: string[];\n id: string;\n has_more: boolean;\n next_cursors: string[];\n cursor?: string;\n meta_map?: Record<string, unknown>;\n editor_map?: Record<string, unknown>;\n [key: string]: unknown;\n };\n}\n\n/**\n * Parse a Feishu document URL.\n * Supports:\n * https://xxx.feishu.cn/wiki/TOKEN\n * https://xxx.feishu.cn/docx/TOKEN\n */\nexport function parseFeishuUrl(url: string): { origin: string; token: string; type: 'wiki' | 'docx' } {\n const parsed = new URL(url);\n const pathParts = parsed.pathname.split('/').filter(Boolean);\n\n if (pathParts.length < 2) {\n throw new Error(`Invalid Feishu URL: ${url}`);\n }\n\n const docType = pathParts[0] as 'wiki' | 'docx';\n if (docType !== 'wiki' && docType !== 'docx') {\n throw new Error(`Unsupported document type: ${docType}. Expected 'wiki' or 'docx'.`);\n }\n\n return {\n origin: parsed.origin,\n token: pathParts[1],\n type: docType,\n };\n}\n\n/**\n * Resolve a wiki token to its underlying docx obj_token.\n */\nasync function resolveWikiToken(\n origin: string,\n wikiToken: string,\n cookie: string,\n csrfToken: string,\n): Promise<string> {\n const url = `${origin}/space/api/wiki/v2/tree/get_node/?wiki_token=${wikiToken}`;\n const resp = await fetch(url, {\n headers: {\n 'Cookie': cookie,\n 'X-CSRFToken': csrfToken,\n },\n });\n\n if (!resp.ok) {\n throw new Error(`Failed to resolve wiki token: HTTP ${resp.status}`);\n }\n\n const body = await resp.json() as any;\n if (body.code !== 0) {\n throw new Error(`Failed to resolve wiki token: ${body.msg}`);\n }\n\n return body.data.obj_token;\n}\n\n/**\n * Fetch document blocks via the clientvar HTTP API.\n * Handles pagination if the document is large.\n */\nasync function fetchClientVars(\n origin: string,\n objToken: string,\n cookie: string,\n csrfToken: string,\n verbose: boolean,\n): Promise<ClientVarResponse['data']> {\n const allBlocks: Record<string, BlockMapEntry> = {};\n let allSequence: string[] = [];\n let docId = '';\n let cursors: string[] = [];\n let isFirst = true;\n\n do {\n const params = new URLSearchParams({\n id: objToken,\n mode: '7',\n limit: '239',\n });\n\n if (!isFirst && cursors.length > 0) {\n params.set('cursor', cursors[0]);\n }\n\n const url = `${origin}/space/api/docx/pages/client_vars?${params}`;\n if (verbose) console.log(`Fetching: ${url}`);\n\n const resp = await fetch(url, {\n headers: {\n 'Cookie': cookie,\n 'X-CSRFToken': csrfToken,\n 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',\n },\n });\n\n if (!resp.ok) {\n throw new Error(`API request failed: HTTP ${resp.status}`);\n }\n\n const body = await resp.json() as ClientVarResponse;\n if (body.code !== 0) {\n throw new Error(`API error: ${body.msg} (code: ${body.code})`);\n }\n\n const data = body.data;\n Object.assign(allBlocks, data.block_map);\n\n if (data.block_sequence) {\n allSequence = allSequence.concat(data.block_sequence);\n }\n\n if (isFirst) {\n docId = data.id;\n }\n\n if (verbose) {\n const count = Object.keys(data.block_map).length;\n console.log(` Got ${count} blocks, has_more: ${data.has_more}`);\n }\n\n cursors = data.next_cursors || [];\n isFirst = false;\n } while (cursors.length > 0);\n\n return {\n block_map: allBlocks,\n block_sequence: allSequence.length > 0 ? allSequence : Object.keys(allBlocks),\n id: docId,\n has_more: false,\n next_cursors: [],\n };\n}\n\n/**\n * Resolve the cookie string from various sources.\n * Priority: --cookie flag > FEISHU_COOKIE env > config file\n */\nexport function resolveCookie(flagCookie?: string): string {\n if (flagCookie) return flagCookie;\n\n const envCookie = process.env.FEISHU_COOKIE;\n if (envCookie) return envCookie;\n\n if (existsSync(CONFIG_PATH)) {\n try {\n const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));\n if (config.cookie) return config.cookie;\n } catch {\n // ignore parse errors\n }\n }\n\n throw new Error(\n 'No cookie provided. Use one of:\\n' +\n ' --cookie \"your_cookie_string\"\\n' +\n ' FEISHU_COOKIE environment variable\\n' +\n ` ${CONFIG_PATH} with {\"cookie\": \"...\"}`\n );\n}\n\n/**\n * Extract the _csrf_token from a cookie string.\n */\nfunction extractCsrfToken(cookie: string): string {\n const match = cookie.match(/_csrf_token=([^;]+)/);\n return match ? match[1] : '';\n}\n\n/**\n * Convert clientvar block_map to ClipboardData format for larkToMarkdown.\n */\nfunction blockMapToClipboardData(data: ClientVarResponse['data']): ClipboardData {\n const recordMap: Record<string, { id: string; snapshot: Record<string, unknown> }> = {};\n\n for (const [id, entry] of Object.entries(data.block_map)) {\n recordMap[id] = {\n id: entry.id,\n snapshot: entry.data,\n };\n }\n\n return {\n isCut: false,\n rootId: data.id,\n parentId: '',\n blockIds: [],\n recordIds: data.block_sequence || Object.keys(data.block_map),\n recordMap: recordMap as any,\n payloadMap: {},\n selection: [],\n extra: {\n channel: '',\n pasteRandomId: '',\n mention_page_title: {},\n external_mention_url: {},\n isEqualBlockSelection: false,\n },\n isKeepQuoteContainer: false,\n pasteFlag: '',\n };\n}\n\n/**\n * Fetch a Feishu document and return it as ClipboardData.\n */\nexport async function fetchFeishuDocument(\n url: string,\n options: FetchOptions = {}\n): Promise<ClipboardData> {\n const { verbose = false } = options;\n\n const cookie = resolveCookie(options.cookie);\n const csrfToken = extractCsrfToken(cookie);\n const { origin, token, type } = parseFeishuUrl(url);\n\n if (verbose) console.log(`Document type: ${type}, token: ${token}`);\n\n // Resolve wiki token to obj_token if needed\n let objToken = token;\n if (type === 'wiki') {\n if (verbose) console.log('Resolving wiki token...');\n objToken = await resolveWikiToken(origin, token, cookie, csrfToken);\n if (verbose) console.log(`Resolved to obj_token: ${objToken}`);\n }\n\n // Fetch document blocks\n const data = await fetchClientVars(origin, objToken, cookie, csrfToken, verbose);\n\n const blockCount = Object.keys(data.block_map).length;\n if (verbose) console.log(`Total blocks: ${blockCount}`);\n\n if (blockCount === 0) {\n throw new Error('No blocks found. The document may be empty or inaccessible.');\n }\n\n return blockMapToClipboardData(data);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ import { ClipboardData } from './index.mjs';
2
+
3
+ interface ImageMeta {
4
+ token: string;
5
+ width?: number;
6
+ height?: number;
7
+ mimeType?: string;
8
+ name?: string;
9
+ }
10
+ interface ImageDownloadOptions {
11
+ cookie: string;
12
+ domain: string;
13
+ outputDir: string;
14
+ concurrency?: number;
15
+ verbose?: boolean;
16
+ }
17
+ /**
18
+ * Extract image tokens and metadata from ClipboardData.
19
+ */
20
+ declare function extractImageTokens(data: ClipboardData): ImageMeta[];
21
+ /**
22
+ * Download a single image by token, returns the saved filename.
23
+ */
24
+ declare function downloadImage(token: string, options: Pick<ImageDownloadOptions, 'cookie' | 'domain' | 'outputDir'>): Promise<string>;
25
+ /**
26
+ * Download all images in batches, returns token → filename mapping.
27
+ */
28
+ declare function downloadAllImages(images: ImageMeta[], options: ImageDownloadOptions): Promise<Map<string, string>>;
29
+
30
+ export { type ImageDownloadOptions, type ImageMeta, downloadAllImages, downloadImage, extractImageTokens };
@@ -0,0 +1,30 @@
1
+ import { ClipboardData } from './index.js';
2
+
3
+ interface ImageMeta {
4
+ token: string;
5
+ width?: number;
6
+ height?: number;
7
+ mimeType?: string;
8
+ name?: string;
9
+ }
10
+ interface ImageDownloadOptions {
11
+ cookie: string;
12
+ domain: string;
13
+ outputDir: string;
14
+ concurrency?: number;
15
+ verbose?: boolean;
16
+ }
17
+ /**
18
+ * Extract image tokens and metadata from ClipboardData.
19
+ */
20
+ declare function extractImageTokens(data: ClipboardData): ImageMeta[];
21
+ /**
22
+ * Download a single image by token, returns the saved filename.
23
+ */
24
+ declare function downloadImage(token: string, options: Pick<ImageDownloadOptions, 'cookie' | 'domain' | 'outputDir'>): Promise<string>;
25
+ /**
26
+ * Download all images in batches, returns token → filename mapping.
27
+ */
28
+ declare function downloadAllImages(images: ImageMeta[], options: ImageDownloadOptions): Promise<Map<string, string>>;
29
+
30
+ export { type ImageDownloadOptions, type ImageMeta, downloadAllImages, downloadImage, extractImageTokens };
package/dist/image.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/imageDownloader.ts
21
+ var imageDownloader_exports = {};
22
+ __export(imageDownloader_exports, {
23
+ downloadAllImages: () => downloadAllImages,
24
+ downloadImage: () => downloadImage,
25
+ extractImageTokens: () => extractImageTokens
26
+ });
27
+ module.exports = __toCommonJS(imageDownloader_exports);
28
+ var import_promises = require("fs/promises");
29
+ var import_path = require("path");
30
+ function extractImageTokens(data) {
31
+ const seen = /* @__PURE__ */ new Set();
32
+ const images = [];
33
+ for (const record of Object.values(data.recordMap)) {
34
+ const snapshot = record.snapshot;
35
+ if (snapshot?.type === "image" && snapshot.image?.token) {
36
+ const img = snapshot.image;
37
+ if (seen.has(img.token)) continue;
38
+ seen.add(img.token);
39
+ images.push({
40
+ token: img.token,
41
+ width: img.width,
42
+ height: img.height,
43
+ mimeType: img.mimeType,
44
+ name: img.name
45
+ });
46
+ }
47
+ }
48
+ return images;
49
+ }
50
+ function extFromContentType(contentType) {
51
+ if (contentType.includes("jpeg") || contentType.includes("jpg")) return ".jpg";
52
+ if (contentType.includes("png")) return ".png";
53
+ if (contentType.includes("gif")) return ".gif";
54
+ if (contentType.includes("webp")) return ".webp";
55
+ if (contentType.includes("svg")) return ".svg";
56
+ return ".png";
57
+ }
58
+ async function downloadImage(token, options) {
59
+ if (!/^[a-zA-Z0-9_-]+$/.test(token)) {
60
+ throw new Error(`Invalid image token format: ${token}`);
61
+ }
62
+ const { cookie, domain, outputDir } = options;
63
+ const url = `${domain}/space/api/box/stream/download/all/${token}`;
64
+ const res = await fetch(url, {
65
+ headers: {
66
+ Cookie: cookie,
67
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"
68
+ },
69
+ redirect: "follow"
70
+ });
71
+ if (!res.ok) throw new Error(`Failed to download image ${token}: ${res.status}`);
72
+ const contentType = res.headers.get("content-type") || "image/png";
73
+ const ext = extFromContentType(contentType);
74
+ const filename = `${token}${ext}`;
75
+ const buffer = Buffer.from(await res.arrayBuffer());
76
+ await (0, import_promises.writeFile)((0, import_path.join)(outputDir, filename), buffer);
77
+ return filename;
78
+ }
79
+ async function downloadAllImages(images, options) {
80
+ const { outputDir, concurrency = 5, verbose = false } = options;
81
+ await (0, import_promises.mkdir)(outputDir, { recursive: true });
82
+ const tokenToFile = /* @__PURE__ */ new Map();
83
+ for (let i = 0; i < images.length; i += concurrency) {
84
+ const batch = images.slice(i, i + concurrency);
85
+ const results = await Promise.allSettled(
86
+ batch.map((img) => downloadImage(img.token, options))
87
+ );
88
+ for (let j = 0; j < results.length; j++) {
89
+ const result = results[j];
90
+ const img = batch[j];
91
+ if (result.status === "fulfilled") {
92
+ tokenToFile.set(img.token, result.value);
93
+ if (verbose) console.log(` Downloaded: ${img.token} \u2192 ${result.value}`);
94
+ } else {
95
+ console.warn(` Failed to download ${img.token}: ${result.reason}`);
96
+ }
97
+ }
98
+ }
99
+ return tokenToFile;
100
+ }
101
+ // Annotate the CommonJS export names for ESM import in node:
102
+ 0 && (module.exports = {
103
+ downloadAllImages,
104
+ downloadImage,
105
+ extractImageTokens
106
+ });
107
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/imageDownloader.ts"],"sourcesContent":["import { mkdir, writeFile } from 'fs/promises';\nimport { join } from 'path';\nimport type { ClipboardData } from './index.js';\n\nexport interface ImageMeta {\n token: string;\n width?: number;\n height?: number;\n mimeType?: string;\n name?: string;\n}\n\nexport interface ImageDownloadOptions {\n cookie: string;\n domain: string;\n outputDir: string;\n concurrency?: number;\n verbose?: boolean;\n}\n\n/**\n * Extract image tokens and metadata from ClipboardData.\n */\nexport function extractImageTokens(data: ClipboardData): ImageMeta[] {\n const seen = new Set<string>();\n const images: ImageMeta[] = [];\n for (const record of Object.values(data.recordMap)) {\n const snapshot = record.snapshot;\n if (snapshot?.type === 'image' && snapshot.image?.token) {\n const img = snapshot.image;\n if (seen.has(img.token)) continue;\n seen.add(img.token);\n images.push({\n token: img.token,\n width: img.width,\n height: img.height,\n mimeType: img.mimeType,\n name: img.name,\n });\n }\n }\n return images;\n}\n\nfunction extFromContentType(contentType: string): string {\n if (contentType.includes('jpeg') || contentType.includes('jpg')) return '.jpg';\n if (contentType.includes('png')) return '.png';\n if (contentType.includes('gif')) return '.gif';\n if (contentType.includes('webp')) return '.webp';\n if (contentType.includes('svg')) return '.svg';\n return '.png';\n}\n\n/**\n * Download a single image by token, returns the saved filename.\n */\nexport async function downloadImage(\n token: string,\n options: Pick<ImageDownloadOptions, 'cookie' | 'domain' | 'outputDir'>\n): Promise<string> {\n if (!/^[a-zA-Z0-9_-]+$/.test(token)) {\n throw new Error(`Invalid image token format: ${token}`);\n }\n const { cookie, domain, outputDir } = options;\n const url = `${domain}/space/api/box/stream/download/all/${token}`;\n\n const res = await fetch(url, {\n headers: {\n Cookie: cookie,\n 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)',\n },\n redirect: 'follow',\n });\n\n if (!res.ok) throw new Error(`Failed to download image ${token}: ${res.status}`);\n\n const contentType = res.headers.get('content-type') || 'image/png';\n const ext = extFromContentType(contentType);\n const filename = `${token}${ext}`;\n\n const buffer = Buffer.from(await res.arrayBuffer());\n await writeFile(join(outputDir, filename), buffer);\n\n return filename;\n}\n\n/**\n * Download all images in batches, returns token → filename mapping.\n */\nexport async function downloadAllImages(\n images: ImageMeta[],\n options: ImageDownloadOptions\n): Promise<Map<string, string>> {\n const { outputDir, concurrency = 5, verbose = false } = options;\n await mkdir(outputDir, { recursive: true });\n\n const tokenToFile = new Map<string, string>();\n\n for (let i = 0; i < images.length; i += concurrency) {\n const batch = images.slice(i, i + concurrency);\n const results = await Promise.allSettled(\n batch.map(img => downloadImage(img.token, options))\n );\n for (let j = 0; j < results.length; j++) {\n const result = results[j];\n const img = batch[j];\n if (result.status === 'fulfilled') {\n tokenToFile.set(img.token, result.value);\n if (verbose) console.log(` Downloaded: ${img.token} → ${result.value}`);\n } else {\n console.warn(` Failed to download ${img.token}: ${result.reason}`);\n }\n }\n }\n\n return tokenToFile;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAiC;AACjC,kBAAqB;AAsBd,SAAS,mBAAmB,MAAkC;AACnE,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,SAAsB,CAAC;AAC7B,aAAW,UAAU,OAAO,OAAO,KAAK,SAAS,GAAG;AAClD,UAAM,WAAW,OAAO;AACxB,QAAI,UAAU,SAAS,WAAW,SAAS,OAAO,OAAO;AACvD,YAAM,MAAM,SAAS;AACrB,UAAI,KAAK,IAAI,IAAI,KAAK,EAAG;AACzB,WAAK,IAAI,IAAI,KAAK;AAClB,aAAO,KAAK;AAAA,QACV,OAAO,IAAI;AAAA,QACX,OAAO,IAAI;AAAA,QACX,QAAQ,IAAI;AAAA,QACZ,UAAU,IAAI;AAAA,QACd,MAAM,IAAI;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,aAA6B;AACvD,MAAI,YAAY,SAAS,MAAM,KAAK,YAAY,SAAS,KAAK,EAAG,QAAO;AACxE,MAAI,YAAY,SAAS,KAAK,EAAG,QAAO;AACxC,MAAI,YAAY,SAAS,KAAK,EAAG,QAAO;AACxC,MAAI,YAAY,SAAS,MAAM,EAAG,QAAO;AACzC,MAAI,YAAY,SAAS,KAAK,EAAG,QAAO;AACxC,SAAO;AACT;AAKA,eAAsB,cACpB,OACA,SACiB;AACjB,MAAI,CAAC,mBAAmB,KAAK,KAAK,GAAG;AACnC,UAAM,IAAI,MAAM,+BAA+B,KAAK,EAAE;AAAA,EACxD;AACA,QAAM,EAAE,QAAQ,QAAQ,UAAU,IAAI;AACtC,QAAM,MAAM,GAAG,MAAM,sCAAsC,KAAK;AAEhE,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,IAChB;AAAA,IACA,UAAU;AAAA,EACZ,CAAC;AAED,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,4BAA4B,KAAK,KAAK,IAAI,MAAM,EAAE;AAE/E,QAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;AACvD,QAAM,MAAM,mBAAmB,WAAW;AAC1C,QAAM,WAAW,GAAG,KAAK,GAAG,GAAG;AAE/B,QAAM,SAAS,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAClD,YAAM,+BAAU,kBAAK,WAAW,QAAQ,GAAG,MAAM;AAEjD,SAAO;AACT;AAKA,eAAsB,kBACpB,QACA,SAC8B;AAC9B,QAAM,EAAE,WAAW,cAAc,GAAG,UAAU,MAAM,IAAI;AACxD,YAAM,uBAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAE1C,QAAM,cAAc,oBAAI,IAAoB;AAE5C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,aAAa;AACnD,UAAM,QAAQ,OAAO,MAAM,GAAG,IAAI,WAAW;AAC7C,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,MAAM,IAAI,SAAO,cAAc,IAAI,OAAO,OAAO,CAAC;AAAA,IACpD;AACA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,YAAM,MAAM,MAAM,CAAC;AACnB,UAAI,OAAO,WAAW,aAAa;AACjC,oBAAY,IAAI,IAAI,OAAO,OAAO,KAAK;AACvC,YAAI,QAAS,SAAQ,IAAI,iBAAiB,IAAI,KAAK,WAAM,OAAO,KAAK,EAAE;AAAA,MACzE,OAAO;AACL,gBAAQ,KAAK,wBAAwB,IAAI,KAAK,KAAK,OAAO,MAAM,EAAE;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}