@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,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
|
+
})
|