Files
admin9webs 322b7737eb archive: 紫鸟/HubStudio 文档快照(3889 篇 Markdown)
来源:紫鸟帮助中心/博客/主站资讯 + HubStudio API 文档/帮助中心 + 三个官方 skill 仓库。
只入库 Markdown 与元数据;原始 HTML 与 tarball 留在本机。
2026-10-05 12:11:35 +00:00

605 lines
20 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const YAML = require("yaml");
const GENERATED_PATH = path.join(__dirname, "commands.generated.json");
const OPENAPI_PATH = path.join(__dirname, "openapi.yaml");
const generated = JSON.parse(fs.readFileSync(GENERATED_PATH, "utf8"));
const BASE_URL = process.env.HUBSTUDIO_BASE_URL || generated.baseUrl || "http://127.0.0.1:6873";
const COMMANDS = generated.commands || [];
const COMMAND_MAP = new Map(COMMANDS.map((c) => [c.command, c]));
const ALIASES = {
browserCreate: "postV1EnvCreate",
browserStart: "postV1BrowserStart",
browserStop: "postV1BrowserStop",
browserStatus: "postV1BrowserAllBrowserStatus",
browserForeground: "postV1BrowserForeground",
browserStopAll: "postV1BrowserStopAll",
displayAll: "postV1DisplayAll",
browserArrange: "postV1BrowserArrange",
};
class InputValidationError extends Error {
constructor(message, fieldErrors = [], command = null) {
super(message);
this.name = "InputValidationError";
this.code = "VALIDATION_ERROR";
this.fieldErrors = fieldErrors;
this.command = command;
}
}
function loadOpenApi() {
try {
const content = fs.readFileSync(OPENAPI_PATH, "utf8");
return YAML.parse(content) || {};
} catch {
return {};
}
}
const OPENAPI = loadOpenApi();
function resolveSchema(schema) {
if (!schema || typeof schema !== "object") {
return schema;
}
if (!schema.$ref || typeof schema.$ref !== "string") {
return schema;
}
if (!schema.$ref.startsWith("#/")) {
return schema;
}
const parts = schema.$ref
.replace(/^#\//, "")
.split("/")
.map((x) => x.replace(/~1/g, "/").replace(/~0/g, "~"));
let current = OPENAPI;
for (const part of parts) {
if (!current || typeof current !== "object" || !(part in current)) {
return schema;
}
current = current[part];
}
if (!current || typeof current !== "object") {
return schema;
}
return current;
}
function getOperationSchema(method, endpointPath) {
const methodKey = String(method || "post").toLowerCase();
const op = OPENAPI && OPENAPI.paths && OPENAPI.paths[endpointPath] && OPENAPI.paths[endpointPath][methodKey];
if (!op) {
return { querySchema: null, bodySchema: null, requestBodyRequired: false };
}
const queryParams = Array.isArray(op.parameters) ? op.parameters.filter((p) => p && p.in === "query") : [];
const queryProperties = {};
const queryRequired = [];
for (const param of queryParams) {
if (!param || !param.name) continue;
queryProperties[param.name] = resolveSchema(param.schema || {});
if (param.required) {
queryRequired.push(param.name);
}
}
const querySchema = queryParams.length
? {
type: "object",
properties: queryProperties,
required: queryRequired,
}
: null;
const jsonBodySchema =
op.requestBody &&
op.requestBody.content &&
op.requestBody.content["application/json"] &&
op.requestBody.content["application/json"].schema
? resolveSchema(op.requestBody.content["application/json"].schema)
: null;
return {
querySchema,
bodySchema: jsonBodySchema,
requestBodyRequired: Boolean(op.requestBody && op.requestBody.required),
};
}
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isEmptyValue(value) {
if (value === undefined || value === null) return true;
if (typeof value === "string") return value.trim().length === 0;
if (Array.isArray(value)) return value.length === 0;
if (isPlainObject(value)) return Object.keys(value).length === 0;
return false;
}
function pushValidationError(errors, field, reason, expected, actual) {
errors.push({
field,
reason,
expected,
actualType: actual,
});
}
function validateStringFormat(value, schema, fieldPath, errors) {
const format = schema.format;
if (!format) return;
if (format === "email") {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
pushValidationError(errors, fieldPath, "invalid email format", "email", "string");
}
return;
}
if (format === "uri" || format === "url") {
try {
new URL(value);
} catch {
pushValidationError(errors, fieldPath, "invalid URL format", "url", "string");
}
}
}
function validateBySchema(value, schema, fieldPath, errors, options = {}) {
const resolved = resolveSchema(schema || {});
if (!resolved || typeof resolved !== "object") return;
if (value === undefined) return;
if (value === null) {
if (resolved.nullable || resolved.type === "null") return;
pushValidationError(errors, fieldPath, "value cannot be null", resolved.type || "non-null", "null");
return;
}
if (Array.isArray(resolved.type)) {
const typePass = resolved.type.some((oneType) => {
const localErrors = [];
validateBySchema(value, { ...resolved, type: oneType }, fieldPath, localErrors, options);
return localErrors.length === 0;
});
if (!typePass) {
pushValidationError(errors, fieldPath, "type mismatch", resolved.type.join("|"), typeof value);
}
return;
}
const inferredType = resolved.type || (resolved.properties ? "object" : undefined);
if (!inferredType) return;
if (inferredType === "string") {
if (typeof value !== "string") {
pushValidationError(errors, fieldPath, "type mismatch", "string", Array.isArray(value) ? "array" : typeof value);
return;
}
if (typeof resolved.minLength === "number" && value.length < resolved.minLength) {
pushValidationError(errors, fieldPath, "string too short", `minLength=${resolved.minLength}`, "string");
}
if (typeof resolved.maxLength === "number" && value.length > resolved.maxLength) {
pushValidationError(errors, fieldPath, "string too long", `maxLength=${resolved.maxLength}`, "string");
}
if (resolved.pattern) {
try {
const reg = new RegExp(resolved.pattern);
if (!reg.test(value)) {
pushValidationError(errors, fieldPath, "string pattern mismatch", resolved.pattern, "string");
}
} catch {
// Ignore invalid server-side regex pattern definitions.
}
}
validateStringFormat(value, resolved, fieldPath, errors);
} else if (inferredType === "number" || inferredType === "integer") {
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
pushValidationError(errors, fieldPath, "type mismatch", inferredType, Array.isArray(value) ? "array" : typeof value);
return;
}
if (inferredType === "integer" && !Number.isInteger(value)) {
pushValidationError(errors, fieldPath, "type mismatch", "integer", "number");
}
if (typeof resolved.minimum === "number" && value < resolved.minimum) {
pushValidationError(errors, fieldPath, "number below minimum", `>=${resolved.minimum}`, "number");
}
if (typeof resolved.maximum === "number" && value > resolved.maximum) {
pushValidationError(errors, fieldPath, "number above maximum", `<=${resolved.maximum}`, "number");
}
if (typeof resolved.exclusiveMinimum === "number" && value <= resolved.exclusiveMinimum) {
pushValidationError(errors, fieldPath, "number below exclusiveMinimum", `>${resolved.exclusiveMinimum}`, "number");
}
if (typeof resolved.exclusiveMaximum === "number" && value >= resolved.exclusiveMaximum) {
pushValidationError(errors, fieldPath, "number above exclusiveMaximum", `<${resolved.exclusiveMaximum}`, "number");
}
} else if (inferredType === "boolean") {
if (typeof value !== "boolean") {
pushValidationError(errors, fieldPath, "type mismatch", "boolean", Array.isArray(value) ? "array" : typeof value);
return;
}
} else if (inferredType === "array") {
if (!Array.isArray(value)) {
pushValidationError(errors, fieldPath, "type mismatch", "array", typeof value);
return;
}
if (typeof resolved.minItems === "number" && value.length < resolved.minItems) {
pushValidationError(errors, fieldPath, "array too short", `minItems=${resolved.minItems}`, "array");
}
if (typeof resolved.maxItems === "number" && value.length > resolved.maxItems) {
pushValidationError(errors, fieldPath, "array too long", `maxItems=${resolved.maxItems}`, "array");
}
if (resolved.items) {
value.forEach((item, idx) => {
validateBySchema(item, resolved.items, `${fieldPath}[${idx}]`, errors, options);
});
}
} else if (inferredType === "object") {
if (!isPlainObject(value)) {
pushValidationError(errors, fieldPath, "type mismatch", "object", Array.isArray(value) ? "array" : typeof value);
return;
}
const properties = resolved.properties || {};
const required = Array.isArray(resolved.required) ? resolved.required : [];
for (const reqKey of required) {
const reqPath = fieldPath ? `${fieldPath}.${reqKey}` : reqKey;
if (!(reqKey in value) || isEmptyValue(value[reqKey])) {
pushValidationError(errors, reqPath, "required field missing or empty", "required", "missing");
}
}
for (const key of Object.keys(value)) {
const childPath = fieldPath ? `${fieldPath}.${key}` : key;
if (!(key in properties)) {
if (options.allowUnknownFields) {
continue;
}
pushValidationError(errors, childPath, "unknown field", "defined property", "unknown");
continue;
}
validateBySchema(value[key], properties[key], childPath, errors, options);
}
}
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
const enumPass = resolved.enum.some((x) => x === value);
if (!enumPass) {
pushValidationError(errors, fieldPath, "enum mismatch", JSON.stringify(resolved.enum), typeof value);
}
}
}
function validateCommandInputs(commandName, generatedCmd, body, query, strictValidation = true) {
const errors = [];
const { querySchema, bodySchema, requestBodyRequired } = getOperationSchema(generatedCmd.method, generatedCmd.path);
const options = { allowUnknownFields: !strictValidation };
if (requestBodyRequired && isEmptyValue(body)) {
pushValidationError(errors, "body", "required request body missing or empty", "non-empty object", "missing");
}
if (querySchema) {
validateBySchema(query || {}, querySchema, "query", errors, options);
} else if (query && isPlainObject(query) && Object.keys(query).length > 0) {
pushValidationError(errors, "query", "unexpected query parameters", "no query params", "object");
}
if (bodySchema) {
validateBySchema(body || {}, bodySchema, "body", errors, options);
} else if (body && isPlainObject(body) && Object.keys(body).length > 0) {
pushValidationError(errors, "body", "unexpected request body", "no body", "object");
}
if (errors.length > 0) {
throw new InputValidationError("Request validation failed", errors, commandName);
}
}
function printHelp() {
console.log(`HubStudio CLI
Usage:
node hubstudio.js <command> [args] [--body '{"k":"v"}'] [--query '{"k":"v"}'] [--strict true|false]
Commands:
browserCreate [name] Create browser environment
browserStart <containerCode> Start browser environment
browserStop <containerCode> Stop browser environment
browserStatus [containerCode] Query running browser status
browserForeground <containerCode> Bring browser window to front
browserStopAll [clearOpening] Close all environments
displayAll Get all physical screens
browserArrange [screenId] Arrange browser windows
list List all generated commands
testAll Test all generated endpoints
help Show this help
Generated commands:
${COMMANDS.length} commands are available, e.g.:
- postV1BrowserStart
- postV1BrowserStop
- postV1EnvCreate
Env:
HUBSTUDIO_BASE_URL Default: http://127.0.0.1:6873
HUBSTUDIO_STRICT_VALIDATION Default: true (set false to allow unknown fields)
`);
}
function parseJsonArg(flag, value) {
if (!value) {
throw new Error(`Missing value for ${flag}`);
}
try {
return JSON.parse(value);
} catch (err) {
throw new Error(`Invalid JSON for ${flag}: ${err.message}`);
}
}
function parseCommandArgs(rawArgs) {
const positional = [];
let bodyOverride = null;
let queryOverride = null;
let strictValidation = process.env.HUBSTUDIO_STRICT_VALIDATION !== "false";
for (let i = 0; i < rawArgs.length; i += 1) {
const token = rawArgs[i];
if (token === "--body") {
bodyOverride = parseJsonArg("--body", rawArgs[i + 1]);
i += 1;
continue;
}
if (token === "--query") {
queryOverride = parseJsonArg("--query", rawArgs[i + 1]);
i += 1;
continue;
}
if (token === "--strict") {
const next = rawArgs[i + 1];
if (!next || next.startsWith("--")) {
strictValidation = true;
} else {
const normalized = String(next).toLowerCase();
if (normalized === "true") {
strictValidation = true;
} else if (normalized === "false") {
strictValidation = false;
} else {
throw new InputValidationError("Request validation failed", [
{
field: "--strict",
reason: "invalid boolean flag value",
expected: "true|false",
actualType: typeof next,
},
]);
}
i += 1;
}
continue;
}
positional.push(token);
}
return { positional, bodyOverride, queryOverride, strictValidation };
}
async function post(endpointPath, body = {}, query = null) {
const url = new URL(endpointPath, BASE_URL);
if (query && typeof query === "object") {
for (let [k, v] of Object.entries(query)) {
if (v !== undefined && v !== null) {
url.searchParams.set(k, String(v));
}
}
}
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
let payload = null;
try {
payload = await response.json();
} catch {
payload = { raw: await response.text() };
}
return { status: response.status, payload };
}
function printResult(result) {
console.log(JSON.stringify(result, null, 2));
}
function requiredArg(name, value) {
if (!value) {
throw new InputValidationError("Request validation failed", [
{
field: name,
reason: "required argument missing or empty",
expected: "non-empty",
actualType: "missing",
},
]);
}
}
function getGeneratedCommand(commandName) {
const resolved = ALIASES[commandName] || commandName;
const cmd = COMMAND_MAP.get(resolved);
if (!cmd) {
throw new Error(`Unknown command: ${commandName}`);
}
return cmd;
}
function resolveCallInputs(commandName, positional, bodyOverride, queryOverride) {
const generatedCmd = getGeneratedCommand(commandName);
const body = { ...(generatedCmd.defaultBody || {}) };
const query = { ...(generatedCmd.defaultQuery || {}) };
if (commandName === "browserCreate") {
const name = positional[0] || `NodeAuto-${Date.now()}`;
body.containerName = name;
body.asDynamicType = 1;
body.proxyTypeName = "不使用代理";
} else if (commandName === "browserStart" || commandName === "browserForeground") {
requiredArg("containerCode", positional[0]);
body.containerCode = positional[0];
} else if (commandName === "browserStop") {
requiredArg("containerCode", positional[0]);
body.containerCode = positional[0];
query.containerCode = positional[0];
} else if (commandName === "browserStatus") {
if (positional[0]) {
body.containerCode = [positional[0]];
}
} else if (commandName === "browserStopAll") {
body.clearOpening = positional[0] ? positional[0] === "true" : false;
} else if (commandName === "browserArrange" && positional[0]) {
body.screenId = Number(positional[0]);
}
const finalBody = bodyOverride && typeof bodyOverride === "object" ? bodyOverride : body;
const finalQuery = queryOverride && typeof queryOverride === "object" ? queryOverride : query;
return { generatedCmd, finalBody, finalQuery };
}
function normalizeResult(command, generatedCmd, result, body, query) {
return {
command,
method: generatedCmd.method,
path: generatedCmd.path,
status: result.status,
code: result.payload && Object.prototype.hasOwnProperty.call(result.payload, "code") ? result.payload.code : null,
msg: result.payload && Object.prototype.hasOwnProperty.call(result.payload, "msg") ? result.payload.msg : null,
success:
result.status < 400 &&
(!result.payload || !Object.prototype.hasOwnProperty.call(result.payload, "code") || result.payload.code === 0),
request: { body, query },
payload: result.payload,
};
}
async function runTestAll() {
const results = [];
for (const cmd of COMMANDS) {
try {
const result = await post(cmd.path, cmd.defaultBody || {}, cmd.defaultQuery || {});
results.push(normalizeResult(cmd.command, cmd, result, cmd.defaultBody || {}, cmd.defaultQuery || {}));
} catch (err) {
results.push({
command: cmd.command,
method: cmd.method,
path: cmd.path,
status: null,
code: null,
msg: err.message,
success: false,
request: { body: cmd.defaultBody || {}, query: cmd.defaultQuery || {} },
payload: null,
});
}
}
const summary = {
total: results.length,
success: results.filter((x) => x.success).length,
failed: results.filter((x) => !x.success).length,
};
const report = {
baseUrl: BASE_URL,
testedAt: new Date().toISOString(),
summary,
results,
};
const reportPath = path.join(__dirname, "scripts", "node_test_all_report.json");
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
return { summary, reportPath, report };
}
async function main() {
const [, , command, ...rawArgs] = process.argv;
if (!command || command === "help" || command === "--help" || command === "-h") {
printHelp();
return;
}
if (command === "list") {
const items = COMMANDS.map((c) => ({
command: c.command,
method: c.method,
path: c.path,
summary: c.summary,
}));
printResult({ total: items.length, commands: items });
return;
}
if (command === "testAll") {
const out = await runTestAll();
printResult({
summary: out.summary,
reportPath: out.reportPath,
});
if (out.summary.failed > 0) {
process.exitCode = 2;
}
return;
}
const { positional, bodyOverride, queryOverride } = parseCommandArgs(rawArgs);
const { generatedCmd, finalBody, finalQuery } = resolveCallInputs(
command,
positional,
bodyOverride,
queryOverride
);
validateCommandInputs(command, generatedCmd, finalBody, finalQuery);
const result = await post(generatedCmd.path, finalBody, finalQuery);
const normalized = normalizeResult(command, generatedCmd, result, finalBody, finalQuery);
printResult(normalized);
if (normalized.status >= 400) {
process.exitCode = 1;
return;
}
if (normalized.code !== null && normalized.code !== 0) {
process.exitCode = 2;
}
}
main().catch((err) => {
if (err instanceof InputValidationError) {
const payload = {
success: false,
error: {
code: err.code,
message: err.message,
command: err.command || null,
fieldErrors: err.fieldErrors || [],
},
};
console.error(JSON.stringify(payload, null, 2));
process.exit(3);
return;
}
console.error(err.message || String(err));
process.exit(1);
});