AgentCPM研报助手JavaScript SDK开发教程:实现浏览器端快速调用
你是不是也遇到过这样的场景?公司内部有个很棒的AI研报生成服务(AgentCPM),但每次调用都得写一堆重复的HTTP请求代码,处理各种错误和参数,麻烦得很。尤其是在前端项目里,每次想用这个服务,都得把fetch或者axios那一套再写一遍,不仅代码冗余,维护起来也头疼。
今天,咱们就来解决这个问题。我会手把手带你封装一个专属于AgentCPM研报助手的JavaScript SDK。有了它,以后在前端页面或者Node.js脚本里,只需要一两行代码,就能轻松调用研报生成服务,就像调用一个本地函数一样简单。我们会从最基础的封装开始,一步步加上错误重试、请求拦截、TypeScript类型支持这些实用功能,最终打造一个既健壮又好用的工具库。
1. 项目初始化与核心设计
在动手写代码之前,我们先花几分钟把思路理清楚。一个好的SDK,核心目标是让使用者几乎感觉不到它的存在——调用简单,行为可靠。
1.1 我们要做一个什么样的SDK?
想象一下,作为使用者,你希望怎么调用这个服务?我猜大概是这样的:
// 理想中的调用方式,简单直观 const report = await agentCPM.generate({ topic: "新能源汽车电池技术发展趋势", format: "ppt" }); console.log(report.content);而不是这样:
// 繁琐的原生HTTP调用 const response = await fetch('https://api.example.com/v1/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your_token_here' }, body: JSON.stringify({ topic: "新能源汽车电池技术发展趋势", format: "ppt" }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // 还要处理各种可能的业务错误码...我们的SDK就是要消灭下面这种繁琐的代码,提供上面那种优雅的体验。具体来说,它需要做到:
- 开箱即用:引入后简单配置就能调用。
- Promise化:完全基于Promise,支持
async/await,符合现代JavaScript开发习惯。 - 错误处理友好:网络错误、服务端错误、业务逻辑错误,都能以统一的方式捕获和处理。
- 可扩展:预留钩子,方便未来增加日志、监控、缓存等功能。
- 类型安全(如果使用TypeScript):有完整的类型定义,编码时有提示,减少低级错误。
1.2 初始化你的开发环境
首先,我们创建一个新的项目目录。这里我们选择用npm来管理,当然你用yarn或pnpm也完全没问题。
打开你的终端,执行以下命令:
# 创建一个新目录并进入 mkdir agentcpm-js-sdk cd agentcpm-js-sdk # 初始化npm项目,一路回车用默认值就行 npm init -y初始化完成后,你会得到一个package.json文件。我们先稍微修改一下它,给它起个名字,比如@your-scope/agentcpm-sdk(记得把your-scope换成你自己的范围名,或者直接用agentcpm-sdk),并添加一些基本信息。
// package.json { "name": "agentcpm-sdk", "version": "1.0.0", "description": "A lightweight JavaScript SDK for AgentCPM research report generation service.", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "scripts": { "build": "rollup -c", "dev": "rollup -c -w" }, "keywords": ["agentcpm", "sdk", "javascript", "report"], "author": "Your Name", "license": "MIT" }注意上面main、module和types字段,它们分别指向CommonJS版本、ES Module版本和类型定义文件的输出路径。我们计划用Rollup来打包,生成多种格式的产物以兼容不同环境。
接下来,安装我们初期需要的开发依赖。主要是打包工具、TypeScript编译器以及代码格式化和检查工具。
npm install -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript typescript tslib npm install -D prettier然后,在项目根目录创建一个简单的tsconfig.json文件来配置TypeScript。即使你暂时不用TypeScript写源码,先配置好也对生成类型定义文件有帮助。
// tsconfig.json { "compilerOptions": { "target": "es2015", "module": "esnext", "lib": ["dom", "esnext"], "declaration": true, "outDir": "./dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }最后,创建我们的源码目录和入口文件。
mkdir src touch src/index.ts好了,环境准备就绪。接下来,我们就要开始编写SDK最核心的部分了。
2. 实现基础请求客户端
万丈高楼平地起,我们先来实现最核心、最基础的HTTP请求功能。这一节的目标是创建一个AgentCPMClient类,它能处理认证、发送请求和解析响应。
2.1 创建Client类与构造函数
在src目录下,我们创建一个client.ts文件。
// src/client.ts // 先定义一些我们SDK会用到的类型,放在这里方便管理 export interface AgentCPMConfig { /** 服务的基础地址,例如:https://api.agentcpm.com */ baseURL: string; /** API访问令牌 */ apiKey: string; /** 请求超时时间(毫秒),默认10秒 */ timeout?: number; /** 自定义的请求头 */ headers?: Record<string, string>; } export interface GenerateReportParams { /** 研报主题 */ topic: string; /** 期望的输出格式,如 'pdf', 'ppt', 'markdown' */ format: string; /** 其他可选的参数,比如语言、长度等 */ [key: string]: any; } export interface ApiResponse<T = any> { /** 业务状态码,0表示成功 */ code: number; /** 状态信息 */ message: string; /** 实际的数据负载 */ data: T; } // 自定义错误类,用于抛出SDK相关的错误 export class AgentCPMError extends Error { constructor( public code: number, message: string, public originalError?: any ) { super(message); this.name = 'AgentCPMError'; } } // 核心的客户端类 export class AgentCPMClient { private config: AgentCPMConfig; constructor(config: AgentCPMConfig) { // 提供默认值,让配置更简单 this.config = { timeout: 10000, // 默认10秒超时 headers: { 'Content-Type': 'application/json', }, ...config, // 用户传入的配置覆盖默认值 }; // 简单的参数校验 if (!this.config.baseURL) { throw new Error('baseURL is required'); } if (!this.config.apiKey) { throw new Error('apiKey is required'); } } }这个构造函数做了几件小事:合并默认配置和用户配置,确保必要的参数存在。这样,用户初始化时最少只需要提供baseURL和apiKey。
2.2 封装通用的请求方法
接下来,我们在AgentCPMClient类内部添加一个私有的_request方法。这个方法将统一处理所有HTTP请求的细节,比如添加认证头、处理超时、解析JSON响应等。
// 在 AgentCPMClient 类中添加 private async _request<T>( endpoint: string, options: RequestInit = {} ): Promise<T> { const { baseURL, apiKey, timeout, headers: defaultHeaders } = this.config; // 1. 准备请求URL和参数 const url = `${baseURL.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); // 2. 合并请求头:默认头 + 认证头 + 用户自定义头(options中的优先级最高) const headers = new Headers({ ...defaultHeaders, 'Authorization': `Bearer ${apiKey}`, }); // 如果options里也传了headers,就合并进来 if (options.headers) { const optionHeaders = new Headers(options.headers); optionHeaders.forEach((value, key) => headers.set(key, value)); } // 3. 发起fetch请求 try { const response = await fetch(url, { ...options, headers, signal: controller.signal, }); clearTimeout(timeoutId); // 请求完成,清除超时定时器 // 4. 处理HTTP层面的错误(如404, 500等) if (!response.ok) { let errorMsg = `HTTP Error: ${response.status} ${response.statusText}`; try { // 尝试读取服务端返回的错误信息 const errorBody = await response.text(); if (errorBody) errorMsg += ` - ${errorBody}`; } catch {} throw new Error(errorMsg); } // 5. 解析JSON响应 const result: ApiResponse<T> = await response.json(); // 6. 处理业务层面的错误(如参数错误、额度不足等,假设code非0为错误) if (result.code !== 0) { throw new AgentCPMError(result.code, result.message || 'Unknown business error'); } // 7. 返回真正的数据 return result.data; } catch (error) { clearTimeout(timeoutId); // 对错误进行包装再抛出,方便上层统一处理 if (error instanceof AgentCPMError) { throw error; // 业务错误直接抛 } else if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms`); } else { // 其他未知错误,比如网络错误 throw new Error(`Request failed: ${error.message}`); } } }这个方法看起来有点长,但逻辑是清晰的流水线:准备请求 → 发送请求 → 处理HTTP错误 → 解析JSON → 处理业务错误 → 返回数据。任何一个环节出错,都会抛出携带明确信息的错误。
2.3 暴露具体的业务方法
有了通用的_request方法,现在添加具体的API方法就非常简单了。我们以生成研报的接口为例。
// 在 AgentCPMClient 类中添加 /** * 生成一份研报 * @param params 生成参数 * @returns 生成的研报内容等信息 */ async generateReport(params: GenerateReportParams): Promise<any> { return this._request<any>('v1/report/generate', { method: 'POST', body: JSON.stringify(params), }); } // 你可以继续添加其他API方法,比如获取任务状态、列出历史报告等 // async getReportStatus(taskId: string): Promise<any> { ... } // async listReports(options?: any): Promise<any[]> { ... }看,现在一个完整的、具备基础能力的客户端就完成了。它隐藏了所有HTTP和认证的细节,对外只暴露干净的异步方法。
3. 增强SDK的健壮性
基础功能跑通了,但一个用于生产环境的SDK还需要更可靠。这一节,我们给它加上错误自动重试和请求拦截器这两个非常实用的功能。
3.1 实现错误重试机制
网络请求总有可能因为短暂的波动而失败。对于5xx服务器错误或者网络超时,自动重试几次能显著提高成功率。我们来修改_request方法,加入重试逻辑。
首先,在配置接口里增加重试相关的参数。
// 更新 src/client.ts 中的 AgentCPMConfig 接口 export interface AgentCPMConfig { baseURL: string; apiKey: string; timeout?: number; headers?: Record<string, string>; /** 重试次数,默认3次 */ retryCount?: number; /** 重试延迟的基础时间(毫秒),默认500ms,会配合退避算法 */ retryDelay?: number; /** 哪些HTTP状态码需要重试,默认500, 502, 503, 504 */ retryStatusCodes?: number[]; }然后,我们实现一个带重试的请求核心方法。这里我们采用一个“指数退避”策略,每次重试的等待时间逐渐增加,避免对服务端造成压力。
// 在 AgentCPMClient 类中替换或修改 _request 方法 private async _requestWithRetry<T>( endpoint: string, options: RequestInit = {}, retryAttempt = 0 ): Promise<T> { const { retryCount = 3, retryDelay = 500, retryStatusCodes = [500, 502, 503, 504], } = this.config; try { // 调用我们之前写的不带重试的基础请求方法(需要稍作拆分,这里假设我们有一个私有方法叫 _makeSingleRequest) return await this._makeSingleRequest<T>(endpoint, options); } catch (error) { // 判断是否应该重试 const shouldRetry = retryAttempt < retryCount && // 如果是网络超时或连接错误,重试 (error.message.includes('timeout') || error.message.includes('Failed to fetch')) || // 如果是可重试的HTTP状态码,重试 (error.message.includes('HTTP Error') && retryStatusCodes.some(code => error.message.includes(`HTTP Error: ${code}`))); if (!shouldRetry) { throw error; // 不满足重试条件,直接抛出错误 } // 计算下一次重试的等待时间(指数退避) const delay = retryDelay * Math.pow(2, retryAttempt); console.warn(`Request failed, retrying (${retryAttempt + 1}/${retryCount}) after ${delay}ms...`, error.message); // 等待一段时间 await new Promise(resolve => setTimeout(resolve, delay)); // 递归调用自身,进行下一次尝试 return this._requestWithRetry<T>(endpoint, options, retryAttempt + 1); } } // 原来的 _request 方法可以改名为 _makeSingleRequest,专注于单次请求 private async _makeSingleRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> { // 这里放入之前 _request 方法中 try 块之前和 catch 块之外的逻辑 // 即:准备URL、headers、发起fetch、处理HTTP错误、解析JSON、处理业务错误 // 注意:这里 catch 块只负责包装网络和超时错误,不再包含重试逻辑 // ... (代码与之前 _request 的 try 块基本一致,但错误直接抛出) }最后,记得把对外的generateReport等方法内部调用的方法从_request改为_requestWithRetry。这样,你的SDK就拥有了自动从短暂故障中恢复的能力。
3.2 添加请求与响应拦截器
拦截器是一个强大的模式,它允许你在请求发出前和收到响应后插入自定义逻辑,比如添加全局参数、统一打印日志、处理特定错误等。
我们设计一个简单的拦截器管理器。首先定义拦截器的类型。
// src/interceptor.ts export interface RequestInterceptor { (config: RequestConfig): RequestConfig | Promise<RequestConfig>; } export interface ResponseInterceptor<T = any> { (response: T): T | Promise<T>; } export interface RequestConfig { url: string; options: RequestInit; } export class InterceptorManager { private requestInterceptors: RequestInterceptor[] = []; private responseInterceptors: ResponseInterceptor[] = []; // 添加请求拦截器 useRequestInterceptor(interceptor: RequestInterceptor): void { this.requestInterceptors.push(interceptor); } // 添加响应拦截器 useResponseInterceptor<T>(interceptor: ResponseInterceptor<T>): void { this.responseInterceptors.push(interceptor); } // 执行所有请求拦截器 async runRequestInterceptors(config: RequestConfig): Promise<RequestConfig> { let currentConfig = config; for (const interceptor of this.requestInterceptors) { currentConfig = await interceptor(currentConfig); } return currentConfig; } // 执行所有响应拦截器 async runResponseInterceptors<T>(response: T): Promise<T> { let currentResponse = response; for (const interceptor of this.responseInterceptors) { currentResponse = await interceptor(currentResponse); } return currentResponse; } }然后,在AgentCPMClient中集成这个拦截器管理器。
// 在 src/client.ts 的 AgentCPMClient 类中 import { InterceptorManager, RequestConfig } from './interceptor'; export class AgentCPMClient { private config: AgentCPMConfig; public interceptors: { request: InterceptorManager; response: InterceptorManager; }; constructor(config: AgentCPMConfig) { this.config = { ...config }; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; // ... 其他初始化 } // 修改 _makeSingleRequest 方法,加入拦截器调用 private async _makeSingleRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> { const { baseURL, apiKey, timeout, headers: defaultHeaders } = this.config; const url = `${baseURL.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`; // 1. 构建初始请求配置 let requestConfig: RequestConfig = { url, options: { ...options, headers: { ...defaultHeaders, 'Authorization': `Bearer ${apiKey}`, }, }, }; // 2. 执行请求拦截器链 requestConfig = await this.interceptors.request.runRequestInterceptors(requestConfig); // 3. 使用被拦截器处理后的配置发起请求 (controller, timeout逻辑照旧) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(requestConfig.url, { ...requestConfig.options, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { /* ... 处理HTTP错误 ... */ } const result: ApiResponse<T> = await response.json(); if (result.code !== 0) { /* ... 处理业务错误 ... */ } // 4. 执行响应拦截器链 const finalData = await this.interceptors.response.runResponseInterceptors(result.data); return finalData; } catch (error) { clearTimeout(timeoutId); throw error; } } }现在,使用者就可以非常灵活地使用拦截器了:
const client = new AgentCPMClient({ baseURL: '...', apiKey: '...' }); // 添加一个请求拦截器,为所有请求添加一个跟踪ID client.interceptors.request.useRequestInterceptor((config) => { config.options.headers['X-Trace-Id'] = generateTraceId(); console.log(`Sending request to: ${config.url}`); return config; }); // 添加一个响应拦截器,统一处理某种数据结构 client.interceptors.response.useResponseInterceptor((data) => { if (data && data.formattedContent) { // 对返回的数据做一点后处理 return { ...data, formattedContent: data.formattedContent.replace(/\n/g, '<br/>') }; } return data; });4. 打包、发布与使用
SDK代码写好了,我们得把它打包成库,方便分发和使用。然后,我们看看在浏览器和Node.js里怎么用它。
4.1 配置Rollup进行打包
在项目根目录创建rollup.config.js文件。
// rollup.config.js import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import pkg from './package.json' assert { type: 'json' }; export default { input: 'src/index.ts', // 入口文件 output: [ { file: pkg.main, format: 'cjs', // CommonJS,用于Node.js sourcemap: true, }, { file: pkg.module, format: 'esm', // ES Module,用于现代打包工具和浏览器 sourcemap: true, }, ], plugins: [ resolve(), // 解析node_modules中的模块 commonjs(), // 将CommonJS模块转换为ES6 typescript({ tsconfig: './tsconfig.json' }), // 编译TypeScript ], external: [], // 如果有不希望打包进去的依赖(如axios),在这里声明 };然后,更新package.json中的scripts,并添加files字段来控制发布到npm的文件。
// package.json { "scripts": { "build": "rollup -c", "dev": "rollup -c -w", "prepublishOnly": "npm run build" // 发布前自动构建 }, "files": ["dist"], // 只发布dist目录 // ... 其他字段 }运行npm run build,你会在dist目录下看到生成好的index.js(CommonJS)和index.esm.js(ES Module)以及对应的类型定义文件index.d.ts。
4.2 创建简洁的入口文件
我们的src/index.ts应该导出所有用户需要的东西。
// src/index.ts export { AgentCPMClient } from './client'; export type { AgentCPMConfig, GenerateReportParams, ApiResponse, AgentCPMError, } from './client'; // 如果需要,也可以导出拦截器相关类型 // export { InterceptorManager } from './interceptor';4.3 在浏览器和Node.js中使用
在浏览器中使用(通过<script>标签):虽然我们主要打包成模块,但也可以通过额外配置生成一个UMD包供浏览器直接引用。这里我们先展示模块化引入。假设你使用Webpack、Vite等现代构建工具。
<!-- 在你的HTML中 --> <script type="module"> import { AgentCPMClient } from 'https://cdn.your-domain.com/agentcpm-sdk.esm.js'; const client = new AgentCPMClient({ baseURL: 'https://api.your-service.com', apiKey: 'your_actual_api_key_here', }); async function generateReport() { try { const report = await client.generateReport({ topic: '量子计算对加密技术的影响', format: 'markdown' }); document.getElementById('output').innerText = report.content; } catch (error) { console.error('生成失败:', error); } } </script>在Node.js中使用:这更简单,因为Node.js原生支持CommonJS。
// node-script.js const { AgentCPMClient } = require('agentcpm-sdk'); // 如果发布到npm // 或者如果本地开发:require('./dist/index.js') (async () => { const client = new AgentCPMClient({ baseURL: process.env.AGENTCPM_API_URL, apiKey: process.env.AGENTCPM_API_KEY, }); try { const weeklyReport = await client.generateReport({ topic: '本周金融市场回顾与展望', format: 'pdf', language: 'zh-CN' }); console.log('研报生成成功,任务ID:', weeklyReport.taskId); // 可以继续调用 client.getReportStatus(weeklyReport.taskId) 来轮询结果 } catch (err) { console.error('出错了:', err.message); } })();5. 总结
走完这一趟,我们从零开始构建了一个功能相对完整的JavaScript SDK。它不仅仅是一个简单的API包装器,而是包含了配置管理、错误处理、自动重试、拦截器扩展等生产级特性。
回顾一下关键点:设计以开发者体验为先,把复杂的HTTP细节隐藏起来;用Promise和async/await构建异步核心,让代码现代且易读;重视错误处理的健壮性,区分网络错误、HTTP错误和业务错误,并加入重试机制;通过拦截器提供强大的扩展点,满足日志、监控、参数处理等个性化需求;最后,用TypeScript和现代打包工具保障代码质量和分发便利性。
这个SDK现在可以直接用在你的项目里了。当然,它还有很多可以完善的地方,比如更完善的文档(JSDoc)、单元测试、对取消请求(AbortSignal)的更好支持、以及适配更多特定的AgentCPM API。你可以把它当作一个坚实的基础,根据实际需求继续添砖加瓦。希望这个教程能帮你省下大量重复造轮子的时间,让你更专注于业务逻辑本身。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。