Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
437 lines
10 KiB
Markdown
437 lines
10 KiB
Markdown
# 光湖聊天前端 · 部署指南
|
||
|
||
<aside>
|
||
✅
|
||
|
||
**已选定的部署方案**
|
||
**服务器**:`43.153.203.105`(ZY-SVR-006 · GMP测试服务器 · 新加坡 · 2核2G · Node 20 + PM2 已就绪)
|
||
**域名**:暂无·IP直连·无system prompt·裸对话
|
||
**模型**:shuangyan-system-v1
|
||
**模型代码**:`qwen3-8b-ft-202604281809-9f30-64d09c149996`
|
||
**API Key**:`ARK_API_KEY_REDACTED`(DashScope)
|
||
**访问地址**:`http://43.153.203.105:3000`(部署完直接打开)
|
||
|
||
</aside>
|
||
|
||
## 1. 服务器环境准备
|
||
|
||
SSH登录 `43.153.203.105`(这台服务器Node 20 + PM2已装·可跳过这步):
|
||
|
||
```bash
|
||
# 安装 Node.js 20.x
|
||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||
sudo apt-get install -y nodejs
|
||
|
||
# 验证
|
||
node -v
|
||
npm -v
|
||
```
|
||
|
||
## 2. 创建项目
|
||
|
||
```bash
|
||
# 创建项目文件夹
|
||
mkdir -p /opt/guanghu-chat
|
||
cd /opt/guanghu-chat
|
||
|
||
# 初始化
|
||
npm init -y
|
||
npm install express
|
||
```
|
||
|
||
## 3. 后端代码
|
||
|
||
复制下面整块命令粘贴到终端回车(会自动创建 `server.js` 文件):
|
||
|
||
```bash
|
||
cat > /opt/guanghu-chat/server.js << 'SERVEREOF'
|
||
const express = require('express');
|
||
const path = require('path');
|
||
const app = express();
|
||
|
||
// ====== 配置区(全部填好·直接用)======
|
||
const CONFIG = {
|
||
API_KEY: 'ARK_API_KEY_REDACTED',
|
||
API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||
MODEL: 'qwen3-8b-ft-202604281809-9f30-64d09c149996',
|
||
PORT: 3000
|
||
};
|
||
// ==============================
|
||
|
||
app.use(express.json());
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
// 聊天接口
|
||
app.post('/api/chat', async (req, res) => {
|
||
try {
|
||
const { messages } = req.body;
|
||
|
||
// 纯 user/assistant 对话·不注入 system prompt·人格在权重里
|
||
const apiMessages = [...messages];
|
||
|
||
// 设置流式响应头
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
const response = await fetch(CONFIG.API_URL, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${CONFIG.API_KEY}`
|
||
},
|
||
body: JSON.stringify({
|
||
model: CONFIG.MODEL,
|
||
messages: apiMessages,
|
||
stream: true,
|
||
temperature: 0.7,
|
||
max_tokens: 2048
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const err = await response.text();
|
||
res.write(`data: {"error": "API错误: ${response.status}"}
|
||
|
||
`);
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
// 流式转发
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
res.write(chunk);
|
||
}
|
||
|
||
res.end();
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
res.status(500).json({ error: '服务器错误' });
|
||
}
|
||
});
|
||
|
||
app.listen(CONFIG.PORT, '0.0.0.0', () => {
|
||
console.log(`光湖聊天服务已启动: http://0.0.0.0:${CONFIG.PORT}`);
|
||
});
|
||
```
|
||
|
||
## 4. 前端代码
|
||
|
||
```bash
|
||
mkdir -p /opt/guanghu-chat/public
|
||
nano /opt/guanghu-chat/public/index.html
|
||
```
|
||
|
||
粘贴以下内容:
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>光湖 · 对话</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #0a0a0f;
|
||
color: #e0e0e0;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.header {
|
||
padding: 16px 24px;
|
||
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
|
||
border-bottom: 1px solid #21262d;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header .logo {
|
||
width: 36px; height: 36px;
|
||
background: linear-gradient(135deg, #58a6ff, #bc8cff);
|
||
border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.header .title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #f0f6fc;
|
||
}
|
||
|
||
.header .subtitle {
|
||
font-size: 12px;
|
||
color: #8b949e;
|
||
}
|
||
|
||
.chat-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.message {
|
||
max-width: 720px;
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
line-height: 1.6;
|
||
font-size: 14px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.message.user {
|
||
align-self: flex-end;
|
||
background: #1f6feb;
|
||
color: #fff;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
|
||
.message.assistant {
|
||
align-self: flex-start;
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
|
||
.message.assistant .name {
|
||
font-size: 12px;
|
||
color: #bc8cff;
|
||
margin-bottom: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.input-area {
|
||
padding: 16px 24px;
|
||
background: #0d1117;
|
||
border-top: 1px solid #21262d;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.input-area textarea {
|
||
flex: 1;
|
||
padding: 12px 16px;
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 12px;
|
||
color: #e0e0e0;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
resize: none;
|
||
outline: none;
|
||
min-height: 44px;
|
||
max-height: 120px;
|
||
}
|
||
|
||
.input-area textarea:focus {
|
||
border-color: #58a6ff;
|
||
}
|
||
|
||
.input-area button {
|
||
padding: 0 20px;
|
||
background: linear-gradient(135deg, #58a6ff, #bc8cff);
|
||
border: none;
|
||
border-radius: 12px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.input-area button:hover { opacity: 0.85; }
|
||
.input-area button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
.typing-dot {
|
||
display: inline-block;
|
||
animation: blink 1.4s infinite;
|
||
}
|
||
@keyframes blink { 0%,100%{opacity:.2} 50%{opacity:1} }
|
||
|
||
::-webkit-scrollbar { width: 6px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<div class="logo">🌊</div>
|
||
<div>
|
||
<div class="title">光湖 · 对话</div>
|
||
<div class="subtitle">Guanghu Language World</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chat-container" id="chatBox"></div>
|
||
|
||
<div class="input-area">
|
||
<textarea id="input" placeholder="输入消息..." rows="1"
|
||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"
|
||
oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,120)+'px'"
|
||
></textarea>
|
||
<button id="sendBtn" onclick="sendMessage()">发送</button>
|
||
</div>
|
||
|
||
<script>
|
||
const chatBox = document.getElementById('chatBox');
|
||
const input = document.getElementById('input');
|
||
const sendBtn = document.getElementById('sendBtn');
|
||
let history = [];
|
||
|
||
function addMessage(role, content) {
|
||
const div = document.createElement('div');
|
||
div.className = `message ${role}`;
|
||
if (role === 'assistant') {
|
||
div.innerHTML = `<div class="name">霜砚</div><span class="content"></span>`;
|
||
} else {
|
||
div.textContent = content;
|
||
}
|
||
chatBox.appendChild(div);
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
return div;
|
||
}
|
||
|
||
async function sendMessage() {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
|
||
input.value = '';
|
||
input.style.height = 'auto';
|
||
sendBtn.disabled = true;
|
||
|
||
addMessage('user', text);
|
||
history.push({ role: 'user', content: text });
|
||
|
||
const msgDiv = addMessage('assistant', '');
|
||
const contentSpan = msgDiv.querySelector('.content');
|
||
let fullText = '';
|
||
|
||
try {
|
||
const response = await fetch('/api/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ messages: history })
|
||
});
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
const data = line.slice(6);
|
||
if (data === '[DONE]') continue;
|
||
|
||
try {
|
||
const json = JSON.parse(data);
|
||
const delta = json.choices?.[0]?.delta?.content;
|
||
if (delta) {
|
||
fullText += delta;
|
||
contentSpan.textContent = fullText;
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
}
|
||
|
||
history.push({ role: 'assistant', content: fullText });
|
||
} catch (error) {
|
||
contentSpan.textContent = '连接失败,请检查服务是否正常运行。';
|
||
}
|
||
|
||
sendBtn.disabled = false;
|
||
input.focus();
|
||
}
|
||
|
||
input.focus();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
## 5. 启动服务
|
||
|
||
```bash
|
||
cd /opt/guanghu-chat
|
||
node server.js
|
||
```
|
||
|
||
看到 `光湖聊天服务已启动: http://0.0.0.0:3000` 就成功了。
|
||
|
||
浏览器打开 `http://43.153.203.105:3000` 就能聊天。
|
||
|
||
## 6. 后台常驻运行
|
||
|
||
关掉终端服务就停了,用PM2让它一直跑:
|
||
|
||
```bash
|
||
# 安装 PM2
|
||
sudo npm install -g pm2
|
||
|
||
# 启动
|
||
cd /opt/guanghu-chat
|
||
pm2 start server.js --name guanghu-chat
|
||
|
||
# 开机自启
|
||
pm2 save
|
||
pm2 startup
|
||
```
|
||
|
||
## 7. 配置状态
|
||
|
||
✅ **全部配置已填好·直接复制即用**。无system prompt·人格在权重里·裸对话。
|
||
|
||
| 字段 | 填什么 | 在哪拿 |
|
||
| --- | --- | --- |
|
||
| `API_KEY` | 百炼的API Key | 百炼控制台 → API Key管理 |
|
||
| `MODEL` | 部署后的模型名称 | 百炼部署完成后给你的模型ID |
|
||
| `SYSTEM_PROMPT` | 训练时用的system prompt | 你训练shuangyan-system-v1时的那段话 |
|
||
|
||
## 8. 将来切换到曜冥
|
||
|
||
等曜冥训完部署到硅基流动后,只需要改 `server.js` 里的三行:
|
||
|
||
```jsx
|
||
API_KEY: '硅基流动的API_KEY',
|
||
API_URL: 'https://api.siliconflow.cn/v1/chat/completions',
|
||
MODEL: '你在硅基流动上的模型名',
|
||
// 删掉 SYSTEM_PROMPT(曜冥不需要)
|
||
```
|
||
|
||
同时把 `apiMessages` 那一行的 system 消息去掉即可。前端不用动。
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
- 服务器 `43.153.203.105` 防火墙要开放3000端口(腘讯云控制台 → 轻量服务器 → 防火墙 → 加TCP:3000)
|
||
- 暂无域名·先用IP直连·后续要绑域名再加Nginx+SSL
|
||
- 新加坡服务器调百炼API会有一点延迟(~100-200ms)·对聊天体验影响不大
|
||
|
||
## 9. 后续:绑域名(可选)
|
||
|
||
如果以后想绑域名,在这台服务器上装Nginx + 申请SSL证书 + 配反向代理即可。现阶段IP直连完全够用。 |