

























@@ -2,7 +2,9 @@ import { execFileSync } from "node:child_process";
22import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
33import { homedir } from "node:os";
44import { dirname, join } from "node:path";
5+import { pathToFileURL } from "node:url";
56import { isRecord } from "../src/utils.js";
7+import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
6879function writeStdoutLine(message = ""): void {
810process.stdout.write(`${message}\n`);
@@ -18,6 +20,7 @@ const GH_MAX_BUFFER = 50 * 1024 * 1024;
1820const PAGE_SIZE = 50;
1921const WORK_BATCH_SIZE = 500;
2022const STATE_VERSION = 1;
23+const DEFAULT_OPENAI_TIMEOUT_MS = 60_000;
2124const STATE_FILE_NAME = "issue-labeler-state.json";
2225const CONFIG_BASE_DIR = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
2326const STATE_FILE_PATH = join(CONFIG_BASE_DIR, "openclaw", STATE_FILE_NAME);
@@ -95,6 +98,13 @@ type ScriptOptions = {
9598model: string;
9699};
97100101+type ClassifyOptions = {
102+apiKey: string;
103+model: string;
104+fetchImpl?: typeof fetch;
105+timeoutMs?: number;
106+};
107+98108type OpenAIResponse = {
99109output_text?: string;
100110output?: OpenAIResponseOutput[];
@@ -235,6 +245,43 @@ function parseArgs(argv: string[]): ScriptOptions {
235245return { limit, dryRun, model };
236246}
237247248+function isMainModule() {
249+const entry = process.argv[1];
250+return entry ? import.meta.url === pathToFileURL(entry).href : false;
251+}
252+253+function resolveOpenAITimeoutMs(raw = process.env.OPENCLAW_LABEL_OPEN_ISSUES_OPENAI_TIMEOUT_MS) {
254+return parseStrictIntegerOption({
255+fallback: DEFAULT_OPENAI_TIMEOUT_MS,
256+label: "OPENCLAW_LABEL_OPEN_ISSUES_OPENAI_TIMEOUT_MS",
257+min: 1,
258+ raw,
259+});
260+}
261+262+async function withOpenAITimeout<T>(
263+label: string,
264+timeoutMs: number,
265+run: (signal: AbortSignal) => Promise<T>,
266+): Promise<T> {
267+const controller = new AbortController();
268+let timeout: ReturnType<typeof setTimeout> | undefined;
269+const timeoutPromise = new Promise<T>((_resolve, reject) => {
270+timeout = setTimeout(() => {
271+const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
272+reject(error);
273+controller.abort(error);
274+}, timeoutMs);
275+});
276+try {
277+return await Promise.race([run(controller.signal), timeoutPromise]);
278+} finally {
279+if (timeout) {
280+clearTimeout(timeout);
281+}
282+}
283+}
284+238285function logHeader(title: string) {
239286writeStdoutLine(`\n${title}`);
240287writeStdoutLine("=".repeat(title.length));
@@ -581,61 +628,70 @@ function normalizeClassification(raw: unknown, issueText: string): Classificatio
581628async function classifyItem(
582629item: LabelItem,
583630kind: "issue" | "pull request",
584-options: { apiKey: string; model: string },
631+options: ClassifyOptions,
585632): Promise<Classification> {
586633const itemText = buildItemPrompt(item, kind);
587-const response = await fetch("https://api.openai.com/v1/responses", {
588-method: "POST",
589-headers: {
590-Authorization: `Bearer ${options.apiKey}`,
591-"Content-Type": "application/json",
592-},
593-body: JSON.stringify({
594-model: options.model,
595-max_output_tokens: 200,
596-text: {
597-format: {
598-type: "json_schema",
599-name: "issue_classification",
600-schema: {
601-type: "object",
602-additionalProperties: false,
603-properties: {
604-category: { type: "string", enum: ["bug", "enhancement"] },
605-isSupport: { type: "boolean" },
606-isSkillOnly: { type: "boolean" },
634+const fetchImpl = options.fetchImpl ?? fetch;
635+const timeoutMs = options.timeoutMs ?? resolveOpenAITimeoutMs();
636+const payload = await withOpenAITimeout(
637+"OpenAI issue label classification request",
638+timeoutMs,
639+async (signal) => {
640+const response = await fetchImpl("https://api.openai.com/v1/responses", {
641+method: "POST",
642+headers: {
643+Authorization: `Bearer ${options.apiKey}`,
644+"Content-Type": "application/json",
645+},
646+body: JSON.stringify({
647+model: options.model,
648+max_output_tokens: 200,
649+text: {
650+format: {
651+type: "json_schema",
652+name: "issue_classification",
653+schema: {
654+type: "object",
655+additionalProperties: false,
656+properties: {
657+category: { type: "string", enum: ["bug", "enhancement"] },
658+isSupport: { type: "boolean" },
659+isSkillOnly: { type: "boolean" },
660+},
661+required: ["category", "isSupport", "isSkillOnly"],
662+},
607663},
608-required: ["category", "isSupport", "isSkillOnly"],
609664},
610-},
611-},
612-input: [
613-{
614-role: "system",
615-content:
616-"You classify GitHub issues and pull requests for OpenClaw. Respond with JSON only, no extra text.",
617-},
618-{
619-role: "user",
620-content: [
621-"Determine classification:\n",
622-"- category: 'bug' if the item reports incorrect behavior, errors, crashes, or regressions; otherwise 'enhancement'.\n",
623-"- isSupport: true if the item is primarily a support request or troubleshooting/how-to question, not a change request.\n",
624-"- isSkillOnly: true if the item solely requests or delivers adding/updating skills (no other feature/bug work).\n\n",
625-itemText,
626-"\n\nReturn JSON with keys: category, isSupport, isSkillOnly.",
627-].join(""),
628-},
629-],
630-}),
631-});
665+input: [
666+{
667+role: "system",
668+content:
669+"You classify GitHub issues and pull requests for OpenClaw. Respond with JSON only, no extra text.",
670+},
671+{
672+role: "user",
673+content: [
674+"Determine classification:\n",
675+"- category: 'bug' if the item reports incorrect behavior, errors, crashes, or regressions; otherwise 'enhancement'.\n",
676+"- isSupport: true if the item is primarily a support request or troubleshooting/how-to question, not a change request.\n",
677+"- isSkillOnly: true if the item solely requests or delivers adding/updating skills (no other feature/bug work).\n\n",
678+itemText,
679+"\n\nReturn JSON with keys: category, isSupport, isSkillOnly.",
680+].join(""),
681+},
682+],
683+}),
684+ signal,
685+});
632686633-if (!response.ok) {
634-const text = await response.text();
635-throw new Error(`OpenAI request failed (${response.status}): ${text}`);
636-}
687+ if (!response.ok) {
688+ const text = await response.text();
689+ throw new Error(`OpenAI request failed (${response.status}): ${text}`);
690+ }
637691638-const payload = (await response.json()) as OpenAIResponse;
692+return (await response.json()) as OpenAIResponse;
693+},
694+);
639695const rawText = extractResponseText(payload);
640696let parsed: unknown = undefined;
641697@@ -691,10 +747,12 @@ async function main() {
691747if (!apiKey) {
692748throw new Error("OPENAI_API_KEY is required to classify issues and pull requests.");
693749}
750+const openAITimeoutMs = resolveOpenAITimeoutMs();
694751695752logHeader("OpenClaw Issue Label Audit");
696753logStep(`Mode: ${dryRun ? "dry-run" : "apply labels"}`);
697754logStep(`Model: ${model}`);
755+logStep(`OpenAI timeout: ${openAITimeoutMs}ms`);
698756logStep(`Issue limit: ${Number.isFinite(limit) ? limit : "unlimited"}`);
699757logStep(`PR limit: ${Number.isFinite(limit) ? limit : "unlimited"}`);
700758logStep(`Batch size: ${WORK_BATCH_SIZE}`);
@@ -749,7 +807,11 @@ async function main() {
749807const labels = new Set(issue.labels.map((label) => label.name));
750808logInfo(`Existing labels: ${Array.from(labels).toSorted().join(", ") || "none"}`);
751809752-const classification = await classifyItem(issue, "issue", { apiKey, model });
810+const classification = await classifyItem(issue, "issue", {
811+ apiKey,
812+ model,
813+timeoutMs: openAITimeoutMs,
814+});
753815logInfo(
754816`Classification: category=${classification.category}, support=${classification.isSupport ? "yes" : "no"}, skill-only=${classification.isSkillOnly ? "yes" : "no"}.`,
755817);
@@ -831,7 +893,11 @@ async function main() {
831893continue;
832894}
833895834-const classification = await classifyItem(pullRequest, "pull request", { apiKey, model });
896+const classification = await classifyItem(pullRequest, "pull request", {
897+ apiKey,
898+ model,
899+timeoutMs: openAITimeoutMs,
900+});
835901logInfo(
836902`Classification: category=${classification.category}, support=${classification.isSupport ? "yes" : "no"}, skill-only=${classification.isSkillOnly ? "yes" : "no"}.`,
837903);
@@ -884,4 +950,12 @@ async function main() {
884950logInfo(`Added r: skill labels (PRs): ${prSkillCount}`);
885951}
886952887-await main();
953+export const testing = {
954+ classifyItem,
955+ normalizeClassification,
956+ resolveOpenAITimeoutMs,
957+};
958+959+if (isMainModule()) {
960+await main();
961+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。