Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
10 KiB
10 KiB
光湖聊天前端 · 部署指南
1. 服务器环境准备
SSH登录 43.153.203.105(这台服务器Node 20 + PM2已装·可跳过这步):
# 安装 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. 创建项目
# 创建项目文件夹
mkdir -p /opt/guanghu-chat
cd /opt/guanghu-chat
# 初始化
npm init -y
npm install express
3. 后端代码
复制下面整块命令粘贴到终端回车(会自动创建 server.js 文件):
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. 前端代码
mkdir -p /opt/guanghu-chat/public
nano /opt/guanghu-chat/public/index.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. 启动服务
cd /opt/guanghu-chat
node server.js
看到 光湖聊天服务已启动: http://0.0.0.0:3000 就成功了。
浏览器打开 http://43.153.203.105:3000 就能聊天。
6. 后台常驻运行
关掉终端服务就停了,用PM2让它一直跑:
# 安装 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 里的三行:
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直连完全够用。