feat: 更新依赖项,重构 fetch 逻辑,添加工具路由和代理功能

This commit is contained in:
2026-03-11 00:34:37 +08:00
parent e4d89b9250
commit a06081dc19
9 changed files with 369 additions and 73 deletions

View File

@@ -2,6 +2,24 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { createAnthropic } from '@ai-sdk/anthropic';
import { generateText } from 'ai';
import 'dotenv/config';
import util from 'node:util';
// 保存原始 fetch 引用
const originalFetch = fetch;
export const defaultFetch = {
fetch: async (input: string | URL | Request, init?: BunFetchRequestInit) => {
console.log('请求 URL:', input);
console.log('请求选项:', init);
const response = await originalFetch(input, init); // 调用原始 fetch
console.log('响应状态:', response.status);
const responseBody = await response.clone().text(); // 使用 clone 避免消耗原始 response
console.log('响应体:', responseBody);
return response;
}
}
export function resolveEnvVars(value: string): string {
return value.replace(/{env:([^}]+)}/g, (_, varName) => {
@@ -19,6 +37,8 @@ export const models = {
'MiniMax-M2.1': 'MiniMax-M2.1',
'qwen3-coder-plus': 'qwen3-coder-plus',
'hunyuan-a13b': 'hunyuan-a13b',
'qwen-plus': 'qwen-plus',
'auto': 'AUTO_Models',
}
export const bailian = createOpenAICompatible({
baseURL: 'https://coding.dashscope.aliyuncs.com/v1',
@@ -36,6 +56,7 @@ export const minimax = createAnthropic({
baseURL: 'https://api.minimaxi.com/anthropic/v1',
name: 'custom-minimax',
apiKey: process.env.MINIMAX_API_KEY!,
fetch: defaultFetch.fetch as any
});
export const doubao = createOpenAICompatible({
@@ -44,8 +65,20 @@ export const doubao = createOpenAICompatible({
apiKey: process.env.DOUBAO_API_KEY!,
});
export const cnb = createOpenAICompatible({
baseURL: resolveEnvVars('https://api.cnb.cool/{env:CNB_REPO_SLUG}/-/ai/'),
baseURL: 'https://api.cnb.cool/kevisual/kevisual/-/ai/',
// baseURL: resolveEnvVars('https://api.cnb.cool/{env:CNB_REPO_SLUG}/-/ai/'),
name: 'custom-cnb',
apiKey: process.env.CNB_API_KEY!,
fetch: defaultFetch.fetch as any
});
export const proxyCnb = createOpenAICompatible({
baseURL: 'http://localhost:4005/api',
name: 'proxy-cnb',
apiKey: process.env.CNB_API_KEY!,
});
export const showMore = (obj: any) => {
return util.inspect(obj, { depth: null, colors: true });
}

83
src/routes/ai.ts Normal file
View File

@@ -0,0 +1,83 @@
import { generateText, tool, type ModelMessage, type ToolApprovalResponse } from 'ai';
import { z } from 'zod';
import { cnb, proxyCnb, models, showMore } from '../common.ts';
import { app } from './app.ts';
import type { App } from '@kevisual/router';
const createTool = async (app: App, message: { path: string, key: string }) => {
const route = app.findRoute({ path: message.path, key: message.key });
if (!route) {
console.error(`未找到路径 ${message.path} 和 key ${message.key} 的路由`);
return null;
}
const _tool = tool({
description: route.description || '无描述',
inputSchema: z.object({
...route.metadata?.args
}), // 这里可以根据实际需要定义输入参数的 schema
execute: async (args: any) => {
console.log(`执行工具 ${message.path} ${message.key},输入参数:`, args);
// 这里可以根据实际需要调用对应的路由处理函数
const res = await app.run({ path: message.path, key: message.key, payload: args });
// 假设路由处理函数返回一个字符串结果
console.log(`工具 ${message.path} ${message.key} 执行结果:`, res);
// return '任务列表:' + JSON.stringify(res);
return res;
}
});
return _tool;
}
const createTools = async (app: App) => {
const tools: Record<string, any> = {};
for (const route of app.routes) {
const id = route.id!;
const _tool = await createTool(app, { path: route.path!, key: route.key! });
if (_tool && id) {
tools[id] = _tool;
}
}
return tools;
}
const tools = await createTools(app);
let messages: ModelMessage[] = [
{
role: 'user',
content: '任务步骤:第一步获取今天的任务列表,第二步完成id为1的任务。'
}
]
let result = await generateText({
model: proxyCnb(models['auto']),
tools,
messages
// prompt: '今天的任务是什么? 如果有id为1的任务,请完成他。'
// prompt: '完成今天的任务1。'
});
console.log('生成结果:', result.text);
console.log('=== 生成内容详情 ===');
console.log(showMore(result));
messages.push(...result.response.messages);
const result2 = await generateText({
model: cnb(models['auto']),
tools,
messages
});
console.log('生成结果2:', result2.text);
console.log('=== 生成内容详情 ===');
console.log(showMore(result2));
messages.push(...result2.response.messages);
const result3 = await generateText({
model: cnb(models['auto']),
tools,
messages
});
console.log('生成结果3:', result3.text);
console.log('=== 生成内容详情 ===');
console.log(showMore(result3));

20
src/routes/ai2.ts Normal file
View File

@@ -0,0 +1,20 @@
import { app } from './app.ts';
import { cnb, proxyCnb, models, showMore } from '../common.ts';
import { runAgent } from './lib.ts';
const result = await runAgent({
app,
query:"WHERE path='task' AND key='today'",
languageModel: proxyCnb(models['auto']),
messages: [
{
role: 'user',
content: '任务步骤:第一步获取今天的任务列表,第二步完成id为1的任务。'
}
]
})
console.log('最终结果:', result.text);
console.log('=== 生成内容详情 ===');
console.log(showMore(result.steps));

67
src/routes/app.ts Normal file
View File

@@ -0,0 +1,67 @@
import { App } from '@kevisual/router'
import z from 'zod';
export const app = new App();
const tasks = [
{
id: 1,
title: 'Task 1',
description: 'This is the first task.',
completed: false,
},
{
id: 2,
title: 'Task 2',
description: 'This is the second task.',
completed: true,
},
]
app.route({
path: 'task',
key: 'today',
description: '获取今日待办任务列表',
}).define(async (ctx) => {
const list = tasks.map(task => ({
id: task.id,
title: task.title,
description: task.description,
completed: task.completed,
}))
ctx.body = { list }
}).addTo(app)
app.route({
path: 'task',
key: 'done',
description: '完成任务',
metadata: {
args: {
id: z.number().describe('任务ID'),
}
}
}).define(async (ctx) => {
const { id } = ctx.args as { id: number };
const task = tasks.find(t => t.id === id);
if (task) {
task.completed = true;
ctx.body = { message: `任务 ${id} 已完成` };
} else {
ctx.body = { message: `未找到任务 ${id}` };
}
}).addTo(app)
app.route({
path: 'task',
key: 'list',
description: '获取所有任务列表',
}).define(async (ctx) => {
const list = tasks.map(task => ({
id: task.id,
title: task.title,
description: task.description,
completed: task.completed,
}))
ctx.body = { list }
}).addTo(app)

66
src/routes/lib.ts Normal file
View File

@@ -0,0 +1,66 @@
import { App, type RouteInfo } from '@kevisual/router'
import { generateText, tool, type ModelMessage, type LanguageModel, type GenerateTextResult } from 'ai';
import z from 'zod';
import { filter } from '@kevisual/js-filter'
const createTool = async (app: App, message: { path: string, key: string }) => {
const route = app.findRoute({ path: message.path, key: message.key });
if (!route) {
console.error(`未找到路径 ${message.path} 和 key ${message.key} 的路由`);
return null;
}
const _tool = tool({
description: route.description || '无描述',
inputSchema: z.object({
...route.metadata?.args
}), // 这里可以根据实际需要定义输入参数的 schema
execute: async (args: any) => {
console.log(`执行工具 ${message.path} ${message.key},输入参数:`, args);
// 这里可以根据实际需要调用对应的路由处理函数
const res = await app.run({ path: message.path, key: message.key, payload: args });
// 假设路由处理函数返回一个字符串结果
console.log(`工具 ${message.path} ${message.key} 执行结果:`, res);
// return '任务列表:' + JSON.stringify(res);
return res;
}
});
return _tool;
}
const createTools = async (app: App) => {
const tools: Record<string, any> = {};
for (const route of app.routes) {
const id = route.id!;
const _tool = await createTool(app, { path: route.path!, key: route.key! });
if (_tool && id) {
tools[id] = _tool;
}
}
return tools;
}
type Route = Partial<RouteInfo>
export const reCallAgent = async (opts: { messages?: ModelMessage[], tools?: Record<string, any>, languageModel: LanguageModel }): Promise<GenerateTextResult<Record<string, any>, any>> => {
const { messages = [], tools = {}, languageModel } = opts;
const result = await generateText({
model: languageModel,
messages,
tools,
});
const step = result.steps[0]!;
if (step.finishReason === 'tool-calls') {
messages.push(...result.response.messages);
return reCallAgent({ messages, tools, languageModel });
}
return result
}
export const runAgent = async (opts: { app: App, messages?: ModelMessage[], routes?: Route[], query?: string, languageModel: LanguageModel }) => {
const { app, languageModel } = opts;
let messages = opts.messages || [];
let routes = app.routes;
if (opts.query) {
routes = filter(routes, opts.query);
};
const tools = await createTools(app);
return await reCallAgent({ messages, tools, languageModel });
}

View File

@@ -17,7 +17,7 @@ function resolveEnvVars(value: string): string {
const cnb = createOpenAICompatible({
baseURL: resolveEnvVars('{env:CNB_API_ENDPOINT}/{env:CNB_REPO_SLUG}/-/ai/'),
name: 'custom-cnb',
apiKey: process.env.CNB_API_KEY!,
// apiKey: process.env.CNB_API_KEY!,
});
const { text } = await generateText({

9
src/test-minimax.ts Normal file
View File

@@ -0,0 +1,9 @@
import { minimax } from "./common"
import { generateText } from 'ai';
const { text } = await generateText({
model: minimax('MiniMax-M2.1'),
prompt: 'What is an agent?',
});
console.log('Response:', text);