@leeguoo/yapi-mcp 0.3.21 → 0.3.23
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/README.md +25 -14
- package/dist/config.d.ts +2 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +5 -1
- package/dist/server.js.map +1 -1
- package/dist/services/yapi/api.d.ts +14 -0
- package/dist/services/yapi/api.js +154 -57
- package/dist/services/yapi/api.js.map +1 -1
- package/dist/services/yapi/authCache.d.ts +1 -0
- package/dist/services/yapi/authCache.js +7 -0
- package/dist/services/yapi/authCache.js.map +1 -1
- package/dist/yapi-cli.js +251 -20
- package/dist/yapi-cli.js.map +1 -1
- package/package.json +3 -2
- package/skill-template/SKILL.md +58 -80
package/dist/yapi-cli.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const os_1 = __importDefault(require("os"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
@@ -12,6 +13,7 @@ const readline_1 = __importDefault(require("readline"));
|
|
|
12
13
|
const markdown_1 = require("./docs/markdown");
|
|
13
14
|
const install_1 = require("./skill/install");
|
|
14
15
|
const auth_1 = require("./services/yapi/auth");
|
|
16
|
+
const authCache_1 = require("./services/yapi/authCache");
|
|
15
17
|
function parseKeyValue(raw) {
|
|
16
18
|
if (!raw || !raw.includes("="))
|
|
17
19
|
throw new Error("expected key=value");
|
|
@@ -122,6 +124,162 @@ function resolveToken(tokenValue, projectId) {
|
|
|
122
124
|
}
|
|
123
125
|
return tokenValue;
|
|
124
126
|
}
|
|
127
|
+
function resolveLocalBin(binName) {
|
|
128
|
+
const baseName = process.platform === "win32" ? `${binName}.cmd` : binName;
|
|
129
|
+
const localBin = path_1.default.resolve(__dirname, "..", "node_modules", ".bin", baseName);
|
|
130
|
+
if (fs_1.default.existsSync(localBin))
|
|
131
|
+
return localBin;
|
|
132
|
+
return binName;
|
|
133
|
+
}
|
|
134
|
+
function parseJsonLoose(text) {
|
|
135
|
+
const raw = String(text || "").trim();
|
|
136
|
+
if (!raw)
|
|
137
|
+
return null;
|
|
138
|
+
const direct = parseJsonMaybe(raw);
|
|
139
|
+
if (direct !== null)
|
|
140
|
+
return direct;
|
|
141
|
+
const lines = raw
|
|
142
|
+
.split(/\r?\n/)
|
|
143
|
+
.map((line) => line.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
146
|
+
const parsed = parseJsonMaybe(lines[i]);
|
|
147
|
+
if (parsed !== null)
|
|
148
|
+
return parsed;
|
|
149
|
+
}
|
|
150
|
+
const firstBrace = raw.indexOf("{");
|
|
151
|
+
const firstBracket = raw.indexOf("[");
|
|
152
|
+
const starts = [firstBrace, firstBracket].filter((n) => n >= 0);
|
|
153
|
+
if (!starts.length)
|
|
154
|
+
return null;
|
|
155
|
+
const start = Math.min(...starts);
|
|
156
|
+
const lastBrace = raw.lastIndexOf("}");
|
|
157
|
+
const lastBracket = raw.lastIndexOf("]");
|
|
158
|
+
const end = Math.max(lastBrace, lastBracket);
|
|
159
|
+
if (end <= start)
|
|
160
|
+
return null;
|
|
161
|
+
const sliced = raw.slice(start, end + 1);
|
|
162
|
+
return parseJsonMaybe(sliced);
|
|
163
|
+
}
|
|
164
|
+
function extractCookiesFromPayload(payload) {
|
|
165
|
+
const queue = [payload];
|
|
166
|
+
const visited = new Set();
|
|
167
|
+
while (queue.length) {
|
|
168
|
+
const current = queue.shift();
|
|
169
|
+
if (current === null || current === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
if (visited.has(current))
|
|
172
|
+
continue;
|
|
173
|
+
visited.add(current);
|
|
174
|
+
if (Array.isArray(current)) {
|
|
175
|
+
if (current.every((item) => item && typeof item === "object")) {
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (typeof current !== "object")
|
|
181
|
+
continue;
|
|
182
|
+
const obj = current;
|
|
183
|
+
if (Array.isArray(obj.cookies))
|
|
184
|
+
return obj.cookies;
|
|
185
|
+
if (Array.isArray(obj.data))
|
|
186
|
+
return obj.data;
|
|
187
|
+
if (obj.data && typeof obj.data === "object")
|
|
188
|
+
queue.push(obj.data);
|
|
189
|
+
if (obj.result && typeof obj.result === "object")
|
|
190
|
+
queue.push(obj.result);
|
|
191
|
+
if (obj.payload && typeof obj.payload === "object")
|
|
192
|
+
queue.push(obj.payload);
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
function normalizeCookieExpiresMs(raw) {
|
|
197
|
+
const n = Number(raw);
|
|
198
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
return n > 1_000_000_000_000 ? n : n * 1000;
|
|
201
|
+
}
|
|
202
|
+
function runAgentBrowser(args, options = {}) {
|
|
203
|
+
const bins = [
|
|
204
|
+
resolveLocalBin("agent-browser-stealth"),
|
|
205
|
+
resolveLocalBin("agent-browser"),
|
|
206
|
+
"agent-browser-stealth",
|
|
207
|
+
"agent-browser",
|
|
208
|
+
];
|
|
209
|
+
const uniq = Array.from(new Set(bins));
|
|
210
|
+
let lastError = null;
|
|
211
|
+
for (const bin of uniq) {
|
|
212
|
+
try {
|
|
213
|
+
if (options.captureOutput) {
|
|
214
|
+
const output = (0, child_process_1.execFileSync)(bin, args, {
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
217
|
+
});
|
|
218
|
+
return String(output || "");
|
|
219
|
+
}
|
|
220
|
+
(0, child_process_1.execFileSync)(bin, args, { stdio: "inherit" });
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const code = error.code;
|
|
225
|
+
if (code === "ENOENT") {
|
|
226
|
+
lastError = error;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (options.ignoreError)
|
|
230
|
+
return "";
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (lastError?.code === "ENOENT") {
|
|
235
|
+
throw new Error("未找到 agent-browser-stealth。请先执行: pnpm -C packages/yapi-mcp add agent-browser-stealth && pnpm -C packages/yapi-mcp exec agent-browser-stealth install");
|
|
236
|
+
}
|
|
237
|
+
throw new Error("启动 agent-browser-stealth 失败");
|
|
238
|
+
}
|
|
239
|
+
async function loginByBrowserAndReadCookie(baseUrl, loginUrlOption) {
|
|
240
|
+
const normalizedBase = String(baseUrl || "").replace(/\/+$/, "");
|
|
241
|
+
const defaultLoginUrl = normalizedBase;
|
|
242
|
+
let loginUrl = String(loginUrlOption || "").trim();
|
|
243
|
+
if (!loginUrl) {
|
|
244
|
+
const answer = await promptText(`YApi page URL [${defaultLoginUrl}]: `);
|
|
245
|
+
loginUrl = answer.trim() || defaultLoginUrl;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const parsed = new URL(loginUrl);
|
|
249
|
+
loginUrl = parsed.toString();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
throw new Error(`invalid login url: ${loginUrl}`);
|
|
253
|
+
}
|
|
254
|
+
const sessionName = `yapi-login-${Date.now()}-${crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
255
|
+
console.log(`opening browser for login: ${loginUrl}`);
|
|
256
|
+
console.log(`browser session name: ${sessionName}`);
|
|
257
|
+
runAgentBrowser(["open", loginUrl, "--headed", "--session-name", sessionName]);
|
|
258
|
+
await promptText("请在浏览器完成登录,然后按回车继续...");
|
|
259
|
+
let cookies = [];
|
|
260
|
+
try {
|
|
261
|
+
const output = runAgentBrowser(["cookies", "--json", "--session-name", sessionName], {
|
|
262
|
+
captureOutput: true,
|
|
263
|
+
});
|
|
264
|
+
const payload = parseJsonLoose(output);
|
|
265
|
+
cookies = extractCookiesFromPayload(payload);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
runAgentBrowser(["close", "--session-name", sessionName], { ignoreError: true });
|
|
269
|
+
}
|
|
270
|
+
if (!cookies.length) {
|
|
271
|
+
throw new Error(`未读取到浏览器 cookie。请确认登录完成后再回车;如果仍失败,请先自检:\nagent-browser-stealth cookies --json --session-name ${sessionName}`);
|
|
272
|
+
}
|
|
273
|
+
const tokenCookie = cookies.find((item) => String(item?.name || "") === "_yapi_token");
|
|
274
|
+
const uidCookie = cookies.find((item) => String(item?.name || "") === "_yapi_uid");
|
|
275
|
+
const yapiToken = String(tokenCookie?.value || "").trim();
|
|
276
|
+
if (!yapiToken) {
|
|
277
|
+
throw new Error("未找到 _yapi_token,请确认登录站点是目标 YApi 域名");
|
|
278
|
+
}
|
|
279
|
+
const yapiUid = String(uidCookie?.value || "").trim() || undefined;
|
|
280
|
+
const expiresAt = normalizeCookieExpiresMs(tokenCookie?.expiresAt) ?? normalizeCookieExpiresMs(tokenCookie?.expires);
|
|
281
|
+
return { yapiToken, yapiUid, expiresAt };
|
|
282
|
+
}
|
|
125
283
|
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
126
284
|
const controller = new AbortController();
|
|
127
285
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -184,6 +342,14 @@ function parseArgs(argv) {
|
|
|
184
342
|
options.baseUrl = arg.slice(11);
|
|
185
343
|
continue;
|
|
186
344
|
}
|
|
345
|
+
if (arg === "--login-url") {
|
|
346
|
+
options.loginUrl = argv[++i];
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (arg.startsWith("--login-url=")) {
|
|
350
|
+
options.loginUrl = arg.slice(12);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
187
353
|
if (arg === "--token") {
|
|
188
354
|
options.token = argv[++i];
|
|
189
355
|
continue;
|
|
@@ -208,6 +374,10 @@ function parseArgs(argv) {
|
|
|
208
374
|
options.authMode = arg.slice(12);
|
|
209
375
|
continue;
|
|
210
376
|
}
|
|
377
|
+
if (arg === "--browser") {
|
|
378
|
+
options.browser = true;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
211
381
|
if (arg === "--email") {
|
|
212
382
|
options.email = argv[++i];
|
|
213
383
|
continue;
|
|
@@ -640,6 +810,7 @@ function usage() {
|
|
|
640
810
|
" yapi docs-sync [options] [dir...]",
|
|
641
811
|
" yapi docs-sync bind <action> [options]",
|
|
642
812
|
" yapi login [options]",
|
|
813
|
+
" yapi logout [options]",
|
|
643
814
|
" yapi whoami [options]",
|
|
644
815
|
" yapi search [options]",
|
|
645
816
|
" yapi install-skill [options]",
|
|
@@ -740,12 +911,24 @@ function loginUsage() {
|
|
|
740
911
|
"Options:",
|
|
741
912
|
" --config <path> config file path (default: ~/.yapi/config.toml)",
|
|
742
913
|
" --base-url <url> YApi base URL",
|
|
914
|
+
" --login-url <url> page URL for browser login (default: <base-url>)",
|
|
915
|
+
" --browser force browser login and cookie sync",
|
|
743
916
|
" --email <email> login email for global mode",
|
|
744
917
|
" --password <pwd> login password for global mode",
|
|
745
918
|
" --timeout <ms> request timeout in ms",
|
|
746
919
|
" -h, --help show help",
|
|
747
920
|
].join("\n");
|
|
748
921
|
}
|
|
922
|
+
function logoutUsage() {
|
|
923
|
+
return [
|
|
924
|
+
"Usage:",
|
|
925
|
+
" yapi logout [options]",
|
|
926
|
+
"Options:",
|
|
927
|
+
" --config <path> config file path (default: ~/.yapi/config.toml)",
|
|
928
|
+
" --base-url <url> YApi base URL",
|
|
929
|
+
" -h, --help show help",
|
|
930
|
+
].join("\n");
|
|
931
|
+
}
|
|
749
932
|
function whoamiUsage() {
|
|
750
933
|
return [
|
|
751
934
|
"Usage:",
|
|
@@ -1141,6 +1324,8 @@ async function runLogin(rawArgs) {
|
|
|
1141
1324
|
let baseUrl = options.baseUrl || config.base_url || "";
|
|
1142
1325
|
let email = options.email || config.email || "";
|
|
1143
1326
|
let password = options.password || config.password || "";
|
|
1327
|
+
const projectId = options.projectId || config.project_id || "";
|
|
1328
|
+
const token = options.token || config.token || "";
|
|
1144
1329
|
let updated = false;
|
|
1145
1330
|
if (!baseUrl) {
|
|
1146
1331
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -1150,21 +1335,50 @@ async function runLogin(rawArgs) {
|
|
|
1150
1335
|
baseUrl = await promptRequired("YApi base URL: ", false);
|
|
1151
1336
|
updated = true;
|
|
1152
1337
|
}
|
|
1153
|
-
|
|
1338
|
+
const useBrowserLogin = Boolean(options.browser) || !email || !password;
|
|
1339
|
+
if (useBrowserLogin) {
|
|
1154
1340
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1155
|
-
console.error("
|
|
1341
|
+
console.error("browser login requires interactive terminal");
|
|
1156
1342
|
return 2;
|
|
1157
1343
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1344
|
+
try {
|
|
1345
|
+
const session = await loginByBrowserAndReadCookie(baseUrl, options.loginUrl);
|
|
1346
|
+
const cache = new authCache_1.YApiAuthCache(baseUrl, "warn");
|
|
1347
|
+
cache.saveSession({
|
|
1348
|
+
yapiToken: session.yapiToken,
|
|
1349
|
+
yapiUid: session.yapiUid,
|
|
1350
|
+
expiresAt: session.expiresAt,
|
|
1351
|
+
updatedAt: Date.now(),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1164
1356
|
return 2;
|
|
1165
1357
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1358
|
+
const shouldWriteConfig = updated || !fs_1.default.existsSync(configPath) || config.auth_mode !== "global";
|
|
1359
|
+
if (shouldWriteConfig) {
|
|
1360
|
+
const mergedConfig = {
|
|
1361
|
+
base_url: baseUrl,
|
|
1362
|
+
auth_mode: "global",
|
|
1363
|
+
email,
|
|
1364
|
+
password,
|
|
1365
|
+
token,
|
|
1366
|
+
project_id: projectId,
|
|
1367
|
+
};
|
|
1368
|
+
writeConfig(configPath, mergedConfig);
|
|
1369
|
+
}
|
|
1370
|
+
console.log("login success (cookie synced from browser to ~/.yapi-mcp/auth-*.json)");
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
const authService = new auth_1.YApiAuthService(baseUrl, email, password, "warn", {
|
|
1375
|
+
timeoutMs: options.timeout || 30000,
|
|
1376
|
+
});
|
|
1377
|
+
await authService.getCookieHeaderWithLogin({ forceLogin: true });
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1381
|
+
return 2;
|
|
1168
1382
|
}
|
|
1169
1383
|
const shouldWriteConfig = updated || !fs_1.default.existsSync(configPath) || config.auth_mode !== "global";
|
|
1170
1384
|
if (shouldWriteConfig) {
|
|
@@ -1173,22 +1387,36 @@ async function runLogin(rawArgs) {
|
|
|
1173
1387
|
auth_mode: "global",
|
|
1174
1388
|
email,
|
|
1175
1389
|
password,
|
|
1176
|
-
token
|
|
1177
|
-
project_id:
|
|
1390
|
+
token,
|
|
1391
|
+
project_id: projectId,
|
|
1178
1392
|
};
|
|
1179
1393
|
writeConfig(configPath, mergedConfig);
|
|
1180
1394
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1395
|
+
console.log("login success (cookie cached in ~/.yapi-mcp/auth-*.json)");
|
|
1396
|
+
return 0;
|
|
1397
|
+
}
|
|
1398
|
+
async function runLogout(rawArgs) {
|
|
1399
|
+
const options = parseArgs(rawArgs);
|
|
1400
|
+
if (options.help) {
|
|
1401
|
+
console.log(logoutUsage());
|
|
1402
|
+
return 0;
|
|
1186
1403
|
}
|
|
1187
|
-
|
|
1188
|
-
console.
|
|
1404
|
+
if (options.version) {
|
|
1405
|
+
console.log(readVersion());
|
|
1406
|
+
return 0;
|
|
1407
|
+
}
|
|
1408
|
+
const configPath = options.config || globalConfigPath();
|
|
1409
|
+
const config = fs_1.default.existsSync(configPath)
|
|
1410
|
+
? parseSimpleToml(fs_1.default.readFileSync(configPath, "utf8"))
|
|
1411
|
+
: {};
|
|
1412
|
+
const baseUrl = options.baseUrl || config.base_url || "";
|
|
1413
|
+
if (!baseUrl) {
|
|
1414
|
+
console.error("missing --base-url or config base_url");
|
|
1189
1415
|
return 2;
|
|
1190
1416
|
}
|
|
1191
|
-
|
|
1417
|
+
const cache = new authCache_1.YApiAuthCache(baseUrl, "warn");
|
|
1418
|
+
cache.clearSession();
|
|
1419
|
+
console.log("logout success (session cleared from ~/.yapi-mcp/auth-*.json)");
|
|
1192
1420
|
return 0;
|
|
1193
1421
|
}
|
|
1194
1422
|
async function runWhoami(rawArgs) {
|
|
@@ -2489,6 +2717,9 @@ async function main() {
|
|
|
2489
2717
|
if (rawArgs[0] === "login") {
|
|
2490
2718
|
return await runLogin(rawArgs.slice(1));
|
|
2491
2719
|
}
|
|
2720
|
+
if (rawArgs[0] === "logout") {
|
|
2721
|
+
return await runLogout(rawArgs.slice(1));
|
|
2722
|
+
}
|
|
2492
2723
|
if (rawArgs[0] === "whoami") {
|
|
2493
2724
|
return await runWhoami(rawArgs.slice(1));
|
|
2494
2725
|
}
|