feat: 增加 Sub2API 用户反馈只读追踪
This commit is contained in:
@@ -21,8 +21,9 @@ import {
|
||||
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
||||
|
||||
import type { ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types";
|
||||
import type { ConfirmOptions, DisclosureOptions, FeedbackOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types";
|
||||
import { codexPoolCleanupProbes, codexPoolConfigureLocal, codexPoolExpose, codexPoolPlan, codexPoolSentinelImage, codexPoolSentinelProbe, codexPoolSentinelReport, codexPoolSync, codexPoolTrace, codexPoolValidate } from "./actions";
|
||||
import { codexPoolFeedback } from "./feedback";
|
||||
import { renderCodexPoolPlan } from "./render";
|
||||
import { codexPoolRuntime } from "./runtime";
|
||||
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
||||
@@ -39,6 +40,7 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
|
||||
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
|
||||
if (action === "runtime") return await codexPoolRuntime(config, args.slice(1));
|
||||
if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1)));
|
||||
if (action === "feedback") return await codexPoolFeedback(config, parseFeedbackOptions(args.slice(1)));
|
||||
if (action === "sentinel-image") return await codexPoolSentinelImage(config, parseSentinelImageOptions(args.slice(1)));
|
||||
if (action === "sentinel-probe") return await codexPoolSentinelProbe(config, parseSentinelProbeOptions(args.slice(1)));
|
||||
if (action === "sentinel-report") return await codexPoolSentinelReport(config, parseSentinelReportOptions(args.slice(1)));
|
||||
@@ -181,6 +183,7 @@ export function parseSentinelReportOptions(args: string[]): SentinelReportOption
|
||||
|
||||
export function parseTraceOptions(args: string[]): TraceOptions {
|
||||
let requestId: string | null = null;
|
||||
let pageToken: string | null = null;
|
||||
let since = "24h";
|
||||
let tail = 20_000;
|
||||
let contextSeconds = 300;
|
||||
@@ -258,6 +261,64 @@ export function parseTraceOptions(args: string[]): TraceOptions {
|
||||
return { ...disclosure, requestId, since, tail, contextSeconds, showLines };
|
||||
}
|
||||
|
||||
export function parseFeedbackOptions(args: string[]): FeedbackOptions {
|
||||
let user: string | null = null;
|
||||
let window: FeedbackOptions["window"] = "30m";
|
||||
let requestId: string | null = null;
|
||||
let pageToken: string | null = null;
|
||||
let json = false;
|
||||
const targetArgs: string[] = [];
|
||||
const windows = new Set<FeedbackOptions["window"]>(["5m", "30m", "1h", "6h", "24h", "7d", "30d"]);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--json") {
|
||||
json = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--target" || arg.startsWith("--target=")) {
|
||||
targetArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
targetArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const readValue = (option: string): string => {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`);
|
||||
index += 1;
|
||||
return value.trim();
|
||||
};
|
||||
if (arg === "--user") user = readValue(arg);
|
||||
else if (arg.startsWith("--user=")) user = arg.slice("--user=".length).trim();
|
||||
else if (arg === "--window") {
|
||||
const value = readValue(arg) as FeedbackOptions["window"];
|
||||
if (!windows.has(value)) throw new Error("--window must be one of 5m, 30m, 1h, 6h, 24h, 7d, 30d");
|
||||
window = value;
|
||||
} else if (arg.startsWith("--window=")) {
|
||||
const value = arg.slice("--window=".length) as FeedbackOptions["window"];
|
||||
if (!windows.has(value)) throw new Error("--window must be one of 5m, 30m, 1h, 6h, 24h, 7d, 30d");
|
||||
window = value;
|
||||
} else if (arg === "--request-id") requestId = readValue(arg);
|
||||
else if (arg.startsWith("--request-id=")) requestId = arg.slice("--request-id=".length).trim();
|
||||
else if (arg === "--page-token") pageToken = readValue(arg);
|
||||
else if (arg.startsWith("--page-token=")) pageToken = arg.slice("--page-token=".length).trim();
|
||||
else throw new Error(`unsupported option: ${arg}`);
|
||||
}
|
||||
if (user === null || user.length === 0) throw new Error("feedback requires --user <email-or-id>");
|
||||
if (user.length > 320 || /[\r\n]/u.test(user)) throw new Error("--user must be a single email or positive user id");
|
||||
if (requestId !== null && (requestId.length === 0 || requestId.length > 256 || /[\r\n]/u.test(requestId))) {
|
||||
throw new Error("--request-id must be a non-empty stable request id up to 256 characters");
|
||||
}
|
||||
if (pageToken !== null && (pageToken.length === 0 || pageToken.length > 1024 || !/^[A-Za-z0-9_-]+$/u.test(pageToken))) {
|
||||
throw new Error("--page-token must be a URL-safe cursor token");
|
||||
}
|
||||
if (requestId !== null && pageToken !== null) throw new Error("--request-id and --page-token cannot be combined");
|
||||
return { user, window, requestId, pageToken, json, targetId: parseTargetId(targetArgs) };
|
||||
}
|
||||
|
||||
export function parseKubectlDuration(raw: string): string {
|
||||
const value = raw.trim();
|
||||
if (!/^[1-9][0-9]*(?:s|m|h)$/u.test(value)) throw new Error("--since must be a kubectl duration such as 24h, 90m, or 300s");
|
||||
|
||||
Reference in New Issue
Block a user