Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

10 KiB
Raw Permalink Blame History

光湖聊天前端 · 部署指南

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直连完全够用。