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,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
})();
|