2026-05-20 21:39:12 +08:00

230 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const CALLBACK_PATH = '/agent-invoke-callback';
const LEGACY_CALLBACK_PATH = '/ai-audit-callback';
function emit(stage, message, status = 'running', extra = {}) {
process.stderr.write(JSON.stringify({
type: 'process_event',
stage,
message,
status,
timestamp: new Date().toISOString(),
...extra,
}) + '\n');
}
function readInput() {
const raw = String(process.argv[2] || process.env.SKILL_INPUT || process.env.AIFLOW_SKILL_INPUT || fs.readFileSync(0, 'utf8')).trim();
return raw ? JSON.parse(raw) : {};
}
function boolValue(value, fallback) {
if (value === undefined || value === null || value === '') return fallback;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
return ['1', 'true', 'yes', 'y', 'on'].includes(String(value).toLowerCase());
}
function normalizeStatus(status, fallback = 'failed') {
const value = String(status || '').trim().replace(/[-\s]/g, '_').toLowerCase();
const map = {
approved: 'approved',
approve: 'approved',
pass: 'approved',
passed: 'approved',
success: 'approved',
ok: 'approved',
rejected: 'rejected',
reject: 'rejected',
deny: 'rejected',
denied: 'rejected',
refuse: 'rejected',
refused: 'rejected',
manual_review: 'manual_review',
manualreview: 'manual_review',
manual: 'manual_review',
human_review: 'manual_review',
human: 'manual_review',
need_manual_review: 'manual_review',
failed: 'failed',
fail: 'failed',
failure: 'failed',
error: 'failed',
exception: 'failed',
timeout: 'failed',
};
return map[value] || fallback;
}
function firstNonEmpty(...values) {
for (const value of values) {
if (value !== undefined && value !== null && value !== '') return value;
}
return undefined;
}
function queryValueFromUrl(url, key) {
try {
return new URL(url).searchParams.get(key) || '';
} catch {
const match = String(url || '').match(new RegExp(`[?&]${key}=([^&#]+)`));
return match ? decodeURIComponent(match[1]) : '';
}
}
function normalizeCallbackUrl(callbackUrl, orderNo) {
if (!callbackUrl) throw new Error('callbackUrl is required');
let url;
try {
url = new URL(String(callbackUrl));
} catch {
throw new Error('callbackUrl must be an absolute URL');
}
if (url.pathname.includes(LEGACY_CALLBACK_PATH)) {
url.pathname = url.pathname.replace(LEGACY_CALLBACK_PATH, CALLBACK_PATH);
}
if (!url.pathname.includes(CALLBACK_PATH)) {
const normalizedPath = url.pathname.replace(/\/+$/, '');
url.pathname = `${normalizedPath}${CALLBACK_PATH}`;
}
if (orderNo && !url.searchParams.get('orderNo')) {
url.searchParams.set('orderNo', String(orderNo));
}
url.searchParams.delete('token');
return url.toString();
}
function buildBody(input) {
const auditResult = input.auditResult && typeof input.auditResult === 'object' ? input.auditResult : {};
const orderNo = firstNonEmpty(input.orderNo, auditResult.orderNo, auditResult.bizNo, auditResult.repairNum, queryValueFromUrl(input.callbackUrl, 'orderNo'));
if (!orderNo) throw new Error('orderNo is required');
const status = normalizeStatus(firstNonEmpty(
input.status,
input.auditStatus,
input.conclusion,
auditResult.status,
auditResult.auditStatus,
auditResult.conclusion,
));
const reason = firstNonEmpty(
input.reason,
input.errorMessage,
input.error,
auditResult.reason,
auditResult.message,
auditResult.errorMessage,
status === 'failed' ? 'AI审核失败未返回明确失败原因' : '',
);
const body = {
orderNo: String(orderNo),
status,
reason: String(reason || ''),
reportMarkdown: String(firstNonEmpty(input.reportMarkdown, input.report, auditResult.reportMarkdown, auditResult.report, '') || ''),
};
if (input.auditResult !== undefined) body.auditResult = input.auditResult;
if (input.extra && typeof input.extra === 'object' && !Array.isArray(input.extra)) Object.assign(body, input.extra);
body.orderNo = String(orderNo);
body.status = normalizeStatus(body.status);
body.reason = String(body.reason || (body.status === 'failed' ? 'AI审核失败未返回明确失败原因' : ''));
body.reportMarkdown = String(body.reportMarkdown || '');
return body;
}
async function postJson(url, bodyText, headers, timeoutMs, retries) {
let lastErr;
for (let attempt = 0; attempt <= retries; attempt += 1) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: bodyText,
signal: controller.signal,
});
const text = await response.text();
let parsed = text;
try {
parsed = text ? JSON.parse(text) : {};
} catch {
parsed = text;
}
if (!response.ok) {
const err = new Error(`HTTP ${response.status}: ${typeof parsed === 'string' ? parsed.slice(0, 300) : JSON.stringify(parsed).slice(0, 300)}`);
err.statusCode = response.status;
err.response = parsed;
throw err;
}
return { statusCode: response.status, response: parsed };
} catch (err) {
lastErr = err;
if (attempt < retries) await new Promise(resolve => setTimeout(resolve, 300 * (attempt + 1)));
} finally {
clearTimeout(timer);
}
}
throw lastErr;
}
async function run(input, env) {
const body = buildBody(input);
const callbackUrl = normalizeCallbackUrl(input.callbackUrl, body.orderNo);
const timeoutMs = Number(input.timeoutMs || env.TYCM_CALLBACK_TIMEOUT_MS || 15000);
const retries = Number(input.retries ?? env.TYCM_CALLBACK_RETRIES ?? 1);
const bodyText = JSON.stringify(body);
const headers = {
'Content-Type': 'application/json',
};
const request = {
url: callbackUrl,
authMode: 'none',
headers,
body,
};
if (boolValue(input.dryRun, false)) {
emit('dry-run', 'TYCM agent-invoke-callback dryRun完成', 'completed');
return { success: true, dryRun: true, request };
}
emit('request', 'POST回推TYCM审核结果到agent-invoke-callback');
const response = await postJson(callbackUrl, bodyText, headers, timeoutMs, retries);
emit('complete', 'TYCM agent-invoke-callback完成', 'completed', { statusCode: response.statusCode });
return {
success: true,
dryRun: false,
...response,
request,
};
}
(async () => {
try {
const input = readInput();
process.stdout.write(JSON.stringify(await run(input, process.env)));
} catch (err) {
process.stdout.write(JSON.stringify({
success: false,
error: err && err.message ? err.message : String(err),
statusCode: err && err.statusCode ? err.statusCode : undefined,
response: err && err.response ? err.response : undefined,
}));
}
})();