gamealgo-cli 0.1.7 → 0.2.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 +31 -49
- package/dist/args.js +80 -0
- package/dist/client.js +232 -0
- package/dist/commands/auth.js +75 -0
- package/dist/commands/events.js +79 -0
- package/dist/commands/experiment.js +373 -0
- package/dist/commands/integration.js +30 -0
- package/dist/commands/keys.js +113 -0
- package/dist/commands/marketing.js +79 -0
- package/dist/commands/report.js +175 -0
- package/dist/commands/resources.js +95 -0
- package/dist/compatibility.js +62 -0
- package/dist/config.js +80 -0
- package/dist/docs.js +606 -0
- package/dist/http.js +54 -0
- package/dist/main.js +106 -0
- package/dist/output.js +82 -0
- package/dist/resources.js +93 -0
- package/dist/target.js +3 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +23 -0
- package/package.json +16 -7
- package/cli/README.md +0 -503
- package/dist/gamealgo.js +0 -1885
package/dist/docs.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { parseFlags } from "./args.js";
|
|
3
|
+
import { loadConfig, loadPackageMetadata } from "./config.js";
|
|
4
|
+
import { isTimeoutError, truncateBody } from "./http.js";
|
|
5
|
+
import { printResult } from "./output.js";
|
|
6
|
+
import { objectValue, writeTextFile } from "./resources.js";
|
|
7
|
+
import { normalizeHost, optionalString } from "./utils.js";
|
|
8
|
+
export const DEFAULT_CLI_HELP_HOSTS = [
|
|
9
|
+
"https://game-algo-admin.dictapis.cn",
|
|
10
|
+
"https://dirichlet.ai/algo_admin",
|
|
11
|
+
];
|
|
12
|
+
export async function handleHelp(args, global) {
|
|
13
|
+
const flags = parseFlags(args);
|
|
14
|
+
const query = normalizeCliHelpQuery(args.join(" "));
|
|
15
|
+
const saved = await loadConfig();
|
|
16
|
+
const configuredHost = normalizeHost(global.host || saved?.host || "");
|
|
17
|
+
const loaded = await loadCliHelpCatalog(configuredHost ? [configuredHost] : DEFAULT_CLI_HELP_HOSTS);
|
|
18
|
+
const packageMetadata = await loadPackageMetadata();
|
|
19
|
+
const visibleCommands = loaded.catalog.commands.filter((command) => !command.internal);
|
|
20
|
+
const selection = selectCliHelpCommands(visibleCommands, query);
|
|
21
|
+
if (!selection.length) {
|
|
22
|
+
const suggestions = suggestCliHelpCommands(visibleCommands, query);
|
|
23
|
+
const suffix = suggestions.length ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
24
|
+
throw new Error(`Unknown CLI command: ${query}.${suffix}`);
|
|
25
|
+
}
|
|
26
|
+
const detail = query && selection.length === 1 && cliHelpCommandMatchesInvocation(selection[0], query)
|
|
27
|
+
? selection[0]
|
|
28
|
+
: undefined;
|
|
29
|
+
const markdown = detail
|
|
30
|
+
? formatCliHelpCommand(detail, loaded.catalog.commonOptions)
|
|
31
|
+
: formatCliHelpIndex(loaded.catalog, selection, query);
|
|
32
|
+
const out = optionalString(flags.out);
|
|
33
|
+
if (flags.out === true)
|
|
34
|
+
throw new Error("--out requires a file path");
|
|
35
|
+
if (out) {
|
|
36
|
+
const payload = out.toLowerCase().endsWith(".json")
|
|
37
|
+
? `${JSON.stringify(cliHelpResult(loaded, packageMetadata.version, query, selection, detail), null, 2)}\n`
|
|
38
|
+
: `${markdown.trimEnd()}\n`;
|
|
39
|
+
await writeTextFile(out, payload);
|
|
40
|
+
await printResult({ ok: true, out, source: loaded.url, version: loaded.catalog.version }, global);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (global.json) {
|
|
44
|
+
await printResult(cliHelpResult(loaded, packageMetadata.version, query, selection, detail), global);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log(markdown.trimEnd());
|
|
48
|
+
}
|
|
49
|
+
export async function loadCliHelpCatalog(hosts) {
|
|
50
|
+
const failures = [];
|
|
51
|
+
for (const candidate of hosts) {
|
|
52
|
+
const host = normalizeHost(candidate);
|
|
53
|
+
const url = `${docsContentBaseUrl(host)}cli-commands.json`;
|
|
54
|
+
try {
|
|
55
|
+
return { catalog: parseCliHelpCatalog(await requestLiveTextUrl(url)), host, url };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
failures.push(`${host}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Unable to load the current CLI help from the Server. ${failures.join(" | ")}`);
|
|
62
|
+
}
|
|
63
|
+
export async function requestLiveTextUrl(url) {
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(url, {
|
|
67
|
+
cache: "no-store",
|
|
68
|
+
headers: { "Cache-Control": "no-cache" },
|
|
69
|
+
signal: AbortSignal.timeout(10_000),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (isTimeoutError(error))
|
|
74
|
+
throw new Error(`Help request timed out: ${url}`);
|
|
75
|
+
throw new Error(`Help request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
throw new Error(`Help request returned ${response.status}: ${truncateBody(text)}`);
|
|
80
|
+
return text;
|
|
81
|
+
}
|
|
82
|
+
export function parseCliHelpCatalog(text) {
|
|
83
|
+
let value;
|
|
84
|
+
try {
|
|
85
|
+
value = JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error("CLI command catalog is not valid JSON");
|
|
89
|
+
}
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
+
throw new Error("CLI command catalog must be an object");
|
|
92
|
+
}
|
|
93
|
+
const body = value;
|
|
94
|
+
if (body.schemaVersion !== 1 || !Array.isArray(body.groups) || !Array.isArray(body.commands)) {
|
|
95
|
+
throw new Error("CLI command catalog schema is invalid");
|
|
96
|
+
}
|
|
97
|
+
const groups = body.groups.map((item) => {
|
|
98
|
+
const group = objectValue(item);
|
|
99
|
+
const id = optionalString(group?.id);
|
|
100
|
+
const title = optionalString(group?.title);
|
|
101
|
+
if (!id || !title)
|
|
102
|
+
throw new Error("CLI command catalog contains an invalid group");
|
|
103
|
+
return { id, title, description: optionalString(group?.description) };
|
|
104
|
+
});
|
|
105
|
+
const groupIds = new Set(groups.map((group) => group.id));
|
|
106
|
+
const commands = body.commands.map((item) => parseCliHelpCommand(item, groupIds));
|
|
107
|
+
const paths = new Set(commands.map((command) => normalizeCliHelpQuery(command.path)));
|
|
108
|
+
if (paths.size !== commands.length)
|
|
109
|
+
throw new Error("CLI command catalog contains duplicate command paths");
|
|
110
|
+
const commonOptions = Array.isArray(body.commonOptions)
|
|
111
|
+
? body.commonOptions.map((item) => parseCliHelpOption(item, "common option"))
|
|
112
|
+
: [];
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
version: String(body.version || "current"),
|
|
116
|
+
updatedAt: String(body.updatedAt || ""),
|
|
117
|
+
cliPackage: String(body.cliPackage || "gamealgo-cli"),
|
|
118
|
+
groups,
|
|
119
|
+
commonOptions,
|
|
120
|
+
commands,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function parseCliHelpCommand(value, groupIds) {
|
|
124
|
+
const command = objectValue(value);
|
|
125
|
+
const path = optionalString(command?.path);
|
|
126
|
+
const group = optionalString(command?.group);
|
|
127
|
+
const summary = optionalString(command?.summary);
|
|
128
|
+
const usage = optionalString(command?.usage);
|
|
129
|
+
if (!path || !group || !summary || !usage || !groupIds.has(group)) {
|
|
130
|
+
throw new Error(`CLI command catalog contains an invalid command: ${path || "unknown"}`);
|
|
131
|
+
}
|
|
132
|
+
const options = Array.isArray(command?.options)
|
|
133
|
+
? command.options.map((item) => parseCliHelpOption(item, path))
|
|
134
|
+
: undefined;
|
|
135
|
+
return {
|
|
136
|
+
path,
|
|
137
|
+
group,
|
|
138
|
+
summary,
|
|
139
|
+
usage,
|
|
140
|
+
description: optionalString(command?.description),
|
|
141
|
+
aliases: stringArray(command?.aliases),
|
|
142
|
+
options,
|
|
143
|
+
notes: stringArray(command?.notes),
|
|
144
|
+
examples: stringArray(command?.examples),
|
|
145
|
+
sinceVersion: optionalString(command?.sinceVersion),
|
|
146
|
+
internal: command?.internal === true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function parseCliHelpOption(value, owner) {
|
|
150
|
+
const option = objectValue(value);
|
|
151
|
+
const name = optionalString(option?.name);
|
|
152
|
+
const description = optionalString(option?.description);
|
|
153
|
+
if (!name || !description)
|
|
154
|
+
throw new Error(`CLI command catalog contains an invalid option for ${owner}`);
|
|
155
|
+
return { name, description };
|
|
156
|
+
}
|
|
157
|
+
export function stringArray(value) {
|
|
158
|
+
if (!Array.isArray(value))
|
|
159
|
+
return undefined;
|
|
160
|
+
const result = value.map(optionalString).filter((item) => Boolean(item));
|
|
161
|
+
return result.length ? result : undefined;
|
|
162
|
+
}
|
|
163
|
+
export function normalizeCliHelpQuery(value) {
|
|
164
|
+
return value.trim().replace(/^gamealgo\s+/i, "").replace(/\s+/g, " ").toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
export function selectCliHelpCommands(commands, query) {
|
|
167
|
+
if (!query)
|
|
168
|
+
return commands;
|
|
169
|
+
const exact = commands.filter((command) => (normalizeCliHelpQuery(command.path) === query
|
|
170
|
+
|| command.aliases?.some((alias) => normalizeCliHelpQuery(alias) === query)));
|
|
171
|
+
if (exact.length)
|
|
172
|
+
return exact;
|
|
173
|
+
const invocationMatches = commands
|
|
174
|
+
.filter((command) => cliHelpCommandMatchesInvocation(command, query))
|
|
175
|
+
.sort((left, right) => normalizeCliHelpQuery(right.path).length - normalizeCliHelpQuery(left.path).length);
|
|
176
|
+
if (invocationMatches.length)
|
|
177
|
+
return [invocationMatches[0]];
|
|
178
|
+
return commands.filter((command) => (normalizeCliHelpQuery(command.path).startsWith(`${query} `)
|
|
179
|
+
|| command.aliases?.some((alias) => normalizeCliHelpQuery(alias).startsWith(`${query} `))));
|
|
180
|
+
}
|
|
181
|
+
export function cliHelpCommandMatchesInvocation(command, query) {
|
|
182
|
+
const paths = [command.path, ...(command.aliases || [])].map(normalizeCliHelpQuery);
|
|
183
|
+
return paths.some((path) => query === path || query.startsWith(`${path} `));
|
|
184
|
+
}
|
|
185
|
+
export function suggestCliHelpCommands(commands, query) {
|
|
186
|
+
return commands
|
|
187
|
+
.map((command) => ({ command, score: docsApiSimilarity(query, normalizeCliHelpQuery(command.path)) }))
|
|
188
|
+
.filter((item) => item.score >= 0.35)
|
|
189
|
+
.sort((left, right) => right.score - left.score)
|
|
190
|
+
.slice(0, 5)
|
|
191
|
+
.map((item) => item.command.path);
|
|
192
|
+
}
|
|
193
|
+
export function formatCliHelpIndex(catalog, commands, query) {
|
|
194
|
+
const lines = [`# GameAlgo CLI 命令`, "", `目录版本:${catalog.version} · 更新于 ${catalog.updatedAt}`, ""];
|
|
195
|
+
for (const group of catalog.groups) {
|
|
196
|
+
const grouped = commands.filter((command) => command.group === group.id);
|
|
197
|
+
if (!grouped.length)
|
|
198
|
+
continue;
|
|
199
|
+
lines.push(`## ${group.title}`, "");
|
|
200
|
+
if (group.description)
|
|
201
|
+
lines.push(group.description, "");
|
|
202
|
+
for (const command of grouped)
|
|
203
|
+
lines.push(`- \`gamealgo ${command.path}\`:${command.summary}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
if (query)
|
|
207
|
+
lines.push(`继续查询:\`gamealgo help <完整命令>\``);
|
|
208
|
+
else
|
|
209
|
+
lines.push("查询单条命令:`gamealgo help <命令路径>`");
|
|
210
|
+
lines.push("显式保存当次最新文档:`gamealgo help --out gamealgo-cli-help.md`");
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
export function formatCliHelpCommand(command, commonOptions) {
|
|
214
|
+
const lines = [
|
|
215
|
+
`# gamealgo ${command.path}`,
|
|
216
|
+
"",
|
|
217
|
+
command.summary,
|
|
218
|
+
"",
|
|
219
|
+
"## 用法",
|
|
220
|
+
"",
|
|
221
|
+
"```bash",
|
|
222
|
+
command.usage,
|
|
223
|
+
"```",
|
|
224
|
+
];
|
|
225
|
+
if (command.description)
|
|
226
|
+
lines.push("", "## 说明", "", command.description);
|
|
227
|
+
const options = [...(command.options || []), ...commonOptions];
|
|
228
|
+
if (options.length) {
|
|
229
|
+
lines.push("", "## 参数与选项", "");
|
|
230
|
+
for (const option of options)
|
|
231
|
+
lines.push(`- \`${option.name}\`:${option.description}`);
|
|
232
|
+
}
|
|
233
|
+
if (command.notes?.length) {
|
|
234
|
+
lines.push("", "## 约束", "");
|
|
235
|
+
for (const note of command.notes)
|
|
236
|
+
lines.push(`- ${note}`);
|
|
237
|
+
}
|
|
238
|
+
if (command.examples?.length) {
|
|
239
|
+
lines.push("", "## 示例", "");
|
|
240
|
+
for (const example of command.examples)
|
|
241
|
+
lines.push("```bash", example, "```");
|
|
242
|
+
}
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
}
|
|
245
|
+
export function cliHelpResult(loaded, cliVersion, query, commands, detail) {
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
cliVersion,
|
|
249
|
+
catalogVersion: loaded.catalog.version,
|
|
250
|
+
updatedAt: loaded.catalog.updatedAt,
|
|
251
|
+
source: loaded.url,
|
|
252
|
+
query: query || null,
|
|
253
|
+
...(detail ? { command: detail, commonOptions: loaded.catalog.commonOptions } : { commands }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export async function handleDocs(args, global) {
|
|
257
|
+
const flags = parseFlags(args);
|
|
258
|
+
const topicId = String(args.shift() || "").trim();
|
|
259
|
+
const apiQuery = topicId === "api" ? args.join(" ").trim() : "";
|
|
260
|
+
if (topicId !== "api" && args.length) {
|
|
261
|
+
throw new Error("usage: gamealgo docs [topic] [--open] [--json] [--host <admin-url>]");
|
|
262
|
+
}
|
|
263
|
+
const saved = await loadConfig();
|
|
264
|
+
const host = normalizeHost(global.host || saved?.host || "");
|
|
265
|
+
if (!host)
|
|
266
|
+
throw new Error("--host is required before login; after login the saved Admin host is used");
|
|
267
|
+
const loaded = await loadDocsManifest(host);
|
|
268
|
+
const manifest = loaded.manifest;
|
|
269
|
+
const visibleTopics = manifest.topics.filter((item) => !item.hidden);
|
|
270
|
+
const resolvedTopicId = topicId === "api" ? "public-sdk-api" : topicId;
|
|
271
|
+
const topic = resolvedTopicId
|
|
272
|
+
? manifest.topics.find((item) => item.id === resolvedTopicId && (topicId === "api" || !item.hidden))
|
|
273
|
+
: null;
|
|
274
|
+
if (topicId && !topic) {
|
|
275
|
+
if (topicId === "api") {
|
|
276
|
+
throw new Error("The Admin host does not publish the public SDK API index yet");
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Unknown docs topic: ${topicId}. Available topics: ${visibleTopics.map((item) => item.id).join(", ")}`);
|
|
279
|
+
}
|
|
280
|
+
const webUrl = docsWebUrl(host, topic?.id);
|
|
281
|
+
if (flags.open) {
|
|
282
|
+
openExternalUrl(webUrl);
|
|
283
|
+
await printResult({
|
|
284
|
+
ok: true,
|
|
285
|
+
opened: webUrl,
|
|
286
|
+
version: manifest.version,
|
|
287
|
+
...(topic ? { topic: topic.id, title: topic.title } : {}),
|
|
288
|
+
cachedManifest: loaded.cached,
|
|
289
|
+
}, global);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!topic) {
|
|
293
|
+
if (global.json) {
|
|
294
|
+
await printResult({
|
|
295
|
+
ok: true,
|
|
296
|
+
version: manifest.version,
|
|
297
|
+
updatedAt: manifest.updatedAt,
|
|
298
|
+
defaultTopic: manifest.defaultTopic,
|
|
299
|
+
cached: loaded.cached,
|
|
300
|
+
webUrl,
|
|
301
|
+
topics: visibleTopics,
|
|
302
|
+
}, global);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
printDocsIndex(manifest, webUrl);
|
|
306
|
+
if (loaded.cached)
|
|
307
|
+
console.error("Warning: using cached documentation index because the Admin host could not be reached");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const document = await loadDocsDocument(host, manifest, topic, loaded.cached);
|
|
311
|
+
if (topicId === "api") {
|
|
312
|
+
await printDocsApiResult({
|
|
313
|
+
query: apiQuery,
|
|
314
|
+
platform: parseDocsApiPlatform(flags.platform),
|
|
315
|
+
manifest,
|
|
316
|
+
topic,
|
|
317
|
+
document,
|
|
318
|
+
webUrl,
|
|
319
|
+
host,
|
|
320
|
+
global,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (global.json) {
|
|
325
|
+
await printResult({
|
|
326
|
+
ok: true,
|
|
327
|
+
version: manifest.version,
|
|
328
|
+
updatedAt: manifest.updatedAt,
|
|
329
|
+
topic: topic.id,
|
|
330
|
+
title: topic.title,
|
|
331
|
+
summary: topic.summary,
|
|
332
|
+
cached: document.cached,
|
|
333
|
+
webUrl,
|
|
334
|
+
rawUrl: docsRawUrl(host, topic.file),
|
|
335
|
+
content: document.content,
|
|
336
|
+
}, global);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
console.log(document.content.trimEnd());
|
|
340
|
+
if (document.cached)
|
|
341
|
+
console.error("Warning: using cached documentation because the Admin host could not be reached");
|
|
342
|
+
}
|
|
343
|
+
export function parseDocsApiPlatform(value) {
|
|
344
|
+
if (value === undefined || value === false || value === "")
|
|
345
|
+
return undefined;
|
|
346
|
+
const platform = String(value).trim().toLowerCase();
|
|
347
|
+
if (platform === "maker")
|
|
348
|
+
return "lua";
|
|
349
|
+
if (platform === "lua" || platform === "ios" || platform === "android" || platform === "rest") {
|
|
350
|
+
return platform;
|
|
351
|
+
}
|
|
352
|
+
throw new Error("--platform must be lua, maker, ios, android, or rest");
|
|
353
|
+
}
|
|
354
|
+
export async function printDocsApiResult(options) {
|
|
355
|
+
const { query, platform, manifest, topic, document, webUrl, host, global } = options;
|
|
356
|
+
const entries = parseDocsApiEntries(document.content).filter((entry) => !platform || entry.platforms.includes(platform));
|
|
357
|
+
if (!entries.length) {
|
|
358
|
+
throw new Error(platform ? `No public SDK APIs are documented for platform: ${platform}` : "Public SDK API document has no indexed entries");
|
|
359
|
+
}
|
|
360
|
+
const normalizedQuery = normalizeDocsApiQuery(query);
|
|
361
|
+
const listMode = !normalizedQuery || normalizedQuery === "list" || normalizedQuery === "ls";
|
|
362
|
+
if (listMode) {
|
|
363
|
+
if (global.json) {
|
|
364
|
+
await printResult({
|
|
365
|
+
ok: true,
|
|
366
|
+
version: manifest.version,
|
|
367
|
+
updatedAt: manifest.updatedAt,
|
|
368
|
+
topic: topic.id,
|
|
369
|
+
cached: document.cached,
|
|
370
|
+
webUrl,
|
|
371
|
+
rawUrl: docsRawUrl(host, topic.file),
|
|
372
|
+
...(platform ? { platform } : {}),
|
|
373
|
+
apis: entries.map(({ name, symbols, platforms, summary }) => ({ name, symbols, platforms, summary })),
|
|
374
|
+
}, global);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(`GameAlgo 公开 SDK API ${manifest.version}${platform ? ` · ${platform}` : ""}`);
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
console.log(`\n${entry.name} [${entry.platforms.join(", ")}]`);
|
|
380
|
+
console.log(` ${entry.symbols.join(" / ")}`);
|
|
381
|
+
console.log(` ${entry.summary}`);
|
|
382
|
+
}
|
|
383
|
+
console.log(`\n查询函数: gamealgo docs api <函数名>${platform ? ` --platform ${platform}` : ""}`);
|
|
384
|
+
if (document.cached)
|
|
385
|
+
console.error("Warning: using cached documentation because the Admin host could not be reached");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const matches = findDocsApiEntries(entries, query);
|
|
389
|
+
if (!matches.length) {
|
|
390
|
+
const suggestions = suggestDocsApiEntries(entries, query);
|
|
391
|
+
const suffix = suggestions.length ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
392
|
+
throw new Error(`Unknown public SDK API: ${query}.${suffix}`);
|
|
393
|
+
}
|
|
394
|
+
if (global.json) {
|
|
395
|
+
await printResult({
|
|
396
|
+
ok: true,
|
|
397
|
+
version: manifest.version,
|
|
398
|
+
updatedAt: manifest.updatedAt,
|
|
399
|
+
topic: topic.id,
|
|
400
|
+
query,
|
|
401
|
+
cached: document.cached,
|
|
402
|
+
webUrl,
|
|
403
|
+
rawUrl: docsRawUrl(host, topic.file),
|
|
404
|
+
...(platform ? { platform } : {}),
|
|
405
|
+
matches,
|
|
406
|
+
}, global);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.log(matches.map((entry) => entry.content.trim()).join("\n\n"));
|
|
410
|
+
if (document.cached)
|
|
411
|
+
console.error("Warning: using cached documentation because the Admin host could not be reached");
|
|
412
|
+
}
|
|
413
|
+
export function parseDocsApiEntries(content) {
|
|
414
|
+
const headings = [...content.matchAll(/^###\s+(.+)$/gm)];
|
|
415
|
+
const entries = [];
|
|
416
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
417
|
+
const heading = headings[index];
|
|
418
|
+
const start = heading.index ?? 0;
|
|
419
|
+
const end = headings[index + 1]?.index ?? content.length;
|
|
420
|
+
const section = content.slice(start, end).trim();
|
|
421
|
+
const symbols = parseDocsApiMetadataList(section, "API");
|
|
422
|
+
if (!symbols.length)
|
|
423
|
+
continue;
|
|
424
|
+
const platforms = parseDocsApiMetadataList(section, "平台")
|
|
425
|
+
.map((item) => item.toLowerCase())
|
|
426
|
+
.filter((item) => item === "lua" || item === "ios" || item === "android" || item === "rest");
|
|
427
|
+
const summaryMatch = section.match(/^-\s*用途[::]\s*(.+)$/m);
|
|
428
|
+
entries.push({
|
|
429
|
+
name: stripInlineMarkdown(heading[1]),
|
|
430
|
+
symbols,
|
|
431
|
+
platforms: [...new Set(platforms)],
|
|
432
|
+
summary: stripInlineMarkdown(summaryMatch?.[1] || ""),
|
|
433
|
+
content: section,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return entries;
|
|
437
|
+
}
|
|
438
|
+
export function parseDocsApiMetadataList(section, key) {
|
|
439
|
+
const match = section.match(new RegExp(`^-\\s*${key}[::]\\s*(.+)$`, "m"));
|
|
440
|
+
if (!match)
|
|
441
|
+
return [];
|
|
442
|
+
const inlineCode = [...match[1].matchAll(/`([^`]+)`/g)].map((item) => item[1].trim()).filter(Boolean);
|
|
443
|
+
if (inlineCode.length)
|
|
444
|
+
return inlineCode;
|
|
445
|
+
return match[1].split(/[,,]/).map((item) => stripInlineMarkdown(item)).filter(Boolean);
|
|
446
|
+
}
|
|
447
|
+
export function stripInlineMarkdown(value) {
|
|
448
|
+
return value.replace(/[`*]/g, "").trim();
|
|
449
|
+
}
|
|
450
|
+
export function normalizeDocsApiQuery(value) {
|
|
451
|
+
return stripInlineMarkdown(value)
|
|
452
|
+
.replace(/\(.*\)$/, "")
|
|
453
|
+
.replace(/\s+/g, "")
|
|
454
|
+
.toLowerCase();
|
|
455
|
+
}
|
|
456
|
+
export function docsApiSearchTerms(entry) {
|
|
457
|
+
return [entry.name, ...entry.symbols, ...entry.symbols.map((symbol) => symbol.split(/[.#]/).pop() || symbol)]
|
|
458
|
+
.map(normalizeDocsApiQuery)
|
|
459
|
+
.filter(Boolean);
|
|
460
|
+
}
|
|
461
|
+
export function findDocsApiEntries(entries, query) {
|
|
462
|
+
const needle = normalizeDocsApiQuery(query);
|
|
463
|
+
const exact = entries.filter((entry) => docsApiSearchTerms(entry).includes(needle));
|
|
464
|
+
if (exact.length)
|
|
465
|
+
return exact;
|
|
466
|
+
return entries.filter((entry) => (docsApiSearchTerms(entry).some((term) => term.includes(needle))
|
|
467
|
+
|| normalizeDocsApiQuery(entry.summary).includes(needle)));
|
|
468
|
+
}
|
|
469
|
+
export function suggestDocsApiEntries(entries, query) {
|
|
470
|
+
const needle = normalizeDocsApiQuery(query);
|
|
471
|
+
return entries
|
|
472
|
+
.map((entry) => {
|
|
473
|
+
const terms = docsApiSearchTerms(entry);
|
|
474
|
+
const score = Math.max(...terms.map((term) => docsApiSimilarity(needle, term)));
|
|
475
|
+
return { entry, score };
|
|
476
|
+
})
|
|
477
|
+
.filter((item) => item.score >= 0.35)
|
|
478
|
+
.sort((left, right) => right.score - left.score)
|
|
479
|
+
.slice(0, 5)
|
|
480
|
+
.map(({ entry }) => entry.symbols[0] || entry.name);
|
|
481
|
+
}
|
|
482
|
+
export function docsApiSimilarity(left, right) {
|
|
483
|
+
if (!left || !right)
|
|
484
|
+
return 0;
|
|
485
|
+
if (left.includes(right) || right.includes(left)) {
|
|
486
|
+
return Math.min(left.length, right.length) / Math.max(left.length, right.length) + 0.5;
|
|
487
|
+
}
|
|
488
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
489
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
490
|
+
const current = [leftIndex];
|
|
491
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
492
|
+
const substitution = previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
493
|
+
current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, substitution);
|
|
494
|
+
}
|
|
495
|
+
previous.splice(0, previous.length, ...current);
|
|
496
|
+
}
|
|
497
|
+
return 1 - previous[right.length] / Math.max(left.length, right.length);
|
|
498
|
+
}
|
|
499
|
+
export async function loadDocsManifest(host) {
|
|
500
|
+
const text = await requestTextUrl(`${docsContentBaseUrl(host)}manifest.json`);
|
|
501
|
+
return { manifest: parseDocsManifest(text), cached: false };
|
|
502
|
+
}
|
|
503
|
+
export async function loadDocsDocument(host, manifest, topic, _preferCache) {
|
|
504
|
+
void manifest;
|
|
505
|
+
return { content: await requestTextUrl(docsRawUrl(host, topic.file)), cached: false };
|
|
506
|
+
}
|
|
507
|
+
export async function requestTextUrl(url) {
|
|
508
|
+
let response;
|
|
509
|
+
try {
|
|
510
|
+
response = await fetch(url, {
|
|
511
|
+
cache: "no-store",
|
|
512
|
+
headers: { "Cache-Control": "no-cache" },
|
|
513
|
+
signal: AbortSignal.timeout(10_000),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (isTimeoutError(error))
|
|
518
|
+
throw new Error(`Documentation request timed out: ${url}`);
|
|
519
|
+
throw new Error(`Documentation request failed: ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
520
|
+
}
|
|
521
|
+
const text = await response.text();
|
|
522
|
+
if (!response.ok) {
|
|
523
|
+
throw new Error(`Documentation request returned ${response.status}: ${truncateBody(text)}`);
|
|
524
|
+
}
|
|
525
|
+
return text;
|
|
526
|
+
}
|
|
527
|
+
export function parseDocsManifest(text) {
|
|
528
|
+
let value;
|
|
529
|
+
try {
|
|
530
|
+
value = JSON.parse(text);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
throw new Error("Documentation manifest is not valid JSON");
|
|
534
|
+
}
|
|
535
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
536
|
+
throw new Error("Documentation manifest must be an object");
|
|
537
|
+
}
|
|
538
|
+
const body = value;
|
|
539
|
+
if (body.schemaVersion !== 1 || !Array.isArray(body.topics) || !body.topics.length) {
|
|
540
|
+
throw new Error("Documentation manifest schema is invalid");
|
|
541
|
+
}
|
|
542
|
+
const topics = body.topics.map((item) => {
|
|
543
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
544
|
+
throw new Error("Documentation manifest contains an invalid topic");
|
|
545
|
+
}
|
|
546
|
+
const topic = item;
|
|
547
|
+
const id = String(topic.id || "");
|
|
548
|
+
const file = String(topic.file || "");
|
|
549
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(id) || !/^[a-z][a-z0-9-]{0,63}\.md$/.test(file)) {
|
|
550
|
+
throw new Error(`Documentation manifest contains an unsafe topic: ${id || file}`);
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
id,
|
|
554
|
+
title: String(topic.title || id),
|
|
555
|
+
summary: String(topic.summary || ""),
|
|
556
|
+
group: String(topic.group || "其他"),
|
|
557
|
+
file,
|
|
558
|
+
hidden: topic.hidden === true,
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
const ids = new Set(topics.map((topic) => topic.id));
|
|
562
|
+
if (ids.size !== topics.length)
|
|
563
|
+
throw new Error("Documentation manifest contains duplicate topic ids");
|
|
564
|
+
const defaultTopic = String(body.defaultTopic || topics[0].id);
|
|
565
|
+
if (!ids.has(defaultTopic))
|
|
566
|
+
throw new Error("Documentation manifest defaultTopic does not exist");
|
|
567
|
+
return {
|
|
568
|
+
schemaVersion: 1,
|
|
569
|
+
version: String(body.version || "current"),
|
|
570
|
+
updatedAt: String(body.updatedAt || ""),
|
|
571
|
+
defaultTopic,
|
|
572
|
+
topics,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
export function docsWebUrl(host, topicId) {
|
|
576
|
+
const url = new URL(`${normalizeHost(host)}/dashboard/docs/`);
|
|
577
|
+
if (topicId)
|
|
578
|
+
url.searchParams.set("topic", topicId);
|
|
579
|
+
return url.toString();
|
|
580
|
+
}
|
|
581
|
+
export function docsContentBaseUrl(host) {
|
|
582
|
+
return `${normalizeHost(host)}/dashboard/docs/content/`;
|
|
583
|
+
}
|
|
584
|
+
export function docsRawUrl(host, file) {
|
|
585
|
+
return `${docsContentBaseUrl(host)}${encodeURIComponent(file)}`;
|
|
586
|
+
}
|
|
587
|
+
export function printDocsIndex(manifest, webUrl) {
|
|
588
|
+
console.log(`GameAlgo AI 文档 ${manifest.version}${manifest.updatedAt ? ` (${manifest.updatedAt})` : ""}`);
|
|
589
|
+
let currentGroup = "";
|
|
590
|
+
for (const topic of manifest.topics.filter((item) => !item.hidden)) {
|
|
591
|
+
if (topic.group !== currentGroup) {
|
|
592
|
+
currentGroup = topic.group;
|
|
593
|
+
console.log(`\n${currentGroup}`);
|
|
594
|
+
}
|
|
595
|
+
console.log(` ${topic.id}\n ${topic.title}:${topic.summary}`);
|
|
596
|
+
}
|
|
597
|
+
console.log(`\n读取文档: gamealgo docs <topic>`);
|
|
598
|
+
console.log(`网页目录: ${webUrl}`);
|
|
599
|
+
}
|
|
600
|
+
export function openExternalUrl(url) {
|
|
601
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
602
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
603
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
604
|
+
child.on("error", (error) => console.error(`Unable to open browser: ${error.message}`));
|
|
605
|
+
child.unref();
|
|
606
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { formatDuration } from "./output.js";
|
|
2
|
+
import { normalizeHost } from "./utils.js";
|
|
3
|
+
export async function requestJson(host, path, init) {
|
|
4
|
+
const { timeoutMs, ...requestInit } = init;
|
|
5
|
+
let response;
|
|
6
|
+
try {
|
|
7
|
+
response = await fetch(`${normalizeHost(host)}${path}`, {
|
|
8
|
+
...requestInit,
|
|
9
|
+
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : requestInit.signal,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (isTimeoutError(error)) {
|
|
14
|
+
throw new Error(`HTTP request timed out after ${formatDuration(timeoutMs ?? 0)}`);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
const json = parseJsonResponse(text);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(formatHttpError(response.status, response.statusText, text, json));
|
|
22
|
+
}
|
|
23
|
+
if (text && json === undefined) {
|
|
24
|
+
throw new Error(`Invalid JSON response from ${path}: ${truncateBody(text)}`);
|
|
25
|
+
}
|
|
26
|
+
return json ?? null;
|
|
27
|
+
}
|
|
28
|
+
export function parseJsonResponse(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function formatHttpError(status, statusText, text, json) {
|
|
39
|
+
if (json && typeof json === "object") {
|
|
40
|
+
const body = json;
|
|
41
|
+
const message = body.message || body.error;
|
|
42
|
+
if (message)
|
|
43
|
+
return `HTTP ${status}: ${String(message)}`;
|
|
44
|
+
}
|
|
45
|
+
const bodyText = truncateBody(text);
|
|
46
|
+
return bodyText ? `HTTP ${status} ${statusText}: ${bodyText}` : `HTTP ${status} ${statusText}`;
|
|
47
|
+
}
|
|
48
|
+
export function truncateBody(text) {
|
|
49
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
50
|
+
return normalized.length > 500 ? `${normalized.slice(0, 500)}...` : normalized;
|
|
51
|
+
}
|
|
52
|
+
export function isTimeoutError(error) {
|
|
53
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
54
|
+
}
|