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