271 lines
7.6 KiB
JavaScript
271 lines
7.6 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
|
||
|
|
const ROUTES = {
|
||
|
|
2: {
|
||
|
|
brand: '长安',
|
||
|
|
subAgentKey: 'changan_new_car_audit',
|
||
|
|
description: '长安新车审核子Agent',
|
||
|
|
agentId: 6,
|
||
|
|
agentName: 'agent_c020c385',
|
||
|
|
},
|
||
|
|
11: {
|
||
|
|
brand: '赛力斯',
|
||
|
|
subAgentKey: 'seres_new_car_audit',
|
||
|
|
description: '赛力斯审核子Agent',
|
||
|
|
agentId: 9,
|
||
|
|
agentName: 'agent_6a442e52',
|
||
|
|
},
|
||
|
|
16: {
|
||
|
|
brand: '阿维塔',
|
||
|
|
subAgentKey: 'avatr_new_car_audit',
|
||
|
|
description: '阿维塔审核子Agent',
|
||
|
|
agentId: 10,
|
||
|
|
agentName: 'agent_7d380038',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
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 parseMessage(message) {
|
||
|
|
if (message === undefined || message === null || message === '') return {};
|
||
|
|
if (typeof message === 'string') {
|
||
|
|
try {
|
||
|
|
return JSON.parse(message);
|
||
|
|
} catch {
|
||
|
|
return { text: message };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof message === 'object' && !Array.isArray(message)) return message;
|
||
|
|
return { value: message };
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeOemId(value) {
|
||
|
|
if (value === undefined || value === null || value === '') return undefined;
|
||
|
|
const text = String(value).trim();
|
||
|
|
if (!/^\d+$/.test(text)) return undefined;
|
||
|
|
return Number(text);
|
||
|
|
}
|
||
|
|
|
||
|
|
function pickOemId(input, message) {
|
||
|
|
return normalizeOemId(input.oemId)
|
||
|
|
?? normalizeOemId(message.oemId)
|
||
|
|
?? normalizeOemId(message.data && message.data.oemId);
|
||
|
|
}
|
||
|
|
|
||
|
|
function firstNonEmpty(...values) {
|
||
|
|
for (const value of values) {
|
||
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
function pickAgentName(input, env, oemId) {
|
||
|
|
const key = String(oemId);
|
||
|
|
const route = ROUTES[oemId];
|
||
|
|
const agentNames = input.agentNames && typeof input.agentNames === 'object' ? input.agentNames : {};
|
||
|
|
return firstNonEmpty(
|
||
|
|
agentNames[key],
|
||
|
|
input[`agentName${key}`],
|
||
|
|
env[`OEM_AGENT_NAME_${key}`],
|
||
|
|
route && route.agentName,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildChatUrl(input, env) {
|
||
|
|
const url = firstNonEmpty(input.chatUrl, env.AIFLOW_NETACLAW_CHAT_URL);
|
||
|
|
if (!url) return '';
|
||
|
|
try {
|
||
|
|
return new URL(String(url)).toString();
|
||
|
|
} catch {
|
||
|
|
throw new Error('chatUrl must be an absolute URL');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildChatRequest(input, message, agentName) {
|
||
|
|
const forwardedMessage = { ...message };
|
||
|
|
if (boolValue(input.stripCallbackUrl, true)) {
|
||
|
|
delete forwardedMessage.callbackUrl;
|
||
|
|
if (forwardedMessage.data && typeof forwardedMessage.data === 'object' && !Array.isArray(forwardedMessage.data)) {
|
||
|
|
forwardedMessage.data = { ...forwardedMessage.data };
|
||
|
|
delete forwardedMessage.data.callbackUrl;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const requestBody = {
|
||
|
|
message: JSON.stringify(forwardedMessage),
|
||
|
|
agentName,
|
||
|
|
};
|
||
|
|
if (input.sessionId) requestBody.sessionId = String(input.sessionId);
|
||
|
|
if (input.userId) requestBody.userId = String(input.userId);
|
||
|
|
return requestBody;
|
||
|
|
}
|
||
|
|
|
||
|
|
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 message = parseMessage(input.message);
|
||
|
|
const oemId = pickOemId(input, message);
|
||
|
|
const route = ROUTES[oemId];
|
||
|
|
|
||
|
|
if (!route) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
routed: false,
|
||
|
|
invoked: false,
|
||
|
|
oemId: oemId ?? null,
|
||
|
|
error: `Unsupported oemId: ${oemId ?? 'missing'}. Supported oemId values are 2, 11, 16.`,
|
||
|
|
message,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = boolValue(input.invoke, true);
|
||
|
|
const subAgentName = String(pickAgentName(input, env, oemId) || '');
|
||
|
|
const base = {
|
||
|
|
success: true,
|
||
|
|
routed: true,
|
||
|
|
invoked: false,
|
||
|
|
oemId,
|
||
|
|
brand: route.brand,
|
||
|
|
subAgentKey: route.subAgentKey,
|
||
|
|
subAgentDescription: route.description,
|
||
|
|
subAgentId: route.agentId,
|
||
|
|
subAgentName,
|
||
|
|
reason: `oemId=${oemId} routed to ${route.description}`,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!invoke) {
|
||
|
|
emit('route', base.reason, 'completed', { oemId, brand: route.brand });
|
||
|
|
return base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!subAgentName) {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
success: false,
|
||
|
|
error: `Missing subAgent internal name for oemId=${oemId}. Provide agentNames["${oemId}"], agentName${oemId}, or OEM_AGENT_NAME_${oemId}.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const chatUrl = buildChatUrl(input, env);
|
||
|
|
if (!chatUrl) {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
success: false,
|
||
|
|
error: 'Missing chatUrl. Provide input.chatUrl or AIFLOW_NETACLAW_CHAT_URL.',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const requestBody = buildChatRequest(input, message, subAgentName);
|
||
|
|
const headers = {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
};
|
||
|
|
if (input.headers && typeof input.headers === 'object' && !Array.isArray(input.headers)) {
|
||
|
|
Object.assign(headers, input.headers);
|
||
|
|
}
|
||
|
|
|
||
|
|
const request = {
|
||
|
|
url: chatUrl,
|
||
|
|
method: 'POST',
|
||
|
|
headers,
|
||
|
|
body: requestBody,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (boolValue(input.dryRun, false)) {
|
||
|
|
emit('dry-run', `OEM路由dryRun完成: ${route.description}`, 'completed', { oemId, brand: route.brand });
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
invoked: true,
|
||
|
|
dryRun: true,
|
||
|
|
request,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const timeoutMs = Number(input.timeoutMs || env.OEM_ROUTER_TIMEOUT_MS || 300000);
|
||
|
|
const retries = Number(input.retries ?? env.OEM_ROUTER_RETRIES ?? 0);
|
||
|
|
emit('invoke', `调用${route.description}`, 'running', { oemId, brand: route.brand, subAgentName });
|
||
|
|
const response = await postJson(chatUrl, JSON.stringify(requestBody), headers, timeoutMs, retries);
|
||
|
|
emit('complete', `${route.description}响应完成`, 'completed', { statusCode: response.statusCode });
|
||
|
|
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
invoked: true,
|
||
|
|
dryRun: false,
|
||
|
|
statusCode: response.statusCode,
|
||
|
|
response: response.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,
|
||
|
|
routed: false,
|
||
|
|
invoked: false,
|
||
|
|
error: err && err.message ? err.message : String(err),
|
||
|
|
statusCode: err && err.statusCode ? err.statusCode : undefined,
|
||
|
|
response: err && err.response ? err.response : undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
})();
|