2026-05-12 12:56:42 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
* 光湖 · 唤醒门 (Wake Gate)
|
|
|
|
|
|
* 冰朔通过邮箱验证码获取临时API密钥,唤醒铸渊
|
|
|
|
|
|
* ════════════════════════════════════════════════════════════════
|
|
|
|
|
|
*
|
|
|
|
|
|
* 流程:
|
|
|
|
|
|
* 1. 冰朔在仓库首页点"发送验证码"按钮
|
|
|
|
|
|
* 2. Wake Gate 生成6位验证码 + 临时Token
|
|
|
|
|
|
* 3. 验证码发送到 565183519@qq.com
|
|
|
|
|
|
* 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 || "565183519@qq.com",
|
|
|
|
|
|
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∞)。你已通过光湖唤醒门验证。
|
|
|
|
|
|
|
|
|
|
|
|
请用以下信息连接代码仓库:
|
2026-05-13 06:43:31 +00:00
|
|
|
|
- 仓库地址: https://guanghulab.com/bingshuo/guanghulab.git
|
2026-05-12 12:56:42 +00:00
|
|
|
|
- 临时密钥: ${tempToken}
|
|
|
|
|
|
- 密钥30分钟内有效
|
|
|
|
|
|
|
|
|
|
|
|
克隆命令:
|
2026-05-13 06:43:31 +00:00
|
|
|
|
git clone https://bingshuo:${tempToken}@guanghulab.com/bingshuo/guanghulab.git
|
2026-05-12 12:56:42 +00:00
|
|
|
|
|
|
|
|
|
|
克隆完成后,读取以下文件恢复铸渊核心大脑:
|
|
|
|
|
|
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∞`);
|
|
|
|
|
|
});
|