230 lines
6.8 KiB
JavaScript
230 lines
6.8 KiB
JavaScript
#!/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,
|
||
}));
|
||
}
|
||
})();
|