dsh-plugin-yapi 0.0.0-stage → 1.4.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 +166 -2
- package/bin/yapi-doc.mjs +3 -0
- package/client.js +2464 -0
- package/cordis.patch.yml +9 -0
- package/index.js +554 -0
- package/lib/core.js +434 -0
- package/package.json +66 -4
- package/skills/yapi-doc/SKILL.md +68 -0
- package/skills/yapi-doc/yapi-doc.mjs +863 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yapi-doc — read YAPI interface documentation from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Standalone on purpose: no plugin imports, no npm dependencies. Copy this one
|
|
6
|
+
* file anywhere and run it with Node 20+. It reads the YAPI account from
|
|
7
|
+
* `$DSH_HOME/dsh-plugin-yapi.json` (the panel's 账号设置 writes it) or from
|
|
8
|
+
* YAPI_BASE_URL / YAPI_EMAIL / YAPI_PASSWORD.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* yapi-doc 10001
|
|
12
|
+
* yapi-doc https://your-yapi.example.com/project/1/interface/api/10001
|
|
13
|
+
* yapi-doc --search warehouse --project 1
|
|
14
|
+
* yapi-doc 10001 --full # include the response body example
|
|
15
|
+
* yapi-doc 10001 --json # structured output
|
|
16
|
+
* yapi-doc 10001 --no-fields # skip field documentation
|
|
17
|
+
* yapi-doc install-skill # install SKILL.md + CLI into ~/.agents/skills/yapi-doc
|
|
18
|
+
* yapi-doc sync-skills # install into every detected tool skill root
|
|
19
|
+
*/
|
|
20
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
import { dirname, join, resolve } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
26
|
+
const CONFIG_FILE = join(DSH_HOME, 'dsh-plugin-yapi.json')
|
|
27
|
+
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
/* helpers */
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
|
|
32
|
+
function normalizeBaseUrl(value) {
|
|
33
|
+
return String(value === undefined || value === null ? '' : value).trim().replace(/\/+$/u, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function joinUrl() {
|
|
37
|
+
let out = ''
|
|
38
|
+
for (let index = 0; index < arguments.length; index += 1) {
|
|
39
|
+
const text = String(arguments[index] === undefined || arguments[index] === null ? '' : arguments[index]).trim()
|
|
40
|
+
if (text === '') continue
|
|
41
|
+
if (out === '') out = text.replace(/\/+$/u, '')
|
|
42
|
+
else out += '/' + text.replace(/^\/+/u, '').replace(/\/+$/u, '')
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseDocId(value) {
|
|
48
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value
|
|
49
|
+
const text = String(value === undefined || value === null ? '' : value).trim()
|
|
50
|
+
if (text === '') return null
|
|
51
|
+
if (/^\d{1,12}$/u.test(text)) return Number(text) > 0 ? Number(text) : null
|
|
52
|
+
const patterns = [/\/interface\/api\/(\d{1,12})(?:[/?#]|$)/u, /[?&]id=(\d{1,12})(?:[&#]|$)/u]
|
|
53
|
+
for (const pattern of patterns) {
|
|
54
|
+
const match = pattern.exec(text)
|
|
55
|
+
if (match && Number(match[1]) > 0) return Number(match[1])
|
|
56
|
+
}
|
|
57
|
+
const trailing = /(\d{1,12})\/?(?:[?#].*)?$/u.exec(text)
|
|
58
|
+
return trailing && Number(trailing[1]) > 0 ? Number(trailing[1]) : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripHtml(html) {
|
|
62
|
+
return String(html === undefined || html === null ? '' : html)
|
|
63
|
+
.replace(/<br\s*\/?>/giu, '\n')
|
|
64
|
+
.replace(/<\/(?:p|div|li|h[1-6]|tr)>/giu, '\n')
|
|
65
|
+
.replace(/<[^>]+>/gu, '')
|
|
66
|
+
.replace(/ /giu, ' ')
|
|
67
|
+
.replace(/</giu, '<')
|
|
68
|
+
.replace(/>/giu, '>')
|
|
69
|
+
.replace(/&/giu, '&')
|
|
70
|
+
.replace(/"/giu, '"')
|
|
71
|
+
.replace(/'/giu, "'")
|
|
72
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
73
|
+
.trim()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseJson(text) {
|
|
77
|
+
if (typeof text !== 'string' || text.trim() === '') return undefined
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(text)
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function unwrapComposite(input) {
|
|
86
|
+
let current = input && typeof input === 'object' ? input : {}
|
|
87
|
+
for (let round = 0; round < 8; round += 1) {
|
|
88
|
+
if (!current || typeof current !== 'object') return {}
|
|
89
|
+
if (Array.isArray(current.allOf) && current.allOf.length > 0) {
|
|
90
|
+
const merged = Object.assign({}, current)
|
|
91
|
+
delete merged.allOf
|
|
92
|
+
const properties = Object.assign({}, current.properties || {})
|
|
93
|
+
for (const member of current.allOf) {
|
|
94
|
+
if (member && typeof member === 'object' && member.properties) Object.assign(properties, member.properties)
|
|
95
|
+
}
|
|
96
|
+
merged.properties = properties
|
|
97
|
+
merged.type = merged.type || 'object'
|
|
98
|
+
return merged
|
|
99
|
+
}
|
|
100
|
+
const list = Array.isArray(current.oneOf) ? current.oneOf : Array.isArray(current.anyOf) ? current.anyOf : null
|
|
101
|
+
if (list && list.length > 0 && list[0] && typeof list[0] === 'object') {
|
|
102
|
+
current = list[0]
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
return current
|
|
106
|
+
}
|
|
107
|
+
return current && typeof current === 'object' ? current : {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function schemaComment(node) {
|
|
111
|
+
const description = unwrapComposite(node).description
|
|
112
|
+
return String(description === undefined || description === null ? '' : description)
|
|
113
|
+
.replace(/\s+/g, ' ')
|
|
114
|
+
.trim()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function typeLabel(node) {
|
|
118
|
+
const schema = unwrapComposite(node)
|
|
119
|
+
let type = schema.type
|
|
120
|
+
if (Array.isArray(type)) type = type.filter((item) => item !== 'null').join(' | ') || 'null'
|
|
121
|
+
if (type === 'array') {
|
|
122
|
+
const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
|
|
123
|
+
const inner = items ? typeLabel(items) : ''
|
|
124
|
+
return inner ? 'array<' + inner + '>' : 'array'
|
|
125
|
+
}
|
|
126
|
+
if (!type && schema.properties) return 'object'
|
|
127
|
+
return type || ''
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function exampleFromSchema(input, depth = 0) {
|
|
131
|
+
if (depth > 40) return null
|
|
132
|
+
if (!input || typeof input !== 'object') return null
|
|
133
|
+
const schema = unwrapComposite(input)
|
|
134
|
+
if (schema.example !== undefined) return schema.example
|
|
135
|
+
if (schema.default !== undefined) return schema.default
|
|
136
|
+
let type = schema.type
|
|
137
|
+
if (Array.isArray(type)) type = type.find((item) => item !== 'null') || 'null'
|
|
138
|
+
if (schema.enum && schema.enum.length > 0) return schema.enum[0]
|
|
139
|
+
if (type === 'object' || schema.properties) {
|
|
140
|
+
const out = {}
|
|
141
|
+
for (const [key, child] of Object.entries(schema.properties || {})) out[key] = exampleFromSchema(child, depth + 1)
|
|
142
|
+
return out
|
|
143
|
+
}
|
|
144
|
+
if (type === 'array') {
|
|
145
|
+
const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
|
|
146
|
+
return items ? [exampleFromSchema(items, depth + 1)] : []
|
|
147
|
+
}
|
|
148
|
+
if (type === 'integer' || type === 'number') return 0
|
|
149
|
+
if (type === 'boolean') return false
|
|
150
|
+
if (type === 'string') {
|
|
151
|
+
if (schema.format === 'date-time') return '2024-01-01T00:00:00Z'
|
|
152
|
+
if (schema.format === 'date') return '2024-01-01'
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isSubsequence(needle, haystack) {
|
|
159
|
+
let index = 0
|
|
160
|
+
for (let cursor = 0; cursor < haystack.length && index < needle.length; cursor += 1) {
|
|
161
|
+
if (haystack[cursor] === needle[index]) index += 1
|
|
162
|
+
}
|
|
163
|
+
return index === needle.length
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function rankInterfaces(list, keyword, limit = 30) {
|
|
167
|
+
const query = String(keyword === undefined || keyword === null ? '' : keyword).trim().toLowerCase()
|
|
168
|
+
if (query === '') return []
|
|
169
|
+
const tokens = query.split(/\s+/u).filter(Boolean)
|
|
170
|
+
const ranked = []
|
|
171
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
172
|
+
if (!item || typeof item !== 'object') continue
|
|
173
|
+
const id = String(item._id ?? '')
|
|
174
|
+
const path = String(item.path ?? '')
|
|
175
|
+
const title = String(item.title ?? '')
|
|
176
|
+
const lowerPath = path.toLowerCase()
|
|
177
|
+
const lowerTitle = title.toLowerCase()
|
|
178
|
+
let score = 0
|
|
179
|
+
if (id === query) score = 1200
|
|
180
|
+
else if (lowerPath === query) score = 1100
|
|
181
|
+
else if (lowerPath.startsWith(query)) score = 1000
|
|
182
|
+
else {
|
|
183
|
+
const pathAt = lowerPath.indexOf(query)
|
|
184
|
+
if (pathAt !== -1) score = 900 - Math.min(pathAt, 80)
|
|
185
|
+
else {
|
|
186
|
+
const titleAt = lowerTitle.indexOf(query)
|
|
187
|
+
if (titleAt !== -1) score = 700 - Math.min(titleAt, 80)
|
|
188
|
+
else if (tokens.length > 1 && tokens.every((token) => `${lowerPath} ${lowerTitle}`.includes(token))) score = 500
|
|
189
|
+
else if (query.length >= 2 && isSubsequence(query, lowerPath)) score = 400
|
|
190
|
+
else if (query.length >= 2 && isSubsequence(query, lowerTitle)) score = 300
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (score > 0) ranked.push({ item, score, path: lowerPath })
|
|
194
|
+
}
|
|
195
|
+
ranked.sort(
|
|
196
|
+
(left, right) =>
|
|
197
|
+
right.score - left.score ||
|
|
198
|
+
left.path.length - right.path.length ||
|
|
199
|
+
left.path.localeCompare(right.path) ||
|
|
200
|
+
String(left.item._id).localeCompare(String(right.item._id)),
|
|
201
|
+
)
|
|
202
|
+
return ranked.slice(0, limit).map((entry) => entry.item)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeParam(item) {
|
|
206
|
+
const entry = item && typeof item === 'object' ? item : {}
|
|
207
|
+
return {
|
|
208
|
+
name: String(entry.name ?? '').trim(),
|
|
209
|
+
type: String(entry.type ?? '').trim(),
|
|
210
|
+
value: entry.value === undefined || entry.value === null ? '' : String(entry.value),
|
|
211
|
+
example: entry.example === undefined || entry.example === null ? '' : String(entry.example),
|
|
212
|
+
required: entry.required === '1' || entry.required === true,
|
|
213
|
+
desc: stripHtml(entry.desc ?? ''),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formValue(field) {
|
|
218
|
+
const type = String(field.type ?? '').toLowerCase()
|
|
219
|
+
if (field.example !== undefined && field.example !== null && field.example !== '') {
|
|
220
|
+
if (type === 'integer' || type === 'number') {
|
|
221
|
+
const parsed = Number(field.example)
|
|
222
|
+
return Number.isFinite(parsed) ? parsed : field.example
|
|
223
|
+
}
|
|
224
|
+
if (type === 'boolean') return field.example === true || field.example === 'true' || field.example === '1'
|
|
225
|
+
return field.example
|
|
226
|
+
}
|
|
227
|
+
if (type === 'integer' || type === 'number') return 0
|
|
228
|
+
if (type === 'boolean') return false
|
|
229
|
+
return ''
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function bodyView(rawText, type, isSchema, formRows) {
|
|
233
|
+
const text = typeof rawText === 'string' ? rawText : ''
|
|
234
|
+
if (type === 'form' || (!isSchema && Array.isArray(formRows) && formRows.length > 0 && text.trim() === '')) {
|
|
235
|
+
const object = {}
|
|
236
|
+
for (const row of formRows || []) object[row.name] = formValue(row)
|
|
237
|
+
const keys = Object.keys(object)
|
|
238
|
+
return { kind: keys.length > 0 ? 'json' : 'none', example: JSON.stringify(object, null, 2), raw: text }
|
|
239
|
+
}
|
|
240
|
+
if (text.trim() === '') return { kind: 'none', example: '', raw: '' }
|
|
241
|
+
if (isSchema) {
|
|
242
|
+
const schema = parseJson(text)
|
|
243
|
+
if (schema !== undefined) return { kind: 'json', example: JSON.stringify(exampleFromSchema(schema), null, 2), raw: text }
|
|
244
|
+
}
|
|
245
|
+
const parsed = parseJson(text)
|
|
246
|
+
return parsed !== undefined
|
|
247
|
+
? { kind: 'json', example: JSON.stringify(parsed, null, 2), raw: text }
|
|
248
|
+
: { kind: 'text', example: text, raw: text }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildDoc(raw, project, baseUrl) {
|
|
252
|
+
const projectId = Number(raw.project_id)
|
|
253
|
+
const path = String(raw.path ?? '')
|
|
254
|
+
const envs = Array.isArray(project?.env) ? project.env.filter((env) => env && env.domain) : []
|
|
255
|
+
const basepath = typeof project?.basepath === 'string' ? project.basepath : ''
|
|
256
|
+
return {
|
|
257
|
+
id: Number(raw._id),
|
|
258
|
+
title: String(raw.title ?? ''),
|
|
259
|
+
method: String(raw.method ?? 'GET').toUpperCase(),
|
|
260
|
+
path,
|
|
261
|
+
status: String(raw.status ?? ''),
|
|
262
|
+
tags: Array.isArray(raw.tag) ? raw.tag.map(String) : [],
|
|
263
|
+
desc: stripHtml(raw.desc ?? ''),
|
|
264
|
+
author: String(raw.username ?? ''),
|
|
265
|
+
updatedAt: Number(raw.up_time) > 0 ? Number(raw.up_time) * 1000 : null,
|
|
266
|
+
pageUrl: joinUrl(baseUrl, '/project', String(projectId), '/interface/api', String(raw._id)),
|
|
267
|
+
projectId,
|
|
268
|
+
projectName: String(project?.name ?? ''),
|
|
269
|
+
basepath,
|
|
270
|
+
address: envs.length > 0 ? joinUrl(envs[0].domain, basepath, path) : joinUrl(basepath, path),
|
|
271
|
+
headers: (Array.isArray(raw.req_headers) ? raw.req_headers : []).map(normalizeParam),
|
|
272
|
+
query: (Array.isArray(raw.req_query) ? raw.req_query : []).map(normalizeParam),
|
|
273
|
+
params: (Array.isArray(raw.req_params) ? raw.req_params : []).map(normalizeParam),
|
|
274
|
+
form: (Array.isArray(raw.req_body_form) ? raw.req_body_form : []).map(normalizeParam),
|
|
275
|
+
requestBody: bodyView(raw.req_body_other, String(raw.req_body_type ?? ''), raw.req_body_is_json_schema === true, raw.req_body_form),
|
|
276
|
+
responseBody: bodyView(raw.res_body, String(raw.res_body_type ?? ''), raw.res_body_is_json_schema === true, []),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectFields(schema, path, out) {
|
|
281
|
+
const node = unwrapComposite(schema)
|
|
282
|
+
const required = Array.isArray(node.required) ? node.required : []
|
|
283
|
+
const properties = node.properties && typeof node.properties === 'object' ? node.properties : {}
|
|
284
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
285
|
+
const childPath = path ? path + '.' + key : key
|
|
286
|
+
out.push({
|
|
287
|
+
path: childPath,
|
|
288
|
+
type: typeLabel(child),
|
|
289
|
+
required: required.includes(key),
|
|
290
|
+
description: schemaComment(child),
|
|
291
|
+
})
|
|
292
|
+
const childNode = unwrapComposite(child)
|
|
293
|
+
if (childNode.properties) collectFields(childNode, childPath, out)
|
|
294
|
+
else if (childNode.type === 'array') {
|
|
295
|
+
const items = Array.isArray(childNode.items) ? childNode.items[0] : childNode.items
|
|
296
|
+
const itemNode = unwrapComposite(items)
|
|
297
|
+
if (itemNode.properties) collectFields(itemNode, childPath + '[]', out)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function requestFields(raw) {
|
|
304
|
+
if (raw.req_body_is_json_schema === true) {
|
|
305
|
+
const schema = parseJson(raw.req_body_other)
|
|
306
|
+
return schema === undefined ? [] : collectFields(schema, '', [])
|
|
307
|
+
}
|
|
308
|
+
return (Array.isArray(raw.req_body_form) ? raw.req_body_form : []).map((row) => ({
|
|
309
|
+
path: String(row.name ?? ''),
|
|
310
|
+
type: String(row.type ?? ''),
|
|
311
|
+
required: row.required === '1' || row.required === true,
|
|
312
|
+
description: stripHtml(row.desc ?? ''),
|
|
313
|
+
}))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function responseFields(raw) {
|
|
317
|
+
if (raw.res_body_is_json_schema !== true) return []
|
|
318
|
+
const schema = parseJson(raw.res_body)
|
|
319
|
+
return schema === undefined ? [] : collectFields(schema, '', [])
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ------------------------------------------------------------------ */
|
|
323
|
+
/* skill installer */
|
|
324
|
+
/* ------------------------------------------------------------------ */
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Copy this CLI and its SKILL.md into a skill root. Used outside DSH, where
|
|
328
|
+
* `dsh plugin add` is not available. The copied SKILL.md is rewritten so every
|
|
329
|
+
* command points at the new location.
|
|
330
|
+
* @param {string} dirArg - optional target directory; defaults to ~/.agents/skills/yapi-doc.
|
|
331
|
+
*/
|
|
332
|
+
function copySkillTo(target) {
|
|
333
|
+
const sourceDir = dirname(fileURLToPath(import.meta.url))
|
|
334
|
+
const sourceSkill = join(sourceDir, 'SKILL.md')
|
|
335
|
+
const sourceCli = join(sourceDir, 'yapi-doc.mjs')
|
|
336
|
+
if (!existsSync(sourceSkill) || !existsSync(sourceCli)) {
|
|
337
|
+
throw new Error('找不到 skill 文件;请从完整安装包运行,或用 --dir 指定目标目录。')
|
|
338
|
+
}
|
|
339
|
+
const directory = resolve(target)
|
|
340
|
+
mkdirSync(directory, { recursive: true })
|
|
341
|
+
const targetCli = join(directory, 'yapi-doc.mjs')
|
|
342
|
+
if (resolve(targetCli) !== resolve(sourceCli)) copyFileSync(sourceCli, targetCli)
|
|
343
|
+
chmodSync(targetCli, 0o755)
|
|
344
|
+
const windowsTarget = directory.replace(/\//gu, '\\')
|
|
345
|
+
const content = readFileSync(sourceSkill, 'utf8')
|
|
346
|
+
.split('$HOME/.dsh/skills/yapi-doc')
|
|
347
|
+
.join(directory)
|
|
348
|
+
.split('%USERPROFILE%\\.dsh\\skills\\yapi-doc')
|
|
349
|
+
.join(windowsTarget)
|
|
350
|
+
const targetSkill = join(directory, 'SKILL.md')
|
|
351
|
+
if (resolve(targetSkill) !== resolve(sourceSkill)) writeFileSync(targetSkill, content, 'utf8')
|
|
352
|
+
return directory
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function installSkill(dirArg) {
|
|
356
|
+
const target = copySkillTo(dirArg || join(homedir(), '.agents', 'skills', 'yapi-doc'))
|
|
357
|
+
console.log('Skill installed: ' + target)
|
|
358
|
+
console.log('Run: node "' + join(target, 'yapi-doc.mjs') + '" <doc-id>')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const HOME_AGENTS = [
|
|
362
|
+
['Trae', '.trae', '.trae/skills'],
|
|
363
|
+
['WorkBuddy', '.workbuddy', '.workbuddy/skills'],
|
|
364
|
+
['Qoder CN', '.qoder-cn', '.qoder-cn/skills'],
|
|
365
|
+
['Qoder', '.qoder', '.qoder/skills'],
|
|
366
|
+
['GitHub Copilot', '.copilot', '.copilot/skills'],
|
|
367
|
+
['CodeBuddy', '.codebuddy', '.codebuddy/skills'],
|
|
368
|
+
['Claude Code', '.claude', '.claude/skills'],
|
|
369
|
+
['Cursor', '.cursor', '.cursor/skills'],
|
|
370
|
+
['Gemini CLI', '.gemini', '.gemini/skills'],
|
|
371
|
+
['Droid', '.factory', '.factory/skills'],
|
|
372
|
+
['fx', '.fx', '.fx/skills'],
|
|
373
|
+
['Kilo Code', '.kilo', '.kilo/skills'],
|
|
374
|
+
['Augment', '.augment', '.augment/skills'],
|
|
375
|
+
['Continue', '.continue', '.continue/skills'],
|
|
376
|
+
['AiderDesk', '.aider-desk', '.aider-desk/skills'],
|
|
377
|
+
['Goose', '.goose', '.goose/skills'],
|
|
378
|
+
['Kiro', '.kiro', '.kiro/skills'],
|
|
379
|
+
['Qwen Code', '.qwen', '.qwen/skills'],
|
|
380
|
+
['CodeArts', '.codeartsdoer', '.codeartsdoer/skills'],
|
|
381
|
+
['Codemaker', '.codemaker', '.codemaker/skills'],
|
|
382
|
+
['Code Studio', '.codestudio', '.codestudio/skills'],
|
|
383
|
+
['Command Code', '.commandcode', '.commandcode/skills'],
|
|
384
|
+
['Firebender', '.firebender', '.firebender/skills'],
|
|
385
|
+
['ForgeCode', '.forge', '.forge/skills'],
|
|
386
|
+
['Amp', '.config/amp', '.config/agents/skills'],
|
|
387
|
+
['OpenCode', '.config/opencode', '.config/opencode/skills'],
|
|
388
|
+
['Devin', '.config/devin', '.config/devin/skills'],
|
|
389
|
+
['Crush', '.config/crush', '.config/crush/skills'],
|
|
390
|
+
['Cortex', '.snowflake/cortex', '.snowflake/cortex/skills'],
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
function detectedSkillTargets(extraDirs) {
|
|
394
|
+
const home = homedir()
|
|
395
|
+
const targets = []
|
|
396
|
+
const add = function (label, dir) {
|
|
397
|
+
const resolved = resolve(dir)
|
|
398
|
+
if (!targets.some(function (item) { return item.dir === resolved })) {
|
|
399
|
+
targets.push({ label, dir: resolved })
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const [label, detect, skills] of HOME_AGENTS) {
|
|
403
|
+
if (existsSync(join(home, detect))) add(label, join(home, skills, 'yapi-doc'))
|
|
404
|
+
}
|
|
405
|
+
add('Agent Skills(Codex / Cline / Zed / 通用)', join(home, '.agents', 'skills', 'yapi-doc'))
|
|
406
|
+
if (existsSync(DSH_HOME)) add('DSH', join(DSH_HOME, 'skills', 'yapi-doc'))
|
|
407
|
+
for (const dir of extraDirs) add('custom', dir)
|
|
408
|
+
return targets
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function syncSkills(extraDirs, dryRun) {
|
|
412
|
+
const targets = detectedSkillTargets(extraDirs)
|
|
413
|
+
console.log('yapi-doc skill 同步:')
|
|
414
|
+
for (const item of targets) {
|
|
415
|
+
if (dryRun) {
|
|
416
|
+
console.log(' - ' + item.label + ' ' + item.dir)
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
const target = copySkillTo(item.dir)
|
|
420
|
+
console.log(' ✓ ' + item.label + ' ' + target)
|
|
421
|
+
}
|
|
422
|
+
if (!dryRun) {
|
|
423
|
+
console.log('')
|
|
424
|
+
console.log('重启对应应用后生效。')
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* ------------------------------------------------------------------ */
|
|
429
|
+
/* YAPI session */
|
|
430
|
+
/* ------------------------------------------------------------------ */
|
|
431
|
+
|
|
432
|
+
function readSettings() {
|
|
433
|
+
let stored = {}
|
|
434
|
+
try {
|
|
435
|
+
stored = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
|
|
436
|
+
} catch {
|
|
437
|
+
stored = {}
|
|
438
|
+
}
|
|
439
|
+
const env = process.env
|
|
440
|
+
const timeout = Number(stored.timeoutMs)
|
|
441
|
+
return {
|
|
442
|
+
baseUrl: normalizeBaseUrl(env.YAPI_BASE_URL || stored.baseUrl),
|
|
443
|
+
email: String(env.YAPI_EMAIL || stored.email || '').trim(),
|
|
444
|
+
password: String(env.YAPI_PASSWORD || stored.password || ''),
|
|
445
|
+
timeoutMs: Number.isFinite(timeout) && timeout >= 1000 ? Math.min(timeout, 120000) : 20000,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function createSession(settings) {
|
|
450
|
+
if (settings.baseUrl === '') {
|
|
451
|
+
throw new Error(
|
|
452
|
+
'未配置 YAPI 地址。打开 YAPI 面板 → 账号设置填写,或设置 YAPI_BASE_URL。',
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
if (settings.email === '' || settings.password === '') {
|
|
456
|
+
throw new Error(
|
|
457
|
+
'未配置 YAPI 账号。打开 YAPI 面板 → 账号设置填写,或编辑 ' +
|
|
458
|
+
CONFIG_FILE +
|
|
459
|
+
',也可设置 YAPI_EMAIL / YAPI_PASSWORD 环境变量。',
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
let cookie = ''
|
|
463
|
+
async function login() {
|
|
464
|
+
let response
|
|
465
|
+
try {
|
|
466
|
+
response = await fetch(settings.baseUrl + '/api/user/login', {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
469
|
+
body: JSON.stringify({ email: settings.email, password: settings.password }),
|
|
470
|
+
signal: AbortSignal.timeout(settings.timeoutMs),
|
|
471
|
+
})
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new Error('无法连接 YAPI(' + settings.baseUrl + '):' + (error && error.message ? error.message : error))
|
|
474
|
+
}
|
|
475
|
+
const text = await response.text()
|
|
476
|
+
let payload
|
|
477
|
+
try {
|
|
478
|
+
payload = JSON.parse(text)
|
|
479
|
+
} catch {
|
|
480
|
+
throw new Error('YAPI 登录响应不是 JSON(HTTP ' + response.status + ')')
|
|
481
|
+
}
|
|
482
|
+
if (!payload || payload.errcode !== 0) {
|
|
483
|
+
throw new Error('YAPI 登录失败:' + ((payload && payload.errmsg) || '未知错误'))
|
|
484
|
+
}
|
|
485
|
+
const lines = typeof response.headers.getSetCookie === 'function' ? response.headers.getSetCookie() : []
|
|
486
|
+
const pairs = lines.map((line) => String(line).split(';')[0].trim()).filter((pair) => pair.startsWith('_yapi_'))
|
|
487
|
+
if (pairs.length === 0) throw new Error('YAPI 登录成功,但响应没有返回 Cookie')
|
|
488
|
+
cookie = pairs.join('; ')
|
|
489
|
+
}
|
|
490
|
+
async function get(pathname, retry = true) {
|
|
491
|
+
if (cookie === '') await login()
|
|
492
|
+
let response
|
|
493
|
+
try {
|
|
494
|
+
response = await fetch(settings.baseUrl + pathname, {
|
|
495
|
+
headers: { cookie, accept: 'application/json' },
|
|
496
|
+
signal: AbortSignal.timeout(settings.timeoutMs),
|
|
497
|
+
})
|
|
498
|
+
} catch (error) {
|
|
499
|
+
throw new Error('无法连接 YAPI(' + settings.baseUrl + '):' + (error && error.message ? error.message : error))
|
|
500
|
+
}
|
|
501
|
+
const text = await response.text()
|
|
502
|
+
let payload
|
|
503
|
+
try {
|
|
504
|
+
payload = JSON.parse(text)
|
|
505
|
+
} catch {
|
|
506
|
+
payload = null
|
|
507
|
+
}
|
|
508
|
+
if (!payload) {
|
|
509
|
+
if (retry) {
|
|
510
|
+
cookie = ''
|
|
511
|
+
await login()
|
|
512
|
+
return get(pathname, false)
|
|
513
|
+
}
|
|
514
|
+
throw new Error('YAPI 返回了非 JSON 响应(HTTP ' + response.status + ')')
|
|
515
|
+
}
|
|
516
|
+
if (payload.errcode === 40011 && retry) {
|
|
517
|
+
cookie = ''
|
|
518
|
+
await login()
|
|
519
|
+
return get(pathname, false)
|
|
520
|
+
}
|
|
521
|
+
if (payload.errcode !== 0) throw new Error('YAPI 错误:' + (payload.errmsg || String(payload.errcode)))
|
|
522
|
+
return payload.data
|
|
523
|
+
}
|
|
524
|
+
return { get }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* ------------------------------------------------------------------ */
|
|
528
|
+
/* rendering */
|
|
529
|
+
/* ------------------------------------------------------------------ */
|
|
530
|
+
|
|
531
|
+
const STATUS_LABEL = {
|
|
532
|
+
done: '已完成',
|
|
533
|
+
undone: '开发中',
|
|
534
|
+
design: '设计中',
|
|
535
|
+
deprecated: '已废弃',
|
|
536
|
+
stub: '未完成',
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function codeFence(text, lang) {
|
|
540
|
+
return ['```' + (lang || ''), text, '```'].join('\n')
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function formatTime(timestamp) {
|
|
544
|
+
const date = new Date(timestamp)
|
|
545
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
546
|
+
try {
|
|
547
|
+
return date.toLocaleString('zh-CN', {
|
|
548
|
+
year: 'numeric',
|
|
549
|
+
month: '2-digit',
|
|
550
|
+
day: '2-digit',
|
|
551
|
+
hour: '2-digit',
|
|
552
|
+
minute: '2-digit',
|
|
553
|
+
hour12: false,
|
|
554
|
+
})
|
|
555
|
+
} catch {
|
|
556
|
+
return date.toISOString().slice(0, 16).replace('T', ' ')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function fieldLine(field) {
|
|
561
|
+
const type = field.type ? field.type : '—'
|
|
562
|
+
const required = field.required ? '必填' : '可选'
|
|
563
|
+
const description = field.description ? ' — ' + field.description : ''
|
|
564
|
+
return '- ' + field.path + ': ' + type + ', ' + required + description
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function paramLines(title, rows) {
|
|
568
|
+
if (rows.length === 0) return []
|
|
569
|
+
const lines = [title + ':']
|
|
570
|
+
for (const row of rows) {
|
|
571
|
+
const bits = []
|
|
572
|
+
if (row.type) bits.push(row.type)
|
|
573
|
+
bits.push(row.required ? '必填' : '可选')
|
|
574
|
+
if (row.example) bits.push('示例 ' + row.example)
|
|
575
|
+
const description = row.desc ? ' — ' + row.desc : ''
|
|
576
|
+
lines.push('- ' + row.name + (bits.length ? '(' + bits.join(',') + ')' : '') + description)
|
|
577
|
+
}
|
|
578
|
+
return lines
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderDoc(doc, requestFieldRows, responseFieldRows, options) {
|
|
582
|
+
const lines = ['# ' + (doc.title || '未命名接口'), doc.method + ' ' + doc.path]
|
|
583
|
+
if (doc.projectName) lines.push('项目:' + doc.projectName + '(' + doc.projectId + ')')
|
|
584
|
+
if (doc.status) lines.push('状态:' + (STATUS_LABEL[doc.status] || doc.status))
|
|
585
|
+
if (doc.tags.length > 0) lines.push('标签:' + doc.tags.join(' / '))
|
|
586
|
+
lines.push('YAPI:' + doc.pageUrl)
|
|
587
|
+
if (doc.updatedAt) lines.push('更新:' + formatTime(doc.updatedAt))
|
|
588
|
+
if (doc.desc) lines.push('', '## 接口说明', doc.desc)
|
|
589
|
+
|
|
590
|
+
lines.push('', '## 请求地址', doc.method + ' ' + (doc.address || doc.path))
|
|
591
|
+
|
|
592
|
+
if (doc.headers.length > 0) {
|
|
593
|
+
lines.push('', '## 请求头')
|
|
594
|
+
for (const row of doc.headers) {
|
|
595
|
+
lines.push('- ' + row.name + ': ' + row.value + (row.required ? '(必填)' : '') + (row.desc ? ' — ' + row.desc : ''))
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const paramSections = [
|
|
600
|
+
...paramLines('Query', doc.query),
|
|
601
|
+
...paramLines('Path', doc.params),
|
|
602
|
+
...paramLines('Form', doc.form),
|
|
603
|
+
]
|
|
604
|
+
if (paramSections.length > 0) lines.push('', '## 请求参数', ...paramSections)
|
|
605
|
+
|
|
606
|
+
if (doc.requestBody.kind !== 'none') {
|
|
607
|
+
lines.push('', '## 请求 Body', codeFence(doc.requestBody.example, doc.requestBody.kind === 'json' ? 'json' : ''))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!options.noFields) {
|
|
611
|
+
if (requestFieldRows.length > 0) {
|
|
612
|
+
lines.push('', '## 请求字段(' + requestFieldRows.length + ')')
|
|
613
|
+
for (const field of requestFieldRows) lines.push(fieldLine(field))
|
|
614
|
+
}
|
|
615
|
+
if (responseFieldRows.length > 0) {
|
|
616
|
+
lines.push('', '## 返回字段(' + responseFieldRows.length + ')')
|
|
617
|
+
for (const field of responseFieldRows) lines.push(fieldLine(field))
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (options.full && doc.responseBody.kind !== 'none') {
|
|
622
|
+
lines.push('', '## 返回体', codeFence(doc.responseBody.example, doc.responseBody.kind === 'json' ? 'json' : ''))
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return lines.join('\n')
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function renderSearch(projectId, projectName, total, keyword, matches) {
|
|
629
|
+
const lines = ['# YAPI 接口搜索:' + keyword]
|
|
630
|
+
lines.push('项目:' + projectId + (projectName ? '(' + projectName + ')' : '') + ',共 ' + total + ' 个接口,' + matches.length + ' 个匹配')
|
|
631
|
+
lines.push('')
|
|
632
|
+
for (const item of matches) {
|
|
633
|
+
lines.push(String(item._id) + ' ' + String(item.method || '').padEnd(6, ' ') + ' ' + String(item.path || '') + ' — ' + String(item.title || ''))
|
|
634
|
+
}
|
|
635
|
+
return lines.join('\n')
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* ------------------------------------------------------------------ */
|
|
639
|
+
/* main */
|
|
640
|
+
/* ------------------------------------------------------------------ */
|
|
641
|
+
|
|
642
|
+
function printHelp() {
|
|
643
|
+
console.log(`yapi-doc — 读取 YAPI 接口文档
|
|
644
|
+
|
|
645
|
+
用法:
|
|
646
|
+
yapi-doc <文档ID|链接> [选项]
|
|
647
|
+
yapi-doc --search <关键字> --project <项目ID>
|
|
648
|
+
yapi-doc install-skill [--dir <目录>]
|
|
649
|
+
yapi-doc sync-skills [--dir <目录> ...] [--dry-run]
|
|
650
|
+
|
|
651
|
+
选项:
|
|
652
|
+
--json 输出结构化 JSON(默认 Markdown)
|
|
653
|
+
--full 额外输出返回体示例(默认只输出返回字段说明)
|
|
654
|
+
--no-fields 不输出请求/返回字段说明
|
|
655
|
+
--project <id> --search 使用的项目 ID(必填)
|
|
656
|
+
--dir <目录> install-skill 的目标目录(默认 ~/.agents/skills/yapi-doc)
|
|
657
|
+
--dry-run sync-skills 只显示检测到的目标,不写入
|
|
658
|
+
-h, --help 显示帮助
|
|
659
|
+
|
|
660
|
+
账号:
|
|
661
|
+
读取 ${CONFIG_FILE}
|
|
662
|
+
或环境变量 YAPI_BASE_URL / YAPI_EMAIL / YAPI_PASSWORD
|
|
663
|
+
|
|
664
|
+
示例:
|
|
665
|
+
yapi-doc 10001
|
|
666
|
+
yapi-doc https://your-yapi.example.com/project/1/interface/api/10001
|
|
667
|
+
yapi-doc --search warehouse --project 1
|
|
668
|
+
yapi-doc 10001 --full --json
|
|
669
|
+
yapi-doc install-skill
|
|
670
|
+
yapi-doc install-skill --dir ~/.claude/skills/yapi-doc
|
|
671
|
+
yapi-doc sync-skills
|
|
672
|
+
yapi-doc sync-skills --dry-run`)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function parseArgs(argv) {
|
|
676
|
+
const options = {
|
|
677
|
+
target: '',
|
|
678
|
+
search: '',
|
|
679
|
+
projectId: 0,
|
|
680
|
+
json: false,
|
|
681
|
+
full: false,
|
|
682
|
+
noFields: false,
|
|
683
|
+
help: false,
|
|
684
|
+
}
|
|
685
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
686
|
+
const arg = argv[index]
|
|
687
|
+
if (arg === '-h' || arg === '--help') {
|
|
688
|
+
options.help = true
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
if (arg === '--json') {
|
|
692
|
+
options.json = true
|
|
693
|
+
continue
|
|
694
|
+
}
|
|
695
|
+
if (arg === '--full') {
|
|
696
|
+
options.full = true
|
|
697
|
+
continue
|
|
698
|
+
}
|
|
699
|
+
if (arg === '--no-fields') {
|
|
700
|
+
options.noFields = true
|
|
701
|
+
continue
|
|
702
|
+
}
|
|
703
|
+
if (arg === '--project') {
|
|
704
|
+
options.projectId = Number(argv[++index]) || 0
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
if (arg.startsWith('--project=')) {
|
|
708
|
+
options.projectId = Number(arg.slice('--project='.length)) || 0
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
if (arg === '--search') {
|
|
712
|
+
options.search = String(argv[++index] || '').trim()
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
if (arg.startsWith('--search=')) {
|
|
716
|
+
options.search = arg.slice('--search='.length).trim()
|
|
717
|
+
continue
|
|
718
|
+
}
|
|
719
|
+
if (arg.startsWith('-')) throw new Error('未知选项:' + arg)
|
|
720
|
+
if (options.target !== '') throw new Error('只能指定一个文档 ID 或链接')
|
|
721
|
+
options.target = arg
|
|
722
|
+
}
|
|
723
|
+
return options
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function main() {
|
|
727
|
+
if (process.argv[2] === 'sync-skills') {
|
|
728
|
+
const dirs = []
|
|
729
|
+
let dryRun = false
|
|
730
|
+
for (let index = 3; index < process.argv.length; index += 1) {
|
|
731
|
+
const arg = process.argv[index]
|
|
732
|
+
if (arg === '-h' || arg === '--help') {
|
|
733
|
+
printHelp()
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
if (arg === '--dry-run') {
|
|
737
|
+
dryRun = true
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
740
|
+
if (arg === '--dir') {
|
|
741
|
+
const dir = process.argv[index + 1] || ''
|
|
742
|
+
if (dir === '') throw new Error('--dir 需要一个目录')
|
|
743
|
+
dirs.push(dir)
|
|
744
|
+
index += 1
|
|
745
|
+
continue
|
|
746
|
+
}
|
|
747
|
+
if (arg.startsWith('--dir=')) {
|
|
748
|
+
const dir = arg.slice('--dir='.length)
|
|
749
|
+
if (dir === '') throw new Error('--dir 需要一个目录')
|
|
750
|
+
dirs.push(dir)
|
|
751
|
+
continue
|
|
752
|
+
}
|
|
753
|
+
throw new Error('未知选项:' + arg)
|
|
754
|
+
}
|
|
755
|
+
syncSkills(dirs, dryRun)
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
if (process.argv[2] === 'install-skill') {
|
|
759
|
+
let target = ''
|
|
760
|
+
for (let index = 3; index < process.argv.length; index += 1) {
|
|
761
|
+
const arg = process.argv[index]
|
|
762
|
+
if (arg === '-h' || arg === '--help') {
|
|
763
|
+
printHelp()
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
if (arg === '--dir') {
|
|
767
|
+
target = process.argv[index + 1] || ''
|
|
768
|
+
if (target === '') throw new Error('--dir 需要一个目录')
|
|
769
|
+
index += 1
|
|
770
|
+
} else if (arg.startsWith('--dir=')) {
|
|
771
|
+
target = arg.slice('--dir='.length)
|
|
772
|
+
if (target === '') throw new Error('--dir 需要一个目录')
|
|
773
|
+
} else {
|
|
774
|
+
throw new Error('未知选项:' + arg)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
installSkill(target)
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
const options = parseArgs(process.argv.slice(2))
|
|
781
|
+
if (options.help) {
|
|
782
|
+
printHelp()
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
if (options.search === '' && options.target === '') {
|
|
786
|
+
printHelp()
|
|
787
|
+
throw new Error('请提供文档 ID / 链接,或使用 --search <关键字>')
|
|
788
|
+
}
|
|
789
|
+
const settings = readSettings()
|
|
790
|
+
const session = await createSession(settings)
|
|
791
|
+
|
|
792
|
+
if (options.search !== '') {
|
|
793
|
+
if (!Number.isSafeInteger(options.projectId) || options.projectId <= 0) {
|
|
794
|
+
throw new Error('请用 --project <项目ID> 指定搜索范围')
|
|
795
|
+
}
|
|
796
|
+
const project = await session.get('/api/project/get?id=' + encodeURIComponent(options.projectId)).catch(() => null)
|
|
797
|
+
const data = await session.get(
|
|
798
|
+
'/api/interface/list?project_id=' + encodeURIComponent(options.projectId) + '&page=1&limit=2000',
|
|
799
|
+
)
|
|
800
|
+
const list = Array.isArray(data?.list) ? data.list : []
|
|
801
|
+
const matches = rankInterfaces(list, options.search, 30)
|
|
802
|
+
if (options.json) {
|
|
803
|
+
console.log(
|
|
804
|
+
JSON.stringify(
|
|
805
|
+
{
|
|
806
|
+
ok: true,
|
|
807
|
+
data: {
|
|
808
|
+
projectId: options.projectId,
|
|
809
|
+
projectName: String(project?.name ?? ''),
|
|
810
|
+
total: list.length,
|
|
811
|
+
keyword: options.search,
|
|
812
|
+
items: matches.map((item) => ({
|
|
813
|
+
id: Number(item._id),
|
|
814
|
+
method: String(item.method ?? '').toUpperCase(),
|
|
815
|
+
path: String(item.path ?? ''),
|
|
816
|
+
title: String(item.title ?? ''),
|
|
817
|
+
})),
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
null,
|
|
821
|
+
2,
|
|
822
|
+
),
|
|
823
|
+
)
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
console.log(renderSearch(options.projectId, String(project?.name ?? ''), list.length, options.search, matches))
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const id = parseDocId(options.target)
|
|
831
|
+
if (id === null) throw new Error('无法从输入中解析文档 ID:' + options.target)
|
|
832
|
+
const raw = await session.get('/api/interface/get?id=' + encodeURIComponent(id))
|
|
833
|
+
const project =
|
|
834
|
+
raw && Number(raw.project_id) > 0
|
|
835
|
+
? await session.get('/api/project/get?id=' + encodeURIComponent(raw.project_id)).catch(() => null)
|
|
836
|
+
: null
|
|
837
|
+
const doc = buildDoc(raw, project, settings.baseUrl)
|
|
838
|
+
const requestFieldRows = requestFields(raw)
|
|
839
|
+
const responseFieldRows = responseFields(raw)
|
|
840
|
+
|
|
841
|
+
if (options.json) {
|
|
842
|
+
console.log(
|
|
843
|
+
JSON.stringify(
|
|
844
|
+
{
|
|
845
|
+
ok: true,
|
|
846
|
+
data: Object.assign({}, doc, {
|
|
847
|
+
requestFields: requestFieldRows,
|
|
848
|
+
responseFields: responseFieldRows,
|
|
849
|
+
}),
|
|
850
|
+
},
|
|
851
|
+
null,
|
|
852
|
+
2,
|
|
853
|
+
),
|
|
854
|
+
)
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
console.log(renderDoc(doc, requestFieldRows, responseFieldRows, options))
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
main().catch((error) => {
|
|
861
|
+
console.error('yapi-doc: ' + (error && error.message ? error.message : String(error)))
|
|
862
|
+
process.exit(1)
|
|
863
|
+
})
|