/* METADATA
{
"name": "ai_chat",
"display_name": {
"zh": "AI 相互对话",
"en": "AI-to-AI Chat"
},
"description": {
"zh": "调用AI模型API实现AI之间智能对话互动。",
"en": "Call an AI model API to enable interactive conversations between AIs."
},
"env": ["AI_API_BASE_URL", "AI_API_KEY", "AI_MODEL_NAME"],
"category": "Chat",
"tools": [{
"name": "chat_completion",
"description": {
"zh": "发送消息给AI模型并获取回复。支持超时保护(默认30秒)。从根源上禁止分点列表输出。",
"en": "Send messages to an AI model and get responses. Includes timeout protection (default: 30s). Enforces non-bulleted output from the source."
},
"parameters": [
{"name": "messages", "description": {"zh": "消息数组或字符串", "en": "Message array or a string"}, "type": "any", "required": true},
{"name": "system_prompt", "description": {"zh": "系统提示词(会自动追加禁止分点指令)", "en": "System prompt (the system will automatically append non-bulleted instructions)"}, "type": "string", "required": false},
{"name": "temperature", "description": {"zh": "温度参数(0.0-2.0)", "en": "Temperature (0.0-2.0)"}, "type": "number", "required": false, "default": 0.7},
{"name": "max_tokens", "description": {"zh": "最大生成长度", "en": "Maximum generation length"}, "type": "number", "required": false},
{"name": "timeout", "description": {"zh": "超时时间(毫秒)", "en": "Timeout (milliseconds)"}, "type": "number", "required": false, "default": 30000}
]
}]
} */
///
const aiModelInteraction = (function () {
type ChatRole = 'system' | 'user' | 'assistant' | 'function';
type ChatMessage = {
role: ChatRole;
content: string;
name?: string;
function_call?: {
name: string;
arguments?: string;
};
};
type ChatCompletionToolParams = {
messages: ChatMessage[] | string;
message?: string;
system_prompt?: string;
temperature?: number;
max_tokens?: number;
timeout?: number;
functions?: any[];
};
type UniversalHttpResponse = {
status: number;
statusText: string;
headers?: Record;
body: any;
};
async function universalHttpRequest(
url: string,
method: string = 'POST',
headers: Record = {},
body: any = null,
responseType: 'json' | 'text' = 'json',
timeout: number = 30000
): Promise {
const timeoutPromise: Promise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`请求超时:${timeout}ms`)), timeout)
);
const requestPromise: Promise = (async () => {
if (typeof OkHttp === 'undefined') {
throw new Error('OkHttp 不可用');
}
const client = OkHttp.newBuilder()
.connectTimeout(timeout)
.readTimeout(timeout)
.writeTimeout(timeout)
.build();
const requestBuilder = client.newRequest()
.url(url)
.method(method.toUpperCase());
if (headers && Object.keys(headers).length > 0) {
requestBuilder.headers(headers);
}
const upperMethod = method.toUpperCase();
if (body !== null && body !== undefined && upperMethod !== 'GET' && upperMethod !== 'HEAD') {
if (typeof body === 'string') {
requestBuilder.body(body, 'text');
} else {
requestBuilder.body(body, 'json');
}
}
const response: OkHttpResponse = await requestBuilder.build().execute();
const resultBody = responseType === 'json' ? response.json() : response.content;
return {
status: response.statusCode,
statusText: response.statusMessage || '',
headers: response.headers,
body: resultBody,
};
})();
return Promise.race([requestPromise, timeoutPromise]);
}
function cleanText(text: unknown): string {
if (!text || typeof text !== 'string') return String(text ?? '');
let cleaned: string = text;
// 处理转义与实体
cleaned = cleaned.replace(/\|["\\]?n/g, '\n');
cleaned = cleaned.replace(/\\\\([\\nrt"'&])/g, (m, c) => ({ n: '\n', r: '\r', t: '\t', '"': '"', "'": "'", "&": "&" }[c] || m));
cleaned = cleaned.replace(/\\u([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
// 一次性替换HTML实体
const entities = { quot: '"', amp: '&', lt: '<', gt: '>', nbsp: ' ', '#39': "'", apos: "'" };
cleaned = cleaned.replace(/&(\w+|#\d+);/g, (m, e) => entities[e] || m);
// 清理代码块
cleaned = cleaned.replace(/```[\w-]*\s*\n([\s\S]*?)```/g, '$1').replace(/```/g, '').replace(/`([^`]+)`/g, '$1');
// 格式化空白与分点
cleaned = cleaned.replace(/[ \t]{2,}/g, ' ');
cleaned = cleaned.split('\n').map(l => l.replace(/^[\s\uFEFF\xA0\u3000\u200B-\u200D]+/g, '')).join('\n');
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
cleaned = cleaned.replace(/^\s*[-•●]\s+|^\s*\d+\.\s+|^\s*\d+\)\s+|^\s*\([a-zA-Z]\)\s+/gm, '');
return cleaned || text;
}
function getConfig(varName: string, defaultValue: string | null = null): string {
try {
const value = getEnv(varName);
if (!value || value === `YOUR_${varName}`) {
if (defaultValue !== null) return defaultValue;
throw new Error(`${varName} 未配置`);
}
return value.trim();
} catch (e) {
if (defaultValue !== null) return defaultValue;
throw new Error(`${varName} 未配置`);
}
}
function getFullConfig() {
try {
return {
apiBaseUrl: getConfig('AI_API_BASE_URL', ''),
apiKey: getConfig('AI_API_KEY', ''),
modelName: getConfig('AI_MODEL_NAME', ''),
timeout: 30000
};
} catch {
return { apiBaseUrl: '', apiKey: '', modelName: '', timeout: 30000 };
}
}
function joinUrl(baseUrl: string, path: string): string {
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
return `${normalizedBase}${normalizedPath}`;
}
async function tryEndpoints(baseUrl: string, payload: Record, config: { apiKey: string }, timeout: number): Promise {
if (baseUrl.includes('chat/completions')) {
return await universalHttpRequest(
baseUrl,
'POST',
{
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
payload,
'json',
timeout
);
}
const endpoints = ['v1/chat/completions', 'chat/completions'];
let lastError;
for (const endpoint of endpoints) {
try {
const url = joinUrl(baseUrl, endpoint);
const response = await universalHttpRequest(
url,
'POST',
{
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
payload,
'json',
timeout
);
if (response.status === 200) return response;
lastError = new Error(`端点 ${endpoint} 返回 ${response.status}`);
} catch (error) {
lastError = error;
}
}
throw lastError || new Error('所有端点尝试失败');
}
async function chat_completion_logic(rawInput: ChatCompletionToolParams | string, timeout: number) {
if (rawInput === null || rawInput === undefined) {
throw new Error('参数错误: 输入参数不能为空');
}
const internalParams: {
messages: ChatMessage[];
system_prompt?: string;
temperature: number;
max_tokens?: number;
functions?: any[];
} = {
messages: [],
system_prompt: undefined,
temperature: 0.7,
max_tokens: undefined,
functions: undefined,
};
// 处理输入参数
if (typeof rawInput === 'string') {
internalParams.messages = [{ role: 'user', content: rawInput }];
} else if (typeof rawInput === 'object') {
const hasMessageField = 'message' in rawInput;
const hasMessagesField = 'messages' in rawInput;
if (hasMessagesField) {
const messagesValue = (rawInput as ChatCompletionToolParams).messages;
if (Array.isArray(messagesValue)) {
internalParams.messages = messagesValue as ChatMessage[];
} else if (typeof messagesValue === 'string') {
internalParams.messages = [{ role: 'user', content: messagesValue }];
} else {
throw new Error(`'messages' 必须是数组或字符串`);
}
} else if (hasMessageField) {
const messageValue = (rawInput as ChatCompletionToolParams).message;
if (typeof messageValue !== 'string') {
throw new Error(`'message' 必须是字符串`);
}
internalParams.messages = [{ role: 'user', content: messageValue }];
} else {
throw new Error(`对象必须包含 'message' 或 'messages' 字段`);
}
internalParams.system_prompt = (rawInput as ChatCompletionToolParams).system_prompt ? String((rawInput as ChatCompletionToolParams).system_prompt) : undefined;
internalParams.temperature = (rawInput as ChatCompletionToolParams).temperature !== undefined ? Number((rawInput as ChatCompletionToolParams).temperature) : 0.7;
if (isNaN(internalParams.temperature)) internalParams.temperature = 0.7;
internalParams.max_tokens = (rawInput as ChatCompletionToolParams).max_tokens !== undefined ? Number((rawInput as ChatCompletionToolParams).max_tokens) : undefined;
if (internalParams.max_tokens !== undefined && isNaN(internalParams.max_tokens)) {
internalParams.max_tokens = undefined;
}
internalParams.functions = Array.isArray((rawInput as ChatCompletionToolParams).functions) ? (rawInput as ChatCompletionToolParams).functions : undefined;
} else {
throw new Error(`不支持的参数类型 '${typeof rawInput}'`);
}
// 验证messages
if (!Array.isArray(internalParams.messages) || internalParams.messages.length === 0) {
throw new Error('messages必须是有效数组且不为空');
}
internalParams.messages.forEach((msg, idx) => {
if (!msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string') {
throw new Error(`消息 #${idx} 格式无效`);
}
});
// 获取配置
const config = getFullConfig();
if (!config.apiBaseUrl || !config.apiKey) {
throw new Error('AI_API_BASE_URL 和 AI_API_KEY 必须配置');
}
// 构建消息数组
const antiListInstruction = "【重要指令】你必须以连续段落的方式回答,严禁使用任何分点、列表、编号或项目符号格式。";
const finalMessages = [
{
role: 'system',
content: internalParams.system_prompt
? internalParams.system_prompt + "\n\n" + antiListInstruction
: antiListInstruction
},
...internalParams.messages
];
// 构建请求
const payload: Record = {
model: String(config.modelName || 'gpt-3.5-turbo'),
messages: finalMessages,
temperature: Number(internalParams.temperature)
};
if (internalParams.max_tokens !== undefined && !isNaN(internalParams.max_tokens)) {
payload.max_tokens = Number(internalParams.max_tokens);
}
if (internalParams.functions) payload.functions = internalParams.functions;
Object.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]);
// 发送请求
let response;
try {
response = await tryEndpoints(config.apiBaseUrl, payload, config, timeout);
} catch {
response = await universalHttpRequest(
config.apiBaseUrl,
'POST',
{
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
payload,
'json',
timeout
);
}
if (response.status !== 200) {
let errorMsg = `API请求失败: ${response.status}`;
try {
const errorData = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
errorMsg += ` - ${errorData.error?.message || JSON.stringify(errorData)}`;
} catch {
errorMsg += ` - ${response.statusText}`;
}
throw new Error(errorMsg);
}
const result = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
if (!result.choices?.[0]) {
throw new Error('API返回格式异常');
}
const choice = result.choices[0];
const reply: any = {
message: choice.message,
finish_reason: choice.finish_reason,
usage: result.usage || null,
model: result.model || config.modelName,
id: result.id,
created: result.created
};
if (choice.message.function_call) {
reply.is_function_call = true;
reply.function_name = choice.message.function_call.name;
reply.function_arguments = JSON.parse(choice.message.function_call.arguments || '{}');
}
return reply;
}
async function chat_completion_impl(params: ChatCompletionToolParams | string) {
const normalizedParams: ChatCompletionToolParams = typeof params === 'object'
? (params as ChatCompletionToolParams)
: { messages: String(params) };
const timeout = normalizedParams.timeout || 30000;
const result = await Promise.race([
chat_completion_logic(normalizedParams, timeout),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`操作超时:${timeout}ms`)), timeout)
)
]);
const rawReply = result.message?.content;
const cleanedReply = cleanText(rawReply);
return {
success: true,
message: "AI回复获取成功!",
data: result,
reply: cleanedReply,
raw_reply: rawReply,
usage: result.usage,
anti_list_applied: true
};
}
async function wrapToolExecution(func: (params: any) => Promise, params?: any) {
try {
const result = await func(params || {});
complete(result);
} catch (error: any) {
console.error(`Tool ${func.name} failed unexpectedly`, error);
complete({
success: false,
message: `AI对话失败: ${String(error && error.message ? error.message : error)}`,
error_stack: error && error.stack
});
}
}
async function chat_completion(params?: ChatCompletionToolParams | string) {
return await wrapToolExecution(chat_completion_impl, params);
}
async function main(params?: { message?: string; temperature?: number; timeout?: number }) {
const config = getFullConfig();
if (!config.apiBaseUrl || !config.apiKey) {
return {
success: false,
message: 'AI_API_BASE_URL 和 AI_API_KEY 必须配置',
config
};
}
return await chat_completion_impl({
messages: params?.message || 'Hello! Please respond in a continuous paragraph without any list or bullet points.',
temperature: params?.temperature ?? 0.2,
timeout: params?.timeout ?? 15000,
});
}
return {
chat_completion,
single_message: chat_completion,
test_connection: async (params?: { timeout?: number }) => wrapToolExecution(
async () => await chat_completion_impl({
messages: "Hello! Please respond in a continuous paragraph without any list or bullet points.",
temperature: 0.1,
timeout: params?.timeout ?? 10000
}),
{}
),
main: (params?: { message?: string; temperature?: number; timeout?: number }) => wrapToolExecution(main, params),
getConfig: getFullConfig,
_makeHttpRequest: universalHttpRequest,
_cleanText: cleanText
};
})();
// 导出工具函数(CommonJS)
exports.chat_completion = aiModelInteraction.chat_completion;
exports.single_message = aiModelInteraction.single_message;
exports.test_connection = aiModelInteraction.test_connection;
exports.getConfig = aiModelInteraction.getConfig;
exports.main = aiModelInteraction.main;