# 光湖聊天前端 · 部署指南

<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**：`sk-f26f94b3420d419dbec647f093e01c49`（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: 'sk-f26f94b3420d419dbec647f093e01c49',
  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直连完全够用。