fix(cli): compact gh view and web-probe failures
This commit is contained in:
@@ -76,6 +76,7 @@ JS
|
||||
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内;能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内,并减少无界 selector/network 等待。确需等待更久时,改用 `web-probe run` 的异步 job/status 语义,或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath`、`reportSha256`、screenshots 或 DOM steps,先按远端报告判断脚本/页面实际状态;最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
|
||||
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence` 或 `summary.issueEvidence`;只有需要展开调查时才粘贴 `probe.script.result`、`probe.steps` 或完整 `reportPath`,避免 stdout、summary 和 report 多层重复同一证据。
|
||||
- stdin heredoc 与 `--script-file` 都按 ES module 加载,脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`;不要在模块顶层直接写 `return`。失败为 `Illegal return statement`、`does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时,先按 probe 脚本入口误用处理,不要归因成 Cloud Web 行为失败。
|
||||
- 自定义脚本需要主动失败时,优先返回 `{ ok: false, failedCondition: "..." }` 或抛出错误;兼容返回 `{ pass: false }`、`{ success: false }`,且 `{ ok: true, pass: false }` 仍按失败处理。`failedCondition`、`errorMessage`、`message`、`error`、`reason` 或 `summary` 会进入失败摘要;不要只把失败埋在普通业务字段里。
|
||||
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
|
||||
- 需要禁用 `EventSource`、mock `Date`/clock、注入 preload hook 或修改浏览器启动前全局对象时,`page.addInitScript()` 必须在目标页面整页加载前注册,并用 `page.goto(new URL(path, origin).toString(), { waitUntil: "domcontentloaded" })` 进入目标 deep link;不要依赖 `gotoStable()` 复用当前 SPA 页面后再期待 init script 生效。若只在已加载页面上 route/block 请求,既有 SSE 或 store 状态可能继续更新,不能作为“无后端刷新依赖”的验收。
|
||||
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`;不要拼出 `//v1/...`。
|
||||
|
||||
+43
-13
@@ -2452,9 +2452,10 @@ function issueWriteDisclosure(options: GitHubOptions, repo: string, issueNumber:
|
||||
};
|
||||
}
|
||||
|
||||
function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
|
||||
function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; includePreview?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
|
||||
const body = issue.body ?? "";
|
||||
const includeBody = options.includeBody ?? true;
|
||||
const includePreview = options.includePreview ?? true;
|
||||
const summary: Record<string, unknown> = {
|
||||
id: issue.id,
|
||||
number: issue.number,
|
||||
@@ -2475,8 +2476,10 @@ function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; prev
|
||||
} else {
|
||||
summary.bodyOmitted = true;
|
||||
summary.fullBodyIncluded = false;
|
||||
summary.bodyPreview = preview(body);
|
||||
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
|
||||
if (includePreview) {
|
||||
summary.bodyPreview = preview(body);
|
||||
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
@@ -4705,16 +4708,18 @@ function prStateDetail(pr: GitHubPullRequest): "open" | "closed" | "merged" {
|
||||
return pr.state === "closed" ? "closed" : "open";
|
||||
}
|
||||
|
||||
function prSummary(pr: GitHubPullRequest): Record<string, unknown> {
|
||||
function prSummary(pr: GitHubPullRequest, options: { includeBody?: boolean; includePreview?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
|
||||
const stateDetail = prStateDetail(pr);
|
||||
const closedAt = pr.closed_at ?? pr.merged_at ?? null;
|
||||
const mergedAt = pr.merged_at ?? null;
|
||||
const mergeCommitSha = pr.merge_commit_sha ?? null;
|
||||
return {
|
||||
const body = pr.body ?? "";
|
||||
const includeBody = options.includeBody ?? true;
|
||||
const includePreview = options.includePreview ?? true;
|
||||
const summary: Record<string, unknown> = {
|
||||
id: pr.id,
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
body: pr.body ?? "",
|
||||
state: pr.state,
|
||||
stateDetail,
|
||||
draft: pr.draft ?? false,
|
||||
@@ -4731,7 +4736,20 @@ function prSummary(pr: GitHubPullRequest): Record<string, unknown> {
|
||||
merged: stateDetail === "merged",
|
||||
mergedAt,
|
||||
mergeCommit: stateDetail === "merged" && mergeCommitSha !== null ? { oid: mergeCommitSha } : null,
|
||||
bodyChars: body.length,
|
||||
bodySha: bodySha(body),
|
||||
};
|
||||
if (includeBody) {
|
||||
summary.body = body;
|
||||
} else {
|
||||
summary.bodyOmitted = true;
|
||||
summary.fullBodyIncluded = false;
|
||||
if (includePreview) {
|
||||
summary.bodyPreview = preview(body);
|
||||
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
function numberOrNull(value: number | undefined): number | null {
|
||||
@@ -5694,21 +5712,24 @@ async function issueRead(repo: string, token: string, issueNumber: number, jsonF
|
||||
const issue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
|
||||
const needsComments = jsonFields === undefined || jsonFields.includes("comments");
|
||||
const includeBody = jsonFields === undefined || jsonFields.includes("body");
|
||||
const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null;
|
||||
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue) });
|
||||
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) });
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
repo,
|
||||
...(disclosure === null ? {} : { disclosure }),
|
||||
issue: issueSummary(issue),
|
||||
issue: issueSummary(issue, { includeBody, includePreview: false }),
|
||||
codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""),
|
||||
...(comments === null ? {} : { comments: comments.map(commentSummary) }),
|
||||
...(jsonFields === undefined ? {} : {
|
||||
jsonFields,
|
||||
json: selectedIssueJson(issue, comments, jsonFields),
|
||||
compatibility: {
|
||||
legacyJsonBodyPath: ".data.issue.body",
|
||||
legacyJsonBodyPath: includeBody ? ".data.issue.body" : null,
|
||||
bodyOmitted: !includeBody,
|
||||
readCommands: includeBody ? null : issueBodyReadCommands(repo, issueNumber),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -7626,10 +7647,12 @@ async function prRead(repo: string, token: string, number: number, jsonFields: P
|
||||
const { owner, name } = repoParts(repo);
|
||||
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
|
||||
if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number });
|
||||
const summary = prSummary(pr);
|
||||
const fullSummary = prSummary(pr);
|
||||
const includeBody = jsonFields === undefined || jsonFields.includes("body");
|
||||
const summary = includeBody ? fullSummary : prSummary(pr, { includeBody: false, includePreview: false });
|
||||
const metadata = needsPrGraphqlMetadata(jsonFields) ? await prGraphqlMetadata(repo, token, number) : null;
|
||||
if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary, requestedJsonFields: jsonFields ?? [], closeoutMetadata: prCloseoutMetadataError(metadata) });
|
||||
const selectionSummary = metadata === null ? summary : { ...summary, ...prMetadataSummary(metadata) };
|
||||
const selectionSummary = metadata === null ? fullSummary : { ...fullSummary, ...prMetadataSummary(metadata) };
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
@@ -7637,7 +7660,14 @@ async function prRead(repo: string, token: string, number: number, jsonFields: P
|
||||
...(disclosure === null ? {} : { disclosure }),
|
||||
pullRequest: summary,
|
||||
...(metadata === null ? {} : { closeoutMetadata: prCloseoutMetadata(metadata) }),
|
||||
...(jsonFields === undefined ? {} : { jsonFields, json: selectedPrJson(selectionSummary, jsonFields) }),
|
||||
...(jsonFields === undefined ? {} : {
|
||||
jsonFields,
|
||||
json: selectedPrJson(selectionSummary, jsonFields),
|
||||
compatibility: {
|
||||
legacyJsonBodyPath: includeBody ? ".data.pullRequest.body" : null,
|
||||
bodyOmitted: !includeBody,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7708,7 +7738,7 @@ export function ghHelp(): unknown {
|
||||
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
|
||||
"issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment delete treats --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment delete treats --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection; full body is included only when requested with --json body, --full, or --raw, and unsupported fields fail structurally.",
|
||||
"issue attachment list/download scan issue body and comments for GitHub user attachment URLs (`https://github.com/user-attachments/assets/...`). list is read-only and returns bounded attachment metadata. download writes the selected attachment to --output or /tmp/unidesk-gh-attachments, returns bytes/SHA-256/content-type/path, redacts redirected signed URL query parameters, and never prints binary bytes.",
|
||||
"--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/update/edit/patch and gh pr list/read/view. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.",
|
||||
"GitHub CLI output larger than 20 KiB is automatically written to /tmp/unidesk-cli-output/*.json; stdout stays bounded JSON with outputTruncated=true, the dump path, total bytes/lines, and head/tail previews.",
|
||||
|
||||
@@ -44,7 +44,7 @@ try {
|
||||
if (fn === null) throw new Error("custom script must export default, run, or probe function");
|
||||
const scriptResult = await fn(scriptHelpers());
|
||||
const safeResult = sanitize(scriptResult);
|
||||
const scriptOk = !(safeResult && typeof safeResult === "object" && safeResult.ok === false);
|
||||
const scriptOk = scriptResultOk(safeResult);
|
||||
const failure = scriptOk ? null : classifiedProbeError(assertionResultError(safeResult));
|
||||
const lastScreenshot = scriptOk ? null : await captureFailureScreenshot("failure.png");
|
||||
const lastUrl = currentPageUrl();
|
||||
@@ -1399,15 +1399,31 @@ function stableProbeError(code, message, readiness) {
|
||||
}
|
||||
|
||||
function assertionResultError(value) {
|
||||
const message = extractFailureMessage(value) || "script returned ok:false";
|
||||
const message = extractFailureMessage(value) || scriptFailureSignalMessage(value) || "script returned a failure result";
|
||||
const error = new Error(message);
|
||||
error.code = "assertion-failed";
|
||||
return error;
|
||||
}
|
||||
|
||||
function scriptResultOk(value) {
|
||||
if (!value || typeof value !== "object") return true;
|
||||
if (value.ok === false) return false;
|
||||
if (value.pass === false) return false;
|
||||
if (value.success === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function scriptFailureSignalMessage(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
if (value.pass === false) return "script returned pass:false";
|
||||
if (value.success === false) return "script returned success:false";
|
||||
if (value.ok === false) return "script returned ok:false";
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractFailureMessage(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
for (const key of ["errorMessage", "message", "error", "reason", "summary"]) {
|
||||
for (const key of ["failedCondition", "errorMessage", "message", "error", "reason", "summary"]) {
|
||||
const nested = value[key];
|
||||
if (typeof nested === "string" && nested.length > 0) return nested;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user