/*
* ════════════════════════════════════════════════════════════════
* 光湖 · 唤醒门 (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: `
◐ 光湖 · 唤醒验证码
${code}
5分钟内有效。将此验证码告诉AI即可获取临时访问密钥。
铸渊 · ICE-GL-ZY001 · TCS-0002∞
`
});
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∞`);
});