@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,404 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ const FEISHU_API_BASE = 'https://open.feishu.cn'
4
+ const RATE_LIMIT_CODE = 99991400
5
+ const DEFAULT_RETRY_DELAYS = [1000, 2000, 4000]
6
+ const LARKCLI_BIN = process.env.FEISHU_LARKCLI_BIN || 'lark-cli'
7
+
8
+ // 后端选择:openapi(默认,应用身份 + Open API)| larkcli(lark-cli,用户身份)| auto。
9
+ let cachedBackend = null
10
+
11
+ function larkcliIdentity() {
12
+ return process.env.FEISHU_LARKCLI_AS || 'user'
13
+ }
14
+
15
+ // 把 lark-cli 调用包成 Promise;body 经 stdin(--data -)传入,避免大 JSON 的参数长度/转义问题。
16
+ function runLarkcli(args, bodyString = null) {
17
+ return new Promise((resolve) => {
18
+ let child
19
+ try {
20
+ child = spawn(LARKCLI_BIN, args, { stdio: ['pipe', 'pipe', 'pipe'] })
21
+ } catch (error) {
22
+ resolve({ exitCode: -1, stdout: '', stderr: error.message })
23
+ return
24
+ }
25
+
26
+ let stdout = ''
27
+ let stderr = ''
28
+ child.stdout.on('data', (chunk) => {
29
+ stdout += chunk
30
+ })
31
+ child.stderr.on('data', (chunk) => {
32
+ stderr += chunk
33
+ })
34
+ child.on('error', (error) => {
35
+ resolve({ exitCode: -1, stdout: '', stderr: error.message })
36
+ })
37
+ child.on('close', (code) => {
38
+ resolve({ exitCode: code, stdout, stderr })
39
+ })
40
+
41
+ if (bodyString != null) child.stdin.write(bodyString)
42
+ child.stdin.end()
43
+ })
44
+ }
45
+
46
+ async function larkcliReady() {
47
+ const { exitCode, stdout } = await runLarkcli(['auth', 'status'])
48
+ if (exitCode !== 0) return false
49
+
50
+ try {
51
+ const status = JSON.parse(stdout)
52
+ const identity = status.identities?.[larkcliIdentity()]
53
+ return Boolean(identity && (identity.status === 'ready' || identity.available))
54
+ } catch {
55
+ return false
56
+ }
57
+ }
58
+
59
+ export async function getBackend() {
60
+ if (cachedBackend) return cachedBackend
61
+
62
+ const raw = (process.env.FEISHU_BACKEND || 'openapi').toLowerCase()
63
+
64
+ if (raw === 'openapi' || raw === 'larkcli') {
65
+ cachedBackend = raw
66
+ return raw
67
+ }
68
+
69
+ if (raw !== 'auto') {
70
+ throw new Error(`Unknown FEISHU_BACKEND: ${raw} (expected openapi | larkcli | auto)`)
71
+ }
72
+
73
+ if (await larkcliReady()) {
74
+ cachedBackend = 'larkcli'
75
+ return cachedBackend
76
+ }
77
+
78
+ if (process.env.FEISHU_APP_ID && process.env.FEISHU_APP_SECRET) {
79
+ cachedBackend = 'openapi'
80
+ return cachedBackend
81
+ }
82
+
83
+ throw new Error(
84
+ 'FEISHU_BACKEND=auto 无可用后端:lark-cli 未登录,且缺少 FEISHU_APP_ID/FEISHU_APP_SECRET'
85
+ )
86
+ }
87
+
88
+ // 拆 path 的 ?query —— lark-cli api 会丢弃 path 内的 query,必须改用 --params 传。
89
+ export function splitPathQuery(path) {
90
+ const index = path.indexOf('?')
91
+ if (index === -1) return { path, params: null }
92
+
93
+ const params = {}
94
+ for (const pair of path.slice(index + 1).split('&')) {
95
+ if (!pair) continue
96
+ const eq = pair.indexOf('=')
97
+ const key = decodeURIComponent(eq === -1 ? pair : pair.slice(0, eq))
98
+ const value = eq === -1 ? '' : decodeURIComponent(pair.slice(eq + 1))
99
+ params[key] = value
100
+ }
101
+
102
+ return { path: path.slice(0, index), params }
103
+ }
104
+
105
+ export function buildLarkcliArgs(method, path, { as, hasBody }) {
106
+ const upper = method.toUpperCase()
107
+ const { path: cleanPath, params } = splitPathQuery(path)
108
+ const args = ['api', upper, cleanPath, '--format', 'json', '--as', as]
109
+
110
+ if (params) args.push('--params', JSON.stringify(params))
111
+ if (hasBody) args.push('--data', '-')
112
+ // 注:lark-cli 的 raw `api` 命令不接受 --yes(那是 typed 域命令的高危写确认标志);
113
+ // raw api 的写操作不需要也不识别 --yes。
114
+
115
+ return args
116
+ }
117
+
118
+ export function parseLarkcliSuccess(stdout) {
119
+ const json = JSON.parse(stdout)
120
+ return { code: 0, data: json.data }
121
+ }
122
+
123
+ export function parseLarkcliError(stderr, exitCode) {
124
+ try {
125
+ const json = JSON.parse(stderr)
126
+ const error = json.error || {}
127
+ return {
128
+ code: error.code ?? -1,
129
+ message: error.message || 'lark-cli error',
130
+ logId: error.log_id
131
+ }
132
+ } catch {
133
+ return { code: -1, message: (stderr || `lark-cli exit ${exitCode}`).trim() }
134
+ }
135
+ }
136
+
137
+ async function larkcliRequest(path, options = {}) {
138
+ const method = (options.method || 'GET').toUpperCase()
139
+ const body = options.body
140
+ const retryDelays = options.retryDelays || DEFAULT_RETRY_DELAYS
141
+ const args = buildLarkcliArgs(method, path, { as: larkcliIdentity(), hasBody: body != null })
142
+
143
+ for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
144
+ const { exitCode, stdout, stderr } = await runLarkcli(args, body ?? null)
145
+
146
+ if (exitCode === 0) {
147
+ try {
148
+ return parseLarkcliSuccess(stdout)
149
+ } catch {
150
+ throw new Error(`${path} larkcli: 无法解析输出 ${stdout.slice(0, 200)}`)
151
+ }
152
+ }
153
+
154
+ const error = parseLarkcliError(stderr, exitCode)
155
+
156
+ if (error.code === RATE_LIMIT_CODE && attempt < retryDelays.length) {
157
+ await delay(retryDelays[attempt])
158
+ continue
159
+ }
160
+
161
+ throw new Error(`${path} ${error.code} ${error.message}`)
162
+ }
163
+ }
164
+
165
+ export function requireEnv(names) {
166
+ const missing = names.filter((name) => !process.env[name])
167
+ if (missing.length > 0) {
168
+ throw new Error(`Missing env: ${missing.join(', ')}`)
169
+ }
170
+ }
171
+
172
+ export function parseArgs(argv = process.argv.slice(2)) {
173
+ const args = {}
174
+
175
+ for (let index = 0; index < argv.length; index += 1) {
176
+ const item = argv[index]
177
+
178
+ if (!item.startsWith('--')) continue
179
+
180
+ const key = item.slice(2)
181
+ const next = argv[index + 1]
182
+
183
+ if (!next || next.startsWith('--')) {
184
+ args[key] = true
185
+ continue
186
+ }
187
+
188
+ args[key] = next
189
+ index += 1
190
+ }
191
+
192
+ return args
193
+ }
194
+
195
+ export async function getTenantAccessToken() {
196
+ if ((await getBackend()) === 'larkcli') {
197
+ if (!(await larkcliReady())) {
198
+ throw new Error('FEISHU_BACKEND=larkcli:lark-cli 未登录,请运行 lark-cli auth login')
199
+ }
200
+ // larkcli 模式由 lark-cli 管理鉴权,无应用 token;返回哨兵,larkRequest 会忽略它。
201
+ return null
202
+ }
203
+
204
+ requireEnv(['FEISHU_APP_ID', 'FEISHU_APP_SECRET'])
205
+
206
+ const response = await fetch(
207
+ `${FEISHU_API_BASE}/open-apis/auth/v3/tenant_access_token/internal`,
208
+ {
209
+ method: 'POST',
210
+ headers: { 'content-type': 'application/json' },
211
+ body: JSON.stringify({
212
+ app_id: process.env.FEISHU_APP_ID,
213
+ app_secret: process.env.FEISHU_APP_SECRET
214
+ })
215
+ }
216
+ )
217
+ const payload = await response.json()
218
+
219
+ if (payload.code !== 0) {
220
+ throw new Error(`auth ${payload.code} ${payload.msg}`)
221
+ }
222
+
223
+ return payload.tenant_access_token
224
+ }
225
+
226
+ function delay(ms) {
227
+ return new Promise((resolve) => {
228
+ setTimeout(resolve, ms)
229
+ })
230
+ }
231
+
232
+ export async function larkRequest(path, options = {}) {
233
+ if ((await getBackend()) === 'larkcli') {
234
+ return larkcliRequest(path, options)
235
+ }
236
+
237
+ const token = options.token || (await getTenantAccessToken())
238
+ const retryDelays = options.retryDelays || DEFAULT_RETRY_DELAYS
239
+ const requestOptions = { ...options }
240
+ delete requestOptions.token
241
+ delete requestOptions.retryDelays
242
+
243
+ for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
244
+ const response = await fetch(`${FEISHU_API_BASE}${path}`, {
245
+ ...requestOptions,
246
+ headers: {
247
+ authorization: `Bearer ${token}`,
248
+ 'content-type': 'application/json',
249
+ ...(options.headers || {})
250
+ }
251
+ })
252
+ const text = await response.text()
253
+ let payload
254
+
255
+ try {
256
+ payload = JSON.parse(text)
257
+ } catch {
258
+ payload = { raw: text }
259
+ }
260
+
261
+ if (payload.code === 0) {
262
+ return payload
263
+ }
264
+
265
+ if (payload.code === RATE_LIMIT_CODE && attempt < retryDelays.length) {
266
+ await delay(retryDelays[attempt])
267
+ continue
268
+ }
269
+
270
+ const detail = payload.error ? ` ${JSON.stringify(payload.error)}` : ''
271
+ throw new Error(`${path} ${payload.code} ${payload.msg}${detail}`)
272
+ }
273
+ }
274
+
275
+ export function extractFeishuToken(input) {
276
+ if (!input) return null
277
+
278
+ try {
279
+ const url = new URL(input)
280
+ const parts = url.pathname.split('/').filter(Boolean)
281
+ const type = parts[0]
282
+ const token = parts[1]
283
+
284
+ if (['wiki', 'docx', 'docs'].includes(type) && token) {
285
+ return { type, token, url: input }
286
+ }
287
+ } catch {
288
+ // Treat plain strings as tokens.
289
+ }
290
+
291
+ return { type: 'token', token: input, url: input }
292
+ }
293
+
294
+ export async function resolveDocument(input, token) {
295
+ const parsed = extractFeishuToken(input)
296
+
297
+ if (!parsed) {
298
+ throw new Error('Missing Feishu document url or token')
299
+ }
300
+
301
+ if (parsed.type === 'wiki') {
302
+ const nodePayload = await larkRequest(
303
+ `/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(parsed.token)}`,
304
+ { token }
305
+ )
306
+ const node = nodePayload.data?.node
307
+
308
+ if (!node) throw new Error('Wiki node not found')
309
+ if (node.obj_type !== 'docx') {
310
+ throw new Error(`Unsupported wiki object type: ${node.obj_type}`)
311
+ }
312
+
313
+ return {
314
+ inputType: 'wiki',
315
+ title: node.title,
316
+ docToken: node.obj_token,
317
+ nodeToken: node.node_token,
318
+ spaceId: node.space_id,
319
+ url: node.url || parsed.url
320
+ }
321
+ }
322
+
323
+ if (parsed.type === 'docx' || parsed.type === 'token') {
324
+ return {
325
+ inputType: 'docx',
326
+ docToken: parsed.token,
327
+ url: parsed.url
328
+ }
329
+ }
330
+
331
+ throw new Error(`Unsupported Feishu link type: ${parsed.type}`)
332
+ }
333
+
334
+ export function textFromElements(elements = []) {
335
+ return elements
336
+ .map((element) => {
337
+ if (element.text_run) return element.text_run.content || ''
338
+ if (element.mention_user) return element.mention_user.name || '@用户'
339
+ if (element.docs_link) return element.docs_link.url || element.docs_link.title || ''
340
+ if (element.file) return element.file.name || ''
341
+ if (element.equation) return element.equation.content || ''
342
+ if (element.reminder) return element.reminder.text || ''
343
+ return ''
344
+ })
345
+ .join('')
346
+ }
347
+
348
+ // 把飞书 text_run 的 text_element_style 按固定嵌套顺序 link > strikethrough > bold > italic > inline_code
349
+ // 包装成 Markdown。inline_code 必须在最内层:Markdown 反引号内的内容不会被解析,
350
+ // 若把 `~~x~~` 反过来包就渲染成字面横线而不是删除线。
351
+ // 下划线和颜色忽略(Markdown 无原生语义)。
352
+ function wrapRichText(content, style = {}) {
353
+ if (!content) return ''
354
+
355
+ const trimmed = content.trim()
356
+ if (!trimmed) return content
357
+
358
+ const leading = content.slice(0, content.length - content.trimStart().length)
359
+ const trailing = content.slice(content.trimEnd().length)
360
+ let inner = trimmed
361
+
362
+ if (style.inline_code) inner = `\`${inner}\``
363
+ if (style.italic) inner = `*${inner}*`
364
+ if (style.bold) inner = `**${inner}**`
365
+ if (style.strikethrough) inner = `~~${inner}~~`
366
+ if (style.link?.url) {
367
+ let url = style.link.url
368
+ try {
369
+ url = decodeURIComponent(url)
370
+ } catch {
371
+ // 保留原样
372
+ }
373
+ inner = `[${inner}](${url})`
374
+ }
375
+
376
+ return `${leading}${inner}${trailing}`
377
+ }
378
+
379
+ export function richTextFromElements(elements = []) {
380
+ return elements
381
+ .map((element) => {
382
+ if (element.text_run) {
383
+ return wrapRichText(element.text_run.content || '', element.text_run.text_element_style)
384
+ }
385
+ if (element.mention_user) return element.mention_user.name || '@用户'
386
+ if (element.docs_link) {
387
+ const url = element.docs_link.url || ''
388
+ const title = element.docs_link.title || url
389
+ return url ? `[${title}](${url})` : title
390
+ }
391
+ if (element.file) return element.file.name || ''
392
+ if (element.equation) {
393
+ const content = element.equation.content || ''
394
+ return content ? `$${content}$` : ''
395
+ }
396
+ if (element.reminder) return element.reminder.text || ''
397
+ return ''
398
+ })
399
+ .join('')
400
+ }
401
+
402
+ export function printJson(value) {
403
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
404
+ }
@@ -0,0 +1,63 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ splitPathQuery,
6
+ buildLarkcliArgs,
7
+ parseLarkcliSuccess,
8
+ parseLarkcliError
9
+ } from './lark_api.mjs'
10
+
11
+ test('splitPathQuery: 无 query 原样返回,params 为 null', () => {
12
+ const r = splitPathQuery('/open-apis/docx/v1/documents/abc')
13
+ assert.equal(r.path, '/open-apis/docx/v1/documents/abc')
14
+ assert.equal(r.params, null)
15
+ })
16
+
17
+ test('splitPathQuery: 拆出 query 为对象,并解码', () => {
18
+ const r = splitPathQuery('/open-apis/docx/v1/documents/abc/blocks?page_size=500&page_token=t%2B1')
19
+ assert.equal(r.path, '/open-apis/docx/v1/documents/abc/blocks')
20
+ assert.deepEqual(r.params, { page_size: '500', page_token: 't+1' })
21
+ })
22
+
23
+ test('buildLarkcliArgs: GET 无 body 无 query —— 不带 --params/--data/--yes', () => {
24
+ const args = buildLarkcliArgs('GET', '/open-apis/x', { as: 'user', hasBody: false })
25
+ assert.deepEqual(args, ['api', 'GET', '/open-apis/x', '--format', 'json', '--as', 'user'])
26
+ })
27
+
28
+ test('buildLarkcliArgs: GET 带 query —— 加 --params JSON', () => {
29
+ const args = buildLarkcliArgs('GET', '/open-apis/x?a=1&b=2', { as: 'user', hasBody: false })
30
+ assert.ok(args.includes('--params'))
31
+ const idx = args.indexOf('--params')
32
+ assert.deepEqual(JSON.parse(args[idx + 1]), { a: '1', b: '2' })
33
+ assert.ok(!args.includes('--data'))
34
+ assert.ok(!args.includes('--yes'))
35
+ })
36
+
37
+ test('buildLarkcliArgs: POST 带 body —— 加 --data -,且不加 --yes(raw api 不识别)', () => {
38
+ const args = buildLarkcliArgs('POST', '/open-apis/x', { as: 'bot', hasBody: true })
39
+ assert.ok(args.includes('--as'))
40
+ assert.equal(args[args.indexOf('--as') + 1], 'bot')
41
+ assert.deepEqual(args.slice(args.indexOf('--data'), args.indexOf('--data') + 2), ['--data', '-'])
42
+ assert.ok(!args.includes('--yes'))
43
+ })
44
+
45
+ test('parseLarkcliSuccess: {ok,data} 重包成 {code:0,data}', () => {
46
+ const out = JSON.stringify({ ok: true, identity: 'user', data: { items: [1, 2], page_token: 'p' } })
47
+ const r = parseLarkcliSuccess(out)
48
+ assert.equal(r.code, 0)
49
+ assert.deepEqual(r.data, { items: [1, 2], page_token: 'p' })
50
+ })
51
+
52
+ test('parseLarkcliError: 结构化 stderr 取 error.code/message', () => {
53
+ const err = JSON.stringify({ ok: false, error: { code: 99991400, message: 'rate limited', log_id: 'L1' } })
54
+ const r = parseLarkcliError(err, 1)
55
+ assert.equal(r.code, 99991400)
56
+ assert.equal(r.message, 'rate limited')
57
+ })
58
+
59
+ test('parseLarkcliError: 非 JSON stderr 回退', () => {
60
+ const r = parseLarkcliError('boom not json', 1)
61
+ assert.equal(r.code, -1)
62
+ assert.ok(r.message.includes('boom'))
63
+ })
@@ -0,0 +1,52 @@
1
+ import {
2
+ getTenantAccessToken,
3
+ larkRequest,
4
+ parseArgs,
5
+ printJson,
6
+ requireEnv,
7
+ resolveDocument
8
+ } from './lark_api.mjs'
9
+
10
+ async function main() {
11
+ const args = parseArgs()
12
+ const token = await getTenantAccessToken()
13
+ const checks = []
14
+
15
+ if (args.url) {
16
+ const document = await resolveDocument(args.url, token)
17
+ checks.push({
18
+ name: 'resolve-document',
19
+ ok: true,
20
+ title: document.title,
21
+ inputType: document.inputType,
22
+ docToken: document.docToken,
23
+ nodeToken: document.nodeToken
24
+ })
25
+ }
26
+
27
+ if (args.parent || process.env.FEISHU_WIKI_PARENT_NODE_TOKEN) {
28
+ requireEnv(['FEISHU_WIKI_PARENT_NODE_TOKEN'])
29
+ const parent = args.parent || process.env.FEISHU_WIKI_PARENT_NODE_TOKEN
30
+ const payload = await larkRequest(
31
+ `/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(parent)}`,
32
+ { token }
33
+ )
34
+ const node = payload.data?.node
35
+
36
+ checks.push({
37
+ name: 'wiki-parent-read',
38
+ ok: true,
39
+ title: node?.title,
40
+ spaceId: node?.space_id,
41
+ nodeToken: node?.node_token,
42
+ objType: node?.obj_type
43
+ })
44
+ }
45
+
46
+ printJson({ ok: true, checks })
47
+ }
48
+
49
+ main().catch((error) => {
50
+ console.error(error.message)
51
+ process.exit(1)
52
+ })