从命令行到图形界面:用Qt给Thorlabs光功率计做个简易上位机(PM100x/PM160/PM200通用)
2026/6/2 10:27:16
在移动互联网时代,小程序因其轻量、便捷、无需安装的特性,已成为连接用户与服务的重要桥梁。对于需要集成人工智能能力(如自然语言处理、知识问答、代码生成等)的小程序应用而言,直接在前端调用第三方 API(如 DeepSeek API)存在诸多挑战:
因此,引入一个中间层(BFF - Backend For Frontend)对 DeepSeek API 进行封装,再由移动端(小程序)间接调用这个封装层,成为解决上述问题的理想方案。
本方案的核心思想是构建一个后端服务作为代理层。这个服务将承担以下职责:
小程序只需要调用这个封装层提供的、符合小程序规范的 API 接口即可。
wx.requestAPI。mkdir deepseek-api-wrapper cd deepseek-api-wrapper npm init -ynpm install express axios dotenv # 基础依赖 npm install redis # 如果需要Redis缓存/限流 npm install morgan winston # 日志 npm install express-rate-limit # 简单限流中间件 npm install --save-dev @types/express @types/node typescript ts-node nodemon # TypeScript 支持tsconfig.json):{ "compilerOptions": { "target": "ES2020", "module": "CommonJS", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }.env):PORT=3000 DEEPSEEK_API_BASE_URL=https://api.deepseek.com/v1 DEEPSEEK_API_KEY=your_deepseek_api_key_here # 务必保密! # Redis 配置 (可选) REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= (如果有) # 应用密钥 (用于小程序端认证) APP_SECRET=your_app_secret_heretimestamp(精确到秒)。timestamp和一个双方约定的nonce(随机字符串) 拼接成一个字符串。APP_SECRET对这个字符串计算 HMAC-SHA256 签名sign。X-App-Id(可以是小程序 AppID)、X-Timestamp、X-Nonce、X-Sign。X-App-Id,X-Timestamp,X-Nonce,X-Sign。X-App-Id查找对应的APP_SECRET(可能存储在环境变量或数据库)。timestamp和nonce)构造字符串。APP_SECRET计算 HMAC-SHA256 签名localSign。localSign和请求头中的X-Sign是否一致。timestamp是否在合理的时间窗口内(例如 ±5 分钟),防止重放攻击。401 Unauthorized。/deepseek/chat)。axios库构造对 DeepSeek API 的请求:const response = await axios.post( `${process.env.DEEPSEEK_API_BASE_URL}/chat/completions`, // DeepSeek 的实际接口路径 { model: req.body.model || 'deepseek-chat', // 默认模型 messages: req.body.messages, // 对话历史 max_tokens: req.body.max_tokens || 2048, temperature: req.body.temperature || 0.7, // ... 其他 DeepSeek 支持的参数 }, { headers: { 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, 'Content-Type': 'application/json' } } );{ "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "model": "deepseek-chat", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I assist you today?" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21 } }const deepseekResponse = response.data; const simplifiedResponse = { success: true, data: { reply: deepseekResponse.choices[0].message.content, usage: deepseekResponse.usage, // ... 其他小程序关心的字段 } }; res.json(simplifiedResponse);if (response.status !== 200) { res.status(response.status).json({ success: false, error: { code: response.data.error?.code || 'DEEPSEEK_API_ERROR', message: response.data.error?.message || 'DeepSeek API request failed' } }); return; }md5(JSON.stringify(req.body)))作为 Redis 键。const redisClient = ...; // 初始化 Redis 客户端 const cacheKey = `deepseek:cache:${md5(JSON.stringify(req.body))}`; // 检查缓存 const cachedReply = await redisClient.get(cacheKey); if (cachedReply) { return res.json({ success: true, data: { reply: cachedReply, cached: true // 标记来自缓存 } }); } // 无缓存,调用 DeepSeek API // ... (调用代码) // 将结果存入缓存 (根据业务逻辑决定是否缓存) if (shouldCache(req.body)) { await redisClient.setEx(cacheKey, CACHE_TTL, deepseekResponse.choices[0].message.content); }X-App-Id或用户登录后的唯一标识 (如user_id)。使用 Redis 计数器(INCR)配合过期时间实现滑动窗口限流。express-rate-limit。const userRateLimitKey = `deepseek:ratelimit:${req.appId}:${req.userId}`; // 假设从认证中获取 appId 和 userId const currentCount = await redisClient.incr(userRateLimitKey); if (currentCount === 1) { // 第一次计数,设置过期时间 (例如 60秒窗口) await redisClient.expire(userRateLimitKey, 60); } if (currentCount > MAX_REQUESTS_PER_MINUTE) { res.status(429).json({ success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests. Please try again later.' } }); return; }morgan记录访问日志。winston记录应用日志(INFO, ERROR 级别)。记录请求参数、响应状态、DeepSeek API 响应、错误详情、用户标识、时间戳等。app.use((err, req, res, next) => { console.error(err.stack); winston.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`); res.status(500).json({ success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: 'Something went wrong on our end.' } }); });src/index.ts)import express, { Request, Response, NextFunction } from 'express'; import axios from 'axios'; import dotenv from 'dotenv'; import crypto from 'crypto'; import morgan from 'morgan'; import winston from 'winston'; import rateLimit from 'express-rate-limit'; import { createClient } from 'redis'; // 如果使用Redis dotenv.config(); const app = express(); const PORT = process.env.PORT || 3000; // 中间件 app.use(express.json()); app.use(morgan('combined')); // 访问日志 // 日志配置 const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); // IP 基础限流 (可选) const ipLimiter = rateLimit({ windowMs: 60 * 1000, // 1分钟 max: 100, // 每分钟100次 message: 'Too many requests from this IP.', }); app.use(ipLimiter); // 初始化 Redis 客户端 (可选) // const redisClient = createClient({ url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}` }); // redisClient.connect().catch(console.error); // 自定义认证中间件 function authenticate(req: Request, res: Response, next: NextFunction) { const appId = req.headers['x-app-id'] as string; const timestamp = req.headers['x-timestamp'] as string; const nonce = req.headers['x-nonce'] as string; const signature = req.headers['x-sign'] as string; if (!appId || !timestamp || !nonce || !signature) { return res.status(401).json({ error: 'Missing authentication headers' }); } // 1. 根据 appId 查找对应的 APP_SECRET (这里简化,从环境变量取) const appSecret = process.env.APP_SECRET; // 实际项目中可能需要根据 appId 查数据库 if (!appSecret) { return res.status(401).json({ error: 'Invalid application' }); } // 2. 验证时间戳 (防止重放攻击) const now = Math.floor(Date.now() / 1000); const requestTime = parseInt(timestamp, 10); if (Math.abs(now - requestTime) > 300) { // 5分钟窗口 return res.status(401).json({ error: 'Timestamp expired' }); } // 3. 构造签名字符串 (按参数名排序后拼接) const params = { ...req.body, timestamp, nonce }; const sortedKeys = Object.keys(params).sort(); const signString = sortedKeys.map(key => `${key}=${params[key]}`).join('&'); // 4. 计算本地签名 const hmac = crypto.createHmac('sha256', appSecret); hmac.update(signString); const localSignature = hmac.digest('hex'); // 5. 比较签名 if (localSignature !== signature) { logger.warn(`Authentication failed for appId: ${appId}. Local: ${localSignature}, Received: ${signature}`); return res.status(401).json({ error: 'Invalid signature' }); } // 认证通过,将 appId 等信息挂载到 req 上供后续使用 req.appId = appId; next(); } // 用户级限流中间件 (Redis实现示例) async function userRateLimit(req: Request, res: Response, next: NextFunction) { // 假设我们通过其他方式获取了 userId (例如从token解析) const userId = 'demo_user'; // 简化示例 const rateLimitKey = `ratelimit:${req.appId}:${userId}`; try { // const currentCount = await redisClient.incr(rateLimitKey); // if (currentCount === 1) { // await redisClient.expire(rateLimitKey, 60); // } // if (currentCount > 10) { // 每分钟10次 // return res.status(429).json({ error: 'User rate limit exceeded' }); // } next(); // 暂时跳过,实际启用需去掉注释 } catch (err) { logger.error('Rate limit error:', err); next(); // 限流失败时放行,避免影响服务可用性 } } // DeepSeek 聊天接口路由 app.post('/deepseek/chat', authenticate, userRateLimit, async (req: Request, res: Response) => { try { logger.info(`Received chat request from app: ${req.appId}`, { body: req.body }); // TODO: 这里可以加入缓存检查逻辑 (使用Redis) // 构造并转发请求到 DeepSeek API const deepseekResponse = await axios.post( `${process.env.DEEPSEEK_API_BASE_URL}/chat/completions`, { model: req.body.model || 'deepseek-chat', messages: req.body.messages || [], max_tokens: req.body.max_tokens || 2048, temperature: req.body.temperature || 0.7, // ... 其他参数 }, { headers: { 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, 'Content-Type': 'application/json', }, } ); // 处理 DeepSeek 响应 const replyContent = deepseekResponse.data.choices[0].message.content; // TODO: 这里可以加入缓存存储逻辑 (使用Redis) // 返回简化格式给小程序 res.json({ success: true, data: { reply: replyContent, usage: deepseekResponse.data.usage, }, }); } catch (error) { logger.error('Error calling DeepSeek API:', error); let status = 500; let errorMessage = 'Internal Server Error'; if (axios.isAxiosError(error)) { status = error.response?.status || 500; errorMessage = error.response?.data?.error?.message || error.message; } res.status(status).json({ success: false, error: { code: 'API_CALL_FAILED', message: errorMessage, }, }); } }); // 全局错误处理 app.use((err: Error, req: Request, res: Response, next: NextFunction) => { logger.error('Unhandled error:', err); res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred.', }, }); }); // 启动服务 app.listen(PORT, () => { logger.info(`DeepSeek API Wrapper service listening on port ${PORT}`); });.env文件中的敏感信息(DEEPSEEK_API_KEY,APP_SECRET)配置到生产环境的环境变量或配置管理服务中,切勿提交到代码仓库。pm2或systemd管理 Node.js 进程,保证其崩溃后自动重启。npm install pm2 -g pm2 start dist/index.js --name deepseek-wrapper设计一个聊天界面,包含:
scroll-view):显示用户和助手的对话历史。input或textarea):用户输入问题。封装一个通用的请求函数,处理签名、头信息添加、错误处理等:
// utils/request.js const APP_ID = 'your_miniprogram_appid'; // 小程序自身的AppID const APP_SECRET = 'your_app_secret'; // 与后端封装层约定的密钥 (注意:这个在小程序端也不安全!) const API_BASE_URL = 'https://your-wrapper-domain.com'; // 封装层部署的域名 function generateSignature(params, secret) { // 1. 参数排序 const sortedKeys = Object.keys(params).sort(); const signString = sortedKeys.map(key => `${key}=${params[key]}`).join('&'); // 2. HMAC-SHA256 // 注意:小程序环境需使用其提供的加密库,或使用兼容的第三方库 // 此处为伪代码 const signature = crypto.createHmac('sha256', secret).update(signString).digest('hex'); return signature; } export function requestWrapper(path, method, data = {}) { return new Promise((resolve, reject) => { // 生成签名所需参数 const timestamp = Math.floor(Date.now() / 1000).toString(); const nonce = Math.random().toString(36).substring(2, 15); // 随机字符串 const signParams = { ...data, timestamp, nonce }; const signature = generateSignature(signParams, APP_SECRET); wx.request({ url: `${API_BASE_URL}${path}`, method: method, data: data, header: { 'Content-Type': 'application/json', 'X-App-Id': APP_ID, 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Sign': signature, }, success(res) { if (res.statusCode === 200) { resolve(res.data); // 假设后端返回 { success, data, error } } else { reject(new Error(`API error: ${res.statusCode}`)); } }, fail(err) { reject(err); }, }); }); }重要安全提示:在小程序端存储APP_SECRET仍然存在风险(反编译)。更安全的做法是:
access_token或session_key。小程序端使用这个token进行认证,而不是APP_SECRET。封装层验证token的有效性。APP_SECRET的签名生成逻辑放在云函数中,小程序端只调用云函数。这在一定程度上隔离了密钥。const crypto = require('crypto'); exports.main = async (event) => { const { data, timestamp, nonce } = event; const signParams = { ...data, timestamp, nonce }; // ... 排序和签名计算 (使用云函数环境变量中的 APP_SECRET) return { signature }; }在聊天页面的发送按钮事件处理函数中:
// pages/chat/chat.js import { requestWrapper } from '../../utils/request'; Page({ data: { messages: [], // 消息列表 { role: 'user'/'assistant', content: '...' } inputValue: '', // 输入框内容 isLoading: false, }, handleInput(e) { this.setData({ inputValue: e.detail.value }); }, async sendMessage() { const content = this.data.inputValue.trim(); if (!content) return; this.setData({ isLoading: true, inputValue: '', }); // 将用户输入加入消息列表 const userMessage = { role: 'user', content }; this.setData({ messages: [...this.data.messages, userMessage], }); try { // 构造请求体 (包含历史上下文) const requestData = { messages: [...this.data.messages, userMessage], // 将新消息也加入上下文 // ... 其他可选参数如 model, max_tokens, temperature }; // 调用封装层 API const response = await requestWrapper('/deepseek/chat', 'POST', requestData); if (response.success) { const assistantMessage = { role: 'assistant', content: response.data.reply }; this.setData({ messages: [...this.data.messages, assistantMessage], }); } else { wx.showToast({ title: response.error.message || '请求失败', icon: 'none', }); } } catch (error) { console.error('Send message error:', error); wx.showToast({ title: '网络或服务异常', icon: 'none', }); } finally { this.setData({ isLoading: false }); } }, });如果 DeepSeek API 支持流式传输(Streaming)以提升响应速度(逐字返回),封装层和小程序端需要做更多工作:
axios或其他支持流的库请求 DeepSeek API,设置stream: true。wx.connectSocket(WebSocket) 或监听wx.onSocketMessage。EventSource)。https://your-wrapper-domain.com)添加到request 合法域名列表中。wx.login+code换取openid/session_key)。openid或unionid传递给封装层,作为用户标识。axios的 HTTP 连接池,复用连接减少握手开销。Cache-Control头部,并前置 CDN。但需注意动态性。X-Request-ID),便于在日志中串联所有相关操作。session_id),避免小程序每次发送整个历史。通过本文的实战教程,我们详细阐述了在移动端(特别是小程序)环境中,如何通过构建一个后端封装层来安全、高效、灵活地集成 DeepSeek API。这种间接调用的架构模式有效解决了密钥安全、协议转换、性能优化、错误处理、限流等关键问题,为小程序提供了稳定可靠的 AI 能力支撑。
核心要点回顾:
随着人工智能技术的不断发展和小程序生态的持续繁荣,这种集成模式将变得越来越重要。未来可以考虑的方向包括:
本教程能为您在移动端集成 DeepSeek API 或其他 AI 服务提供清晰的路径和实用的解决方案。