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
package/lib/core.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the YAPI panel.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in this file touches the network or the DSH runtime: `index.js`
|
|
5
|
+
* turns a YAPI `/api/interface/get` payload into a browser-friendly document,
|
|
6
|
+
* and the tests exercise the same functions directly. Keeping the example
|
|
7
|
+
* generator here means the browser never has to understand YAPI's schema
|
|
8
|
+
* dialect.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** HTTP methods the panel knows how to color and copy. */
|
|
12
|
+
export const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Trim trailing slashes so `baseUrl + pathname` never doubles a separator.
|
|
16
|
+
* An empty value stays empty: the plugin has no built-in YAPI host.
|
|
17
|
+
* @param {unknown} value - configured origin.
|
|
18
|
+
* @returns {string} normalized origin without a trailing slash.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeBaseUrl(value) {
|
|
21
|
+
return String(value ?? '').trim().replace(/\/+$/u, '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Join URL fragments with exactly one slash between them.
|
|
26
|
+
* @param {...unknown} parts - origin, optional base path, optional endpoint.
|
|
27
|
+
* @returns {string} the joined address.
|
|
28
|
+
*/
|
|
29
|
+
export function joinUrl(...parts) {
|
|
30
|
+
let out = ''
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
const text = String(part ?? '').trim()
|
|
33
|
+
if (text === '') continue
|
|
34
|
+
if (out === '') out = text.replace(/\/+$/u, '')
|
|
35
|
+
else out += '/' + text.replace(/^\/+/u, '').replace(/\/+$/u, '')
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pull a YAPI interface id out of a raw id, a numeric string, or a full
|
|
42
|
+
* documentation URL (`.../project/1/interface/api/10001`).
|
|
43
|
+
* @param {unknown} value - user input.
|
|
44
|
+
* @returns {number | null} a positive integer id, or null when unparseable.
|
|
45
|
+
*/
|
|
46
|
+
export function parseDocId(value) {
|
|
47
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value
|
|
48
|
+
const text = String(value ?? '').trim()
|
|
49
|
+
if (text === '') return null
|
|
50
|
+
if (/^\d{1,12}$/u.test(text)) {
|
|
51
|
+
const id = Number(text)
|
|
52
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null
|
|
53
|
+
}
|
|
54
|
+
const patterns = [
|
|
55
|
+
/\/interface\/api\/(\d{1,12})(?:[/?#]|$)/u,
|
|
56
|
+
/[?&]id=(\d{1,12})(?:[&#]|$)/u,
|
|
57
|
+
/\/api\/interface\/get\?id=(\d{1,12})(?:[&#]|$)/u,
|
|
58
|
+
]
|
|
59
|
+
for (const pattern of patterns) {
|
|
60
|
+
const match = pattern.exec(text)
|
|
61
|
+
if (match) {
|
|
62
|
+
const id = Number(match[1])
|
|
63
|
+
if (Number.isSafeInteger(id) && id > 0) return id
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const trailing = /(\d{1,12})\/?(?:[?#].*)?$/u.exec(text)
|
|
67
|
+
if (trailing) {
|
|
68
|
+
const id = Number(trailing[1])
|
|
69
|
+
if (Number.isSafeInteger(id) && id > 0) return id
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Turn YAPI's rich-text description into readable plain text.
|
|
76
|
+
* @param {unknown} html - the `desc` field.
|
|
77
|
+
* @returns {string} text with tags and entities removed.
|
|
78
|
+
*/
|
|
79
|
+
export function stripHtml(html) {
|
|
80
|
+
const text = String(html ?? '')
|
|
81
|
+
if (text === '') return ''
|
|
82
|
+
return text
|
|
83
|
+
.replace(/<br\s*\/?>/giu, '\n')
|
|
84
|
+
.replace(/<\/(?:p|div|li|h[1-6]|tr)>/giu, '\n')
|
|
85
|
+
.replace(/<li[^>]*>/giu, '- ')
|
|
86
|
+
.replace(/<[^>]+>/gu, '')
|
|
87
|
+
.replace(/ /giu, ' ')
|
|
88
|
+
.replace(/</giu, '<')
|
|
89
|
+
.replace(/>/giu, '>')
|
|
90
|
+
.replace(/&/giu, '&')
|
|
91
|
+
.replace(/"/giu, '"')
|
|
92
|
+
.replace(/'/giu, "'")
|
|
93
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
94
|
+
.trim()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {unknown} value @returns {unknown} parsed JSON, or undefined. */
|
|
98
|
+
function tryParseJson(value) {
|
|
99
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(value)
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {unknown} value @returns {string} pretty JSON, or '' when undefined. */
|
|
108
|
+
function pretty(value) {
|
|
109
|
+
if (value === undefined) return ''
|
|
110
|
+
return JSON.stringify(value, null, 2)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @param {Record<string, unknown>} schema @returns {Record<string, unknown>} */
|
|
114
|
+
function unwrapComposite(schema) {
|
|
115
|
+
let current = schema
|
|
116
|
+
for (let round = 0; round < 8; round += 1) {
|
|
117
|
+
if (!current || typeof current !== 'object') return {}
|
|
118
|
+
const allOf = Array.isArray(current.allOf) ? current.allOf : null
|
|
119
|
+
if (allOf && allOf.length > 0) {
|
|
120
|
+
const merged = { ...current }
|
|
121
|
+
delete merged.allOf
|
|
122
|
+
const properties = { ...(current.properties || {}) }
|
|
123
|
+
for (const member of allOf) {
|
|
124
|
+
if (member && typeof member === 'object' && member.properties) Object.assign(properties, member.properties)
|
|
125
|
+
}
|
|
126
|
+
merged.properties = properties
|
|
127
|
+
merged.type = merged.type || 'object'
|
|
128
|
+
return merged
|
|
129
|
+
}
|
|
130
|
+
const list = Array.isArray(current.oneOf)
|
|
131
|
+
? current.oneOf
|
|
132
|
+
: Array.isArray(current.anyOf)
|
|
133
|
+
? current.anyOf
|
|
134
|
+
: null
|
|
135
|
+
if (list && list.length > 0 && list[0] && typeof list[0] === 'object') {
|
|
136
|
+
current = list[0]
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
return current
|
|
140
|
+
}
|
|
141
|
+
return current && typeof current === 'object' ? current : {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a copy-friendly example value from a YAPI JSON schema.
|
|
146
|
+
* @param {unknown} input - a JSON schema node.
|
|
147
|
+
* @param {{ maxNodes?: number, maxDepth?: number }} [options] - safety limits.
|
|
148
|
+
* @returns {{ value: unknown, truncated: boolean, nodes: number }} example value and limits status.
|
|
149
|
+
*/
|
|
150
|
+
export function exampleFromSchema(input, options = {}) {
|
|
151
|
+
const maxNodes = Number.isFinite(options.maxNodes) ? Number(options.maxNodes) : 20000
|
|
152
|
+
const maxDepth = Number.isFinite(options.maxDepth) ? Number(options.maxDepth) : 48
|
|
153
|
+
const state = { nodes: 0, truncated: false }
|
|
154
|
+
|
|
155
|
+
const walk = (node, depth) => {
|
|
156
|
+
state.nodes += 1
|
|
157
|
+
if (state.nodes > maxNodes) {
|
|
158
|
+
state.truncated = true
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
if (typeof node === 'boolean') return node ? {} : null
|
|
162
|
+
if (!node || typeof node !== 'object') return null
|
|
163
|
+
if (depth > maxDepth) {
|
|
164
|
+
state.truncated = true
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const schema = unwrapComposite(node)
|
|
169
|
+
// YAPI keeps a resolved-schema marker in `$$ref`; it is documentation, not data.
|
|
170
|
+
if (schema.example !== undefined) return schema.example
|
|
171
|
+
if (schema.default !== undefined) return schema.default
|
|
172
|
+
|
|
173
|
+
let type = schema.type
|
|
174
|
+
if (Array.isArray(type)) type = type.find((item) => item !== 'null') || 'null'
|
|
175
|
+
if (!type) {
|
|
176
|
+
if (schema.properties) type = 'object'
|
|
177
|
+
else if (schema.items) type = 'array'
|
|
178
|
+
else if (schema.enum) type = typeof schema.enum[0] === 'number' ? 'number' : 'string'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (schema.enum && schema.enum.length > 0) return schema.enum[0]
|
|
182
|
+
if (Array.isArray(schema.examples) && schema.examples.length > 0) return schema.examples[0]
|
|
183
|
+
|
|
184
|
+
switch (type) {
|
|
185
|
+
case 'object': {
|
|
186
|
+
const out = {}
|
|
187
|
+
const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {}
|
|
188
|
+
for (const [key, child] of Object.entries(properties)) out[key] = walk(child, depth + 1)
|
|
189
|
+
if (Object.keys(properties).length === 0 && schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
190
|
+
out['additionalProp1'] = walk(schema.additionalProperties, depth + 1)
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
case 'array': {
|
|
195
|
+
const items = Array.isArray(schema.items) ? schema.items[0] : schema.items
|
|
196
|
+
if (!items) return []
|
|
197
|
+
return [walk(items, depth + 1)]
|
|
198
|
+
}
|
|
199
|
+
case 'integer':
|
|
200
|
+
return 0
|
|
201
|
+
case 'number':
|
|
202
|
+
return 0
|
|
203
|
+
case 'boolean':
|
|
204
|
+
return false
|
|
205
|
+
case 'null':
|
|
206
|
+
return null
|
|
207
|
+
case 'string': {
|
|
208
|
+
if (schema.format === 'date-time') return '2024-01-01T00:00:00Z'
|
|
209
|
+
if (schema.format === 'date') return '2024-01-01'
|
|
210
|
+
if (schema.format === 'email') return 'user@example.com'
|
|
211
|
+
if (schema.format === 'uuid') return '00000000-0000-4000-8000-000000000000'
|
|
212
|
+
return ''
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const value = walk(input, 0)
|
|
220
|
+
return { value, truncated: state.truncated, nodes: state.nodes }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build the copyable request body plus the raw schema text.
|
|
225
|
+
* @param {Record<string, unknown>} raw - YAPI interface payload.
|
|
226
|
+
* @returns {{ kind: 'none'|'json'|'text', example: string, raw: string, truncated: boolean }} body views.
|
|
227
|
+
*/
|
|
228
|
+
export function buildRequestPayload(raw) {
|
|
229
|
+
const type = String(raw.req_body_type ?? '').toLowerCase()
|
|
230
|
+
const bodyText = typeof raw.req_body_other === 'string' ? raw.req_body_other : ''
|
|
231
|
+
const form = Array.isArray(raw.req_body_form) ? raw.req_body_form : []
|
|
232
|
+
|
|
233
|
+
if (type === 'form' || (type === '' && form.length > 0)) {
|
|
234
|
+
if (form.length === 0) return { kind: 'none', example: '', raw: bodyText, truncated: false }
|
|
235
|
+
const object = {}
|
|
236
|
+
for (const field of form) {
|
|
237
|
+
if (!field || typeof field !== 'object') continue
|
|
238
|
+
const name = String(field.name ?? '').trim() || 'field'
|
|
239
|
+
object[name] = formExample(field)
|
|
240
|
+
}
|
|
241
|
+
return { kind: 'json', example: pretty(object), raw: bodyText, truncated: false }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (bodyText.trim() === '') return { kind: 'none', example: '', raw: '', truncated: false }
|
|
245
|
+
|
|
246
|
+
if (raw.req_body_is_json_schema === true) {
|
|
247
|
+
const schema = tryParseJson(bodyText)
|
|
248
|
+
if (schema !== undefined) {
|
|
249
|
+
const generated = exampleFromSchema(schema)
|
|
250
|
+
return { kind: 'json', example: pretty(generated.value), raw: bodyText, truncated: generated.truncated }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parsed = tryParseJson(bodyText)
|
|
255
|
+
if (parsed !== undefined) return { kind: 'json', example: pretty(parsed), raw: bodyText, truncated: false }
|
|
256
|
+
return { kind: 'text', example: bodyText, raw: bodyText, truncated: false }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Build the copyable response body plus the raw schema text.
|
|
261
|
+
* @param {Record<string, unknown>} raw - YAPI interface payload.
|
|
262
|
+
* @returns {{ kind: 'none'|'json'|'text', example: string, raw: string, truncated: boolean }} body views.
|
|
263
|
+
*/
|
|
264
|
+
export function buildResponsePayload(raw) {
|
|
265
|
+
const bodyText = typeof raw.res_body === 'string' ? raw.res_body : ''
|
|
266
|
+
if (bodyText.trim() === '') return { kind: 'none', example: '', raw: '', truncated: false }
|
|
267
|
+
|
|
268
|
+
if (raw.res_body_is_json_schema === true) {
|
|
269
|
+
const schema = tryParseJson(bodyText)
|
|
270
|
+
if (schema !== undefined) {
|
|
271
|
+
const generated = exampleFromSchema(schema)
|
|
272
|
+
return { kind: 'json', example: pretty(generated.value), raw: bodyText, truncated: generated.truncated }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const parsed = tryParseJson(bodyText)
|
|
277
|
+
if (parsed !== undefined) return { kind: 'json', example: pretty(parsed), raw: bodyText, truncated: false }
|
|
278
|
+
return { kind: 'text', example: bodyText, raw: bodyText, truncated: false }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** @param {Record<string, unknown>} field @returns {unknown} */
|
|
282
|
+
function formExample(field) {
|
|
283
|
+
const type = String(field.type ?? '').toLowerCase()
|
|
284
|
+
const example = field.example
|
|
285
|
+
if (example !== undefined && example !== null && example !== '') {
|
|
286
|
+
if (type === 'integer' || type === 'number') {
|
|
287
|
+
const parsed = Number(example)
|
|
288
|
+
return Number.isFinite(parsed) ? parsed : example
|
|
289
|
+
}
|
|
290
|
+
if (type === 'boolean') return example === true || example === 'true' || example === '1'
|
|
291
|
+
return example
|
|
292
|
+
}
|
|
293
|
+
if (type === 'file') return ''
|
|
294
|
+
if (type === 'integer' || type === 'number') return 0
|
|
295
|
+
if (type === 'boolean') return false
|
|
296
|
+
return ''
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @param {unknown} item @returns {{ name: string, type: string, required: boolean, example: string, desc: string, value: string }} */
|
|
300
|
+
function normalizeParam(item) {
|
|
301
|
+
const entry = item && typeof item === 'object' ? item : {}
|
|
302
|
+
return {
|
|
303
|
+
name: String(entry.name ?? '').trim(),
|
|
304
|
+
type: String(entry.type ?? '').trim(),
|
|
305
|
+
required: entry.required === '1' || entry.required === true,
|
|
306
|
+
example: entry.example === undefined || entry.example === null ? '' : String(entry.example),
|
|
307
|
+
desc: stripHtml(entry.desc ?? ''),
|
|
308
|
+
value: entry.value === undefined || entry.value === null ? '' : String(entry.value),
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Whether every character of `needle` appears in `haystack` in order.
|
|
314
|
+
* @param {string} needle - lower-case query.
|
|
315
|
+
* @param {string} haystack - lower-case target.
|
|
316
|
+
* @returns {boolean} true for an in-order subsequence match.
|
|
317
|
+
*/
|
|
318
|
+
function isSubsequence(needle, haystack) {
|
|
319
|
+
let index = 0
|
|
320
|
+
for (let cursor = 0; cursor < haystack.length && index < needle.length; cursor += 1) {
|
|
321
|
+
if (haystack[cursor] === needle[index]) index += 1
|
|
322
|
+
}
|
|
323
|
+
return index === needle.length
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Rank a YAPI project's interface list against one keyword.
|
|
328
|
+
*
|
|
329
|
+
* Paths are the primary key (the user asked for "接口地址" search), titles are
|
|
330
|
+
* the fallback, and a cheap in-order subsequence match handles acronym-style
|
|
331
|
+
* queries such as `whlist` for `/admin/warehouse/list`.
|
|
332
|
+
* @param {unknown} list - `/api/interface/list` rows.
|
|
333
|
+
* @param {unknown} keyword - user query; spaces split it into required tokens.
|
|
334
|
+
* @param {number} [limit] - maximum rows returned.
|
|
335
|
+
* @returns {Array<Record<string, unknown>>} matching rows, best first.
|
|
336
|
+
*/
|
|
337
|
+
export function rankInterfaces(list, keyword, limit = 30) {
|
|
338
|
+
const query = String(keyword ?? '').trim().toLowerCase()
|
|
339
|
+
if (query === '') return []
|
|
340
|
+
const tokens = query.split(/\s+/u).filter(Boolean)
|
|
341
|
+
const ranked = []
|
|
342
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
343
|
+
if (!item || typeof item !== 'object') continue
|
|
344
|
+
const id = String(item._id ?? '')
|
|
345
|
+
const path = String(item.path ?? '')
|
|
346
|
+
const title = String(item.title ?? '')
|
|
347
|
+
const lowerPath = path.toLowerCase()
|
|
348
|
+
const lowerTitle = title.toLowerCase()
|
|
349
|
+
let score = 0
|
|
350
|
+
if (id === query) score = 1200
|
|
351
|
+
else if (lowerPath === query) score = 1100
|
|
352
|
+
else if (lowerPath.startsWith(query)) score = 1000
|
|
353
|
+
else {
|
|
354
|
+
const pathAt = lowerPath.indexOf(query)
|
|
355
|
+
if (pathAt !== -1) score = 900 - Math.min(pathAt, 80)
|
|
356
|
+
else {
|
|
357
|
+
const titleAt = lowerTitle.indexOf(query)
|
|
358
|
+
if (titleAt !== -1) score = 700 - Math.min(titleAt, 80)
|
|
359
|
+
else if (tokens.length > 1 && tokens.every((token) => `${lowerPath} ${lowerTitle}`.includes(token))) score = 500
|
|
360
|
+
else if (query.length >= 2 && isSubsequence(query, lowerPath)) score = 400
|
|
361
|
+
else if (query.length >= 2 && isSubsequence(query, lowerTitle)) score = 300
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (score > 0) ranked.push({ item, score, path: lowerPath })
|
|
365
|
+
}
|
|
366
|
+
ranked.sort(
|
|
367
|
+
(left, right) =>
|
|
368
|
+
right.score - left.score ||
|
|
369
|
+
left.path.length - right.path.length ||
|
|
370
|
+
left.path.localeCompare(right.path) ||
|
|
371
|
+
String(left.item._id).localeCompare(String(right.item._id)),
|
|
372
|
+
)
|
|
373
|
+
return ranked.slice(0, Math.max(1, Number.isFinite(limit) ? limit : 30)).map((entry) => entry.item)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Shape one YAPI interface payload into the document the browser renders.
|
|
378
|
+
*
|
|
379
|
+
* The browser receives both views of each body: `example` (generated, good for
|
|
380
|
+
* copying into a client) and `raw` (the schema exactly as YAPI stores it).
|
|
381
|
+
* @param {Record<string, unknown>} raw - `/api/interface/get` data.
|
|
382
|
+
* @param {Record<string, unknown> | null} project - `/api/project/get` data.
|
|
383
|
+
* @param {string} baseUrl - the YAPI origin.
|
|
384
|
+
* @returns {Record<string, unknown>} normalized document.
|
|
385
|
+
*/
|
|
386
|
+
export function normalizeInterface(raw, project, baseUrl) {
|
|
387
|
+
const id = Number(raw._id)
|
|
388
|
+
const projectId = Number(raw.project_id)
|
|
389
|
+
const envs = Array.isArray(project?.env)
|
|
390
|
+
? project.env
|
|
391
|
+
.filter((env) => env && typeof env === 'object' && env.domain)
|
|
392
|
+
.map((env) => ({
|
|
393
|
+
name: String(env.name ?? '默认'),
|
|
394
|
+
domain: String(env.domain ?? ''),
|
|
395
|
+
}))
|
|
396
|
+
: []
|
|
397
|
+
const basepath = typeof project?.basepath === 'string' ? project.basepath : ''
|
|
398
|
+
const path = String(raw.path ?? '')
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
id,
|
|
402
|
+
title: String(raw.title ?? ''),
|
|
403
|
+
method: String(raw.method ?? 'GET').toUpperCase(),
|
|
404
|
+
path,
|
|
405
|
+
status: String(raw.status ?? ''),
|
|
406
|
+
tags: Array.isArray(raw.tag) ? raw.tag.map((tag) => String(tag)).filter(Boolean) : [],
|
|
407
|
+
desc: String(raw.desc ?? ''),
|
|
408
|
+
markdown: String(raw.markdown ?? ''),
|
|
409
|
+
author: String(raw.username ?? ''),
|
|
410
|
+
updatedAt: Number(raw.up_time) > 0 ? Number(raw.up_time) * 1000 : null,
|
|
411
|
+
pageUrl: joinUrl(baseUrl, '/project', String(projectId), '/interface/api', String(id)),
|
|
412
|
+
project: {
|
|
413
|
+
id: projectId,
|
|
414
|
+
name: String(project?.name ?? ''),
|
|
415
|
+
basepath,
|
|
416
|
+
envs,
|
|
417
|
+
address: envs.length > 0 ? joinUrl(envs[0].domain, basepath, path) : joinUrl(basepath, path),
|
|
418
|
+
},
|
|
419
|
+
request: {
|
|
420
|
+
headers: (Array.isArray(raw.req_headers) ? raw.req_headers : []).map(normalizeParam),
|
|
421
|
+
query: (Array.isArray(raw.req_query) ? raw.req_query : []).map(normalizeParam),
|
|
422
|
+
params: (Array.isArray(raw.req_params) ? raw.req_params : []).map(normalizeParam),
|
|
423
|
+
form: (Array.isArray(raw.req_body_form) ? raw.req_body_form : []).map(normalizeParam),
|
|
424
|
+
bodyType: String(raw.req_body_type ?? ''),
|
|
425
|
+
bodyIsJsonSchema: raw.req_body_is_json_schema === true,
|
|
426
|
+
body: buildRequestPayload(raw),
|
|
427
|
+
},
|
|
428
|
+
response: {
|
|
429
|
+
bodyType: String(raw.res_body_type ?? ''),
|
|
430
|
+
bodyIsJsonSchema: raw.res_body_is_json_schema === true,
|
|
431
|
+
body: buildResponsePayload(raw),
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-yapi",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "YAPI 接口文档面板 for DeepSeek Harness:搜索、多标签、字段树、勾选复制,附带 yapi-doc skill 与零依赖 CLI。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"client.js",
|
|
15
|
+
"lib/**/*.js",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"bin/**/*.mjs",
|
|
19
|
+
"skills/**/*",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"dsh": {
|
|
23
|
+
"manifestVersion": 1,
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"client": {
|
|
28
|
+
"platform": "web",
|
|
29
|
+
"immediately": true,
|
|
30
|
+
"inject": [
|
|
31
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"dsh-plugin",
|
|
41
|
+
"deepseek-harness",
|
|
42
|
+
"yapi",
|
|
43
|
+
"yapi-doc",
|
|
44
|
+
"api-docs",
|
|
45
|
+
"api-documentation",
|
|
46
|
+
"skills"
|
|
47
|
+
],
|
|
48
|
+
"bin": {
|
|
49
|
+
"yapi-doc": "bin/yapi-doc.mjs"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"author": "jinshan3690",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/jinshan3690/dsh-plugin-yapi.git"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/jinshan3690/dsh-plugin-yapi#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/jinshan3690/dsh-plugin-yapi/issues"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"test": "node --test",
|
|
66
|
+
"prepublishOnly": "node --test"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yapi-doc
|
|
3
|
+
description: Read a YAPI interface document by ID, full URL, or path keyword. Use when a task needs an API method, path, headers, parameters, request body, response fields, or response example. The bundled CLI reads the local YAPI account and prints Markdown in one command.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# YAPI 文档读取
|
|
7
|
+
|
|
8
|
+
当任务需要 YAPI 接口的方法、路径、请求头、参数、请求/返回字段或返回示例时使用本 skill。优先用随包 CLI 读取,不要打开 YAPI 页面逐个翻,也不要让用户反复贴文档。
|
|
9
|
+
|
|
10
|
+
## 命令
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 按文档 ID 或完整链接读取(默认输出 Markdown)
|
|
14
|
+
node "$HOME/.dsh/skills/yapi-doc/yapi-doc.mjs" 10001
|
|
15
|
+
node "$HOME/.dsh/skills/yapi-doc/yapi-doc.mjs" https://your-yapi.example.com/project/1/interface/api/10001
|
|
16
|
+
|
|
17
|
+
# 不知道 ID 时,先按路径 / 标题关键字搜索
|
|
18
|
+
node "$HOME/.dsh/skills/yapi-doc/yapi-doc.mjs" --search warehouse --project 1
|
|
19
|
+
|
|
20
|
+
# 结构化输出,方便自己过滤
|
|
21
|
+
node "$HOME/.dsh/skills/yapi-doc/yapi-doc.mjs" 10001 --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Windows 下把 `$HOME` 换成 `%USERPROFILE%`,路径为
|
|
25
|
+
`%USERPROFILE%\.dsh\skills\yapi-doc\yapi-doc.mjs`。
|
|
26
|
+
|
|
27
|
+
## 安装到其他工具
|
|
28
|
+
|
|
29
|
+
DSH 用户由插件自动注册,不需要执行本节。
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g github:jinshan3690/dsh-plugin-yapi
|
|
33
|
+
yapi-doc sync-skills # 自动同步到检测到的 Trae / WorkBuddy / Qoder CN / Copilot / CodeBuddy / Agent Skills / DSH
|
|
34
|
+
yapi-doc install-skill # 默认装到 ~/.agents/skills/yapi-doc
|
|
35
|
+
yapi-doc install-skill --dir ~/.trae/skills/yapi-doc # Trae
|
|
36
|
+
yapi-doc install-skill --dir ~/.workbuddy/skills/yapi-doc # WorkBuddy
|
|
37
|
+
yapi-doc install-skill --dir <目录> # 其他 agent 的 skills 目录
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`install-skill` 会复制本 `SKILL.md` 和 CLI,并把下面的路径改成实际安装目录。
|
|
41
|
+
|
|
42
|
+
## 选项
|
|
43
|
+
|
|
44
|
+
- `--no-fields`:不输出字段说明,输出最小(方法、路径、请求头、参数、请求 Body)。只关心请求怎么发时用它。
|
|
45
|
+
- `--full`:额外输出返回体示例,可能很大;只有确实需要完整响应 JSON 时才用。
|
|
46
|
+
- `--json`:输出 `{ ok, data }` 结构化 JSON。
|
|
47
|
+
- `--project <id>`:`--search` 使用的项目 ID(必填)。
|
|
48
|
+
- `-h, --help`:帮助。
|
|
49
|
+
|
|
50
|
+
## 建议的读取顺序
|
|
51
|
+
|
|
52
|
+
1. 不知道 ID:先 `--search <关键字> --project <项目ID>`,从结果里拿 ID。
|
|
53
|
+
2. 只需要请求信息:`--no-fields`,输出最小。
|
|
54
|
+
3. 需要响应字段 / 备注:不加 `--no-fields`,默认包含请求字段和返回字段说明。
|
|
55
|
+
4. 需要精确响应 JSON:再加 `--full`。
|
|
56
|
+
5. 要程序化处理:`--json`。
|
|
57
|
+
|
|
58
|
+
## 账号
|
|
59
|
+
|
|
60
|
+
CLI 读取 `~/.dsh/dsh-plugin-yapi.json`(YAPI 面板 → 账号设置维护),也支持
|
|
61
|
+
`YAPI_BASE_URL` / `YAPI_EMAIL` / `YAPI_PASSWORD` 环境变量。不需要额外配置。
|
|
62
|
+
|
|
63
|
+
## 可移植性
|
|
64
|
+
|
|
65
|
+
`yapi-doc.mjs` 是**零依赖的 Node 脚本**,任何能执行 shell 的工具(Codex、Claude Code、
|
|
66
|
+
CI、普通终端)都可以直接运行。`SKILL.md` 本身是给支持 skill 目录约定的 agent 读的;
|
|
67
|
+
`yapi-doc install-skill --dir <目录>` 可以装到任意 agent 的 skills 目录。
|
|
68
|
+
DSH 会从 `~/.dsh/skills`、`.dsh/skills`、`.agents/skills` 等目录发现它。
|