feat: add decision center service and cli
This commit is contained in:
@@ -39,6 +39,7 @@ function unifiedLogRotationItem(): CheckItem {
|
||||
"src/components/microservices/project-manager/src/index.ts",
|
||||
"src/components/microservices/baidu-netdisk/src/index.ts",
|
||||
"src/components/microservices/oa-event-flow/src/index.ts",
|
||||
"src/components/microservices/decision-center/src/index.ts",
|
||||
];
|
||||
const offenders = serviceFiles.flatMap((path) => {
|
||||
const text = readFileSync(rootPath(path), "utf8");
|
||||
@@ -70,6 +71,7 @@ export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckIte
|
||||
fileItem("src/components/microservices/oa-event-flow/src/index.ts"),
|
||||
fileItem("src/components/microservices/k3sctl-adapter/src/index.ts"),
|
||||
fileItem("src/components/microservices/mdtodo/src/index.ts"),
|
||||
fileItem("src/components/microservices/decision-center/src/index.ts"),
|
||||
fileItem("scripts/src/deploy.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
unifiedLogRotationItem(),
|
||||
|
||||
+28
-17
@@ -51,6 +51,13 @@ type CodexRequestInit = { method?: string; body?: unknown };
|
||||
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
||||
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
||||
|
||||
const codeQueueProxyPrefix = "/api/microservices/code-queue/proxy";
|
||||
|
||||
function codeQueueProxyPath(path: string): string {
|
||||
if (!path.startsWith("/")) throw new Error("Code Queue proxy path must start with /");
|
||||
return `${codeQueueProxyPrefix}${path}`;
|
||||
}
|
||||
|
||||
function requireTaskId(value: string | undefined, command: string): string {
|
||||
if (value === undefined || value.trim().length === 0) throw new Error(`${command} requires task id`);
|
||||
return value.trim();
|
||||
@@ -105,7 +112,11 @@ function upstreamError(response: unknown): string {
|
||||
if (record === null) return String(response);
|
||||
const body = asRecord(record.body);
|
||||
const bodyError = body?.error;
|
||||
if (typeof bodyError === "string") return bodyError;
|
||||
if (typeof bodyError === "string") {
|
||||
const requestId = typeof body?.requestId === "string" ? ` requestId=${body.requestId}` : "";
|
||||
const providerId = typeof body?.providerId === "string" ? ` providerId=${body.providerId}` : "";
|
||||
return `${bodyError}${providerId}${requestId}`;
|
||||
}
|
||||
const status = typeof record.status === "number" ? `HTTP ${record.status}` : "upstream request failed";
|
||||
return `${status}: ${JSON.stringify(response).slice(0, 1200)}`;
|
||||
}
|
||||
@@ -488,7 +499,7 @@ function queryString(params: Record<string, string | number | boolean | null | u
|
||||
}
|
||||
|
||||
function codexTaskSummary(taskId: string, options: CodexTaskOptions, fetcher: CodexResponseFetcher): unknown {
|
||||
const summaryPath = `/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`;
|
||||
const summaryPath = codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`);
|
||||
const summaryResponse = unwrapCodexResponse(fetcher(summaryPath));
|
||||
const summary = summaryResponse.body.summary;
|
||||
const result: Record<string, unknown> = {
|
||||
@@ -501,8 +512,8 @@ function codexTaskSummary(taskId: string, options: CodexTaskOptions, fetcher: Co
|
||||
if (options.traceMode === "tail") traceParams.tail = 1;
|
||||
if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq;
|
||||
if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq;
|
||||
const traceSummaryResponse = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`));
|
||||
const traceResponse = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`));
|
||||
const traceSummaryResponse = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)));
|
||||
const traceResponse = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)));
|
||||
result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options);
|
||||
}
|
||||
return result;
|
||||
@@ -548,7 +559,7 @@ function codexTaskOutput(taskId: string, options: CodexOutputOptions, fetcher: C
|
||||
if (options.mode === "tail") params.tail = 1;
|
||||
if (options.mode === "after") params.afterSeq = options.afterSeq;
|
||||
if (options.mode === "before") params.beforeSeq = options.beforeSeq;
|
||||
const response = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`));
|
||||
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)));
|
||||
return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) };
|
||||
}
|
||||
|
||||
@@ -558,7 +569,7 @@ function codexTaskJudge(taskId: string, options: CodexJudgeOptions, fetcher: Cod
|
||||
dryRun: options.dryRun ? 1 : undefined,
|
||||
includePrompt: options.includePrompt ? 1 : undefined,
|
||||
});
|
||||
const response = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/judge${params}`, { method: "POST" }));
|
||||
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/judge${params}`), { method: "POST" }));
|
||||
return { upstream: response.upstream, judgeReplay: response.body };
|
||||
}
|
||||
|
||||
@@ -575,7 +586,7 @@ export function codexJudgeQuery(taskId: string, optionArgs: string[], fetcher: C
|
||||
}
|
||||
|
||||
async function codexTaskSummaryAsync(taskId: string, options: CodexTaskOptions, fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
|
||||
const summaryPath = `/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`;
|
||||
const summaryPath = codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`);
|
||||
const summaryResponse = unwrapCodexResponse(await fetcher(summaryPath));
|
||||
const summary = summaryResponse.body.summary;
|
||||
const result: Record<string, unknown> = {
|
||||
@@ -588,8 +599,8 @@ async function codexTaskSummaryAsync(taskId: string, options: CodexTaskOptions,
|
||||
if (options.traceMode === "tail") traceParams.tail = 1;
|
||||
if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq;
|
||||
if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq;
|
||||
const traceSummaryResponse = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`));
|
||||
const traceResponse = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`));
|
||||
const traceSummaryResponse = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)));
|
||||
const traceResponse = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)));
|
||||
result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options);
|
||||
}
|
||||
return result;
|
||||
@@ -604,7 +615,7 @@ async function codexTaskOutputAsync(taskId: string, options: CodexOutputOptions,
|
||||
if (options.mode === "tail") params.tail = 1;
|
||||
if (options.mode === "after") params.afterSeq = options.afterSeq;
|
||||
if (options.mode === "before") params.beforeSeq = options.beforeSeq;
|
||||
const response = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`));
|
||||
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)));
|
||||
return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) };
|
||||
}
|
||||
|
||||
@@ -614,7 +625,7 @@ async function codexTaskJudgeAsync(taskId: string, options: CodexJudgeOptions, f
|
||||
dryRun: options.dryRun ? 1 : undefined,
|
||||
includePrompt: options.includePrompt ? 1 : undefined,
|
||||
});
|
||||
const response = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/judge${params}`, { method: "POST" }));
|
||||
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/judge${params}`), { method: "POST" }));
|
||||
return { upstream: response.upstream, judgeReplay: response.body };
|
||||
}
|
||||
|
||||
@@ -700,19 +711,19 @@ function requireMergeTargetQueueId(args: string[], command: string): string {
|
||||
}
|
||||
|
||||
function codeQueues(): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/queues"));
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
|
||||
}
|
||||
|
||||
function codexCreateQueue(queueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/queues", { method: "POST", body: { queueId } }));
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } }));
|
||||
}
|
||||
|
||||
function codexMergeQueue(sourceQueueId: string, targetQueueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/queues/${encodeURIComponent(targetQueueId)}/merge`, { method: "POST", body: { sourceQueueId } }));
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/queues/${encodeURIComponent(targetQueueId)}/merge`), { method: "POST", body: { sourceQueueId } }));
|
||||
}
|
||||
|
||||
function codexMoveTask(taskId: string, queueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } }));
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/move`), { method: "POST", body: { queueId } }));
|
||||
}
|
||||
|
||||
function promptFromSubmitArgs(args: string[]): string {
|
||||
@@ -845,7 +856,7 @@ function codexSubmitTask(args: string[]): unknown {
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/tasks", { method: "POST", body: payload }));
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/tasks"), { method: "POST", body: payload }));
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
tasks: asArray(response.body.tasks).map(compactTaskMutationResponse),
|
||||
@@ -854,7 +865,7 @@ function codexSubmitTask(args: string[]): unknown {
|
||||
}
|
||||
|
||||
function codexInterruptTask(taskId: string): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(`/api/microservices/k3sctl-adapter/proxy/api/services/code-queue-scheduler/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST" }));
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/interrupt`), { method: "POST" }));
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
task: compactTaskMutationResponse(response.body.task),
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { type UniDeskConfig, repoRoot } from "./config";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
type DecisionRecordType = "meeting" | "decision" | "goal" | "blocker" | "debt" | "experiment";
|
||||
type DecisionRecordLevel = "G0" | "G1" | "G2" | "G3" | "P0" | "P1" | "P2" | "P3" | "none";
|
||||
type DecisionRecordStatus = "active" | "blocked" | "parked" | "done";
|
||||
|
||||
const serviceId = "decision-center";
|
||||
const typeValues = new Set<DecisionRecordType>(["meeting", "decision", "goal", "blocker", "debt", "experiment"]);
|
||||
const levelValues = new Set<DecisionRecordLevel>(["G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "none"]);
|
||||
const statusValues = new Set<DecisionRecordStatus>(["active", "blocked", "parked", "done"]);
|
||||
|
||||
function optionValue(args: string[], names: string[]): string | undefined {
|
||||
for (const name of names) {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) continue;
|
||||
const raw = args[index + 1];
|
||||
if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${name} requires a non-empty value`);
|
||||
return raw;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function optionValues(args: string[], names: string[]): string[] {
|
||||
const values: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
if (!names.includes(args[index] ?? "")) continue;
|
||||
const raw = args[index + 1];
|
||||
if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${args[index]} requires a non-empty value`);
|
||||
values.push(raw);
|
||||
index += 1;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function positionalArgs(args: string[]): string[] {
|
||||
const positions: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const value = args[index] ?? "";
|
||||
if (value.startsWith("--")) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
positions.push(value);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function parseType(raw: string | undefined, fallback: DecisionRecordType): DecisionRecordType {
|
||||
const value = raw || fallback;
|
||||
if (!typeValues.has(value as DecisionRecordType)) throw new Error(`--type must be one of: ${Array.from(typeValues).join(", ")}`);
|
||||
return value as DecisionRecordType;
|
||||
}
|
||||
|
||||
function parseLevel(raw: string | undefined, fallback: DecisionRecordLevel): DecisionRecordLevel {
|
||||
const value = raw || fallback;
|
||||
if (!levelValues.has(value as DecisionRecordLevel)) throw new Error(`--level must be one of: ${Array.from(levelValues).join(", ")}`);
|
||||
return value as DecisionRecordLevel;
|
||||
}
|
||||
|
||||
function parseStatus(raw: string | undefined, fallback: DecisionRecordStatus): DecisionRecordStatus {
|
||||
const value = raw || fallback;
|
||||
if (!statusValues.has(value as DecisionRecordStatus)) throw new Error(`--status must be one of: ${Array.from(statusValues).join(", ")}`);
|
||||
return value as DecisionRecordStatus;
|
||||
}
|
||||
|
||||
function splitList(values: string[]): string[] {
|
||||
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function readMarkdownFile(path: string): { absolutePath: string; markdown: string } {
|
||||
const absolutePath = resolve(repoRoot, path);
|
||||
const markdown = readFileSync(absolutePath, "utf8");
|
||||
if (markdown.trim().length === 0) throw new Error(`markdown file is empty: ${absolutePath}`);
|
||||
if (markdown.length > 1_000_000) throw new Error(`markdown file is too large: ${absolutePath}`);
|
||||
return { absolutePath, markdown };
|
||||
}
|
||||
|
||||
function decisionProxy(path: string, init?: { method?: string; body?: unknown }): unknown {
|
||||
return coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
|
||||
}
|
||||
|
||||
async function decisionProxyAsync(
|
||||
fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>,
|
||||
path: string,
|
||||
init?: { method?: string; body?: unknown },
|
||||
): Promise<unknown> {
|
||||
return await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
|
||||
}
|
||||
|
||||
function unwrapProxyResponse(response: unknown): unknown {
|
||||
const record = typeof response === "object" && response !== null && !Array.isArray(response) ? response as Record<string, unknown> : {};
|
||||
if (record.ok !== true) return response;
|
||||
const body = record.body;
|
||||
return { upstream: { ok: record.ok, status: record.status }, body };
|
||||
}
|
||||
|
||||
function uploadMeeting(args: string[]): unknown {
|
||||
const file = positionalArgs(args)[0];
|
||||
if (!file) throw new Error("decision upload requires markdown file");
|
||||
const { absolutePath, markdown } = readMarkdownFile(file);
|
||||
const type = parseType(optionValue(args, ["--type"]), "meeting");
|
||||
const payload = {
|
||||
markdown,
|
||||
title: optionValue(args, ["--title"]),
|
||||
type,
|
||||
level: parseLevel(optionValue(args, ["--level"]), "none"),
|
||||
status: parseStatus(optionValue(args, ["--status"]), "active"),
|
||||
linkedGoalId: optionValue(args, ["--linked-goal-id", "--linkedGoalId"]),
|
||||
tags: splitList(optionValues(args, ["--tag", "--tags"])),
|
||||
evidenceLinks: splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])),
|
||||
sourceSession: optionValue(args, ["--source-session", "--sourceSession"]),
|
||||
taskId: optionValue(args, ["--task-id", "--taskId"]),
|
||||
commitId: optionValue(args, ["--commit-id", "--commitId"]),
|
||||
};
|
||||
const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records";
|
||||
const body = type === "meeting" ? payload : { ...payload, body: markdown };
|
||||
return { file: absolutePath, result: unwrapProxyResponse(decisionProxy(endpoint, { method: "POST", body })) };
|
||||
}
|
||||
|
||||
async function uploadMeetingAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
const file = positionalArgs(args)[0];
|
||||
if (!file) throw new Error("decision upload requires markdown file");
|
||||
const { absolutePath, markdown } = readMarkdownFile(file);
|
||||
const type = parseType(optionValue(args, ["--type"]), "meeting");
|
||||
const payload = {
|
||||
markdown,
|
||||
title: optionValue(args, ["--title"]),
|
||||
type,
|
||||
level: parseLevel(optionValue(args, ["--level"]), "none"),
|
||||
status: parseStatus(optionValue(args, ["--status"]), "active"),
|
||||
linkedGoalId: optionValue(args, ["--linked-goal-id", "--linkedGoalId"]),
|
||||
tags: splitList(optionValues(args, ["--tag", "--tags"])),
|
||||
evidenceLinks: splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])),
|
||||
sourceSession: optionValue(args, ["--source-session", "--sourceSession"]),
|
||||
taskId: optionValue(args, ["--task-id", "--taskId"]),
|
||||
commitId: optionValue(args, ["--commit-id", "--commitId"]),
|
||||
};
|
||||
const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records";
|
||||
const body = type === "meeting" ? payload : { ...payload, body: markdown };
|
||||
return { file: absolutePath, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, endpoint, { method: "POST", body })) };
|
||||
}
|
||||
|
||||
function listRecords(args: string[]): unknown {
|
||||
const params = new URLSearchParams();
|
||||
const type = optionValue(args, ["--type"]);
|
||||
const status = optionValue(args, ["--status"]);
|
||||
const level = optionValue(args, ["--level"]);
|
||||
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
|
||||
const limit = optionValue(args, ["--limit"]);
|
||||
if (type !== undefined) params.set("type", parseType(type, "meeting"));
|
||||
if (status !== undefined) params.set("status", parseStatus(status, "active"));
|
||||
if (level !== undefined) params.set("level", parseLevel(level, "none"));
|
||||
if (linkedGoalId !== undefined) params.set("linkedGoalId", linkedGoalId);
|
||||
if (limit !== undefined) params.set("limit", limit);
|
||||
const query = params.toString();
|
||||
return unwrapProxyResponse(decisionProxy(`/api/records${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
async function listRecordsAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
const params = new URLSearchParams();
|
||||
const type = optionValue(args, ["--type"]);
|
||||
const status = optionValue(args, ["--status"]);
|
||||
const level = optionValue(args, ["--level"]);
|
||||
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
|
||||
const limit = optionValue(args, ["--limit"]);
|
||||
if (type !== undefined) params.set("type", parseType(type, "meeting"));
|
||||
if (status !== undefined) params.set("status", parseStatus(status, "active"));
|
||||
if (level !== undefined) params.set("level", parseLevel(level, "none"));
|
||||
if (linkedGoalId !== undefined) params.set("linkedGoalId", linkedGoalId);
|
||||
if (limit !== undefined) params.set("limit", limit);
|
||||
const query = params.toString();
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
function showRecord(id: string | undefined): unknown {
|
||||
if (!id) throw new Error("decision show requires record id");
|
||||
return unwrapProxyResponse(decisionProxy(`/api/records/${encodeURIComponent(id)}`));
|
||||
}
|
||||
|
||||
async function showRecordAsync(id: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
if (!id) throw new Error("decision show requires record id");
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records/${encodeURIComponent(id)}`));
|
||||
}
|
||||
|
||||
export async function runDecisionCenterCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
const [action = "list", id] = args;
|
||||
if (action === "upload") return uploadMeeting(args.slice(1));
|
||||
if (action === "list") return listRecords(args.slice(1));
|
||||
if (action === "show") return showRecord(id);
|
||||
if (action === "health") return unwrapProxyResponse(coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/health`));
|
||||
throw new Error("decision command must be one of: upload, list, show, health");
|
||||
}
|
||||
|
||||
export async function runDecisionCenterCommandAsync(
|
||||
_config: UniDeskConfig,
|
||||
args: string[],
|
||||
fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>,
|
||||
): Promise<unknown> {
|
||||
const [action = "list", id] = args;
|
||||
if (action === "upload") return uploadMeetingAsync(args.slice(1), fetcher);
|
||||
if (action === "list") return listRecordsAsync(args.slice(1), fetcher);
|
||||
if (action === "show") return showRecordAsync(id, fetcher);
|
||||
if (action === "health") return unwrapProxyResponse(await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/health`));
|
||||
throw new Error("decision command must be one of: upload, list, show, health");
|
||||
}
|
||||
+154
-15
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
|
||||
import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
|
||||
@@ -116,7 +117,7 @@ function deployHelp(action: string | undefined = undefined): Record<string, unkn
|
||||
apply: "Start an async target-side reconcile job unless --run-now is explicitly present.",
|
||||
},
|
||||
options: [
|
||||
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root." },
|
||||
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js." },
|
||||
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
|
||||
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
|
||||
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
|
||||
@@ -360,10 +361,7 @@ function positionalArgs(args: string[]): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function readDeployManifest(file: string): DeployManifest {
|
||||
const path = resolve(repoRoot, file);
|
||||
if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`);
|
||||
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
||||
function parseDeployManifest(parsed: unknown): DeployManifest {
|
||||
const record = asRecord(parsed);
|
||||
if (record === null) throw new Error("deploy manifest must be an object");
|
||||
if (record.schemaVersion !== 1) throw new Error("deploy manifest schemaVersion must be 1");
|
||||
@@ -385,6 +383,67 @@ function readDeployManifest(file: string): DeployManifest {
|
||||
return { schemaVersion: 1, services };
|
||||
}
|
||||
|
||||
async function readDeployManifest(file: string): Promise<DeployManifest> {
|
||||
const path = resolve(repoRoot, file);
|
||||
if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`);
|
||||
if (/\.(?:mjs|js|cjs)$/iu.test(path)) {
|
||||
const moduleValue = await import(`${pathToFileURL(path).href}?unideskDeployManifest=${Date.now()}`);
|
||||
const moduleRecord = asRecord(moduleValue);
|
||||
const parsed = moduleRecord?.default ?? moduleRecord?.manifest;
|
||||
if (parsed === undefined) throw new Error(`deploy JS manifest must export default or named manifest: ${path}`);
|
||||
return parseDeployManifest(parsed);
|
||||
}
|
||||
return parseDeployManifest(JSON.parse(readFileSync(path, "utf8")) as unknown);
|
||||
}
|
||||
|
||||
function frontendCoreDeployService(config: UniDeskConfig): UniDeskMicroserviceConfig {
|
||||
return {
|
||||
id: "frontend",
|
||||
name: "UniDesk Frontend",
|
||||
providerId: "main-server",
|
||||
description: "UniDesk core frontend entrypoint deployed by the main-server direct Compose executor.",
|
||||
repository: {
|
||||
url: unideskRepoUrl,
|
||||
commitId: "local",
|
||||
dockerfile: "src/components/frontend/Dockerfile",
|
||||
composeFile: config.docker.composeFile,
|
||||
composeService: "frontend",
|
||||
containerName: "unidesk-frontend",
|
||||
},
|
||||
backend: {
|
||||
nodeBaseUrl: `http://127.0.0.1:${config.network.frontend.port}`,
|
||||
nodeBindHost: "127.0.0.1",
|
||||
nodePort: config.network.frontend.port,
|
||||
proxyMode: "core-direct",
|
||||
frontendOnly: false,
|
||||
public: true,
|
||||
allowedMethods: ["GET", "HEAD"],
|
||||
allowedPathPrefixes: ["/"],
|
||||
healthPath: "/health",
|
||||
timeoutMs: 8000,
|
||||
},
|
||||
deployment: { mode: "unidesk-direct" },
|
||||
development: {
|
||||
providerId: "main-server",
|
||||
sshPassthrough: false,
|
||||
worktreePath: repoRoot,
|
||||
},
|
||||
frontend: {
|
||||
route: "/",
|
||||
integrated: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function coreDeployService(config: UniDeskConfig, id: string): UniDeskMicroserviceConfig | undefined {
|
||||
if (id === "frontend") return frontendCoreDeployService(config);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isCoreDeployService(service: UniDeskMicroserviceConfig): boolean {
|
||||
return service.id === "frontend" && service.providerId === "main-server" && service.backend.proxyMode === "core-direct";
|
||||
}
|
||||
|
||||
function selectServices(config: UniDeskConfig, manifest: DeployManifest, serviceId: string | null): Array<{
|
||||
desired: DeployManifestService;
|
||||
config: UniDeskMicroserviceConfig;
|
||||
@@ -393,8 +452,8 @@ function selectServices(config: UniDeskConfig, manifest: DeployManifest, service
|
||||
const selected = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
|
||||
if (serviceId !== null && selected.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
|
||||
return selected.map((desired) => {
|
||||
const service = configById.get(desired.id);
|
||||
if (service === undefined) throw new Error(`deploy manifest service ${desired.id} is not present in config.json microservices`);
|
||||
const service = configById.get(desired.id) ?? coreDeployService(config, desired.id);
|
||||
if (service === undefined) throw new Error(`deploy manifest service ${desired.id} is not present in config.json microservices or supported core deploy services`);
|
||||
return { desired, config: service };
|
||||
});
|
||||
}
|
||||
@@ -424,7 +483,7 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str
|
||||
|
||||
function targetWorkDir(service: UniDeskMicroserviceConfig): string {
|
||||
if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir;
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) {
|
||||
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) {
|
||||
return rootPath(".state", "deploy", "work", safeId(service.id));
|
||||
}
|
||||
return service.development.worktreePath;
|
||||
@@ -475,12 +534,12 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
|
||||
}
|
||||
|
||||
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return targetWorkDir(service);
|
||||
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return targetWorkDir(service);
|
||||
return "";
|
||||
}
|
||||
|
||||
function directDockerfileOverride(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return service.repository.dockerfile;
|
||||
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return service.repository.dockerfile;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -587,7 +646,7 @@ function buildCachePrelude(dockerfileVariable: string): string[] {
|
||||
}
|
||||
|
||||
function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, exportDir: string): string {
|
||||
if (targetIsMain(service) && desired.repo === unideskRepoUrl) {
|
||||
if (targetIsMain(service) && isUnideskRepo(desired.repo)) {
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`repo=${shellQuote(repoRoot)}`,
|
||||
@@ -848,10 +907,63 @@ function imageLabelVerifyScript(service: UniDeskMicroserviceConfig, expectedComm
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function composeDeployScript(service: UniDeskMicroserviceConfig): string {
|
||||
function directComposeDeployEnv(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): Record<string, string> {
|
||||
if (service.id !== "frontend") return {};
|
||||
return {
|
||||
UNIDESK_FRONTEND_DEPLOY_SERVICE_ID: service.id,
|
||||
UNIDESK_FRONTEND_DEPLOY_REPO: desired.repo,
|
||||
UNIDESK_FRONTEND_DEPLOY_COMMIT: resolvedCommit,
|
||||
UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: desired.commitId,
|
||||
};
|
||||
}
|
||||
|
||||
function composeEnvStampLines(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string[] {
|
||||
const deployEnv = directComposeDeployEnv(service, desired, resolvedCommit);
|
||||
const entries = Object.entries(deployEnv);
|
||||
if (entries.length === 0) return [];
|
||||
const encoded = Buffer.from(JSON.stringify(deployEnv), "utf8").toString("base64");
|
||||
return [
|
||||
`deploy_env_b64=${shellQuote(encoded)}`,
|
||||
"if [ -n \"$compose_env_file\" ]; then",
|
||||
" python3 - \"$compose_env_file\" \"$deploy_env_b64\" <<'PY'",
|
||||
"import base64, json, re, sys",
|
||||
"path = sys.argv[1]",
|
||||
"updates = json.loads(base64.b64decode(sys.argv[2]).decode('utf-8'))",
|
||||
"def env_value(value):",
|
||||
" text = str(value)",
|
||||
" return text if re.match(r'^[A-Za-z0-9_./:@-]+$', text) else json.dumps(text)",
|
||||
"try:",
|
||||
" lines = open(path, encoding='utf-8').read().splitlines()",
|
||||
"except FileNotFoundError:",
|
||||
" lines = []",
|
||||
"seen = set()",
|
||||
"out = []",
|
||||
"for line in lines:",
|
||||
" if '=' not in line or line.startswith('#'):",
|
||||
" out.append(line)",
|
||||
" continue",
|
||||
" key = line.split('=', 1)[0]",
|
||||
" if key in updates:",
|
||||
" out.append(f'{key}={env_value(updates[key])}')",
|
||||
" seen.add(key)",
|
||||
" else:",
|
||||
" out.append(line)",
|
||||
"for key, value in updates.items():",
|
||||
" if key not in seen:",
|
||||
" out.append(f'{key}={env_value(value)}')",
|
||||
"with open(path, 'w', encoding='utf-8') as handle:",
|
||||
" handle.write('\\n'.join(out) + '\\n')",
|
||||
"PY",
|
||||
" echo compose_env_deploy_stamp=updated",
|
||||
"fi",
|
||||
];
|
||||
}
|
||||
|
||||
function composeDeployScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string {
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
directComposeResolveScript(service),
|
||||
...composeEnvStampLines(service, desired, resolvedCommit),
|
||||
"docker compose \"${compose_env_args[@]}\" -f \"$compose_file\" -p \"$project\" up -d --no-build --no-deps --force-recreate \"$compose_service\"",
|
||||
"ready=0",
|
||||
"for attempt in $(seq 1 90); do",
|
||||
@@ -1146,6 +1258,33 @@ function healthSummary(response: unknown): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
async function directHttpJson(url: string, timeoutMs: number): Promise<unknown> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
const text = await response.text();
|
||||
let body: unknown = null;
|
||||
try {
|
||||
body = text.length > 0 ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = { text };
|
||||
}
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
} catch (error) {
|
||||
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function serviceHealth(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise<unknown> {
|
||||
if (isCoreDeployService(service)) {
|
||||
return await directHttpJson(`${service.backend.nodeBaseUrl}${service.backend.healthPath}`, service.backend.timeoutMs);
|
||||
}
|
||||
return coreInternalFetch(`/api/microservices/${encodeURIComponent(service.id)}/health`);
|
||||
}
|
||||
|
||||
function commitMatches(actual: string | null, desired: string): boolean {
|
||||
if (actual === null || actual.length === 0) return false;
|
||||
const normalized = actual.toLowerCase();
|
||||
@@ -1397,7 +1536,7 @@ async function readK8sCommit(config: UniDeskConfig, service: UniDeskMicroservice
|
||||
|
||||
async function readRuntimeState(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService): Promise<ServiceRuntimeState> {
|
||||
const reason = unsupportedReason(service);
|
||||
const health = coreInternalFetch(`/api/microservices/${encodeURIComponent(service.id)}/health`);
|
||||
const health = await serviceHealth(config, service);
|
||||
const healthBody = coreBody(health);
|
||||
const healthCommit = healthDeployCommit(healthBody);
|
||||
const healthRecord = asRecord(health);
|
||||
@@ -1553,7 +1692,7 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
|
||||
if (!pushStep(steps, cleanup)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
||||
}
|
||||
} else {
|
||||
const deploy = await step(config, service, "compose-up", composeDeployScript(service), targetIsMain(service) ? repoRoot : targetWorkDir(service), 180_000, !targetIsMain(service));
|
||||
const deploy = await step(config, service, "compose-up", composeDeployScript(service, desired, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 180_000, !targetIsMain(service));
|
||||
if (!pushStep(steps, deploy)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
||||
const imageVerify = await step(config, service, "image-label-verify", imageLabelVerifyScript(service, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 60_000, false);
|
||||
if (!pushStep(steps, imageVerify)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
||||
@@ -1638,7 +1777,7 @@ export async function runDeployCommand(config: UniDeskConfig, args: string[]): P
|
||||
if (!["check", "plan", "apply"].includes(actionRaw)) throw new Error("deploy command must be one of: check, plan, apply");
|
||||
const action = actionRaw as DeployAction;
|
||||
const options = parseOptions(args.slice(1));
|
||||
const manifest = resolveManifestCommits(readDeployManifest(options.file), options.serviceId);
|
||||
const manifest = resolveManifestCommits(await readDeployManifest(options.file), options.serviceId);
|
||||
if (action === "check" || action === "plan") return await checkOrPlan(config, manifest, options, action);
|
||||
if (!options.runNow) return applyJob(config, args, options);
|
||||
return await runApplyNow(config, manifest, options);
|
||||
|
||||
@@ -125,6 +125,10 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_LOG_DAY: logDay,
|
||||
UNIDESK_LOG_PREFIX: logPrefix,
|
||||
UNIDESK_LOG_RETENTION_BYTES: runtimeSecret("UNIDESK_LOG_RETENTION_BYTES") || "1GiB",
|
||||
UNIDESK_FRONTEND_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_SERVICE_ID") || "frontend",
|
||||
UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"),
|
||||
UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"),
|
||||
UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"),
|
||||
UNIDESK_HOST_ROOT_SSH_DIR: process.env.UNIDESK_HOST_ROOT_SSH_DIR || "/root/.ssh",
|
||||
UNIDESK_HOST_SSH_KEY_DIR: config.sshForwarding.keyDir,
|
||||
UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
|
||||
|
||||
+121
-3
@@ -39,6 +39,7 @@ const NETWORK_CHECK_NAMES = [
|
||||
"network:todo-note-public-blocked",
|
||||
"network:code-queue-public-blocked",
|
||||
"network:oa-event-flow-public-blocked",
|
||||
"network:decision-center-public-blocked",
|
||||
"network:filebrowser-public-blocked",
|
||||
] as const;
|
||||
|
||||
@@ -61,6 +62,7 @@ const SERVICE_CHECK_NAMES = [
|
||||
"microservice:catalog-todo-note",
|
||||
"microservice:catalog-oa-event-flow",
|
||||
"microservice:catalog-code-queue",
|
||||
"microservice:catalog-decision-center",
|
||||
"microservice:k3sctl-adapter-status",
|
||||
"microservice:k3sctl-control-plane",
|
||||
"microservice:catalog-filebrowser",
|
||||
@@ -92,6 +94,9 @@ const SERVICE_CHECK_NAMES = [
|
||||
"microservice:code-queue-status",
|
||||
"microservice:code-queue-health",
|
||||
"microservice:code-queue-tasks",
|
||||
"microservice:decision-center-status",
|
||||
"microservice:decision-center-health",
|
||||
"microservice:decision-center-records",
|
||||
"microservice:oa-event-flow-status",
|
||||
"microservice:oa-event-flow-health",
|
||||
"microservice:oa-event-flow-diagnostics",
|
||||
@@ -129,6 +134,7 @@ const FRONTEND_CHECK_NAMES = [
|
||||
"frontend:todo-note-integrated-visible",
|
||||
"frontend:findjob-integrated-visible",
|
||||
"frontend:oa-event-flow-visible",
|
||||
"frontend:decision-center-visible",
|
||||
"frontend:code-queue-integrated-visible",
|
||||
"frontend:code-queue-enqueue-await-smoke",
|
||||
"frontend:code-queue-summary-mobile-wrap",
|
||||
@@ -321,6 +327,7 @@ const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record<string, string> = {
|
||||
"/app/met-nonlinear/": "met-nonlinear-page",
|
||||
"/app/claudeqq/": "claudeqq-page",
|
||||
"/app/oa-event-flow/": "oa-event-flow-page",
|
||||
"/app/decision-center/": "decision-center-page",
|
||||
"/app/code-queue/": "code-queue-page",
|
||||
};
|
||||
|
||||
@@ -1025,6 +1032,7 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
|
||||
const codeQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500);
|
||||
const oaEventFlowPublic = await fetchProbe(`http://${config.network.publicHost}:4255/health`, 2500);
|
||||
const oaEventFlowRestriction = dockerUserPortRestriction(4255, allowedSourceCidrs);
|
||||
const decisionCenterPublic = await fetchProbe(`http://${config.network.publicHost}:4277/health`, 2500);
|
||||
const filebrowserPublic = await fetchProbe(`http://${config.network.publicHost}:4251/health`, 2500);
|
||||
addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(":14222->"), portSummary);
|
||||
addSelectedCheck(checks, options, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic);
|
||||
@@ -1035,6 +1043,7 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
|
||||
addSelectedCheck(checks, options, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic);
|
||||
addSelectedCheck(checks, options, "network:code-queue-public-blocked", (codeQueuePublic as { reachable?: boolean }).reachable === false, codeQueuePublic);
|
||||
addSelectedCheck(checks, options, "network:oa-event-flow-public-blocked", publicProbeBlockedOrRestricted(oaEventFlowPublic, oaEventFlowRestriction), { publicProbe: oaEventFlowPublic, restriction: oaEventFlowRestriction });
|
||||
addSelectedCheck(checks, options, "network:decision-center-public-blocked", (decisionCenterPublic as { reachable?: boolean }).reachable === false, decisionCenterPublic);
|
||||
addSelectedCheck(checks, options, "network:filebrowser-public-blocked", (filebrowserPublic as { reachable?: boolean }).reachable === false, filebrowserPublic);
|
||||
}
|
||||
|
||||
@@ -1077,6 +1086,9 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const codeQueueStatus = dockerCoreJson("/api/microservices/code-queue/status");
|
||||
const codeQueueHealth = dockerCoreJson("/api/microservices/code-queue/health");
|
||||
const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId=");
|
||||
const decisionCenterStatus = dockerCoreJson("/api/microservices/decision-center/status");
|
||||
const decisionCenterHealth = dockerCoreJson("/api/microservices/decision-center/health");
|
||||
const decisionCenterRecords = dockerCoreJson("/api/microservices/decision-center/proxy/api/records?limit=20");
|
||||
const filebrowserHealth = dockerCoreJson("/api/microservices/filebrowser/health");
|
||||
const filebrowserWebui = dockerCoreJson("/api/microservices/filebrowser/proxy/");
|
||||
const filebrowserD601Health = dockerCoreJson("/api/microservices/filebrowser-d601/health");
|
||||
@@ -1127,6 +1139,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const todoNote = microserviceList.find((service) => service.id === "todo-note");
|
||||
const oaEventFlow = microserviceList.find((service) => service.id === "oa-event-flow");
|
||||
const codeQueue = microserviceList.find((service) => service.id === "code-queue");
|
||||
const decisionCenter = microserviceList.find((service) => service.id === "decision-center");
|
||||
const filebrowser = microserviceList.find((service) => service.id === "filebrowser");
|
||||
const filebrowserD601 = microserviceList.find((service) => service.id === "filebrowser-d601");
|
||||
const findjobSummaryBody = (findjobSummary as { body?: { totalJobs?: number; prioritizedJobs?: number } }).body;
|
||||
@@ -1150,6 +1163,8 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const oaEventFlowStatsBody = (oaEventFlowStats as { body?: { ok?: boolean; stats?: unknown[]; returned?: number } }).body;
|
||||
const codeQueueHealthBody = (codeQueueHealth as { body?: { ok?: boolean; egressProxy?: { connected?: boolean }; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record<string, string> } } }).body;
|
||||
const codeQueueTasksBody = (codeQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record<string, string> }; tasks?: unknown[] } }).body;
|
||||
const decisionCenterHealthBody = (decisionCenterHealth as { body?: { ok?: boolean; service?: string; storage?: string; schemaReady?: boolean; recordCount?: number; deploy?: { commit?: string } } }).body;
|
||||
const decisionCenterRecordsBody = (decisionCenterRecords as { body?: { ok?: boolean; records?: unknown[]; returned?: number } }).body;
|
||||
const k3sctlControlPlaneBody = (k3sctlControlPlane as { body?: {
|
||||
ok?: boolean;
|
||||
clusterId?: string;
|
||||
@@ -1169,6 +1184,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
} }).body;
|
||||
const k3sctlCodeQueueService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue");
|
||||
const k3sctlClaudeqqService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "claudeqq");
|
||||
const k3sctlDecisionCenterService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "decision-center");
|
||||
const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body;
|
||||
const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body;
|
||||
const filebrowserWebuiText = String((filebrowserWebui as { body?: { text?: string } }).body?.text || "");
|
||||
@@ -1216,6 +1232,15 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& codeQueue.runtime?.orchestrator === "k3sctl"
|
||||
&& codeQueue.runtime?.container === null,
|
||||
{ microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-decision-center",
|
||||
(microservices as { ok?: boolean }).ok === true
|
||||
&& decisionCenter?.providerId === "D601"
|
||||
&& decisionCenter.backend?.public === false
|
||||
&& decisionCenter.backend?.proxyMode === "k3sctl-adapter-http"
|
||||
&& decisionCenter.deployment?.mode === "k3sctl-managed"
|
||||
&& decisionCenter.runtime?.orchestrator === "k3sctl"
|
||||
&& decisionCenter.runtime?.container === null,
|
||||
{ microservices });
|
||||
addSelectedCheck(checks, options, "microservice:k3sctl-adapter-status",
|
||||
(k3sctlStatus as { ok?: boolean; body?: { microservice?: { id?: string; providerId?: string } } }).ok === true
|
||||
&& (k3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.id === "k3sctl-adapter"
|
||||
@@ -1238,6 +1263,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& k3sctlClaudeqqService?.servingHealthy === true
|
||||
&& k3sctlClaudeqqService?.active?.id === "D601"
|
||||
&& k3sctlClaudeqqService?.active?.healthy === true
|
||||
&& k3sctlDecisionCenterService?.status === "healthy"
|
||||
&& k3sctlDecisionCenterService?.topologyComplete === true
|
||||
&& k3sctlDecisionCenterService?.servingHealthy === true
|
||||
&& k3sctlDecisionCenterService?.active?.id === "D601"
|
||||
&& k3sctlDecisionCenterService?.active?.healthy === true
|
||||
&& (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601")
|
||||
&& (k3sctlCodeQueueService?.missingNodeIds ?? []).length === 0
|
||||
&& (k3sctlCodeQueueService?.instances ?? []).some((instance) => instance.id === "D601" && instance.healthy === true),
|
||||
@@ -1248,6 +1278,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
kubeApiProxy: k3sctlControlPlaneBody?.kubeApiProxy,
|
||||
service: k3sctlCodeQueueService,
|
||||
claudeqq: k3sctlClaudeqqService,
|
||||
decisionCenter: k3sctlDecisionCenterService,
|
||||
});
|
||||
addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true
|
||||
&& filebrowser?.providerId === "D518"
|
||||
@@ -1319,6 +1350,9 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addSelectedCheck(checks, options, "microservice:code-queue-status", (codeQueueStatus as { ok?: boolean }).ok === true && (codeQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", codeQueueStatus);
|
||||
addSelectedCheck(checks, options, "microservice:code-queue-health", (codeQueueHealth as { ok?: boolean }).ok === true && codeQueueHealthBody?.ok === true && codeQueueHealthBody.egressProxy?.connected === true && codeQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codeQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueHealth);
|
||||
addSelectedCheck(checks, options, "microservice:code-queue-tasks", (codeQueueTasks as { ok?: boolean }).ok === true && codeQueueTasksBody?.ok === true && Array.isArray(codeQueueTasksBody.tasks) && codeQueueTasksBody.queue?.defaultModel === "gpt-5.5" && codeQueueTasksBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueTasks);
|
||||
addSelectedCheck(checks, options, "microservice:decision-center-status", (decisionCenterStatus as { ok?: boolean }).ok === true && (decisionCenterStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", decisionCenterStatus);
|
||||
addSelectedCheck(checks, options, "microservice:decision-center-health", (decisionCenterHealth as { ok?: boolean }).ok === true && decisionCenterHealthBody?.ok === true && decisionCenterHealthBody.service === "decision-center" && decisionCenterHealthBody.storage === "postgres" && decisionCenterHealthBody.schemaReady === true, decisionCenterHealth);
|
||||
addSelectedCheck(checks, options, "microservice:decision-center-records", (decisionCenterRecords as { ok?: boolean }).ok === true && decisionCenterRecordsBody?.ok === true && Array.isArray(decisionCenterRecordsBody.records), decisionCenterRecords);
|
||||
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
|
||||
method: "POST",
|
||||
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
|
||||
@@ -1417,6 +1451,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const needTodoNote = wants("frontend:todo-note-integrated-visible");
|
||||
const needFindJob = wants("frontend:findjob-integrated-visible");
|
||||
const needOaEventFlow = wants("frontend:oa-event-flow-visible");
|
||||
const needDecisionCenter = wants("frontend:decision-center-visible");
|
||||
const needCodeQueue = wantsAny([
|
||||
"frontend:code-queue-integrated-visible",
|
||||
"frontend:code-queue-enqueue-await-smoke",
|
||||
@@ -1502,6 +1537,10 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
let findjobText = "";
|
||||
let oaEventFlowText = "";
|
||||
let oaEventFlowMetrics: any = { pageVisible: false, eventTableVisible: false, statsVisible: false, tagFilterValue: "", rawButtonCount: 0 };
|
||||
let decisionCenterText = "";
|
||||
let decisionCenterE2eRecord: any = null;
|
||||
let decisionCenterDeleteResult: any = null;
|
||||
let decisionCenterMetrics: any = { pageVisible: false, tableVisible: false, rawButtonCount: 0, rawJsonBlocks: 0, chatInputCount: 0, bodyContainsRecord: false };
|
||||
let codeQueueText = "";
|
||||
let codeQueueOutputText = "";
|
||||
let codeQueueTaskCount = 0;
|
||||
@@ -1717,9 +1756,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
}
|
||||
}
|
||||
|
||||
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
|
||||
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needDecisionCenter || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
|
||||
await page.getByRole("button", { name: /用户服务/ }).click();
|
||||
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
|
||||
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needDecisionCenter || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
|
||||
await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 });
|
||||
}
|
||||
if (needMicroserviceCatalog) {
|
||||
@@ -1730,6 +1769,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-oa-event-flow"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-code-queue"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="microservice-row-decision-center"]', { timeout: 10000 });
|
||||
microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 });
|
||||
}
|
||||
if (needTodoNote) {
|
||||
@@ -1806,6 +1846,65 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
};
|
||||
});
|
||||
}
|
||||
if (needDecisionCenter) {
|
||||
const decisionCenterE2eTitle = `E2E Decision Center ${Date.now()}`;
|
||||
decisionCenterE2eRecord = await page.evaluate(async (title) => {
|
||||
const response = await fetch("/api/microservices/decision-center/proxy/api/records", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
type: "meeting",
|
||||
level: "G1",
|
||||
status: "active",
|
||||
title,
|
||||
body: "E2E seeded meeting record for Decision Center frontend validation.",
|
||||
tags: ["e2e", "decision-center"],
|
||||
evidenceLinks: ["https://example.com/unidesk/decision-center-e2e"],
|
||||
}),
|
||||
});
|
||||
return { ok: response.ok, status: response.status, body: await response.json().catch(() => null) };
|
||||
}, decisionCenterE2eTitle);
|
||||
await page.getByRole("button", { name: /Decision Center/ }).click();
|
||||
await page.waitForSelector('[data-testid="decision-center-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="decision-center-filters"]', { timeout: 30000 });
|
||||
await page.waitForSelector('[data-testid="decision-center-record-table"]', { timeout: 30000 });
|
||||
await page.waitForFunction((title) => {
|
||||
const text = document.body.innerText;
|
||||
return text.includes("Decision Center")
|
||||
&& text.includes("G0/G1 目标")
|
||||
&& text.includes("P0/P1 Blocker")
|
||||
&& text.includes("停放事项")
|
||||
&& text.includes("最近会议/决议")
|
||||
&& text.includes("查看原始JSON")
|
||||
&& text.includes(String(title));
|
||||
}, decisionCenterE2eTitle, { timeout: 30000 });
|
||||
decisionCenterText = await page.locator('[data-testid="decision-center-page"]').innerText({ timeout: 5000 });
|
||||
decisionCenterMetrics = await page.evaluate(() => {
|
||||
const root = document.querySelector('[data-testid="decision-center-page"]') as HTMLElement | null;
|
||||
const table = document.querySelector('[data-testid="decision-center-record-table"]') as HTMLElement | null;
|
||||
return {
|
||||
pageVisible: Boolean(root),
|
||||
tableVisible: Boolean(table),
|
||||
rawButtonCount: root?.querySelectorAll('[data-testid^="raw-decision-center"], .ghost-btn').length ?? 0,
|
||||
rawJsonBlocks: root?.querySelectorAll("pre.raw-json, [data-testid='raw-json']").length ?? 0,
|
||||
chatInputCount: root?.querySelectorAll("textarea, [contenteditable='true']").length ?? 0,
|
||||
recordCardCount: root?.querySelectorAll('[data-testid^="decision-record-"]').length ?? 0,
|
||||
tableRows: table?.querySelectorAll("tbody tr").length ?? 0,
|
||||
textPreview: root?.innerText.slice(0, 1000) || "",
|
||||
};
|
||||
});
|
||||
const decisionCenterRecordId = String(decisionCenterE2eRecord?.body?.record?.id || "");
|
||||
if (decisionCenterRecordId) {
|
||||
decisionCenterDeleteResult = await page.evaluate(async (id) => {
|
||||
const response = await fetch(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(String(id))}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
return { ok: response.ok, status: response.status, body: await response.json().catch(() => null) };
|
||||
}, decisionCenterRecordId);
|
||||
}
|
||||
}
|
||||
if (needCodeQueue) {
|
||||
await page.getByLabel("用户服务 子功能").getByRole("button", { name: "Code Queue" }).click();
|
||||
await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 10000 });
|
||||
@@ -2787,6 +2886,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const microserviceCatalogTextLower = microserviceCatalogText.toLowerCase();
|
||||
const todoNoteTextLower = todoNoteText.toLowerCase();
|
||||
const findjobTextLower = findjobText.toLowerCase();
|
||||
const decisionCenterTextLower = decisionCenterText.toLowerCase();
|
||||
const codeQueueTextLower = codeQueueText.toLowerCase();
|
||||
const claudeqqTextLower = claudeqqText.toLowerCase();
|
||||
const pipelineTextLower = pipelineText.toLowerCase();
|
||||
@@ -2821,7 +2921,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addSelectedCheck(checks, options, "frontend:gateway-duration-subsecond-visible", gatewayHasSubsecondDuration && !gatewayHasRoundedZeroDuration, { gatewayHasSubsecondDuration, gatewayHasRoundedZeroDuration, gatewayTextPreview: gatewayText.slice(0, 900) });
|
||||
addSelectedCheck(checks, options, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts });
|
||||
addSelectedCheck(checks, options, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) });
|
||||
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("oa event flow") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) });
|
||||
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("oa event flow") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogTextLower.includes("decision center") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) });
|
||||
addSelectedCheck(checks, options, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) });
|
||||
addSelectedCheck(checks, options, "frontend:oa-event-flow-visible",
|
||||
@@ -2837,6 +2937,24 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& Number(oaEventFlowMetrics.rawButtonCount || 0) >= 2
|
||||
&& !oaEventFlowText.includes("{\n"),
|
||||
{ oaEventFlowMetrics, oaEventFlowTextPreview: oaEventFlowText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:decision-center-visible",
|
||||
decisionCenterTextLower.includes("decision center")
|
||||
&& decisionCenterText.includes("G0/G1 目标")
|
||||
&& decisionCenterText.includes("P0/P1 Blocker")
|
||||
&& decisionCenterText.includes("停放事项")
|
||||
&& decisionCenterText.includes("最近会议/决议")
|
||||
&& decisionCenterText.includes("全部记录")
|
||||
&& decisionCenterText.includes("PostgreSQL")
|
||||
&& decisionCenterText.includes("查看原始JSON")
|
||||
&& decisionCenterMetrics.pageVisible === true
|
||||
&& decisionCenterMetrics.tableVisible === true
|
||||
&& decisionCenterE2eRecord?.ok === true
|
||||
&& decisionCenterDeleteResult?.ok === true
|
||||
&& Number(decisionCenterMetrics.rawButtonCount || 0) >= 1
|
||||
&& Number(decisionCenterMetrics.rawJsonBlocks || 0) === 0
|
||||
&& Number(decisionCenterMetrics.chatInputCount || 0) === 0
|
||||
&& !decisionCenterText.includes("{\n"),
|
||||
{ decisionCenterMetrics, decisionCenterE2eRecord, decisionCenterDeleteResult, decisionCenterTextPreview: decisionCenterText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-integrated-visible", codeQueueTextLower.includes("code queue") && codeQueueText.includes("gpt-5.4-mini") && codeQueueText.includes("gpt-5.4") && codeQueueText.includes("gpt-5.5") && codeQueueText.includes("提交任务") && codeQueueText.includes("执行 Provider") && codeQueueText.includes("入队份数") && codeQueueText.includes("追加 prompt") && codeQueueText.includes("打断") && codeQueueTextLower.includes("查看 queue") && codeQueueText.includes("创建 queue") && codeQueueText.includes("合并 queue") && codeQueueOptions.some((text) => text.includes("All queues")) && codeQueueTracePlacement.firstChildIsTrace === true && codeQueueTracePlacement.noPageTopStatus === true && codeQueueTracePlacement.filterInsideTracePanel === true && codeQueueTracePlacement.taskSearchVisible === true && codeQueueTracePlacement.traceStatusVisible === true && codeQueueTracePlacement.markAllReadVisible === true && codeQueueGlobalStatus.activeMicroserviceVisible === true && codeQueueSidebarUpdateMetrics.hasRecentUpdateLabel === true && codeQueueHtmlGuard.rootAttrMissing === true && codeQueueHtmlGuard.sourceAttrMissing === true && codeQueueHtmlGuard.sourceNoBasePrompt === true && codeQueueSubmitQueueControl.tagName === "select" && codeQueueSubmitQueueControl.createButtonVisible === true && codeQueueSubmitQueueControl.mergeButtonVisible === true && codeQueueSubmitQueueControl.mergeSourceInlineMissing === true && codeQueueSubmitQueueControl.mergeDialogMissingBeforeClick === true && (codeQueueSubmitQueueControl.mergeButtonDisabled === true || (codeQueueSubmitQueueControl.mergeDialogVisible === true && codeQueueSubmitQueueControl.mergeDialogSelectVisible === true && Number(codeQueueSubmitQueueControl.mergeDialogSourceOptionCount || 0) > 1 && codeQueueSubmitQueueControl.mergeDialogSelectInsideSubmitForm !== true && codeQueueSubmitQueueControl.mergeDialogUsesCommonComponent === true && codeQueueSubmitQueueControl.mergeDialogDeleteNoteVisible === true)) && codeQueueSubmitQueueControl.oldInputMissing === true && codeQueueSubmitQueueControl.providerValue === "D601" && codeQueueSubmitQueueControl.cwdValue === "/workspace" && Array.isArray(codeQueueSubmitQueueControl.providerOptions) && codeQueueSubmitQueueControl.providerOptions.some((item: any) => item.value === "D601" && String(item.text || "").includes("/workspace")) && codeQueueSubmitQueueControl.maxAttemptsMax === "99" && codeQueueSubmitQueueControl.maxAttemptsValue === "99" && codeQueueSubmitQueueControl.moveQueueVisible === true && codeQueuePromptDefaultEmpty === true && codeQueueSubmitGuard.batchRowVisible === true && codeQueueSubmitGuard.checkboxVisible === true && codeQueueSubmitGuard.disabledBeforeConfirm === true && codeQueueSubmitGuard.enabledAfterConfirm === true && codeQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codeQueueScrollbarMetrics.transcriptThin === true && codeQueueScrollbarMetrics.toolHorizontalHidden === true && (codeQueueSwitchMetrics.optionCount <= 1 || codeQueueSwitchMetrics.switched === true) && codeQueueTextLower.includes("attempts") && codeQueueText.includes("仅 UniDesk frontend 代理访问") && (codeQueueTaskCount === 0 || codeQueueOutputText.includes("Submitted prompt")), { codeQueueTaskCount, codeQueueOptions, codeQueueSwitchMetrics, codeQueueSubmitQueueControl, codeQueueSubmitGuard, codeQueueScrollbarMetrics, codeQueuePromptDefaultEmpty, codeQueueTracePlacement, codeQueueGlobalStatus, codeQueueSidebarUpdateMetrics, codeQueueHtmlGuard, codeQueueOutputPreview: codeQueueOutputText.slice(0, 900), codeQueueTextPreview: codeQueueText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-enqueue-await-smoke",
|
||||
codeQueueEnqueueAwaitSmoke.checked === true
|
||||
|
||||
+15
-1
@@ -5,6 +5,7 @@ import { summarizeMicroserviceProxyResponse } from "./microservices";
|
||||
import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf";
|
||||
import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh";
|
||||
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexTaskQueryAsync } from "./code-queue";
|
||||
import { runDecisionCenterCommandAsync } from "./decision-center";
|
||||
|
||||
export interface RemoteCliOptions {
|
||||
host: string | null;
|
||||
@@ -558,7 +559,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
|
||||
emitRemoteJson(name, {
|
||||
transport: "frontend",
|
||||
baseUrl: session.baseUrl,
|
||||
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice proxy <id> <path>", "codex task <taskId>", "codex judge <taskId> --attempt N", "network perf"],
|
||||
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex judge <taskId> --attempt N", "network perf"],
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
@@ -578,6 +579,19 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
|
||||
emitRemoteJson(name, await remoteMicroservice(session, args));
|
||||
return 0;
|
||||
}
|
||||
if (top === "decision" || top === "decision-center") {
|
||||
const fetcher = (path: string, init?: { method?: string; body?: unknown }): Promise<FetchJsonResult> => {
|
||||
const requestInit = init === undefined
|
||||
? undefined
|
||||
: {
|
||||
method: init.method,
|
||||
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
||||
};
|
||||
return frontendJson(session, path, requestInit, 30_000);
|
||||
};
|
||||
emitRemoteJson(name, await runDecisionCenterCommandAsync(config, args.slice(1), fetcher));
|
||||
return 0;
|
||||
}
|
||||
if (top === "codex") {
|
||||
emitRemoteJson(name, await remoteCodeQueue(session, args));
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user