@dev-workflow/skill 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/cli.mjs +9 -0
- package/package.json +39 -0
- package/skills/artifact/README.md +23 -0
- package/skills/execution/devFlow/SKILL.md +341 -0
- package/skills/execution/devFlow/agents/openai.yaml +7 -0
- package/skills/execution/devFlow/domains/backend/references/api-tech.md +173 -0
- package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.mjs +170 -0
- package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.test.mjs +89 -0
- package/skills/execution/devFlow/domains/backend/templates/api-tech.md +164 -0
- package/skills/execution/devFlow/domains/frontend/references/contract-check.md +270 -0
- package/skills/execution/devFlow/domains/frontend/references/foundation-freeze.md +92 -0
- package/skills/execution/devFlow/domains/frontend/references/lightweight-flow.md +55 -0
- package/skills/execution/devFlow/domains/frontend/references/page-build.md +268 -0
- package/skills/execution/devFlow/domains/frontend/references/page-tech.md +414 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/field-common.md +69 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-complex.md +105 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-id-code.md +44 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-number-date.md +106 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-relation.md +144 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-selectors.md +102 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-text.md +130 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/index.md +42 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/interaction.md +61 -0
- package/skills/execution/devFlow/domains/frontend/scripts/check_page_tech_doc.mjs +82 -0
- package/skills/execution/devFlow/domains/frontend/scripts/contract_check_static.mjs +145 -0
- package/skills/execution/devFlow/domains/frontend/scripts/generate_foundation_summary.mjs +224 -0
- package/skills/execution/devFlow/domains/frontend/templates/contract-report.md +66 -0
- package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l0.md +7 -0
- package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l1.md +9 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/component.tsx.tpl +8 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/constants.ts.tpl +5 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/index.ts.tpl +1 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/route.tsx.tpl +11 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/service.ts.tpl +17 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/types.ts.tpl +11 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-tech.md +103 -0
- package/skills/execution/devFlow/domains/requirement/references/prd-review.md +129 -0
- package/skills/execution/devFlow/domains/requirement/templates/prd-review.md +57 -0
- package/skills/execution/figmaSync/SKILL.md +349 -0
- package/skills/execution/figmaSync/chapters/apply.md +177 -0
- package/skills/execution/figmaSync/chapters/plan.md +359 -0
- package/skills/execution/figmaSync/chapters/prepare.md +47 -0
- package/skills/execution/figmaSync/examples/native-css.md +129 -0
- package/skills/execution/figmaSync/examples/plan-doc-sample.md +39 -0
- package/skills/execution/figmaSync/scripts/_shared/session-log.mjs +82 -0
- package/skills/execution/figmaSync/scripts/_shared/token-resolver.mjs +132 -0
- package/skills/execution/figmaSync/scripts/_shared/token-source.mjs +145 -0
- package/skills/execution/figmaSync/scripts/figma-sync-report.mjs +448 -0
- package/skills/execution/figmaSync/scripts/icon-inventory.mjs +165 -0
- package/skills/execution/figmaSync/scripts/lookup-var.mjs +184 -0
- package/skills/execution/figmaSync/scripts/match-token.mjs +210 -0
- package/skills/execution/figmaSync/scripts/prepare-check.mjs +290 -0
- package/skills/execution/figmaSync/scripts/verify-plan.mjs +458 -0
- package/skills/execution/figmaSync/snapshots/README.md +13 -0
- package/skills/execution/figmaSync/snapshots/last-sync.md +1538 -0
- package/skills/execution/figmaSync/templates/PLAN.md.tpl +227 -0
- package/skills/review/consistency-checker/SKILL.md +112 -0
- package/skills/review/consistency-checker/rules/README.md +16 -0
- package/src/cli/agents.mjs +16 -0
- package/src/cli/copy.mjs +95 -0
- package/src/cli/install.mjs +131 -0
- package/src/cli/prompts.mjs +66 -0
- package/tools/lark/README.md +78 -0
- package/tools/lark/references/lark-doc.md +156 -0
- package/tools/lark/references/lark-read.md +235 -0
- package/tools/lark/references/prepare.md +147 -0
- package/tools/lark/scripts/lark_api.mjs +404 -0
- package/tools/lark/scripts/lark_api.test.mjs +63 -0
- package/tools/lark/scripts/lark_check_permissions.mjs +52 -0
- package/tools/lark/scripts/lark_publish_doc.mjs +299 -0
- package/tools/lark/scripts/lark_read_docx.mjs +328 -0
- package/tools/lark/scripts/markdown_to_lark_blocks.mjs +397 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { getTenantAccessToken, larkRequest, parseArgs, printJson, requireEnv } from './lark_api.mjs'
|
|
4
|
+
import { markdownToLarkBlocks } from './markdown_to_lark_blocks.mjs'
|
|
5
|
+
|
|
6
|
+
const WRITE_BATCH_SIZE = 40
|
|
7
|
+
|
|
8
|
+
function sanitizeBlock(block) {
|
|
9
|
+
return JSON.parse(JSON.stringify(block))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createTextElements(text) {
|
|
13
|
+
const elements = []
|
|
14
|
+
const pattern = /(`[^`\n]+`)|(\*\*[^*\n]+\*\*)/g
|
|
15
|
+
let lastIndex = 0
|
|
16
|
+
let match
|
|
17
|
+
|
|
18
|
+
while ((match = pattern.exec(text))) {
|
|
19
|
+
if (match.index > lastIndex) {
|
|
20
|
+
elements.push({ text_run: { content: text.slice(lastIndex, match.index) } })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = match[0]
|
|
24
|
+
if (token.startsWith('`')) {
|
|
25
|
+
elements.push({
|
|
26
|
+
text_run: { content: token.slice(1, -1), text_element_style: { inline_code: true } }
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
elements.push({
|
|
30
|
+
text_run: { content: token.slice(2, -2), text_element_style: { bold: true } }
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lastIndex = pattern.lastIndex
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (lastIndex < text.length) {
|
|
38
|
+
elements.push({ text_run: { content: text.slice(lastIndex) } })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return elements.length ? elements : [{ text_run: { content: '' } }]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getParentNode(token, parentToken) {
|
|
45
|
+
const payload = await larkRequest(
|
|
46
|
+
`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(parentToken)}`,
|
|
47
|
+
{ token }
|
|
48
|
+
)
|
|
49
|
+
const node = payload.data?.node
|
|
50
|
+
|
|
51
|
+
if (!node?.space_id) throw new Error('Wiki parent node not found')
|
|
52
|
+
return node
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function createWikiDoc(token, parentNode, title, parentToken) {
|
|
56
|
+
const payload = await larkRequest(`/open-apis/wiki/v2/spaces/${parentNode.space_id}/nodes`, {
|
|
57
|
+
token,
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
obj_type: 'docx',
|
|
61
|
+
node_type: 'origin',
|
|
62
|
+
parent_node_token: parentToken,
|
|
63
|
+
title
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
const node = payload.data?.node
|
|
67
|
+
|
|
68
|
+
if (!node?.obj_token) throw new Error('Create wiki doc failed')
|
|
69
|
+
return node
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function writeBlocks(token, docToken, blocks) {
|
|
73
|
+
let written = 0
|
|
74
|
+
let buffer = []
|
|
75
|
+
|
|
76
|
+
async function flushBuffer() {
|
|
77
|
+
if (!buffer.length) return
|
|
78
|
+
|
|
79
|
+
const children = buffer.map((block) => sanitizeBlock(block))
|
|
80
|
+
let payload
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
payload = await larkRequest(
|
|
84
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks/${docToken}/children`,
|
|
85
|
+
{
|
|
86
|
+
token,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: JSON.stringify({ children })
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
} catch {
|
|
92
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
93
|
+
try {
|
|
94
|
+
const singlePayload = await larkRequest(
|
|
95
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks/${docToken}/children`,
|
|
96
|
+
{
|
|
97
|
+
token,
|
|
98
|
+
method: 'POST',
|
|
99
|
+
body: JSON.stringify({ children: [children[index]] })
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
written += singlePayload.data?.children?.length || 0
|
|
103
|
+
} catch (singleError) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Write block failed at batch index ${index}, block_type ${children[index]?.block_type}: ${singleError.message}`,
|
|
106
|
+
{ cause: singleError }
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
buffer = []
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
written += payload.data?.children?.length || 0
|
|
116
|
+
buffer = []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeTable(block) {
|
|
120
|
+
await flushBuffer()
|
|
121
|
+
|
|
122
|
+
const property = block.table.property
|
|
123
|
+
const values = (block.__tableRows || []).flat()
|
|
124
|
+
|
|
125
|
+
// 用 descendant API 一次性建表 + 单元格 + 文本:单元格只含内容文本块(不带自动空块),
|
|
126
|
+
// 行高更紧凑,也比逐格写入快得多。
|
|
127
|
+
const cellIds = values.map((_, index) => `tc${index}`)
|
|
128
|
+
const descendants = [
|
|
129
|
+
{
|
|
130
|
+
block_id: 'tbl',
|
|
131
|
+
block_type: 31,
|
|
132
|
+
table: { property: { row_size: property.row_size, column_size: property.column_size } },
|
|
133
|
+
children: cellIds
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
values.forEach((value, index) => {
|
|
137
|
+
descendants.push({
|
|
138
|
+
block_id: cellIds[index],
|
|
139
|
+
block_type: 32,
|
|
140
|
+
table_cell: {},
|
|
141
|
+
children: [`tx${index}`]
|
|
142
|
+
})
|
|
143
|
+
descendants.push({
|
|
144
|
+
block_id: `tx${index}`,
|
|
145
|
+
block_type: 2,
|
|
146
|
+
text: { elements: createTextElements(value || '') }
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const payload = await larkRequest(
|
|
151
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks/${docToken}/descendant`,
|
|
152
|
+
{
|
|
153
|
+
token,
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: JSON.stringify({ index: -1, children_id: ['tbl'], descendants })
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
written += payload.data?.block_id_relations?.length || 1
|
|
160
|
+
|
|
161
|
+
// 表头底色 + 满宽列宽:建表后逐项 PATCH(建表时不接受这些属性)。
|
|
162
|
+
const relation = (payload.data?.block_id_relations || []).find(
|
|
163
|
+
(item) => item.temporary_block_id === 'tbl'
|
|
164
|
+
)
|
|
165
|
+
const tableId = relation?.block_id
|
|
166
|
+
|
|
167
|
+
if (tableId) {
|
|
168
|
+
const updatePath = `/open-apis/docx/v1/documents/${docToken}/blocks/${tableId}`
|
|
169
|
+
await larkRequest(updatePath, {
|
|
170
|
+
token,
|
|
171
|
+
method: 'PATCH',
|
|
172
|
+
body: JSON.stringify({ update_table_property: { header_row: true } })
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const widths = block.__columnWidth || []
|
|
176
|
+
for (let index = 0; index < widths.length; index += 1) {
|
|
177
|
+
await larkRequest(updatePath, {
|
|
178
|
+
token,
|
|
179
|
+
method: 'PATCH',
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
update_table_property: { column_index: index, column_width: widths[index] }
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function writeContainerText(blockType, containerKey, containerValue, text) {
|
|
189
|
+
await flushBuffer()
|
|
190
|
+
|
|
191
|
+
const payload = await larkRequest(
|
|
192
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks/${docToken}/descendant`,
|
|
193
|
+
{
|
|
194
|
+
token,
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
index: -1,
|
|
198
|
+
children_id: ['container'],
|
|
199
|
+
descendants: [
|
|
200
|
+
{
|
|
201
|
+
block_id: 'container',
|
|
202
|
+
block_type: blockType,
|
|
203
|
+
[containerKey]: containerValue,
|
|
204
|
+
children: ['container_text']
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
block_id: 'container_text',
|
|
208
|
+
block_type: 2,
|
|
209
|
+
text: { elements: createTextElements(text) }
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
written += payload.data?.descendants?.length || 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const block of blocks) {
|
|
220
|
+
if (block.__tableRows) {
|
|
221
|
+
await writeTable(block)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (block.block_type === 19) {
|
|
226
|
+
await writeContainerText(19, 'callout', block.callout || {}, block.__calloutText || '')
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (block.block_type === 34) {
|
|
231
|
+
await writeContainerText(34, 'quote_container', block.quote_container || {}, block.__quoteText || '')
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
buffer.push(block)
|
|
236
|
+
|
|
237
|
+
if (buffer.length >= WRITE_BATCH_SIZE) {
|
|
238
|
+
await flushBuffer()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await flushBuffer()
|
|
243
|
+
|
|
244
|
+
return written
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function verifyDoc(token, docToken) {
|
|
248
|
+
const payload = await larkRequest(
|
|
249
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks?page_size=20`,
|
|
250
|
+
{
|
|
251
|
+
token
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return payload.data?.items?.length || 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function inferTitle(markdown, explicitTitle) {
|
|
259
|
+
if (explicitTitle) return explicitTitle
|
|
260
|
+
|
|
261
|
+
const heading = /^#\s+(.+)$/m.exec(markdown)
|
|
262
|
+
if (heading) return heading[1].trim()
|
|
263
|
+
|
|
264
|
+
return '技术文档'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function main() {
|
|
268
|
+
const args = parseArgs()
|
|
269
|
+
// 应用凭证由后端感知的 getTenantAccessToken 把关(larkcli 用户身份不需要 app secret);
|
|
270
|
+
// Wiki 父节点是两种后端都需要的目标参数。
|
|
271
|
+
requireEnv(['FEISHU_WIKI_PARENT_NODE_TOKEN'])
|
|
272
|
+
|
|
273
|
+
const markdown = args.file ? readFileSync(args.file, 'utf8') : readFileSync(0, 'utf8')
|
|
274
|
+
const title = inferTitle(markdown, args.title)
|
|
275
|
+
const parentToken = args.parent || process.env.FEISHU_WIKI_PARENT_NODE_TOKEN
|
|
276
|
+
const token = await getTenantAccessToken()
|
|
277
|
+
const parentNode = await getParentNode(token, parentToken)
|
|
278
|
+
const node = await createWikiDoc(token, parentNode, title, parentToken)
|
|
279
|
+
const blocks = markdownToLarkBlocks(markdown)
|
|
280
|
+
const written = await writeBlocks(token, node.obj_token, blocks)
|
|
281
|
+
const verifyCount = await verifyDoc(token, node.obj_token)
|
|
282
|
+
|
|
283
|
+
printJson({
|
|
284
|
+
ok: true,
|
|
285
|
+
title,
|
|
286
|
+
url: node.url,
|
|
287
|
+
nodeToken: node.node_token,
|
|
288
|
+
docToken: node.obj_token,
|
|
289
|
+
blockCount: written,
|
|
290
|
+
verifyCount
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
295
|
+
main().catch((error) => {
|
|
296
|
+
console.error(error.message)
|
|
297
|
+
process.exit(1)
|
|
298
|
+
})
|
|
299
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getTenantAccessToken,
|
|
7
|
+
larkRequest,
|
|
8
|
+
parseArgs,
|
|
9
|
+
printJson,
|
|
10
|
+
resolveDocument,
|
|
11
|
+
richTextFromElements,
|
|
12
|
+
textFromElements
|
|
13
|
+
} from './lark_api.mjs'
|
|
14
|
+
|
|
15
|
+
const CACHE_TTL_MS = 5 * 60 * 1000
|
|
16
|
+
const CACHE_DIR = path.join(os.tmpdir(), 'devFlow-lark-cache')
|
|
17
|
+
|
|
18
|
+
function headingLevel(block) {
|
|
19
|
+
for (let level = 1; level <= 6; level += 1) {
|
|
20
|
+
if (block[`heading${level}`]) return level
|
|
21
|
+
}
|
|
22
|
+
return 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function blockText(block, { rich = false } = {}) {
|
|
26
|
+
const render = rich ? richTextFromElements : textFromElements
|
|
27
|
+
|
|
28
|
+
for (const key of [
|
|
29
|
+
'page',
|
|
30
|
+
'text',
|
|
31
|
+
'heading1',
|
|
32
|
+
'heading2',
|
|
33
|
+
'heading3',
|
|
34
|
+
'heading4',
|
|
35
|
+
'heading5',
|
|
36
|
+
'heading6',
|
|
37
|
+
'bullet',
|
|
38
|
+
'ordered',
|
|
39
|
+
'todo',
|
|
40
|
+
'code',
|
|
41
|
+
'quote'
|
|
42
|
+
]) {
|
|
43
|
+
if (block[key]) return render(block[key].elements || [])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function blockPlainText(block) {
|
|
50
|
+
return blockText(block, { rich: false })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function blockRichText(block) {
|
|
54
|
+
return blockText(block, { rich: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createRenderer(items) {
|
|
58
|
+
const byId = new Map(items.map((block) => [block.block_id, block]))
|
|
59
|
+
|
|
60
|
+
function renderTable(block) {
|
|
61
|
+
const cells = block.table?.cells || []
|
|
62
|
+
const cols = block.table?.property?.column_size || 1
|
|
63
|
+
const rows = []
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < cells.length; index += cols) {
|
|
66
|
+
rows.push(cells.slice(index, index + cols).map((cellId) => renderCell(cellId)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!rows.length) return '[表格]'
|
|
70
|
+
|
|
71
|
+
return rows.map((row) => row.join(' | ')).join('\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderCell(cellId) {
|
|
75
|
+
const cell = byId.get(cellId)
|
|
76
|
+
if (!cell) return ''
|
|
77
|
+
|
|
78
|
+
return (cell.children || [])
|
|
79
|
+
.map((childId) => renderBlock(childId))
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join('; ')
|
|
82
|
+
.replace(/\n+/g, '; ')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderBlock(blockId) {
|
|
86
|
+
const block = byId.get(blockId)
|
|
87
|
+
if (!block) return ''
|
|
88
|
+
|
|
89
|
+
if (block.table) return renderTable(block)
|
|
90
|
+
if (block.table_cell || block.grid || block.grid_column || block.callout) {
|
|
91
|
+
return (block.children || [])
|
|
92
|
+
.map((childId) => renderBlock(childId))
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join('\n')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const level = headingLevel(block)
|
|
98
|
+
const text = blockRichText(block)
|
|
99
|
+
let line
|
|
100
|
+
|
|
101
|
+
if (level) line = `${'#'.repeat(level)} ${text}`
|
|
102
|
+
else if (block.bullet) line = `- ${text}`
|
|
103
|
+
else if (block.ordered) line = `1. ${text}`
|
|
104
|
+
else if (block.todo) line = `- [ ] ${text}`
|
|
105
|
+
else if (block.quote) line = `> ${text}`
|
|
106
|
+
else if (block.image) line = '[图片]'
|
|
107
|
+
else line = text
|
|
108
|
+
|
|
109
|
+
const children = (block.children || [])
|
|
110
|
+
.map((childId) => renderBlock(childId))
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join('\n')
|
|
113
|
+
|
|
114
|
+
return [line, children].filter(Boolean).join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderBlockLine(blockId) {
|
|
118
|
+
const block = byId.get(blockId)
|
|
119
|
+
if (!block) return ''
|
|
120
|
+
|
|
121
|
+
if (block.table) return renderTable(block)
|
|
122
|
+
if (block.table_cell || block.grid || block.grid_column || block.callout) return ''
|
|
123
|
+
|
|
124
|
+
const level = headingLevel(block)
|
|
125
|
+
const text = blockRichText(block)
|
|
126
|
+
|
|
127
|
+
if (level) return `${'#'.repeat(level)} ${text}`
|
|
128
|
+
if (block.bullet) return `- ${text}`
|
|
129
|
+
if (block.ordered) return `1. ${text}`
|
|
130
|
+
if (block.todo) return `- [ ] ${text}`
|
|
131
|
+
if (block.quote) return `> ${text}`
|
|
132
|
+
if (block.image) return '[图片]'
|
|
133
|
+
return text
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { byId, renderBlock, renderBlockLine }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readAllBlocks(docToken, token) {
|
|
140
|
+
let pageToken = ''
|
|
141
|
+
const items = []
|
|
142
|
+
|
|
143
|
+
do {
|
|
144
|
+
const payload = await larkRequest(
|
|
145
|
+
`/open-apis/docx/v1/documents/${docToken}/blocks?page_size=500${
|
|
146
|
+
pageToken ? `&page_token=${encodeURIComponent(pageToken)}` : ''
|
|
147
|
+
}`,
|
|
148
|
+
{ token }
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
items.push(...(payload.data?.items || []))
|
|
152
|
+
pageToken = payload.data?.page_token || ''
|
|
153
|
+
} while (pageToken)
|
|
154
|
+
|
|
155
|
+
return items
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getCachePath(docToken) {
|
|
159
|
+
return path.join(CACHE_DIR, `${encodeURIComponent(docToken)}.json`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readCache(docToken) {
|
|
163
|
+
const cachePath = getCachePath(docToken)
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(cachePath)) return null
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'))
|
|
169
|
+
if (Date.now() - cache.createdAt > CACHE_TTL_MS) return null
|
|
170
|
+
return cache
|
|
171
|
+
} catch {
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeCache(docToken, cache) {
|
|
177
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
178
|
+
fs.writeFileSync(getCachePath(docToken), JSON.stringify(cache))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseSections(args) {
|
|
182
|
+
if (args.sections) {
|
|
183
|
+
return String(args.sections)
|
|
184
|
+
.split(/[,,]/)
|
|
185
|
+
.map((item) => item.trim())
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (args.section) return [String(args.section)]
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function findSection(blocks, section, expectedLevel) {
|
|
194
|
+
const exactMatches = blocks
|
|
195
|
+
.map((block, index) => ({ block, index }))
|
|
196
|
+
.filter(({ block }) => {
|
|
197
|
+
const levelMatches = expectedLevel ? block.level === expectedLevel : block.level > 0
|
|
198
|
+
return levelMatches && block.text.trim() === section
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (exactMatches.length === 1) return { match: exactMatches[0], candidates: [] }
|
|
202
|
+
|
|
203
|
+
const fallbackMatches = blocks
|
|
204
|
+
.map((block, index) => ({ block, index }))
|
|
205
|
+
.filter(({ block }) => block.level > 0 && block.text.trim() === section)
|
|
206
|
+
|
|
207
|
+
if (fallbackMatches.length === 1) return { match: fallbackMatches[0], candidates: [] }
|
|
208
|
+
|
|
209
|
+
const candidates = (fallbackMatches.length ? fallbackMatches : exactMatches).map(({ block }) => ({
|
|
210
|
+
level: block.level,
|
|
211
|
+
text: block.text
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
return { match: null, candidates }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractSection(blocks, section, expectedLevel) {
|
|
218
|
+
const { match, candidates } = findSection(blocks, section, expectedLevel)
|
|
219
|
+
|
|
220
|
+
if (!match) {
|
|
221
|
+
return {
|
|
222
|
+
heading: section,
|
|
223
|
+
found: false,
|
|
224
|
+
candidates,
|
|
225
|
+
markdown: ''
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const startIndex = match.index
|
|
230
|
+
const startLevel = blocks[startIndex].level || 1
|
|
231
|
+
let endIndex = blocks.length
|
|
232
|
+
|
|
233
|
+
for (let index = startIndex + 1; index < blocks.length; index += 1) {
|
|
234
|
+
if (blocks[index].level > 0 && blocks[index].level <= startLevel) {
|
|
235
|
+
endIndex = index
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
heading: blocks[startIndex].text,
|
|
242
|
+
level: blocks[startIndex].level,
|
|
243
|
+
found: true,
|
|
244
|
+
markdown: blocks
|
|
245
|
+
.slice(startIndex, endIndex)
|
|
246
|
+
.map((block) => block.markdown)
|
|
247
|
+
.join('\n\n')
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function main() {
|
|
252
|
+
const args = parseArgs()
|
|
253
|
+
const input = args.url || args.token
|
|
254
|
+
|
|
255
|
+
if (!input)
|
|
256
|
+
throw new Error(
|
|
257
|
+
'Usage: node lark_read_docx.mjs --url <feishu-url> [--section name] [--level 3]'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const token = await getTenantAccessToken()
|
|
261
|
+
const document = await resolveDocument(input, token)
|
|
262
|
+
const cached = args['no-cache'] ? null : readCache(document.docToken)
|
|
263
|
+
const meta =
|
|
264
|
+
cached?.meta ||
|
|
265
|
+
(await larkRequest(`/open-apis/docx/v1/documents/${document.docToken}`, { token }))
|
|
266
|
+
const items = cached?.items || (await readAllBlocks(document.docToken, token))
|
|
267
|
+
const { byId, renderBlockLine } = createRenderer(items)
|
|
268
|
+
|
|
269
|
+
if (!cached && !args['no-cache']) {
|
|
270
|
+
writeCache(document.docToken, {
|
|
271
|
+
createdAt: Date.now(),
|
|
272
|
+
meta,
|
|
273
|
+
items
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const root = items[0]
|
|
278
|
+
const blocks = []
|
|
279
|
+
|
|
280
|
+
function flatten(blockId) {
|
|
281
|
+
const block = byId.get(blockId)
|
|
282
|
+
if (!block) return
|
|
283
|
+
|
|
284
|
+
const rendered = renderBlockLine(blockId)
|
|
285
|
+
if (rendered) {
|
|
286
|
+
blocks.push({
|
|
287
|
+
id: blockId,
|
|
288
|
+
level: headingLevel(block),
|
|
289
|
+
text: blockPlainText(block).trim(),
|
|
290
|
+
markdown: rendered
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!block.table) {
|
|
295
|
+
for (const childId of block.children || []) flatten(childId)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const childId of root?.children || []) flatten(childId)
|
|
300
|
+
|
|
301
|
+
const headings = blocks
|
|
302
|
+
.filter((block) => block.level > 0)
|
|
303
|
+
.map(({ level, text }) => ({ level, text }))
|
|
304
|
+
|
|
305
|
+
const expectedLevel = args.level ? Number(args.level) : null
|
|
306
|
+
const requestedSections = parseSections(args)
|
|
307
|
+
const sections = requestedSections.map((section) =>
|
|
308
|
+
extractSection(blocks, section, expectedLevel)
|
|
309
|
+
)
|
|
310
|
+
const sectionMarkdown = sections.length === 1 ? sections[0].markdown : ''
|
|
311
|
+
|
|
312
|
+
printJson({
|
|
313
|
+
title: meta.data?.document?.title || document.title,
|
|
314
|
+
docToken: document.docToken,
|
|
315
|
+
nodeToken: document.nodeToken,
|
|
316
|
+
type: document.inputType,
|
|
317
|
+
blockCount: items.length,
|
|
318
|
+
cacheHit: Boolean(cached),
|
|
319
|
+
headings,
|
|
320
|
+
sections,
|
|
321
|
+
sectionMarkdown
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
main().catch((error) => {
|
|
326
|
+
console.error(error.message)
|
|
327
|
+
process.exit(1)
|
|
328
|
+
})
|