230 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
#!/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,
}));
}
})();