@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/cli.mjs +9 -0
  4. package/package.json +39 -0
  5. package/skills/artifact/README.md +23 -0
  6. package/skills/execution/devFlow/SKILL.md +341 -0
  7. package/skills/execution/devFlow/agents/openai.yaml +7 -0
  8. package/skills/execution/devFlow/domains/backend/references/api-tech.md +173 -0
  9. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.mjs +170 -0
  10. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.test.mjs +89 -0
  11. package/skills/execution/devFlow/domains/backend/templates/api-tech.md +164 -0
  12. package/skills/execution/devFlow/domains/frontend/references/contract-check.md +270 -0
  13. package/skills/execution/devFlow/domains/frontend/references/foundation-freeze.md +92 -0
  14. package/skills/execution/devFlow/domains/frontend/references/lightweight-flow.md +55 -0
  15. package/skills/execution/devFlow/domains/frontend/references/page-build.md +268 -0
  16. package/skills/execution/devFlow/domains/frontend/references/page-tech.md +414 -0
  17. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/field-common.md +69 -0
  18. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-complex.md +105 -0
  19. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-id-code.md +44 -0
  20. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-number-date.md +106 -0
  21. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-relation.md +144 -0
  22. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-selectors.md +102 -0
  23. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-text.md +130 -0
  24. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/index.md +42 -0
  25. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/interaction.md +61 -0
  26. package/skills/execution/devFlow/domains/frontend/scripts/check_page_tech_doc.mjs +82 -0
  27. package/skills/execution/devFlow/domains/frontend/scripts/contract_check_static.mjs +145 -0
  28. package/skills/execution/devFlow/domains/frontend/scripts/generate_foundation_summary.mjs +224 -0
  29. package/skills/execution/devFlow/domains/frontend/templates/contract-report.md +66 -0
  30. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l0.md +7 -0
  31. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l1.md +9 -0
  32. package/skills/execution/devFlow/domains/frontend/templates/page-build/component.tsx.tpl +8 -0
  33. package/skills/execution/devFlow/domains/frontend/templates/page-build/constants.ts.tpl +5 -0
  34. package/skills/execution/devFlow/domains/frontend/templates/page-build/index.ts.tpl +1 -0
  35. package/skills/execution/devFlow/domains/frontend/templates/page-build/route.tsx.tpl +11 -0
  36. package/skills/execution/devFlow/domains/frontend/templates/page-build/service.ts.tpl +17 -0
  37. package/skills/execution/devFlow/domains/frontend/templates/page-build/types.ts.tpl +11 -0
  38. package/skills/execution/devFlow/domains/frontend/templates/page-tech.md +103 -0
  39. package/skills/execution/devFlow/domains/requirement/references/prd-review.md +129 -0
  40. package/skills/execution/devFlow/domains/requirement/templates/prd-review.md +57 -0
  41. package/skills/execution/figmaSync/SKILL.md +349 -0
  42. package/skills/execution/figmaSync/chapters/apply.md +177 -0
  43. package/skills/execution/figmaSync/chapters/plan.md +359 -0
  44. package/skills/execution/figmaSync/chapters/prepare.md +47 -0
  45. package/skills/execution/figmaSync/examples/native-css.md +129 -0
  46. package/skills/execution/figmaSync/examples/plan-doc-sample.md +39 -0
  47. package/skills/execution/figmaSync/scripts/_shared/session-log.mjs +82 -0
  48. package/skills/execution/figmaSync/scripts/_shared/token-resolver.mjs +132 -0
  49. package/skills/execution/figmaSync/scripts/_shared/token-source.mjs +145 -0
  50. package/skills/execution/figmaSync/scripts/figma-sync-report.mjs +448 -0
  51. package/skills/execution/figmaSync/scripts/icon-inventory.mjs +165 -0
  52. package/skills/execution/figmaSync/scripts/lookup-var.mjs +184 -0
  53. package/skills/execution/figmaSync/scripts/match-token.mjs +210 -0
  54. package/skills/execution/figmaSync/scripts/prepare-check.mjs +290 -0
  55. package/skills/execution/figmaSync/scripts/verify-plan.mjs +458 -0
  56. package/skills/execution/figmaSync/snapshots/README.md +13 -0
  57. package/skills/execution/figmaSync/snapshots/last-sync.md +1538 -0
  58. package/skills/execution/figmaSync/templates/PLAN.md.tpl +227 -0
  59. package/skills/review/consistency-checker/SKILL.md +112 -0
  60. package/skills/review/consistency-checker/rules/README.md +16 -0
  61. package/src/cli/agents.mjs +16 -0
  62. package/src/cli/copy.mjs +95 -0
  63. package/src/cli/install.mjs +131 -0
  64. package/src/cli/prompts.mjs +66 -0
  65. package/tools/lark/README.md +78 -0
  66. package/tools/lark/references/lark-doc.md +156 -0
  67. package/tools/lark/references/lark-read.md +235 -0
  68. package/tools/lark/references/prepare.md +147 -0
  69. package/tools/lark/scripts/lark_api.mjs +404 -0
  70. package/tools/lark/scripts/lark_api.test.mjs +63 -0
  71. package/tools/lark/scripts/lark_check_permissions.mjs +52 -0
  72. package/tools/lark/scripts/lark_publish_doc.mjs +299 -0
  73. package/tools/lark/scripts/lark_read_docx.mjs +328 -0
  74. 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
+ })