From 9a8dedd0ab1b3af1265da5aa97f00f55d84cff99 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Tue, 3 Feb 2026 00:40:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20zod=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=EF=BC=8C=E9=87=8D=E6=9E=84=E5=B7=A5=E5=85=B7=E5=92=8C?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E7=A4=BA=E4=BE=8B=EF=BC=8C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- pnpm-lock.yaml | 13 ++++ src/common.ts | 51 +++++++++++++ src/test-bailian-debug.ts | 33 --------- src/test-cnb-debug.ts | 46 ------------ src/tools/a.ts | 60 +++++++++++++++ src/tools/approval-example.ts | 134 ++++++++++++++++++++++++++++++++++ 7 files changed, 260 insertions(+), 80 deletions(-) create mode 100644 src/common.ts delete mode 100644 src/test-bailian-debug.ts delete mode 100644 src/test-cnb-debug.ts create mode 100644 src/tools/a.ts create mode 100644 src/tools/approval-example.ts diff --git a/package.json b/package.json index 7a6dea3..a8c9493 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": true, "devDependencies": { "@types/bun": "latest", - "dotenv": "^17.2.3" + "dotenv": "^17.2.3", + "zod": "^4.3.6" }, "peerDependencies": { "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c20e092..4b4a89e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: ai: specifier: ^6.0.67 version: 6.0.67(zod@4.3.6) + typescript: + specifier: ^5 + version: 5.9.3 devDependencies: '@types/bun': specifier: latest @@ -30,6 +33,9 @@ importers: dotenv: specifier: ^17.2.3 version: 17.2.3 + zod: + specifier: ^4.3.6 + version: 4.3.6 packages: @@ -119,6 +125,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -211,6 +222,8 @@ snapshots: tslib@2.8.1: {} + typescript@5.9.3: {} + undici-types@7.16.0: {} zod@4.3.6: {} diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..5a6fc6f --- /dev/null +++ b/src/common.ts @@ -0,0 +1,51 @@ +import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; +import { createAnthropic } from '@ai-sdk/anthropic'; +import { generateText } from 'ai'; +import 'dotenv/config'; + +export function resolveEnvVars(value: string): string { + return value.replace(/{env:([^}]+)}/g, (_, varName) => { + const envValue = process.env[varName]; + if (!envValue) { + throw new Error(`Environment variable ${varName} is not set`); + } + return envValue; + }); +} + +export const models = { + 'doubao-ark-code-latest': 'doubao-ark-code-latest', + 'GLM-4.7': 'GLM-4.7', + 'MiniMax-M2.1': 'MiniMax-M2.1', + 'qwen3-coder-plus': 'qwen3-coder-plus', + 'hunyuan-a13b': 'hunyuan-a13b', +} +export const bailian = createOpenAICompatible({ + baseURL: 'https://coding.dashscope.aliyuncs.com/v1', + name: 'custom-bailian', + apiKey: process.env.BAILIAN_CODE_API_KEY!, +}); + +export const zhipu = createOpenAICompatible({ + baseURL: 'https://open.bigmodel.cn/api/coding/paas/v4', + name: 'custom-zhipu', + apiKey: process.env.ZHIPU_API_KEY!, +}); + +export const minimax = createAnthropic({ + baseURL: 'https://api.minimaxi.com/anthropic/v1', + name: 'custom-minimax', + apiKey: process.env.MINIMAX_API_KEY!, +}); + +export const doubao = createOpenAICompatible({ + baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3', + name: 'custom-doubao', + apiKey: process.env.DOUBAO_API_KEY!, +}); + +export const cnb = createOpenAICompatible({ + baseURL: resolveEnvVars('https://api.cnb.cool/{env:CNB_REPO_SLUG}/-/ai/'), + name: 'custom-cnb', + apiKey: process.env.CNB_API_KEY!, +}); diff --git a/src/test-bailian-debug.ts b/src/test-bailian-debug.ts deleted file mode 100644 index 02ae190..0000000 --- a/src/test-bailian-debug.ts +++ /dev/null @@ -1,33 +0,0 @@ -import 'dotenv/config'; - -const endpoint = 'https://coding.dashscope.aliyuncs.com/apps/anthropic/messages'; - -// 测试不同的认证方式 -const authMethods = [ - { name: 'x-api-key', headers: { 'x-api-key': process.env.BAILIAN_CODE_API_KEY! } }, - { name: 'Authorization Bearer', headers: { 'Authorization': `Bearer ${process.env.BAILIAN_CODE_API_KEY!}` } }, - { name: 'Authorization Bearer sk-', headers: { 'Authorization': `Bearer sk-${process.env.BAILIAN_CODE_API_KEY!}` } }, - { name: 'Authorization sk-', headers: { 'Authorization': `sk-${process.env.BAILIAN_CODE_API_KEY!}` } }, -]; - -for (const { name, headers } of authMethods) { - console.log(`\nTesting: ${name}`); - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: JSON.stringify({ - model: 'qwen3-coder-plus', - max_tokens: 100, - messages: [{ role: 'user', content: 'Hello' }], - }), - }); - - console.log(`Status: ${response.status}`); - const text = await response.text(); - if (text) { - console.log(`Response: ${text.substring(0, 300)}`); - } -} diff --git a/src/test-cnb-debug.ts b/src/test-cnb-debug.ts deleted file mode 100644 index b6b5550..0000000 --- a/src/test-cnb-debug.ts +++ /dev/null @@ -1,46 +0,0 @@ -import 'dotenv/config'; - -function resolveEnvVars(value: string): string { - return value.replace(/{env:([^}]+)}/g, (_, varName) => { - const envValue = process.env[varName]; - if (!envValue) { - throw new Error(`Environment variable ${varName} is not set`); - } - return envValue; - }); -} - -const endpoint = 'https://cnb.cool/kevisual/cnb/-/ai/chat/completions'; -const apiKey = process.env.CNB_API_KEY!; - -console.log('Endpoint:', endpoint); -console.log('API Key:', apiKey ? 'Set' : 'Not set'); -console.log('API Key length:', apiKey.length); -console.log('API Key prefix:', apiKey.substring(0, 10) + '...'); - -// 尝试不同的认证头 -const authHeaders = [ - { name: 'Authorization Bearer', headers: { 'Authorization': `Bearer ${apiKey}` } }, - { name: 'Authorization (no Bearer)', headers: { 'Authorization': apiKey } }, - { name: 'x-api-key', headers: { 'x-api-key': apiKey } }, - { name: 'api-key', headers: { 'api-key': apiKey } }, -]; - -for (const { name, headers } of authHeaders) { - console.log(`\n=== Testing: ${name} ===`); - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: JSON.stringify({ - model: 'hunyuan-a13b', - messages: [{ role: 'user', content: '你好' }], - }), - }); - - console.log(`Status: ${response.status}`); - const text = await response.text(); - console.log('Response:', text); -} diff --git a/src/tools/a.ts b/src/tools/a.ts new file mode 100644 index 0000000..cb84023 --- /dev/null +++ b/src/tools/a.ts @@ -0,0 +1,60 @@ +import { anthropic } from '@ai-sdk/anthropic'; +import { generateText, stepCountIs, tool, } from 'ai'; +import { zhipu, models, minimax } from '../common.ts'; +import { z } from 'zod' +const runCommand = async (command: string): Promise => { + // Implementation to execute the bash command and return the output + // return `Executed command: ${command}`; // Placeholder implementation + return new Promise((resolve, reject) => { + // const { exec } = require('child_process'); + // exec(command, (error: any, stdout: string, stderr: string) => { + // if (error) { + // reject(`Error: ${stderr}`); + // } else { + // resolve(stdout); + // } + // }); + resolve(`Executed command: ${command}`); // Placeholder implementation + }); +} +const result = await generateText({ + // model: minimax(models['MiniMax-M2.1']), + model: zhipu(models['GLM-4.7']), + tools: { + // bash: minimax.tools + time: tool({ + description: '获取当前系统时间。', + inputSchema: z.object({}), + execute: async (_args: {}) => { + console.log('获取当前系统时间'); + return new Date().toString(); + }, + }), + timeZone: tool({ + description: '将指定时间字符串转换为目标时区格式。必须先使用 time 工具获取时间字符串作为输入。', + inputSchema: z.object({ + zone: z.string().describe('IANA 时区名称,例如 "America/New_York"。'), + date: z.string().describe('从 time 工具获取的日期字符串。'), + }), + // needsApproval: true, + execute: async (args: { zone: string, date: string }) => { + const date = new Date(args.date); + console.log('格式化日期:', args.date, '到时区:', args.zone); + const options: Intl.DateTimeFormatOptions = { + timeZone: args.zone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }; + return new Intl.DateTimeFormat('zh-CN', options).format(date); + }, + }), + }, + stopWhen: stepCountIs(5), + prompt: '我的时区是 America/New_York,请显示我当前系统在该时区的时间。', +}); + +console.log('Response:', result.text); diff --git a/src/tools/approval-example.ts b/src/tools/approval-example.ts new file mode 100644 index 0000000..e95956a --- /dev/null +++ b/src/tools/approval-example.ts @@ -0,0 +1,134 @@ +import { generateText, tool, type ModelMessage, type ToolApprovalResponse } from 'ai'; +import { z } from 'zod'; +import { zhipu, models } from '../common.ts'; + +// 定义一个需要审批的工具 +const timeZone = tool({ + description: '将指定时间字符串转换为目标时区格式。必须先使用 time 工具获取时间字符串作为输入。', + inputSchema: z.object({ + zone: z.string().describe('IANA 时区名称,例如 "America/New_York"。'), + date: z.string().describe('从 time 工具获取的日期字符串。'), + }), + // needsApproval: false, // 设置需要审批 + execute: async ({ zone, date }) => { + const dateObj = new Date(date); + console.log('格式化日期:', date, '到时区:', zone); + const options: Intl.DateTimeFormatOptions = { + timeZone: zone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }; + return new Intl.DateTimeFormat('zh-CN', options).format(dateObj); + }, +}); + +const time = tool({ + description: '获取当前系统时间。', + inputSchema: z.object({}), + // needsApproval: true, + execute: async () => { + console.log('获取当前系统时间'); + return new Date().toString(); + }, +}); + +// 模拟审批函数 - 在实际应用中,这可能是用户通过 UI 确认 +function mockAskUserApproval(toolCall: { toolName: string; input: any }): boolean { + console.log('=== 待审批的工具调用 ==='); + console.log(`工具: ${toolCall.toolName}`); + console.log(`输入: ${JSON.stringify(toolCall.input)}`); + console.log('=========================='); + + // 在实际应用中,这里会等待用户输入 + // 这里模拟用户总是批准 + console.log('模拟用户审批: 批准\n'); + return true; +} + +// 模拟获取拒绝原因 +function mockAskDenialReason(): string { + // 在实际应用中,这里会等待用户输入拒绝原因 + return '用户取消了操作'; +} + +async function runApprovalExample() { + // 初始消息 + const messages: ModelMessage[] = [ + { role: 'user', content: '我的时区是 America/New_York,请显示我当前系统在该时区的时间。' }, + ]; + + // 第一步: 发起请求 + console.log('=== 第一步: 发起请求 ===\n'); + let result = await generateText({ + model: zhipu(models['GLM-4.7']), + tools: { time, timeZone }, + prompt: '我的时区是 America/New_York,请显示我当前系统在该时区的时间。', + }); + console.log('=== 第一步结果 ===\n', result.text, '\n'); + + // 添加响应消息到历史 + messages.push(...result.response.messages); + + // 检查是否有需要审批的工具调用 + let approvalRequests: Array<{ type: 'tool-approval-request', approvalId: string, toolCall: any }> = []; + + for (const part of result.content) { + if (part.type === 'tool-approval-request') { + approvalRequests.push(part); + } + } + + // 如果有审批请求,处理它们 + while (approvalRequests.length > 0) { + console.log('=== 处理审批请求 ===\n'); + + // 收集审批响应 + const approvalResponses: ToolApprovalResponse[] = []; + + for (const request of approvalRequests) { + const approved = mockAskUserApproval({ + toolName: request.toolCall.toolName, + input: request.toolCall.input, + }); + + approvalResponses.push({ + type: 'tool-approval-response', + approvalId: request.approvalId, + approved, + reason: approved ? '用户批准了操作' : mockAskDenialReason(), + }); + } + + // 将审批响应添加到消息 + messages.push({ role: 'tool', content: approvalResponses }); + + // 第二步: 使用审批响应继续执行 + console.log('=== 第二步: 使用审批响应继续 ===\n'); + result = await generateText({ + model: zhipu(models['GLM-4.7']), + tools: { time, timeZone }, + messages, + }); + + // 添加响应消息到历史 + messages.push(...result.response.messages); + + // 检查是否还有新的审批请求(可能有多轮审批) + approvalRequests = []; + for (const part of result.content) { + if (part.type === 'tool-approval-request') { + approvalRequests.push(part); + } + } + } + + console.log('=== 最终结果 ==='); + console.log('响应:', result.text); + console.log('步骤数:', result.steps.length); +} + +runApprovalExample().catch(console.error);