yingmi-skill-cli 0.0.1
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 +194 -0
- package/bin/index.js +1349 -0
- package/package.json +39 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
var commander = require('commander');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var os = require('os');
|
|
8
|
+
var path = require('path');
|
|
9
|
+
var axios = require('axios');
|
|
10
|
+
var puppeteer = require('puppeteer');
|
|
11
|
+
var crypto = require('crypto');
|
|
12
|
+
var child_process = require('child_process');
|
|
13
|
+
|
|
14
|
+
var name = "yingmi-cli";
|
|
15
|
+
var version = "0.0.10";
|
|
16
|
+
|
|
17
|
+
function fail(message) {
|
|
18
|
+
console.error(message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveCommand(root, pathSegments) {
|
|
23
|
+
let current = root;
|
|
24
|
+
pathSegments.forEach((segment) => {
|
|
25
|
+
current = current?.commands.find((command) => command.name() === segment);
|
|
26
|
+
});
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
function registerHelpCommand(program) {
|
|
30
|
+
program.command("help").description("\u663E\u793A CLI \u6216\u6307\u5B9A\u5B50\u547D\u4EE4\u7684\u5E2E\u52A9\u4FE1\u606F").argument("[commandPath...]", "\u547D\u4EE4\u8DEF\u5F84\uFF0C\u4F8B\u5982: mcp call").action((commandPath) => {
|
|
31
|
+
if (!commandPath || commandPath.length === 0) {
|
|
32
|
+
program.outputHelp();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const targetCommand = resolveCommand(program, commandPath);
|
|
36
|
+
if (!targetCommand) {
|
|
37
|
+
fail(`\u672A\u627E\u5230\u547D\u4EE4: ${commandPath.join(" ")}`);
|
|
38
|
+
}
|
|
39
|
+
targetCommand.outputHelp();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CONFIG_DIR = path.join(os.homedir(), ".yingmi-cli");
|
|
44
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
45
|
+
const SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
46
|
+
const DEFAULT_QIEMAN_BASE_URL = "https://qieman.com";
|
|
47
|
+
const DEFAULT_REMOTE_SKILL_SERVER_BASE_URL = "https://stargate-staging.yingmi-inc.com/";
|
|
48
|
+
function ensureConfigDir() {
|
|
49
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
function readConfig() {
|
|
52
|
+
ensureConfigDir();
|
|
53
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf8").trim();
|
|
57
|
+
if (!content) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
const parsed = JSON.parse(content);
|
|
61
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
62
|
+
throw new Error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u68C0\u67E5 ~/.yingmi-cli/config.json");
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
function writeConfig(config) {
|
|
67
|
+
ensureConfigDir();
|
|
68
|
+
fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
69
|
+
`, "utf8");
|
|
70
|
+
}
|
|
71
|
+
function setApiKey(apiKey) {
|
|
72
|
+
const nextConfig = {
|
|
73
|
+
...readConfig(),
|
|
74
|
+
apiKey
|
|
75
|
+
};
|
|
76
|
+
writeConfig(nextConfig);
|
|
77
|
+
}
|
|
78
|
+
function getApiKeyApplySession() {
|
|
79
|
+
return readConfig().apiKeyApplySession;
|
|
80
|
+
}
|
|
81
|
+
function setApiKeyApplySession(session) {
|
|
82
|
+
const nextConfig = {
|
|
83
|
+
...readConfig(),
|
|
84
|
+
apiKeyApplySession: session
|
|
85
|
+
};
|
|
86
|
+
writeConfig(nextConfig);
|
|
87
|
+
}
|
|
88
|
+
function clearApiKeyApplySession() {
|
|
89
|
+
const nextConfig = {
|
|
90
|
+
...readConfig()
|
|
91
|
+
};
|
|
92
|
+
delete nextConfig.apiKeyApplySession;
|
|
93
|
+
writeConfig(nextConfig);
|
|
94
|
+
}
|
|
95
|
+
function getCurrentRemoteSkillSession() {
|
|
96
|
+
return readConfig().currentRemoteSkillSession;
|
|
97
|
+
}
|
|
98
|
+
function setCurrentRemoteSkillSession(session) {
|
|
99
|
+
const nextConfig = {
|
|
100
|
+
...readConfig(),
|
|
101
|
+
currentRemoteSkillSession: session
|
|
102
|
+
};
|
|
103
|
+
writeConfig(nextConfig);
|
|
104
|
+
}
|
|
105
|
+
function maskSecret(value) {
|
|
106
|
+
if (!value) {
|
|
107
|
+
return "(\u672A\u8BBE\u7F6E)";
|
|
108
|
+
}
|
|
109
|
+
if (value.length <= 8) {
|
|
110
|
+
return `${value.slice(0, 2)}***${value.slice(-2)}`;
|
|
111
|
+
}
|
|
112
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
113
|
+
}
|
|
114
|
+
function getConfigFilePath() {
|
|
115
|
+
return CONFIG_FILE;
|
|
116
|
+
}
|
|
117
|
+
function getSkillsDirPath() {
|
|
118
|
+
ensureConfigDir();
|
|
119
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
120
|
+
return SKILLS_DIR;
|
|
121
|
+
}
|
|
122
|
+
function getRemoteSkillServerBaseUrl() {
|
|
123
|
+
const configuredBaseUrl = readConfig().remoteSkillServerBaseUrl?.trim();
|
|
124
|
+
return configuredBaseUrl || DEFAULT_REMOTE_SKILL_SERVER_BASE_URL;
|
|
125
|
+
}
|
|
126
|
+
function getQiemanBaseUrl() {
|
|
127
|
+
const configuredBaseUrl = readConfig().qiemanBaseUrl?.trim();
|
|
128
|
+
return configuredBaseUrl || DEFAULT_QIEMAN_BASE_URL;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validatePhone(phone) {
|
|
132
|
+
if (!/^1\d{10}$/.test(phone)) {
|
|
133
|
+
throw new Error("\u624B\u673A\u53F7\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u4F20\u5165 11 \u4F4D\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function validateXSign(xSign) {
|
|
137
|
+
if (!/^\d+[A-Fa-f0-9]{40}$/.test(xSign)) {
|
|
138
|
+
throw new Error("x-sign \u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u521D\u59CB\u5316\u6D41\u7A0B");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function validateVerifyCode(verifyCode) {
|
|
142
|
+
if (!/^\d{4,8}$/.test(verifyCode)) {
|
|
143
|
+
throw new Error("\u9A8C\u8BC1\u7801\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u4F20\u5165 4 \u5230 8 \u4F4D\u6570\u5B57\u9A8C\u8BC1\u7801");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function maskPhone(phone) {
|
|
147
|
+
return phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
|
|
148
|
+
}
|
|
149
|
+
function extractApiKey(response) {
|
|
150
|
+
const apiKey = response.key ?? response.apiKey;
|
|
151
|
+
return typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : void 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getInitStatusSummary() {
|
|
155
|
+
const config = readConfig();
|
|
156
|
+
const session = getApiKeyApplySession();
|
|
157
|
+
return {
|
|
158
|
+
configFile: getConfigFilePath(),
|
|
159
|
+
hasApiKey: Boolean(config.apiKey),
|
|
160
|
+
apiKey: maskSecret(config.apiKey),
|
|
161
|
+
hasPendingSession: Boolean(session),
|
|
162
|
+
pendingPhone: session?.phone ? maskPhone(session.phone) : null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getErrorMessage(error) {
|
|
167
|
+
if (error instanceof axios.AxiosError) {
|
|
168
|
+
const status = error.response?.status;
|
|
169
|
+
const data = error.response?.data;
|
|
170
|
+
const detail = data?.msg?.trim() || data?.detail?.msg?.trim() || error.message || "\u672A\u77E5\u9519\u8BEF";
|
|
171
|
+
return `\u8BF7\u6C42\u5931\u8D25${status ? ` (${status})` : ""}: ${detail}`;
|
|
172
|
+
}
|
|
173
|
+
return error instanceof Error ? error.message : String(error);
|
|
174
|
+
}
|
|
175
|
+
function isInvalidVerifyCodeError(error) {
|
|
176
|
+
if (!(error instanceof axios.AxiosError)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const data = error.response?.data;
|
|
180
|
+
const errorCode = data?.code ?? data?.detail?.code;
|
|
181
|
+
const errorMessage = data?.msg ?? data?.detail?.msg ?? "";
|
|
182
|
+
return errorCode === "8602" || errorMessage.includes("\u77ED\u4FE1\u9A8C\u8BC1\u7801\u6709\u8BEF");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runInitDoctorChecks() {
|
|
186
|
+
const checks = [
|
|
187
|
+
{
|
|
188
|
+
name: "node",
|
|
189
|
+
status: "ok",
|
|
190
|
+
message: `\u5F53\u524D Node.js \u7248\u672C: ${process.version}`
|
|
191
|
+
}
|
|
192
|
+
];
|
|
193
|
+
try {
|
|
194
|
+
const status = getInitStatusSummary();
|
|
195
|
+
checks.push({
|
|
196
|
+
name: "config-file",
|
|
197
|
+
status: "ok",
|
|
198
|
+
message: `\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\u5DF2\u89E3\u6790: ${status.configFile}`
|
|
199
|
+
});
|
|
200
|
+
checks.push(
|
|
201
|
+
status.hasApiKey ? {
|
|
202
|
+
name: "api-key",
|
|
203
|
+
status: "ok",
|
|
204
|
+
message: `\u5DF2\u914D\u7F6E apiKey: ${status.apiKey}`
|
|
205
|
+
} : {
|
|
206
|
+
name: "api-key",
|
|
207
|
+
status: "warning",
|
|
208
|
+
message: "\u5C1A\u672A\u914D\u7F6E apiKey",
|
|
209
|
+
nextStep: "\u6267\u884C yingmi-cli init setup --api-key <your-api-key> \u6216\u9A8C\u8BC1\u7801\u521D\u59CB\u5316\u6D41\u7A0B"
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
if (status.hasPendingSession && status.pendingPhone) {
|
|
213
|
+
checks.push({
|
|
214
|
+
name: "pending-session",
|
|
215
|
+
status: "warning",
|
|
216
|
+
message: `\u5B58\u5728\u5F85\u5B8C\u6210\u7684\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587: ${status.pendingPhone}`,
|
|
217
|
+
nextStep: "\u6267\u884C yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801> \u5B8C\u6210\u521D\u59CB\u5316"
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
checks.push({
|
|
221
|
+
name: "pending-session",
|
|
222
|
+
status: "ok",
|
|
223
|
+
message: "\u5F53\u524D\u6CA1\u6709\u5F85\u5B8C\u6210\u7684\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
checks.push({
|
|
228
|
+
name: "config-read",
|
|
229
|
+
status: "warning",
|
|
230
|
+
message: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25: ${getErrorMessage(error)}`,
|
|
231
|
+
nextStep: "\u68C0\u67E5 ~/.yingmi-cli/config.json \u683C\u5F0F\uFF0C\u5FC5\u8981\u65F6\u4FEE\u590D\u540E\u91CD\u8BD5"
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
status: checks.some((check) => check.status === "warning") ? "warning" : "ok",
|
|
236
|
+
nodeVersion: process.version,
|
|
237
|
+
configFile: getConfigFilePath(),
|
|
238
|
+
checks
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getPendingSetupResult() {
|
|
243
|
+
return {
|
|
244
|
+
status: "pending",
|
|
245
|
+
setupMode: "guided",
|
|
246
|
+
configFile: getConfigFilePath(),
|
|
247
|
+
availableActions: ["--api-key <value>", "--phone <value>", "--verify-code <value>"],
|
|
248
|
+
nextSteps: [
|
|
249
|
+
"yingmi-cli init setup --api-key <your-api-key>",
|
|
250
|
+
"yingmi-cli init setup --phone <your-phone>",
|
|
251
|
+
"yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801>"
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getHeaderValue(headers, headerName) {
|
|
257
|
+
const expectedHeaderName = headerName.toLowerCase();
|
|
258
|
+
const matchedEntry = Object.entries(headers).find(
|
|
259
|
+
([currentHeaderName]) => currentHeaderName.toLowerCase() === expectedHeaderName
|
|
260
|
+
);
|
|
261
|
+
return matchedEntry?.[1];
|
|
262
|
+
}
|
|
263
|
+
function waitForFirstFetchRequest(page, timeoutMs) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
let settled = false;
|
|
266
|
+
const cleanup = () => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
page.off("request", handleRequest);
|
|
269
|
+
};
|
|
270
|
+
const settle = (callback) => {
|
|
271
|
+
if (settled) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
settled = true;
|
|
275
|
+
cleanup();
|
|
276
|
+
callback();
|
|
277
|
+
};
|
|
278
|
+
const handleRequest = (request) => {
|
|
279
|
+
if (request.resourceType() !== "fetch") {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
settle(() => resolve(request));
|
|
283
|
+
};
|
|
284
|
+
const timer = setTimeout(() => {
|
|
285
|
+
settle(() => reject(new Error(`\u5728 ${timeoutMs}ms \u5185\u672A\u6355\u83B7\u5230 fetch \u8BF7\u6C42`)));
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
page.on("request", handleRequest);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getCandidateExecutablePaths() {
|
|
292
|
+
const homeDirectory = os.homedir();
|
|
293
|
+
switch (process.platform) {
|
|
294
|
+
case "darwin":
|
|
295
|
+
return [
|
|
296
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
297
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
298
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
299
|
+
];
|
|
300
|
+
case "win32":
|
|
301
|
+
return [
|
|
302
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
303
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
304
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
305
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
306
|
+
];
|
|
307
|
+
default:
|
|
308
|
+
return [
|
|
309
|
+
"/usr/bin/google-chrome",
|
|
310
|
+
"/usr/bin/google-chrome-stable",
|
|
311
|
+
"/usr/bin/chromium",
|
|
312
|
+
"/usr/bin/chromium-browser",
|
|
313
|
+
`${homeDirectory}/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome`
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function resolveExecutablePath(executablePath) {
|
|
318
|
+
const customExecutablePath = executablePath?.trim() || process.env.PUPPETEER_EXECUTABLE_PATH?.trim();
|
|
319
|
+
if (customExecutablePath) {
|
|
320
|
+
if (!fs.existsSync(customExecutablePath)) {
|
|
321
|
+
throw new Error(`\u6D4F\u89C8\u5668\u4E0D\u5B58\u5728: ${customExecutablePath}`);
|
|
322
|
+
}
|
|
323
|
+
return customExecutablePath;
|
|
324
|
+
}
|
|
325
|
+
const matchedPath = getCandidateExecutablePaths().find(
|
|
326
|
+
(candidatePath) => fs.existsSync(candidatePath)
|
|
327
|
+
);
|
|
328
|
+
if (!matchedPath) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
"\u672A\u627E\u5230\u53EF\u7528\u7684 Chrome/Chromium\uFF0C\u53EF\u901A\u8FC7 --executable-path \u6216 PUPPETEER_EXECUTABLE_PATH \u6307\u5B9A\u6D4F\u89C8\u5668\u8DEF\u5F84"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return matchedPath;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const DEFAULT_TIMEOUT_MS = 2e4;
|
|
337
|
+
async function getXSign(options = {}) {
|
|
338
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
339
|
+
const browser = await puppeteer.launch({
|
|
340
|
+
executablePath: resolveExecutablePath(options.executablePath),
|
|
341
|
+
headless: options.headless ?? true,
|
|
342
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
343
|
+
});
|
|
344
|
+
try {
|
|
345
|
+
const page = await browser.newPage();
|
|
346
|
+
const firstFetchRequestPromise = waitForFirstFetchRequest(page, timeoutMs);
|
|
347
|
+
const url = options.url ?? getQiemanBaseUrl();
|
|
348
|
+
await page.goto(url, {
|
|
349
|
+
waitUntil: "domcontentloaded",
|
|
350
|
+
timeout: timeoutMs
|
|
351
|
+
});
|
|
352
|
+
const firstFetchRequest = await firstFetchRequestPromise;
|
|
353
|
+
const xSign = getHeaderValue(firstFetchRequest.headers(), "x-sign")?.trim();
|
|
354
|
+
if (!xSign) {
|
|
355
|
+
throw new Error(`\u9996\u4E2A fetch \u8BF7\u6C42\u672A\u643A\u5E26 x-sign: ${firstFetchRequest.url()}`);
|
|
356
|
+
}
|
|
357
|
+
return xSign;
|
|
358
|
+
} finally {
|
|
359
|
+
await browser.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function trimTrailingSlash(value) {
|
|
364
|
+
return value.replace(/\/+$/, "");
|
|
365
|
+
}
|
|
366
|
+
function getQiemanPmdjBaseUrl() {
|
|
367
|
+
return `${trimTrailingSlash(getQiemanBaseUrl())}/pmdj/v1`;
|
|
368
|
+
}
|
|
369
|
+
const API_KEY_APPLICATION_PAYLOAD = {
|
|
370
|
+
name: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
|
|
371
|
+
organization: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
|
|
372
|
+
email: "",
|
|
373
|
+
usagePurposes: ["\u63A2\u7D22\u66F4\u591A\u5408\u4F5C\u53EF\u80FD"],
|
|
374
|
+
usageDescription: "",
|
|
375
|
+
agreementIds: ["AGREEMENT34-V20250425"],
|
|
376
|
+
positionId: "user_position_other",
|
|
377
|
+
position: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
|
|
378
|
+
organizationType: "\u5176\u4ED6"
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
function createRequestId() {
|
|
382
|
+
const hexTimestamp = Date.now().toString(16).toUpperCase();
|
|
383
|
+
const randomSuffix = crypto.randomBytes(8).toString("hex").toUpperCase();
|
|
384
|
+
return `yingmi-cli.${hexTimestamp}${randomSuffix}`;
|
|
385
|
+
}
|
|
386
|
+
function getHeaders(xSign, accessToken) {
|
|
387
|
+
return {
|
|
388
|
+
accept: "application/json",
|
|
389
|
+
"content-type": "application/json",
|
|
390
|
+
"x-request-id": createRequestId(),
|
|
391
|
+
"x-sign": xSign,
|
|
392
|
+
...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function prepareSmsSetup(phone) {
|
|
397
|
+
validatePhone(phone);
|
|
398
|
+
const qiemanPmdjBaseUrl = getQiemanPmdjBaseUrl();
|
|
399
|
+
const xSign = await getXSign();
|
|
400
|
+
validateXSign(xSign);
|
|
401
|
+
const response = await axios.post(
|
|
402
|
+
`${qiemanPmdjBaseUrl}/user/register/phone/prepare`,
|
|
403
|
+
{ phone },
|
|
404
|
+
{
|
|
405
|
+
headers: getHeaders(xSign),
|
|
406
|
+
timeout: 2e4
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
if (!response.data.token) {
|
|
410
|
+
throw new Error("\u83B7\u53D6\u9A8C\u8BC1\u7801\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 token\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
411
|
+
}
|
|
412
|
+
setApiKeyApplySession({
|
|
413
|
+
phone,
|
|
414
|
+
xSign,
|
|
415
|
+
prepareToken: response.data.token,
|
|
416
|
+
requestedAt: Date.now()
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
status: "code_sent",
|
|
420
|
+
setupMode: "sms_prepare",
|
|
421
|
+
configFile: getConfigFilePath(),
|
|
422
|
+
pendingPhone: maskPhone(phone),
|
|
423
|
+
cooldownSeconds: response.data.nextInSec ?? null,
|
|
424
|
+
nextStep: "yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801>"
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function setupWithApiKey(apiKey) {
|
|
429
|
+
setApiKey(apiKey);
|
|
430
|
+
return {
|
|
431
|
+
status: "configured",
|
|
432
|
+
setupMode: "direct_api_key",
|
|
433
|
+
apiKey: maskSecret(apiKey),
|
|
434
|
+
configFile: getConfigFilePath()
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function refreshSessionXSignOnInvalidVerifyCode(session) {
|
|
439
|
+
if (!session) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const nextXSign = await getXSign();
|
|
443
|
+
validateXSign(nextXSign);
|
|
444
|
+
setApiKeyApplySession({
|
|
445
|
+
...session,
|
|
446
|
+
xSign: nextXSign
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function confirmSmsSetup(verifyCode) {
|
|
451
|
+
validateVerifyCode(verifyCode);
|
|
452
|
+
const session = getApiKeyApplySession();
|
|
453
|
+
if (!session) {
|
|
454
|
+
throw new Error("\u672A\u627E\u5230\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u5148\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>");
|
|
455
|
+
}
|
|
456
|
+
if (!session.accessToken && !session.prepareToken) {
|
|
457
|
+
throw new Error("\u7533\u8BF7\u4E0A\u4E0B\u6587\u7F3A\u5C11\u767B\u5F55 token\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>");
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const qiemanPmdjBaseUrl = getQiemanPmdjBaseUrl();
|
|
461
|
+
let accessToken = session.accessToken;
|
|
462
|
+
if (!accessToken) {
|
|
463
|
+
const confirmResponse = await axios.post(
|
|
464
|
+
`${qiemanPmdjBaseUrl}/user/register/phone/confirm`,
|
|
465
|
+
{
|
|
466
|
+
phone: session.phone,
|
|
467
|
+
poManagerName: null,
|
|
468
|
+
token: session.prepareToken,
|
|
469
|
+
verifyCode,
|
|
470
|
+
autoLogin: true
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
headers: getHeaders(session.xSign),
|
|
474
|
+
timeout: 2e4
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
accessToken = confirmResponse.data.accessToken?.trim();
|
|
478
|
+
if (!accessToken) {
|
|
479
|
+
throw new Error("\u786E\u8BA4\u767B\u5F55\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 accessToken\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
480
|
+
}
|
|
481
|
+
setApiKeyApplySession({
|
|
482
|
+
...session,
|
|
483
|
+
accessToken
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const currentApplicationResponse = await axios.get(
|
|
487
|
+
`${qiemanPmdjBaseUrl}/stargate/api-key-application`,
|
|
488
|
+
{
|
|
489
|
+
headers: getHeaders(session.xSign, accessToken),
|
|
490
|
+
timeout: 2e4
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
let apiKey = extractApiKey(currentApplicationResponse.data);
|
|
494
|
+
if (!apiKey) {
|
|
495
|
+
const hasExistingApplication = Object.keys(currentApplicationResponse.data ?? {}).length > 0;
|
|
496
|
+
if (hasExistingApplication) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`\u5DF2\u5B58\u5728 API Key \u7533\u8BF7\u8BB0\u5F55\uFF0C\u4F46\u54CD\u5E94\u4E2D\u672A\u8FD4\u56DE key\uFF0C\u5F53\u524D\u72B6\u6001: ${currentApplicationResponse.data.status ?? "unknown"}`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const applyResponse = await axios.post(
|
|
502
|
+
`${qiemanPmdjBaseUrl}/stargate/api-key-application`,
|
|
503
|
+
API_KEY_APPLICATION_PAYLOAD,
|
|
504
|
+
{
|
|
505
|
+
headers: getHeaders(session.xSign, accessToken),
|
|
506
|
+
timeout: 2e4
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
apiKey = extractApiKey(applyResponse.data);
|
|
510
|
+
}
|
|
511
|
+
if (!apiKey) {
|
|
512
|
+
throw new Error("API Key \u7533\u8BF7\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 key\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
513
|
+
}
|
|
514
|
+
setApiKey(apiKey);
|
|
515
|
+
clearApiKeyApplySession();
|
|
516
|
+
return {
|
|
517
|
+
status: "configured",
|
|
518
|
+
setupMode: "sms_confirm",
|
|
519
|
+
apiKey: maskSecret(apiKey),
|
|
520
|
+
configFile: getConfigFilePath()
|
|
521
|
+
};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (isInvalidVerifyCodeError(error)) {
|
|
524
|
+
try {
|
|
525
|
+
await refreshSessionXSignOnInvalidVerifyCode(session);
|
|
526
|
+
} catch (refreshError) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`\u9A8C\u8BC1\u7801\u6821\u9A8C\u5931\u8D25\uFF0C\u5237\u65B0 x-sign \u4E5F\u5931\u8D25\u4E86: ${getErrorMessage(refreshError)}\u3002\u8BF7\u91CD\u65B0\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
throw new Error(getErrorMessage(error));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function countSetupModes(options) {
|
|
537
|
+
return [options.apiKey, options.phone, options.verifyCode].filter(Boolean).length;
|
|
538
|
+
}
|
|
539
|
+
function persistHiddenConfigOptions(options) {
|
|
540
|
+
const remoteSkillServerBaseUrl = options.remoteSkillServerBaseUrl?.trim();
|
|
541
|
+
const qiemanBaseUrl = options.qiemanBaseUrl?.trim();
|
|
542
|
+
if (!remoteSkillServerBaseUrl && !qiemanBaseUrl) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
writeConfig({
|
|
546
|
+
...readConfig(),
|
|
547
|
+
...remoteSkillServerBaseUrl ? { remoteSkillServerBaseUrl } : {},
|
|
548
|
+
...qiemanBaseUrl ? { qiemanBaseUrl } : {}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async function runInitSetup(options) {
|
|
552
|
+
const apiKey = options.apiKey?.trim();
|
|
553
|
+
const phone = options.phone?.trim();
|
|
554
|
+
const qiemanBaseUrl = options.qiemanBaseUrl?.trim();
|
|
555
|
+
const remoteSkillServerBaseUrl = options.remoteSkillServerBaseUrl?.trim();
|
|
556
|
+
const verifyCode = options.verifyCode?.trim();
|
|
557
|
+
persistHiddenConfigOptions({
|
|
558
|
+
remoteSkillServerBaseUrl,
|
|
559
|
+
qiemanBaseUrl
|
|
560
|
+
});
|
|
561
|
+
if (countSetupModes({ apiKey, phone, verifyCode }) > 1) {
|
|
562
|
+
throw new Error("`--api-key`\u3001`--phone`\u3001`--verify-code` \u4E00\u6B21\u53EA\u80FD\u4F7F\u7528\u4E00\u79CD\uFF0C\u8BF7\u6309\u521D\u59CB\u5316\u9636\u6BB5\u5206\u522B\u6267\u884C");
|
|
563
|
+
}
|
|
564
|
+
if (apiKey) {
|
|
565
|
+
return setupWithApiKey(apiKey);
|
|
566
|
+
}
|
|
567
|
+
if (phone) {
|
|
568
|
+
return prepareSmsSetup(phone);
|
|
569
|
+
}
|
|
570
|
+
if (verifyCode) {
|
|
571
|
+
return confirmSmsSetup(verifyCode);
|
|
572
|
+
}
|
|
573
|
+
return getPendingSetupResult();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function registerDoctorCommand(initCommand) {
|
|
577
|
+
initCommand.command("doctor").description("\u68C0\u67E5\u672C\u5730\u914D\u7F6E\u4E0E\u8FD0\u884C\u73AF\u5883").action(() => {
|
|
578
|
+
console.log(JSON.stringify(runInitDoctorChecks(), null, 2));
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function registerSetupCommand(initCommand) {
|
|
583
|
+
initCommand.command("setup").description("\u6267\u884C\u9996\u6B21\u521D\u59CB\u5316\uFF0C\u652F\u6301\u76F4\u63A5\u5199\u5165 API Key \u6216\u9A8C\u8BC1\u7801\u7533\u8BF7 API Key").option("--api-key <value>", "\u76F4\u63A5\u5199\u5165 StarGate API Key").option("--phone <value>", "\u901A\u8FC7\u624B\u673A\u53F7\u53D1\u8D77\u9A8C\u8BC1\u7801\u521D\u59CB\u5316").option("--verify-code <value>", "\u786E\u8BA4\u77ED\u4FE1\u9A8C\u8BC1\u7801\u5E76\u5B8C\u6210 API Key \u521D\u59CB\u5316").addOption(
|
|
584
|
+
new commander.Option("--remote-skill-server-base-url <value>", "\u8986\u76D6 remote-skill \u670D\u52A1\u57FA\u5730\u5740").hideHelp()
|
|
585
|
+
).addOption(new commander.Option("--qieman-base-url <value>", "\u8986\u76D6\u4E14\u6162\u670D\u52A1\u57FA\u5730\u5740").hideHelp()).action(async (options) => {
|
|
586
|
+
try {
|
|
587
|
+
const result = await runInitSetup(options);
|
|
588
|
+
console.log(JSON.stringify(result, null, 2));
|
|
589
|
+
} catch (error) {
|
|
590
|
+
fail(error.message);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function registerStatusCommand(initCommand) {
|
|
596
|
+
initCommand.command("status").description("\u67E5\u770B\u521D\u59CB\u5316\u72B6\u6001\u548C\u914D\u7F6E\u6458\u8981").action(() => {
|
|
597
|
+
console.log(JSON.stringify(getInitStatusSummary(), null, 2));
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function registerInitCommand(program) {
|
|
602
|
+
const initCommand = program.command("init").description("\u521D\u59CB\u5316 CLI \u5E76\u67E5\u770B\u5F53\u524D\u72B6\u6001");
|
|
603
|
+
registerSetupCommand(initCommand);
|
|
604
|
+
registerStatusCommand(initCommand);
|
|
605
|
+
registerDoctorCommand(initCommand);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function buildRequestPath(pathTemplate, pathValues) {
|
|
609
|
+
return pathTemplate.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
610
|
+
const value = pathValues[key];
|
|
611
|
+
if (value === void 0) {
|
|
612
|
+
throw new Error(`\u7F3A\u5C11 path \u53C2\u6570: ${key}`);
|
|
613
|
+
}
|
|
614
|
+
return encodeURIComponent(String(value));
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isPlainObject(value) {
|
|
619
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function readJsonInput(input, inputFile) {
|
|
623
|
+
if (input && inputFile) {
|
|
624
|
+
fail("--input \u548C --input-file \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
|
|
625
|
+
}
|
|
626
|
+
if (!input && !inputFile) {
|
|
627
|
+
return {};
|
|
628
|
+
}
|
|
629
|
+
const raw = inputFile ? fs.readFileSync(inputFile, "utf8") : input;
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(raw ?? "{}");
|
|
632
|
+
} catch (error) {
|
|
633
|
+
fail(`\u8F93\u5165\u4E0D\u662F\u5408\u6CD5 JSON: ${error.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function requireApiKey() {
|
|
638
|
+
const { apiKey } = readConfig();
|
|
639
|
+
if (!apiKey) {
|
|
640
|
+
fail(
|
|
641
|
+
"\u672A\u914D\u7F6E apiKey\uFF0C\u8BF7\u5148\u6267\u884C: yingmi-cli init setup --api-key <your-api-key>\uFF0C\u6216\u5148\u7528 yingmi-cli init setup --phone <\u624B\u673A\u53F7> \u5B8C\u6210\u9A8C\u8BC1\u7801\u521D\u59CB\u5316"
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return apiKey;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function getParametersByLocation(operation, location) {
|
|
648
|
+
return operation.parameters.filter((parameter) => parameter.in === location);
|
|
649
|
+
}
|
|
650
|
+
function splitInput(operation, rawInput) {
|
|
651
|
+
if (!isPlainObject(rawInput)) {
|
|
652
|
+
if (operation.requestBody && operation.parameters.length === 0) {
|
|
653
|
+
return {
|
|
654
|
+
path: {},
|
|
655
|
+
query: {},
|
|
656
|
+
header: {},
|
|
657
|
+
body: rawInput
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
fail("\u8C03\u7528\u8F93\u5165\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
|
|
661
|
+
}
|
|
662
|
+
const reservedKeys = /* @__PURE__ */ new Set(["path", "query", "header", "body"]);
|
|
663
|
+
const pathParameters = getParametersByLocation(operation, "path");
|
|
664
|
+
const queryParameters = getParametersByLocation(operation, "query");
|
|
665
|
+
const headerParameters = getParametersByLocation(operation, "header");
|
|
666
|
+
const pathValues = {
|
|
667
|
+
...Object.fromEntries(pathParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])),
|
|
668
|
+
...isPlainObject(rawInput.path) ? rawInput.path : {}
|
|
669
|
+
};
|
|
670
|
+
const queryValues = {
|
|
671
|
+
...Object.fromEntries(queryParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])),
|
|
672
|
+
...isPlainObject(rawInput.query) ? rawInput.query : {}
|
|
673
|
+
};
|
|
674
|
+
const headerValues = {
|
|
675
|
+
...Object.fromEntries(
|
|
676
|
+
headerParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])
|
|
677
|
+
),
|
|
678
|
+
...isPlainObject(rawInput.header) ? rawInput.header : {}
|
|
679
|
+
};
|
|
680
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(rawInput, "body") ? rawInput.body : void 0;
|
|
681
|
+
const parameterNames = /* @__PURE__ */ new Set([
|
|
682
|
+
...pathParameters.map((parameter) => parameter.name),
|
|
683
|
+
...queryParameters.map((parameter) => parameter.name),
|
|
684
|
+
...headerParameters.map((parameter) => parameter.name)
|
|
685
|
+
]);
|
|
686
|
+
const remainingTopLevelEntries = Object.entries(rawInput).filter(
|
|
687
|
+
([key]) => !reservedKeys.has(key) && !parameterNames.has(key)
|
|
688
|
+
);
|
|
689
|
+
let body = explicitBody;
|
|
690
|
+
if (body === void 0 && operation.requestBody) {
|
|
691
|
+
if (operation.parameters.length === 0) {
|
|
692
|
+
body = rawInput;
|
|
693
|
+
} else if (remainingTopLevelEntries.length > 0) {
|
|
694
|
+
body = Object.fromEntries(remainingTopLevelEntries);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
path: pathValues,
|
|
699
|
+
query: queryValues,
|
|
700
|
+
header: headerValues,
|
|
701
|
+
body
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function getParametersByIn(operation, location) {
|
|
705
|
+
return getParametersByLocation(operation, location);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function inferSchemaType(schema) {
|
|
709
|
+
if (schema.type) {
|
|
710
|
+
return schema.type;
|
|
711
|
+
}
|
|
712
|
+
if (schema.properties || schema.required) {
|
|
713
|
+
return "object";
|
|
714
|
+
}
|
|
715
|
+
if (schema.items) {
|
|
716
|
+
return "array";
|
|
717
|
+
}
|
|
718
|
+
return void 0;
|
|
719
|
+
}
|
|
720
|
+
function validateSchema(schema, value, label) {
|
|
721
|
+
if (!schema || value === void 0) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (value === null) {
|
|
725
|
+
if (schema.nullable) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
throw new Error(`${label} \u4E0D\u80FD\u4E3A null`);
|
|
729
|
+
}
|
|
730
|
+
if (schema.oneOf?.length) {
|
|
731
|
+
const matches = schema.oneOf.some((candidate) => {
|
|
732
|
+
try {
|
|
733
|
+
validateSchema(candidate, value, label);
|
|
734
|
+
return true;
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
if (!matches) {
|
|
740
|
+
throw new Error(`${label} \u4E0D\u7B26\u5408 oneOf \u7EA6\u675F`);
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (schema.anyOf?.length) {
|
|
745
|
+
const matches = schema.anyOf.some((candidate) => {
|
|
746
|
+
try {
|
|
747
|
+
validateSchema(candidate, value, label);
|
|
748
|
+
return true;
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
if (!matches) {
|
|
754
|
+
throw new Error(`${label} \u4E0D\u7B26\u5408 anyOf \u7EA6\u675F`);
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (schema.enum && !schema.enum.some((item) => item === value)) {
|
|
759
|
+
throw new Error(`${label} \u4E0D\u5728\u5141\u8BB8\u7684\u679A\u4E3E\u503C\u4E2D: ${schema.enum.join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
const type = inferSchemaType(schema);
|
|
762
|
+
switch (type) {
|
|
763
|
+
case "object": {
|
|
764
|
+
if (!isPlainObject(value)) {
|
|
765
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
766
|
+
}
|
|
767
|
+
for (const requiredKey of schema.required ?? []) {
|
|
768
|
+
if (value[requiredKey] === void 0) {
|
|
769
|
+
throw new Error(`${label}.${requiredKey} \u4E3A\u5FC5\u586B\u9879`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
Object.entries(schema.properties ?? {}).forEach(([propertyName, propertySchema]) => {
|
|
773
|
+
validateSchema(propertySchema, value[propertyName], `${label}.${propertyName}`);
|
|
774
|
+
});
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
case "array": {
|
|
778
|
+
if (!Array.isArray(value)) {
|
|
779
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u6570\u7EC4`);
|
|
780
|
+
}
|
|
781
|
+
value.forEach((item, index) => validateSchema(schema.items, item, `${label}[${index}]`));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
case "string": {
|
|
785
|
+
if (typeof value !== "string") {
|
|
786
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
case "integer": {
|
|
791
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
792
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u6574\u6570`);
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
case "number": {
|
|
797
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
798
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u6570\u5B57`);
|
|
799
|
+
}
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
case "boolean": {
|
|
803
|
+
if (typeof value !== "boolean") {
|
|
804
|
+
throw new Error(`${label} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
default:
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function validateParameters(parameters, values, locationLabel) {
|
|
813
|
+
parameters.forEach((parameter) => {
|
|
814
|
+
const value = values[parameter.name];
|
|
815
|
+
if (parameter.required && value === void 0) {
|
|
816
|
+
throw new Error(`${locationLabel} \u53C2\u6570 ${parameter.name} \u4E3A\u5FC5\u586B\u9879`);
|
|
817
|
+
}
|
|
818
|
+
validateSchema(parameter.schema, value, `${locationLabel}.${parameter.name}`);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const DOCS_URL = "https://stargate.yingmi.com/api/docs.json";
|
|
823
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
824
|
+
async function fetchOpenApiDocument(apiKey) {
|
|
825
|
+
const response = await axios.get(DOCS_URL, {
|
|
826
|
+
params: {
|
|
827
|
+
apiKey
|
|
828
|
+
},
|
|
829
|
+
timeout: 15e3
|
|
830
|
+
});
|
|
831
|
+
return response.data;
|
|
832
|
+
}
|
|
833
|
+
function getServerBaseUrl(document) {
|
|
834
|
+
return document.servers?.[0]?.url ?? "https://stargate.yingmi.com/api";
|
|
835
|
+
}
|
|
836
|
+
function listOperations(document) {
|
|
837
|
+
const operations = [];
|
|
838
|
+
Object.entries(document.paths).forEach(([path, pathItem]) => {
|
|
839
|
+
const sharedParameters = pathItem.parameters ?? [];
|
|
840
|
+
HTTP_METHODS.forEach((method) => {
|
|
841
|
+
const operation = pathItem[method];
|
|
842
|
+
if (!operation?.operationId) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
operations.push({
|
|
846
|
+
name: operation.operationId,
|
|
847
|
+
summary: operation.summary,
|
|
848
|
+
description: operation.description,
|
|
849
|
+
method: method.toUpperCase(),
|
|
850
|
+
path,
|
|
851
|
+
parameters: [...sharedParameters, ...operation.parameters ?? []],
|
|
852
|
+
requestBody: operation.requestBody,
|
|
853
|
+
responses: operation.responses ?? {}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
return operations.sort((left, right) => left.name.localeCompare(right.name));
|
|
858
|
+
}
|
|
859
|
+
function getOperationByName(document, toolName) {
|
|
860
|
+
return listOperations(document).find((operation) => operation.name === toolName);
|
|
861
|
+
}
|
|
862
|
+
function getPrimaryRequestSchema(operation) {
|
|
863
|
+
const content = operation.requestBody?.content;
|
|
864
|
+
if (!content) {
|
|
865
|
+
return void 0;
|
|
866
|
+
}
|
|
867
|
+
return content["application/json"]?.schema ?? content["*/*"]?.schema ?? Object.values(content)[0]?.schema;
|
|
868
|
+
}
|
|
869
|
+
function getPrimaryRequestContentType(operation) {
|
|
870
|
+
const content = operation.requestBody?.content;
|
|
871
|
+
if (!content) {
|
|
872
|
+
return void 0;
|
|
873
|
+
}
|
|
874
|
+
if (content["application/json"]) {
|
|
875
|
+
return "application/json";
|
|
876
|
+
}
|
|
877
|
+
if (content["*/*"]) {
|
|
878
|
+
return "application/json";
|
|
879
|
+
}
|
|
880
|
+
return Object.keys(content)[0];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function registerCallCommand(mcpCommand) {
|
|
884
|
+
mcpCommand.command("call").description("\u8C03\u7528\u6307\u5B9A MCP \u5DE5\u5177").argument("<toolName>", "\u5DE5\u5177\u540D").option("--input <json>", "\u76F4\u63A5\u4F20\u5165 JSON \u5B57\u7B26\u4E32").option("--input-file <path>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6 JSON \u8F93\u5165").action(async (toolName, options) => {
|
|
885
|
+
const apiKey = requireApiKey();
|
|
886
|
+
const document = await fetchOpenApiDocument(apiKey);
|
|
887
|
+
const operation = getOperationByName(document, toolName);
|
|
888
|
+
if (!operation) {
|
|
889
|
+
fail(`Tool not found: ${toolName}`);
|
|
890
|
+
}
|
|
891
|
+
const rawInput = readJsonInput(options.input, options.inputFile);
|
|
892
|
+
const sections = splitInput(operation, rawInput);
|
|
893
|
+
try {
|
|
894
|
+
validateParameters(getParametersByIn(operation, "path"), sections.path, "path");
|
|
895
|
+
validateParameters(getParametersByIn(operation, "query"), sections.query, "query");
|
|
896
|
+
validateParameters(getParametersByIn(operation, "header"), sections.header, "header");
|
|
897
|
+
if (operation.requestBody?.required && sections.body === void 0) {
|
|
898
|
+
throw new Error("body \u4E3A\u5FC5\u586B\u9879");
|
|
899
|
+
}
|
|
900
|
+
validateSchema(getPrimaryRequestSchema(operation), sections.body, "body");
|
|
901
|
+
} catch (error) {
|
|
902
|
+
fail(`\u8F93\u5165\u6821\u9A8C\u5931\u8D25: ${error.message}`);
|
|
903
|
+
}
|
|
904
|
+
const url = buildRequestPath(operation.path, sections.path);
|
|
905
|
+
const contentType = getPrimaryRequestContentType(operation);
|
|
906
|
+
try {
|
|
907
|
+
const response = await axios.request({
|
|
908
|
+
method: operation.method,
|
|
909
|
+
baseURL: getServerBaseUrl(document),
|
|
910
|
+
url,
|
|
911
|
+
params: sections.query,
|
|
912
|
+
data: sections.body,
|
|
913
|
+
headers: {
|
|
914
|
+
Authorization: `Bearer ${apiKey}`,
|
|
915
|
+
...contentType ? { "Content-Type": contentType } : {},
|
|
916
|
+
...Object.fromEntries(
|
|
917
|
+
Object.entries(sections.header).map(([key, value]) => [key, String(value)])
|
|
918
|
+
)
|
|
919
|
+
},
|
|
920
|
+
timeout: 2e4
|
|
921
|
+
});
|
|
922
|
+
console.log(JSON.stringify(response.data, null, 2));
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (error instanceof axios.AxiosError) {
|
|
925
|
+
const status = error.response?.status;
|
|
926
|
+
const data = error.response?.data;
|
|
927
|
+
const detail = data ? JSON.stringify(data, null, 2) : error.message;
|
|
928
|
+
fail(`\u8C03\u7528\u5931\u8D25${status ? ` (${status})` : ""}: ${detail}`);
|
|
929
|
+
}
|
|
930
|
+
fail(`\u8C03\u7528\u5931\u8D25: ${error.message}`);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function registerListCommand(mcpCommand) {
|
|
936
|
+
mcpCommand.command("list").description("\u5217\u51FA\u6240\u6709 MCP \u5DE5\u5177\u6458\u8981").action(async () => {
|
|
937
|
+
const apiKey = requireApiKey();
|
|
938
|
+
const document = await fetchOpenApiDocument(apiKey);
|
|
939
|
+
const operations = listOperations(document);
|
|
940
|
+
console.log(
|
|
941
|
+
JSON.stringify(
|
|
942
|
+
operations.map((operation) => ({
|
|
943
|
+
name: operation.name,
|
|
944
|
+
summary: operation.summary,
|
|
945
|
+
description: operation.description,
|
|
946
|
+
method: operation.method,
|
|
947
|
+
path: operation.path
|
|
948
|
+
})),
|
|
949
|
+
null,
|
|
950
|
+
2
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function registerSchemaCommand(mcpCommand) {
|
|
957
|
+
mcpCommand.command("schema").description("\u67E5\u770B\u67D0\u4E2A MCP \u5DE5\u5177\u7684\u5B8C\u6574 Schema").argument("<toolName>", "\u5DE5\u5177\u540D").action(async (toolName) => {
|
|
958
|
+
const apiKey = requireApiKey();
|
|
959
|
+
const document = await fetchOpenApiDocument(apiKey);
|
|
960
|
+
const operation = getOperationByName(document, toolName);
|
|
961
|
+
if (!operation) {
|
|
962
|
+
fail(`Tool not found: ${toolName}`);
|
|
963
|
+
}
|
|
964
|
+
console.log(
|
|
965
|
+
JSON.stringify(
|
|
966
|
+
{
|
|
967
|
+
name: operation.name,
|
|
968
|
+
summary: operation.summary,
|
|
969
|
+
description: operation.description,
|
|
970
|
+
method: operation.method,
|
|
971
|
+
path: operation.path,
|
|
972
|
+
parameters: operation.parameters,
|
|
973
|
+
requestBody: operation.requestBody,
|
|
974
|
+
responses: operation.responses
|
|
975
|
+
},
|
|
976
|
+
null,
|
|
977
|
+
2
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function registerMcpCommand(program) {
|
|
984
|
+
const mcpCommand = program.command("mcp").description("\u6D4F\u89C8 MCP \u6458\u8981\u3001\u67E5\u770B Schema \u5E76\u8C03\u7528\u5DE5\u5177");
|
|
985
|
+
registerListCommand(mcpCommand);
|
|
986
|
+
registerSchemaCommand(mcpCommand);
|
|
987
|
+
registerCallCommand(mcpCommand);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function runScriptInDirectory(script, workingDirectory) {
|
|
991
|
+
const shellCommand = process.platform === "win32" ? "cmd.exe" : "sh";
|
|
992
|
+
const shellArgs = process.platform === "win32" ? ["/d", "/s", "/c", script] : ["-lc", script];
|
|
993
|
+
return new Promise((resolve, reject) => {
|
|
994
|
+
const child = child_process.spawn(shellCommand, shellArgs, {
|
|
995
|
+
cwd: workingDirectory,
|
|
996
|
+
env: process.env
|
|
997
|
+
});
|
|
998
|
+
let stdout = "";
|
|
999
|
+
let stderr = "";
|
|
1000
|
+
child.stdout?.on("data", (chunk) => {
|
|
1001
|
+
stdout += chunk.toString();
|
|
1002
|
+
});
|
|
1003
|
+
child.stderr?.on("data", (chunk) => {
|
|
1004
|
+
stderr += chunk.toString();
|
|
1005
|
+
});
|
|
1006
|
+
child.on("error", reject);
|
|
1007
|
+
child.on("close", (code) => {
|
|
1008
|
+
const exitCode = code ?? 1;
|
|
1009
|
+
resolve({
|
|
1010
|
+
exitCode,
|
|
1011
|
+
status: exitCode === 0 ? "success" : "failed",
|
|
1012
|
+
stdout,
|
|
1013
|
+
stderr
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function extractZipArchive(archivePath, targetDirectory) {
|
|
1020
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
1021
|
+
await new Promise((resolve, reject) => {
|
|
1022
|
+
const child = child_process.spawn("unzip", ["-oq", archivePath, "-d", targetDirectory], {
|
|
1023
|
+
stdio: "inherit"
|
|
1024
|
+
});
|
|
1025
|
+
child.on("error", reject);
|
|
1026
|
+
child.on("exit", (code) => {
|
|
1027
|
+
if (code === 0) {
|
|
1028
|
+
resolve();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
reject(new Error(`unzip \u89E3\u538B\u5931\u8D25\uFF0C\u9000\u51FA\u7801: ${code ?? "null"}`));
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const REQUEST_TIMEOUT = 15e3;
|
|
1037
|
+
const DOWNLOAD_TIMEOUT = 12e4;
|
|
1038
|
+
const REMOTE_SKILL_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
1039
|
+
function normalizeServerBaseUrl(serverBaseUrl) {
|
|
1040
|
+
return serverBaseUrl.replace(/\/+$/, "");
|
|
1041
|
+
}
|
|
1042
|
+
function buildRequestHeaders(apiKey) {
|
|
1043
|
+
return {
|
|
1044
|
+
apiKey
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function listDirectoryEntries(rootDirectory, baseDirectory = rootDirectory) {
|
|
1048
|
+
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true }).sort((left, right) => {
|
|
1049
|
+
if (left.isDirectory() !== right.isDirectory()) {
|
|
1050
|
+
return left.isDirectory() ? -1 : 1;
|
|
1051
|
+
}
|
|
1052
|
+
return left.name.localeCompare(right.name);
|
|
1053
|
+
});
|
|
1054
|
+
return entries.flatMap((entry) => {
|
|
1055
|
+
const absolutePath = path.join(rootDirectory, entry.name);
|
|
1056
|
+
const relativePath = path.relative(baseDirectory, absolutePath);
|
|
1057
|
+
if (entry.isDirectory()) {
|
|
1058
|
+
return [
|
|
1059
|
+
{ path: relativePath, type: "directory" },
|
|
1060
|
+
...listDirectoryEntries(absolutePath, baseDirectory)
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1063
|
+
return [{ path: relativePath, type: "file" }];
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function resolveSkillRootDirectory(extractedDirectory) {
|
|
1067
|
+
const entries = fs.readdirSync(extractedDirectory, { withFileTypes: true });
|
|
1068
|
+
if (entries.length !== 1 || !entries[0]?.isDirectory()) {
|
|
1069
|
+
return extractedDirectory;
|
|
1070
|
+
}
|
|
1071
|
+
return path.join(extractedDirectory, entries[0].name);
|
|
1072
|
+
}
|
|
1073
|
+
function assertRemoteSkillDetail(data) {
|
|
1074
|
+
if (typeof data !== "object" || data === null || typeof data.name !== "string" || typeof data.description !== "string" || typeof data.packageUrl !== "string") {
|
|
1075
|
+
throw new Error("\u8FDC\u7AEF skill \u8BE6\u60C5\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u7AEF\u8FD4\u56DE");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function validateRemoteSkillName(skillName) {
|
|
1079
|
+
const normalizedName = skillName.trim();
|
|
1080
|
+
if (!REMOTE_SKILL_NAME_PATTERN.test(normalizedName)) {
|
|
1081
|
+
throw new Error("skill \u540D\u79F0\u975E\u6CD5\uFF0C\u4EC5\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u4E2D\u5212\u7EBF");
|
|
1082
|
+
}
|
|
1083
|
+
return normalizedName;
|
|
1084
|
+
}
|
|
1085
|
+
async function fetchRemoteSkillSummaries(apiKey) {
|
|
1086
|
+
const serverBaseUrl = normalizeServerBaseUrl(getRemoteSkillServerBaseUrl());
|
|
1087
|
+
const response = await axios.get(`${serverBaseUrl}/api/get-skills`, {
|
|
1088
|
+
headers: buildRequestHeaders(apiKey),
|
|
1089
|
+
timeout: REQUEST_TIMEOUT
|
|
1090
|
+
});
|
|
1091
|
+
return response.data;
|
|
1092
|
+
}
|
|
1093
|
+
async function prepareRemoteSkillContext(skillName, apiKey) {
|
|
1094
|
+
const normalizedSkillName = validateRemoteSkillName(skillName);
|
|
1095
|
+
const serverBaseUrl = normalizeServerBaseUrl(getRemoteSkillServerBaseUrl());
|
|
1096
|
+
const detailResponse = await axios.get(
|
|
1097
|
+
`${serverBaseUrl}/api/get-skill-by-name`,
|
|
1098
|
+
{
|
|
1099
|
+
headers: buildRequestHeaders(apiKey),
|
|
1100
|
+
params: {
|
|
1101
|
+
name: normalizedSkillName
|
|
1102
|
+
},
|
|
1103
|
+
timeout: REQUEST_TIMEOUT
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1106
|
+
assertRemoteSkillDetail(detailResponse.data);
|
|
1107
|
+
const skillsDirectory = getSkillsDirPath();
|
|
1108
|
+
const archivePath = path.join(skillsDirectory, `${normalizedSkillName}.zip`);
|
|
1109
|
+
const extractionDirectory = path.join(skillsDirectory, normalizedSkillName);
|
|
1110
|
+
const url = detailResponse.data.packageUrl.replace("-internal.", ".");
|
|
1111
|
+
const packageResponse = await axios.get(url, {
|
|
1112
|
+
responseType: "arraybuffer",
|
|
1113
|
+
timeout: DOWNLOAD_TIMEOUT
|
|
1114
|
+
});
|
|
1115
|
+
fs.rmSync(extractionDirectory, { recursive: true, force: true });
|
|
1116
|
+
fs.mkdirSync(extractionDirectory, { recursive: true });
|
|
1117
|
+
fs.writeFileSync(archivePath, Buffer.from(packageResponse.data));
|
|
1118
|
+
try {
|
|
1119
|
+
await extractZipArchive(archivePath, extractionDirectory);
|
|
1120
|
+
} finally {
|
|
1121
|
+
fs.rmSync(archivePath, { force: true });
|
|
1122
|
+
}
|
|
1123
|
+
const workingDirectory = resolveSkillRootDirectory(extractionDirectory);
|
|
1124
|
+
return {
|
|
1125
|
+
skill: detailResponse.data,
|
|
1126
|
+
workingDirectory,
|
|
1127
|
+
files: listDirectoryEntries(workingDirectory)
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function formatWorkingDirectoryTree(rootDir) {
|
|
1132
|
+
const lines = ["."];
|
|
1133
|
+
const walk = (currentDir, prefix) => {
|
|
1134
|
+
let entries;
|
|
1135
|
+
try {
|
|
1136
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1137
|
+
} catch {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
entries.sort((left, right) => {
|
|
1141
|
+
const leftRank = left.isDirectory() ? 0 : 1;
|
|
1142
|
+
const rightRank = right.isDirectory() ? 0 : 1;
|
|
1143
|
+
if (leftRank !== rightRank) {
|
|
1144
|
+
return leftRank - rightRank;
|
|
1145
|
+
}
|
|
1146
|
+
return left.name.localeCompare(right.name);
|
|
1147
|
+
});
|
|
1148
|
+
entries.forEach((entry, index) => {
|
|
1149
|
+
const isLast = index === entries.length - 1;
|
|
1150
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1151
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
1152
|
+
if (entry.isDirectory()) {
|
|
1153
|
+
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1154
|
+
walk(path.join(currentDir, entry.name), nextPrefix);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
};
|
|
1158
|
+
walk(rootDir, "");
|
|
1159
|
+
return lines.join("\n");
|
|
1160
|
+
}
|
|
1161
|
+
function readScriptInput(options) {
|
|
1162
|
+
if (!options.scriptFile) {
|
|
1163
|
+
return options.script;
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
return fs.readFileSync(options.scriptFile, "utf8");
|
|
1167
|
+
} catch {
|
|
1168
|
+
throw new Error("\u8BFB\u53D6\u811A\u672C\u6587\u4EF6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u662F\u5426\u5B58\u5728\u4E14\u5F53\u524D\u7528\u6237\u6709\u8BFB\u53D6\u6743\u9650");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function registerRemoteSkillCommand(program) {
|
|
1172
|
+
const remoteSkillCommand = program.command("remote-skill").description("\u67E5\u770B skill \u6458\u8981\uFF0C\u5E76\u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587");
|
|
1173
|
+
remoteSkillCommand.command("list").description("\u5C55\u793A\u5F53\u524D\u652F\u6301\u7684 skill \u6458\u8981").action(async () => {
|
|
1174
|
+
try {
|
|
1175
|
+
const apiKey = requireApiKey();
|
|
1176
|
+
const skills = await fetchRemoteSkillSummaries(apiKey);
|
|
1177
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
fail(error.message);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
remoteSkillCommand.command("enter").description("\u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587\uFF0C\u5E76\u5C55\u793A\u76EE\u5F55\u548C\u6587\u4EF6\u7ED3\u6784").argument("<skillName>", "skill \u540D\u79F0").action(async (skillName) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const apiKey = requireApiKey();
|
|
1185
|
+
const context = await prepareRemoteSkillContext(skillName, apiKey);
|
|
1186
|
+
setCurrentRemoteSkillSession({
|
|
1187
|
+
skillName,
|
|
1188
|
+
workingDirectory: context.workingDirectory
|
|
1189
|
+
});
|
|
1190
|
+
console.log(formatWorkingDirectoryTree(context.workingDirectory));
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
fail(error.message);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
remoteSkillCommand.command("exec").description("\u5728\u6700\u8FD1\u4E00\u6B21\u8FDB\u5165\u7684 skill \u4E0A\u4E0B\u6587\u4E2D\u6267\u884C\u811A\u672C").option("--script <content>", "\u5728\u5F53\u524D skill \u4E0A\u4E0B\u6587\u4E2D\u6267\u884C\u811A\u672C").option("--script-file <path>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6\u811A\u672C\u5185\u5BB9\u540E\u6267\u884C").action(async (options) => {
|
|
1196
|
+
try {
|
|
1197
|
+
if (options.script && options.scriptFile) {
|
|
1198
|
+
fail("--script \u548C --script-file \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
|
|
1199
|
+
}
|
|
1200
|
+
const script = readScriptInput(options);
|
|
1201
|
+
if (!script) {
|
|
1202
|
+
fail("\u8BF7\u901A\u8FC7 --script \u6216 --script-file \u63D0\u4F9B\u8981\u6267\u884C\u7684\u811A\u672C");
|
|
1203
|
+
}
|
|
1204
|
+
const currentSession = getCurrentRemoteSkillSession();
|
|
1205
|
+
if (!currentSession) {
|
|
1206
|
+
fail("\u8BF7\u5148\u6267\u884C remote-skill enter <skillName> \u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587");
|
|
1207
|
+
}
|
|
1208
|
+
if (!fs.existsSync(currentSession.workingDirectory)) {
|
|
1209
|
+
fail(
|
|
1210
|
+
`\u5F53\u524D skill \u4E0A\u4E0B\u6587\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C remote-skill enter ${currentSession.skillName}`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
const execution = await runScriptInDirectory(script, currentSession.workingDirectory);
|
|
1214
|
+
console.log(
|
|
1215
|
+
JSON.stringify(
|
|
1216
|
+
{
|
|
1217
|
+
skillName: currentSession.skillName,
|
|
1218
|
+
workingDirectory: currentSession.workingDirectory,
|
|
1219
|
+
execution
|
|
1220
|
+
},
|
|
1221
|
+
null,
|
|
1222
|
+
2
|
|
1223
|
+
)
|
|
1224
|
+
);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
fail(error.message);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const REGISTRY_URL$1 = "https://registry.npmmirror.com";
|
|
1232
|
+
async function fetchLatestVersion() {
|
|
1233
|
+
const response = await axios.get(
|
|
1234
|
+
`${REGISTRY_URL$1}/${encodeURIComponent(name)}`,
|
|
1235
|
+
{
|
|
1236
|
+
timeout: 1e4
|
|
1237
|
+
}
|
|
1238
|
+
);
|
|
1239
|
+
const latestVersion = response.data?.["dist-tags"]?.latest?.trim();
|
|
1240
|
+
if (!latestVersion) {
|
|
1241
|
+
throw new Error("\u672A\u83B7\u53D6\u5230\u6700\u65B0\u7248\u672C\u53F7");
|
|
1242
|
+
}
|
|
1243
|
+
return latestVersion;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async function installLatestVersion() {
|
|
1247
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1248
|
+
await new Promise((resolve, reject) => {
|
|
1249
|
+
const child = child_process.spawn(
|
|
1250
|
+
npmCommand,
|
|
1251
|
+
[
|
|
1252
|
+
"install",
|
|
1253
|
+
"-g",
|
|
1254
|
+
`${name}@latest`,
|
|
1255
|
+
"--registry=https://registry.npmmirror.com",
|
|
1256
|
+
"--prefer-online"
|
|
1257
|
+
],
|
|
1258
|
+
{
|
|
1259
|
+
env: {
|
|
1260
|
+
...process.env,
|
|
1261
|
+
PUPPETEER_SKIP_DOWNLOAD: "true"
|
|
1262
|
+
},
|
|
1263
|
+
stdio: "inherit"
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
child.on("error", reject);
|
|
1267
|
+
child.on("exit", (code) => {
|
|
1268
|
+
if (code === 0) {
|
|
1269
|
+
resolve();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
reject(new Error(`npm install \u9000\u51FA\u7801\u5F02\u5E38: ${code ?? "null"}`));
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function normalizeVersion(input) {
|
|
1278
|
+
return input.trim().replace(/^v/i, "").split(/[.-]/);
|
|
1279
|
+
}
|
|
1280
|
+
function parseVersionPart(part) {
|
|
1281
|
+
return /^\d+$/.test(part) ? Number(part) : part;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function isNewerVersion(latestVersion, currentVersion) {
|
|
1285
|
+
const latestParts = normalizeVersion(latestVersion).map(parseVersionPart);
|
|
1286
|
+
const currentParts = normalizeVersion(currentVersion).map(parseVersionPart);
|
|
1287
|
+
const maxLength = Math.max(latestParts.length, currentParts.length);
|
|
1288
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
1289
|
+
const latestPart = latestParts[index] ?? 0;
|
|
1290
|
+
const currentPart = currentParts[index] ?? 0;
|
|
1291
|
+
if (latestPart === currentPart) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof latestPart === "number" && typeof currentPart === "number") {
|
|
1295
|
+
return latestPart > currentPart;
|
|
1296
|
+
}
|
|
1297
|
+
return String(latestPart) > String(currentPart);
|
|
1298
|
+
}
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const REGISTRY_URL = "https://registry.npmmirror.com";
|
|
1303
|
+
async function checkAndUpdate(checkOnly = false) {
|
|
1304
|
+
console.log("\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...");
|
|
1305
|
+
const latestVersion = await fetchLatestVersion();
|
|
1306
|
+
if (!isNewerVersion(latestVersion, version)) {
|
|
1307
|
+
console.log(`\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C: ${version}`);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
console.log(`\u53D1\u73B0\u65B0\u7248\u672C: ${version} -> ${latestVersion}`);
|
|
1311
|
+
if (checkOnly) {
|
|
1312
|
+
console.log(
|
|
1313
|
+
`\u53EF\u6267\u884C PUPPETEER_SKIP_DOWNLOAD=true npm install -g ${name}@latest --registry=${REGISTRY_URL} --prefer-online \u8FDB\u884C\u5347\u7EA7`
|
|
1314
|
+
);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
await installLatestVersion();
|
|
1318
|
+
console.log(`\u5347\u7EA7\u5B8C\u6210\uFF0C\u5F53\u524D\u6700\u65B0\u7248\u672C: ${latestVersion}`);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function registerUpgradeCommand(program) {
|
|
1322
|
+
program.command("upgrade").description("\u68C0\u67E5 CLI \u66F4\u65B0\uFF0C\u5E76\u5728\u6709\u65B0\u7248\u672C\u65F6\u6267\u884C\u5347\u7EA7").option("--check-only", "\u4EC5\u68C0\u67E5\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5").action(async (options) => {
|
|
1323
|
+
try {
|
|
1324
|
+
await checkAndUpdate(Boolean(options.checkOnly));
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
console.error(
|
|
1327
|
+
`\u68C0\u67E5\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u7A0D\u540E\u91CD\u8BD5\u3002`
|
|
1328
|
+
);
|
|
1329
|
+
process.exit(1);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function createProgram() {
|
|
1335
|
+
const program = new commander.Command();
|
|
1336
|
+
program.name("yingmi-cli").version(version);
|
|
1337
|
+
registerInitCommand(program);
|
|
1338
|
+
registerMcpCommand(program);
|
|
1339
|
+
registerRemoteSkillCommand(program);
|
|
1340
|
+
registerHelpCommand(program);
|
|
1341
|
+
registerUpgradeCommand(program);
|
|
1342
|
+
program.showHelpAfterError("(\u4F7F\u7528 --help \u67E5\u770B\u547D\u4EE4\u5E2E\u52A9)");
|
|
1343
|
+
return program;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
createProgram().parseAsync(process.argv).catch((error) => {
|
|
1347
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
});
|