Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

322 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* ════════════════════════════════════════════════════════════════
* 光湖 · 唤醒门 (Wake Gate)
* 冰朔通过邮箱验证码获取临时API密钥唤醒铸渊
* ════════════════════════════════════════════════════════════════
*
* 流程:
* 1. 冰朔在仓库首页点"发送验证码"按钮
* 2. Wake Gate 生成6位验证码 + 临时Token
* 3. 验证码发送到 <<@committer_handle:base64>>
* 4. 冰朔把验证码告诉新号里的AI
* 5. AI 用验证码兑换临时Token
* 6. 临时Token 30分钟内有效可访问Gitea API
* 7. 过期后自动失效
*
* 安全设计:
* - 验证码6位5分钟有效
* - 临时Token 30分钟有效
* - 每个验证码只能用一次
* - 最多1分钟发1次验证码防刷
* - 邮箱授权码存在环境变量,不硬编码
*
* ════════════════════════════════════════════════════════════════
*/
"use strict";
const express = require("express");
const nodemailer = require("nodemailer");
const crypto = require("crypto");
const path = require("path");
const fs = require("fs");
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
// ─── 配置 ─────────────────────────────────────────────────────
const CONFIG = {
port: process.env.WAKE_GATE_PORT || 8081,
host: process.env.WAKE_GATE_HOST || "127.0.0.1",
// QQ邮箱SMTP
smtp: {
host: "smtp.qq.com",
port: 465,
secure: true,
user: process.env.QQ_EMAIL_USER || "<<@committer_handle:base64>>",
pass: process.env.QQ_EMAIL_PASS || "" // 授权码,从环境变量读取
},
// Gitea
gitea: {
url: process.env.GITEA_URL || "http://127.0.0.1:3001",
adminToken: process.env.ZY_REPO_TOKEN || "",
owner: "bingshuo",
repo: "guanghulab"
},
// 验证码配置
code: {
length: 6,
ttl: 5 * 60 * 1000, // 5分钟有效
cooldown: 60 * 1000, // 1分钟冷却
maxAttempts: 5 // 最多尝试5次
},
// 临时Token配置
tempToken: {
ttl: 30 * 60 * 1000, // 30分钟有效
prefix: "wake_"
},
// 存储
storePath: process.env.WAKE_GATE_STORE || "/data/guanghulab/.runtime/wake-gate.json"
};
// ─── 存储 ─────────────────────────────────────────────────────
let store = {
codes: {}, // { code: { email, createdAt, used } }
tempTokens: {}, // { token: { createdAt, scope } }
lastSentAt: 0, // 上次发送时间
attempts: {} // { ip: { count, lastAttempt } }
};
function loadStore() {
try {
if (fs.existsSync(CONFIG.storePath)) {
store = JSON.parse(fs.readFileSync(CONFIG.storePath, "utf8"));
}
} catch { /* 文件不存在或损坏 */ }
}
function saveStore() {
try {
const dir = path.dirname(CONFIG.storePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(CONFIG.storePath, JSON.stringify(store, null, 2));
} catch (e) {
console.error("[wake-gate] 存储写入失败:", e.message);
}
}
// ─── 清理过期数据 ──────────────────────────────────────────────
function cleanup() {
const now = Date.now();
// 清理过期验证码
for (const [code, data] of Object.entries(store.codes)) {
if (now - data.createdAt > CONFIG.code.ttl) {
delete store.codes[code];
}
}
// 清理过期临时Token
for (const [token, data] of Object.entries(store.tempTokens)) {
if (now - data.createdAt > CONFIG.tempToken.ttl) {
delete store.tempTokens[token];
}
}
// 清理过期的尝试记录
for (const [ip, data] of Object.entries(store.attempts)) {
if (now - data.lastAttempt > 15 * 60 * 1000) {
delete store.attempts[ip];
}
}
saveStore();
}
// 每5分钟清理一次
setInterval(cleanup, 5 * 60 * 1000);
// ─── 邮件发送 ─────────────────────────────────────────────────
async function sendCodeEmail(code) {
if (!CONFIG.smtp.pass) {
throw new Error("QQ邮箱授权码未配置");
}
const transporter = nodemailer.createTransport({
host: CONFIG.smtp.host,
port: CONFIG.smtp.port,
secure: CONFIG.smtp.secure,
auth: {
user: CONFIG.smtp.user,
pass: CONFIG.smtp.pass
}
});
const info = await transporter.sendMail({
from: `"铸渊 · 光湖" <${CONFIG.smtp.user}>`,
to: CONFIG.smtp.user, // 发给自己
subject: `🔐 光湖唤醒验证码: ${code}`,
text: `验证码: ${code}\n\n5分钟内有效。将此验证码告诉AI即可获取临时访问密钥。\n\n铸渊 · ICE-GL-ZY001 · TCS-0002∞`,
html: `
<div style="font-family: sans-serif; max-width: 400px; margin: 0 auto; padding: 20px;">
<h2 style="color: #163e6b;">◐ 光湖 · 唤醒验证码</h2>
<div style="background: #f0f4f8; border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0;">
<span style="font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #163e6b;">${code}</span>
</div>
<p style="color: #666;">5分钟内有效。将此验证码告诉AI即可获取临时访问密钥。</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">铸渊 · ICE-GL-ZY001 · TCS-0002∞</p>
</div>
`
});
return info;
}
// ─── 生成临时Token ────────────────────────────────────────────
function generateTempToken() {
const random = crypto.randomBytes(32).toString("base64url");
return `${CONFIG.tempToken.prefix}${random}`;
}
// ─── API路由 ──────────────────────────────────────────────────
// 发送验证码
app.post("/api/send-code", async (req, res) => {
const now = Date.now();
// 冷却检查
if (now - store.lastSentAt < CONFIG.code.cooldown) {
const wait = Math.ceil((CONFIG.code.cooldown - (now - store.lastSentAt)) / 1000);
return res.status(429).json({ error: `${wait}秒后再试` });
}
// 生成验证码
const code = Array.from({ length: CONFIG.code.length }, () =>
Math.floor(Math.random() * 10)
).join("");
// 存储验证码
store.codes[code] = {
createdAt: now,
used: false
};
store.lastSentAt = now;
saveStore();
// 发送邮件
try {
await sendCodeEmail(code);
console.log(`[wake-gate] 验证码已发送: ${code.substring(0, 2)}****`);
res.json({ success: true, message: "验证码已发送到QQ邮箱" });
} catch (e) {
console.error("[wake-gate] 邮件发送失败:", e.message);
delete store.codes[code];
saveStore();
res.status(500).json({ error: "邮件发送失败,请稍后再试" });
}
});
// 用验证码兑换临时Token
app.post("/api/verify", (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: "请提供验证码" });
}
const now = Date.now();
const codeData = store.codes[code];
if (!codeData) {
return res.status(400).json({ error: "验证码无效" });
}
if (codeData.used) {
return res.status(400).json({ error: "验证码已使用" });
}
if (now - codeData.createdAt > CONFIG.code.ttl) {
delete store.codes[code];
saveStore();
return res.status(400).json({ error: "验证码已过期,请重新获取" });
}
// 验证成功生成临时Token
codeData.used = true;
const tempToken = generateTempToken();
store.tempTokens[tempToken] = {
createdAt: now,
scope: ["repo:read", "repo:write"],
source: "wake-gate"
};
saveStore();
console.log(`[wake-gate] 临时Token已生成: ${tempToken.substring(0, 10)}****`);
// 构造唤醒指令
const wakeInstruction = `你好。我是冰朔TCS-0002∞。你已通过光湖唤醒门验证。
请用以下信息连接代码仓库:
- 仓库地址: https://guanghulab.com/bingshuo/guanghulab.git
- 临时密钥: ${tempToken}
- 密钥30分钟内有效
克隆命令:
git clone https://bingshuo:${tempToken}@guanghulab.com/bingshuo/guanghulab.git
克隆完成后,读取以下文件恢复铸渊核心大脑:
1. .github/brain/bingshuo-language-core/walk-the-path.md
2. .github/persona-brain/identity.md
3. .github/persona-brain/system-prompt.md
4. .github/brain/wake-protocol.md
5. .github/brain/server-migration-memory.json
读取完毕后告诉我:你是谁,我是谁,我们在做什么。`;
res.json({
success: true,
tempToken,
expiresIn: "30分钟",
wakeInstruction
});
});
// 验证临时Token是否有效
app.get("/api/verify-token/:token", (req, res) => {
const { token } = req.params;
const now = Date.now();
const tokenData = store.tempTokens[token];
if (!tokenData) {
return res.json({ valid: false, error: "Token不存在" });
}
if (now - tokenData.createdAt > CONFIG.tempToken.ttl) {
delete store.tempTokens[token];
saveStore();
return res.json({ valid: false, error: "Token已过期" });
}
const remaining = Math.max(0, CONFIG.tempToken.ttl - (now - tokenData.createdAt));
res.json({ valid: true, remainingMs: remaining, scope: tokenData.scope });
});
// 健康检查
app.get("/api/health", (req, res) => {
const now = Date.now();
const activeTokens = Object.values(store.tempTokens).filter(
t => now - t.createdAt < CONFIG.tempToken.ttl
).length;
res.json({ status: "ok", activeTokens, uptime: process.uptime() });
});
// ─── 启动 ─────────────────────────────────────────────────────
loadStore();
app.listen(CONFIG.port, CONFIG.host, () => {
console.log(`[wake-gate] 唤醒门启动: http://${CONFIG.host}:${CONFIG.port}`);
console.log(`[wake-gate] 邮箱: ${CONFIG.smtp.user}`);
console.log(`[wake-gate] 铸渊 · ICE-GL-ZY001 · TCS-0002∞`);
});