commit 3633952c6bd70ebf877f25a48e6b994adbcebb2f Author: jackyu66git Date: Fri Jul 10 14:49:48 2026 +0800 feat: 大模型聊天工具 - 一步API接入,支持 Claude Fable 5 / Sonnet 4.6 / Opus 4.8 - Node.js 代理服务,API Key 通过 config.json 本地配置 - 流式响应 + Markdown 渲染 + 代码高亮 - 多对话管理,服务端持久化 + localStorage 双保险 - 深色/浅色主题,响应式布局 Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb47580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config.json +node_modules/ +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a441806 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# 一步AI 聊天助手 + +基于 [一步API](https://yibuapi.com) 的 AI 聊天工具,兼容 OpenAI 接口格式,支持多种主流大模型。 + +## 支持模型 + +| 模型 | 说明 | +|------|------| +| GPT-5.2 | OpenAI 最新旗舰模型 | +| GPT-4o / GPT-4o Mini | OpenAI 多模态模型 | +| Claude Sonnet 4.6 / Opus 4.8 / Haiku 4.5 | Anthropic Claude 系列 | +| DeepSeek V3 / R1 | DeepSeek 推理模型 | +| Gemini 2.5 Pro | Google 旗舰模型 | +| Qwen Max | 通义千问旗舰模型 | +| Grok 3 | xAI 模型 | + +## 快速开始 + +### 1. 配置 API Key + +编辑 `config.json`,填入你的一步API密钥: + +```json +{ + "api_key": "sk-你的真实API密钥", + "base_url": "https://api.yibuapi.com/v1" +} +``` + +### 2. 启动服务 + +```bash +node server.js +``` + +然后访问 `http://localhost:8080` + +## 架构说明 + +``` +浏览器 ──→ server.js (Node代理) ──→ 一步API + │ + config.json + (存储API Key) +``` + +- **config.json** — 本地保存 API Key,不暴露到前端 +- **server.js** — Node.js 代理服务器,读取配置并转发请求,支持 SSE 流式传输 +- **index.html** — 聊天界面,通过同一域名调用代理 API + +## 功能特性 + +- ✅ 流式响应,实时查看生成内容 +- ✅ API Key 本地配置文件存储,不暴露前端 +- ✅ Markdown 渲染 + 代码高亮 +- ✅ 多对话管理,自动保存 +- ✅ 深色/浅色主题切换 +- ✅ 可调节 Temperature 和 Max Tokens +- ✅ 一键复制消息 / 重新生成 +- ✅ 响应式设计,支持移动端 diff --git a/index.html b/index.html new file mode 100644 index 0000000..78fe035 --- /dev/null +++ b/index.html @@ -0,0 +1,1232 @@ + + + + + +大模型聊天工具 + + + + + + + + + + + + + + + + +
+
+
+ + 新对话 +
+
+
+ + +
+
+ + +
+ +
+
+ +
+
+
💬
+

大模型聊天工具

+

通过 API 代理接入多种主流大模型,让对话更智能。

+

API Key 已通过本地配置文件加载,开箱即用

+
+
+ +
+
+ + +
+
回答由 AI 生成,请谨慎甄别
+
+
+ + + + diff --git a/server.js b/server.js new file mode 100644 index 0000000..3abc852 --- /dev/null +++ b/server.js @@ -0,0 +1,212 @@ +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB + +// 读取配置文件 +function loadConfig() { + const configPath = path.join(__dirname, 'config.json'); + if (!fs.existsSync(configPath)) { + console.error('❌ 未找到 config.json,请创建配置文件'); + process.exit(1); + } + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + const apiKey = config.api_key; + if (!apiKey || apiKey === 'sk-你的API密钥') { + console.warn('⚠️ 请在 config.json 中配置真实的 API Key(当前为占位值)'); + console.warn('⚠️ API 请求将会失败,但界面可以正常访问\n'); + } + return { apiKey: apiKey || '', baseUrl: config.base_url || 'https://yibuapi.com/v1' }; +} + +const CONFIG = loadConfig(); +console.log(`✅ API Key 已加载: ${CONFIG.apiKey.slice(0, 8)}...`); +console.log(`🌐 API Base URL: ${CONFIG.baseUrl}`); + +// MIME 类型映射 +const MIME_TYPES = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', +}; + +const PORT = 8080; +const TARGET_URL = new URL(CONFIG.baseUrl); + +// 读取请求体,带大小限制 +function readBody(req, limit, callback) { + const chunks = []; + let size = 0; + req.on('data', chunk => { + size += chunk.length; + if (size > limit) { + req.destroy(); + return callback(new Error('请求体超过大小限制')); + } + chunks.push(chunk); + }); + req.on('end', () => callback(null, Buffer.concat(chunks))); + req.on('error', err => callback(err)); +} + +// 代理 API 请求 +function proxyRequest(req, res) { + readBody(req, MAX_BODY_SIZE, (err, body) => { + if (err) { + res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ error: { message: err.message } })); + } + + const options = { + hostname: TARGET_URL.hostname, + port: TARGET_URL.port || 443, + path: req.url, + method: req.method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CONFIG.apiKey}`, + 'Content-Length': Buffer.byteLength(body), + }, + }; + + const proxyReq = https.request(options, proxyRes => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + + proxyRes.on('error', err => { + console.error('[代理] 响应流错误:', err.message); + if (!res.writableEnded) res.end(); + }); + }); + + proxyReq.on('error', err => { + console.error('[代理] 请求失败:', err.message); + if (!res.headersSent) { + res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' }); + } + res.end(JSON.stringify({ error: { message: `API 请求失败: ${err.message}` } })); + }); + + proxyReq.setTimeout(180000, () => { + proxyReq.destroy(); + if (!res.headersSent) { + res.writeHead(504, { 'Content-Type': 'application/json; charset=utf-8' }); + } + res.end(JSON.stringify({ error: { message: '请求超时,请重试' } })); + }); + + res.on('close', () => { + if (!proxyReq.destroyed) proxyReq.destroy(); + }); + + proxyReq.write(body); + proxyReq.end(); + }); +} + +// 对话数据存储 +const DATA_DIR = path.join(__dirname, 'data'); +const CONV_FILE = path.join(DATA_DIR, 'conversations.json'); +const CONV_TMP = CONV_FILE + '.tmp'; + +function loadConversations() { + try { + if (fs.existsSync(CONV_FILE)) { + return JSON.parse(fs.readFileSync(CONV_FILE, 'utf-8')); + } + } catch(e) { console.error('读取对话数据失败:', e.message); } + return []; +} + +// 原子写:先写 .tmp 再 rename,避免写盘崩溃导致文件损坏 +function saveConversations(data) { + if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); + fs.writeFileSync(CONV_TMP, JSON.stringify(data), 'utf-8'); + fs.renameSync(CONV_TMP, CONV_FILE); +} + +// 静态文件服务 +function serveStatic(req, res) { + let filePath = req.url === '/' ? '/index.html' : req.url; + filePath = path.join(__dirname, filePath); + + // 安全检查:防止目录遍历(__dirname 追加 path.sep 避免前缀匹配绕过) + const root = __dirname + path.sep; + if (!path.resolve(filePath).startsWith(root)) { + res.writeHead(403); + res.end('403 Forbidden'); + return; + } + + const ext = path.extname(filePath); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + + fs.readFile(filePath, (err, data) => { + if (err) { + if (err.code === 'ENOENT') { + res.writeHead(404); + res.end('404 Not Found'); + } else { + res.writeHead(500); + res.end('500 Internal Server Error'); + } + return; + } + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); +} + +// 创建服务器 +const server = http.createServer((req, res) => { + // 使用 WHATWG URL API(url.parse 在 Node 24 已弃用) + const parsed = new URL(req.url, 'http://localhost'); + + // API 代理 + if (parsed.pathname.startsWith('/v1/')) { + return proxyRequest(req, res); + } + + // 对话数据接口 + if (parsed.pathname === '/api/conversations') { + if (req.method === 'GET') { + const data = loadConversations(); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify(data)); + } + if (req.method === 'POST') { + readBody(req, MAX_BODY_SIZE, (err, raw) => { + if (err) { + res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: err.message })); + } + try { + const data = JSON.parse(raw.toString()); + saveConversations(data); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ ok: true })); + } catch(e) { + res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ ok: false, error: e.message })); + } + }); + return; + } + } + + // 静态文件 + serveStatic(req, res); +}); + +server.listen(PORT, () => { + const convs = loadConversations(); + console.log(`💬 已加载 ${convs.length} 个对话`); + console.log(`🚀 聊天工具已启动: http://localhost:${PORT}`); + console.log('📋 按 Ctrl+C 停止服务\n'); +});