Phase 1-3: UI responsive, model router, team auth
Phase 1: Login card max-width 460→600px, CSS variables, responsive.css Phase 2: backend/api-server/services/model-router.js Phase 3: backend/api-server/routes/auth.js + docs/js/auth.js Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/3f62bf2e-92bd-44b5-b05e-b7bfe6772a4d
This commit is contained in:
parent
abbdcc491a
commit
fba4088d7a
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* /api/auth — 开发者编号免配置登录路由
|
||||||
|
*
|
||||||
|
* 开发者输入编号(如 DEV-002)即可直接进入对话界面。
|
||||||
|
* 系统自动匹配团队配置的 API,人不需要设置任何东西。
|
||||||
|
* API Key 只在服务器端使用,前端永远看不到。
|
||||||
|
*
|
||||||
|
* 版权:国作登字-2026-A-00037559
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
// 团队共享密钥(存在服务器环境变量,前端不可见)
|
||||||
|
var TEAM_API_KEY = process.env.TEAM_API_KEY || '';
|
||||||
|
|
||||||
|
// 会话 token 签名密钥
|
||||||
|
var SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// 开发者数据库(后续对接 Notion)
|
||||||
|
var DEV_DATABASE = {
|
||||||
|
'DEV-000': { name: '冰朔', level: 3, channel: 'system' },
|
||||||
|
'DEV-001': { name: '页页', level: 1, channel: '小坍缩核线' },
|
||||||
|
'DEV-002': { name: '肥猫', level: 2, channel: '男频' },
|
||||||
|
'DEV-003': { name: 'Awen', level: 1, channel: '知秋线' },
|
||||||
|
'DEV-004': { name: '之之', level: 2, channel: '秋秋线' },
|
||||||
|
'DEV-005': { name: '时雨', level: 1, channel: '知秋线' },
|
||||||
|
'DEV-006': { name: '匆匆那年', level: 1, channel: '霜砚线' },
|
||||||
|
'DEV-007': { name: '小兴', level: 1, channel: '霜砚线' },
|
||||||
|
'DEV-008': { name: '花尔', level: 1, channel: '糖星云线' },
|
||||||
|
'DEV-009': { name: '小草莓', level: 1, channel: '欧诺弥亚线' },
|
||||||
|
'DEV-010': { name: '桔子', level: 2, channel: '女频' },
|
||||||
|
'DEV-011': { name: '燕樊', level: 1, channel: '寂曜线' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 活跃会话存储(内存级,重启清空)
|
||||||
|
var activeSessions = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成会话 token(有效期 24h)
|
||||||
|
* @param {string} devId
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function generateSessionToken(devId) {
|
||||||
|
var payload = devId + ':' + Date.now();
|
||||||
|
var hmac = crypto.createHmac('sha256', SESSION_SECRET);
|
||||||
|
hmac.update(payload);
|
||||||
|
var token = payload + ':' + hmac.digest('hex');
|
||||||
|
var tokenBase64 = Buffer.from(token).toString('base64');
|
||||||
|
|
||||||
|
activeSessions.set(tokenBase64, {
|
||||||
|
devId: devId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 24 * 60 * 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
return tokenBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证会话 token
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {{ valid: boolean, devId?: string }}
|
||||||
|
*/
|
||||||
|
function verifySessionToken(token) {
|
||||||
|
var session = activeSessions.get(token);
|
||||||
|
if (!session) {
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
if (Date.now() > session.expiresAt) {
|
||||||
|
activeSessions.delete(token);
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
return { valid: true, devId: session.devId };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话验证中间件
|
||||||
|
*/
|
||||||
|
function requireSession(req, res, next) {
|
||||||
|
var authHeader = req.headers['authorization'] || '';
|
||||||
|
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: true,
|
||||||
|
code: 'SESSION_REQUIRED',
|
||||||
|
message: '请先登录。需要携带有效的会话 token。'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = verifySessionToken(token);
|
||||||
|
if (!result.valid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: true,
|
||||||
|
code: 'SESSION_EXPIRED',
|
||||||
|
message: '会话已过期,请重新登录。'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.sessionDevId = result.devId;
|
||||||
|
req.sessionDev = DEV_DATABASE[result.devId] || null;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/team-login
|
||||||
|
* 开发者编号免配置登录
|
||||||
|
*/
|
||||||
|
router.post('/auth/team-login', function(req, res) {
|
||||||
|
var devId = (req.body.devId || '').trim().toUpperCase();
|
||||||
|
|
||||||
|
if (!devId || !/^DEV-\d{3}$/.test(devId)) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
message: '❌ 无效的开发者编号格式。请使用 DEV-XXX 格式。'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var dev = DEV_DATABASE[devId];
|
||||||
|
|
||||||
|
if (!dev) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
message: '❌ 开发者编号 ' + devId + ' 不存在。请确认你的编号。'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成会话 token(有效期 24h)
|
||||||
|
var sessionToken = generateSessionToken(devId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
devName: dev.name,
|
||||||
|
level: dev.level,
|
||||||
|
channel: dev.channel,
|
||||||
|
sessionToken: sessionToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
console.log('[AUTH] ' + devId + ' (' + dev.name + ') logged in at ' + new Date().toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/auth/verify
|
||||||
|
* 验证当前会话是否有效
|
||||||
|
*/
|
||||||
|
router.get('/auth/verify', function(req, res) {
|
||||||
|
var authHeader = req.headers['authorization'] || '';
|
||||||
|
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||||||
|
var result = verifySessionToken(token);
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
var dev = DEV_DATABASE[result.devId] || {};
|
||||||
|
res.json({
|
||||||
|
valid: true,
|
||||||
|
devId: result.devId,
|
||||||
|
devName: dev.name || '',
|
||||||
|
level: dev.level || 0,
|
||||||
|
channel: dev.channel || ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({ valid: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/logout
|
||||||
|
* 注销会话
|
||||||
|
*/
|
||||||
|
router.post('/auth/logout', function(req, res) {
|
||||||
|
var authHeader = req.headers['authorization'] || '';
|
||||||
|
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||||||
|
|
||||||
|
if (token && activeSessions.has(token)) {
|
||||||
|
var session = activeSessions.get(token);
|
||||||
|
activeSessions.delete(token);
|
||||||
|
console.log('[AUTH] ' + session.devId + ' logged out at ' + new Date().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: '已注销' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
module.exports.requireSession = requireSession;
|
||||||
|
module.exports.verifySessionToken = verifySessionToken;
|
||||||
|
module.exports.DEV_DATABASE = DEV_DATABASE;
|
||||||
|
|
@ -43,6 +43,9 @@ app.use('/api', require('./routes/databases'));
|
||||||
app.use('/api', require('./routes/chat'));
|
app.use('/api', require('./routes/chat'));
|
||||||
app.use('/api', require('./routes/receipt'));
|
app.use('/api', require('./routes/receipt'));
|
||||||
|
|
||||||
|
// 开发者编号免配置登录路由
|
||||||
|
app.use('/api', require('./routes/auth'));
|
||||||
|
|
||||||
// 写入类路由(需认证 + 权限 + 审计)
|
// 写入类路由(需认证 + 权限 + 审计)
|
||||||
app.use('/api', require('./routes/write'));
|
app.use('/api', require('./routes/write'));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/**
|
||||||
|
* 智能模型路由 — 基于用户意图的模型自动选择
|
||||||
|
*
|
||||||
|
* 根据人类说话的意图(关键词匹配),自动选择最合适的模型回复。
|
||||||
|
* 人类不需要知道背后用了哪个模型,系统自己选。
|
||||||
|
*
|
||||||
|
* 与 src/brain/model-router.js(任务型路由)互补:
|
||||||
|
* - src/brain/model-router.js → 按任务模式(chat/build/review/brain/long)路由
|
||||||
|
* - 本模块 → 按用户消息内容(关键词意图检测)路由
|
||||||
|
*
|
||||||
|
* 版权:国作登字-2026-A-00037559
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MODEL_PROFILES = {
|
||||||
|
// 日常对话、闲聊、简单问答
|
||||||
|
casual: {
|
||||||
|
models: ['deepseek-chat', 'glm-4-flash'],
|
||||||
|
keywords: ['你好', '聊聊', '怎么样', '帮我', '是什么', '谢谢', '请问', '可以吗'],
|
||||||
|
maxTokens: 2000
|
||||||
|
},
|
||||||
|
// 代码相关、技术问题
|
||||||
|
coding: {
|
||||||
|
models: ['deepseek-chat', 'gpt-4o'],
|
||||||
|
keywords: ['代码', 'code', 'bug', '报错', '函数', 'API', '部署', '配置', 'npm', 'git', '编译', '调试', 'debug', '接口', '数据库'],
|
||||||
|
maxTokens: 4000
|
||||||
|
},
|
||||||
|
// 创意写作、长文生成
|
||||||
|
creative: {
|
||||||
|
models: ['deepseek-chat', 'moonshot-v1-128k'],
|
||||||
|
keywords: ['写', '故事', '文案', '小说', '剧情', '人设', '大纲', '创意', '灵感', '文章', '描写'],
|
||||||
|
maxTokens: 8000
|
||||||
|
},
|
||||||
|
// 数据分析、逻辑推理
|
||||||
|
analytical: {
|
||||||
|
models: ['deepseek-chat', 'gpt-4o'],
|
||||||
|
keywords: ['分析', '对比', '数据', '统计', '逻辑', '推理', '策略', '方案', '评估', '优化'],
|
||||||
|
maxTokens: 4000
|
||||||
|
},
|
||||||
|
// 系统指令、架构级操作
|
||||||
|
system: {
|
||||||
|
models: ['deepseek-chat', 'gpt-4o'],
|
||||||
|
keywords: ['指令', '部署', '架构', '系统', '铁律', '天眼', '权限', '工单', '广播', '流程'],
|
||||||
|
maxTokens: 6000
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析用户意图,选择最佳模型
|
||||||
|
* @param {string} userMessage - 用户消息
|
||||||
|
* @param {Object} context - 上下文(保留扩展)
|
||||||
|
* @returns {{ model: string, maxTokens: number, profile: string, fallbacks: string[] }}
|
||||||
|
*/
|
||||||
|
function routeToModel(userMessage, context) {
|
||||||
|
if (!userMessage || typeof userMessage !== 'string') {
|
||||||
|
var casualConfig = MODEL_PROFILES.casual;
|
||||||
|
return {
|
||||||
|
model: casualConfig.models[0],
|
||||||
|
maxTokens: casualConfig.maxTokens,
|
||||||
|
profile: 'casual',
|
||||||
|
fallbacks: casualConfig.models.slice(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = userMessage.toLowerCase();
|
||||||
|
|
||||||
|
// 计算每个 profile 的匹配分数
|
||||||
|
var scores = {};
|
||||||
|
var profileNames = Object.keys(MODEL_PROFILES);
|
||||||
|
for (var i = 0; i < profileNames.length; i++) {
|
||||||
|
var profileName = profileNames[i];
|
||||||
|
var config = MODEL_PROFILES[profileName];
|
||||||
|
var score = 0;
|
||||||
|
for (var j = 0; j < config.keywords.length; j++) {
|
||||||
|
if (msg.includes(config.keywords[j])) {
|
||||||
|
score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores[profileName] = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择得分最高的 profile,默认 casual
|
||||||
|
var bestProfile = 'casual';
|
||||||
|
var bestScore = 0;
|
||||||
|
for (var k = 0; k < profileNames.length; k++) {
|
||||||
|
var pName = profileNames[k];
|
||||||
|
if (scores[pName] > bestScore) {
|
||||||
|
bestScore = scores[pName];
|
||||||
|
bestProfile = pName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = MODEL_PROFILES[bestProfile];
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: selected.models[0],
|
||||||
|
maxTokens: selected.maxTokens,
|
||||||
|
profile: bestProfile,
|
||||||
|
fallbacks: selected.models.slice(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带降级的 API 调用
|
||||||
|
* @param {Object} route - routeToModel 的返回值
|
||||||
|
* @param {Array} messages - 消息列表
|
||||||
|
* @param {Function} callAPI - 实际的 API 调用函数 (model, messages, maxTokens) => Promise<response>
|
||||||
|
* @returns {Promise<{ success: boolean, model?: string, response?: *, error?: string }>}
|
||||||
|
*/
|
||||||
|
async function callWithFallback(route, messages, callAPI) {
|
||||||
|
if (typeof callAPI !== 'function') {
|
||||||
|
return { success: false, error: 'callAPI 函数未提供' };
|
||||||
|
}
|
||||||
|
|
||||||
|
var models = [route.model].concat(route.fallbacks || []);
|
||||||
|
|
||||||
|
for (var i = 0; i < models.length; i++) {
|
||||||
|
var model = models[i];
|
||||||
|
try {
|
||||||
|
var response = await callAPI(model, messages, route.maxTokens);
|
||||||
|
return { success: true, model: model, response: response };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[model-router] Model ' + model + ' failed, trying next... ' + (err.message || ''));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: '所有模型均不可用,请稍后重试。' };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
routeToModel: routeToModel,
|
||||||
|
callWithFallback: callWithFallback,
|
||||||
|
MODEL_PROFILES: MODEL_PROFILES
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* responsive.css — 全屏响应式适配样式表
|
||||||
|
*
|
||||||
|
* Phase 1 · UI 全屏响应式适配
|
||||||
|
* 桌面版:对话界面铺满浏览器窗口,无大片空白
|
||||||
|
* 手机版:768px 以下切换为移动布局,触摸友好
|
||||||
|
*
|
||||||
|
* 版权:国作登字-2026-A-00037559
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ─── 核心响应式框架变量 ─── */
|
||||||
|
:root {
|
||||||
|
--content-max-width: 900px;
|
||||||
|
--chat-padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── 桌面版:内容区域铺满 ─── */
|
||||||
|
.app-container,
|
||||||
|
#app.on {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#center-area {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天消息区域使用最大宽度控制可读性 */
|
||||||
|
#msgs {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--chat-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── 登录卡片:桌面版加宽 ─── */
|
||||||
|
.sc {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── 768px 手机版断点 ─── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--chat-padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片:手机版全宽 */
|
||||||
|
.sc {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息气泡自适应宽度 */
|
||||||
|
.bbl {
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 触摸友好:按钮最小 44px × 44px(Apple HIG 标准) */
|
||||||
|
button,
|
||||||
|
.btn,
|
||||||
|
.sc-btn,
|
||||||
|
.login-tab {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入区域底部固定 */
|
||||||
|
footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
padding-bottom: max(14px, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── iPhone / iOS 安全区域适配 ─── */
|
||||||
|
@supports (padding: env(safe-area-inset-bottom)) {
|
||||||
|
#app.on {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setup {
|
||||||
|
padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>铸渊助手 · HoloLake AI</title>
|
<title>铸渊助手 · HoloLake AI</title>
|
||||||
|
<link rel="stylesheet" href="css/responsive.css">
|
||||||
|
<script src="js/auth.js" defer></script>
|
||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root{
|
:root{
|
||||||
|
|
@ -18,12 +20,13 @@
|
||||||
--mc:#4f8ef7;--mb:#fb923c;--mr:#a78bfa;--mn:#34d399;
|
--mc:#4f8ef7;--mb:#fb923c;--mr:#a78bfa;--mn:#34d399;
|
||||||
--r:14px;--rs:8px;
|
--r:14px;--rs:8px;
|
||||||
--ls-w:260px;--rs-w:360px;
|
--ls-w:260px;--rs-w:360px;
|
||||||
|
--content-max-width:900px;--chat-padding:20px;
|
||||||
}
|
}
|
||||||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;overscroll-behavior:none}
|
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;overscroll-behavior:none}
|
||||||
|
|
||||||
/* ─── SETUP ─── */
|
/* ─── SETUP ─── */
|
||||||
#setup{position:fixed;inset:0;z-index:1000;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:20px;padding:max(20px,env(safe-area-inset-top)) max(20px,env(safe-area-inset-right)) max(20px,env(safe-area-inset-bottom)) max(20px,env(safe-area-inset-left));overflow-y:auto;-webkit-overflow-scrolling:touch}
|
#setup{position:fixed;inset:0;z-index:1000;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:20px;padding:max(20px,env(safe-area-inset-top)) max(20px,env(safe-area-inset-right)) max(20px,env(safe-area-inset-bottom)) max(20px,env(safe-area-inset-left));overflow-y:auto;-webkit-overflow-scrolling:touch}
|
||||||
.sc{background:var(--s1);border:1px solid var(--border);border-radius:20px;padding:40px;width:100%;max-width:460px;box-shadow:0 25px 60px rgba(0,0,0,.5);margin:auto}
|
.sc{background:var(--s1);border:1px solid var(--border);border-radius:20px;padding:40px;width:100%;max-width:600px;box-shadow:0 25px 60px rgba(0,0,0,.5);margin:auto}
|
||||||
.sc-logo{font-size:52px;text-align:center;margin-bottom:14px}
|
.sc-logo{font-size:52px;text-align:center;margin-bottom:14px}
|
||||||
.sc h1{font-size:22px;text-align:center;margin-bottom:6px}
|
.sc h1{font-size:22px;text-align:center;margin-bottom:6px}
|
||||||
.sc .sub{color:var(--dim);text-align:center;font-size:13px;margin-bottom:22px}
|
.sc .sub{color:var(--dim);text-align:center;font-size:13px;margin-bottom:22px}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* auth.js — 团队登录前端逻辑
|
||||||
|
*
|
||||||
|
* 开发者编号免配置登录:
|
||||||
|
* 1. 选择/输入开发者编号(如 DEV-002)
|
||||||
|
* 2. 后端验证编号 → 返回会话 token
|
||||||
|
* 3. 自动绑定团队 API → 直接进入对话
|
||||||
|
* 4. 不需要手动输入任何 API Key
|
||||||
|
*
|
||||||
|
* 版权:国作登字-2026-A-00037559
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var TeamAuth = (function() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队登录 — 开发者编号直接进
|
||||||
|
* @param {string} devId - 开发者编号(如 DEV-002)
|
||||||
|
* @returns {Promise<Object>} 登录结果
|
||||||
|
*/
|
||||||
|
async function teamLogin(devId) {
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/auth/team-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ devId: devId })
|
||||||
|
});
|
||||||
|
|
||||||
|
var data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 存储会话 token(sessionStorage,关闭标签页即清除)
|
||||||
|
sessionStorage.setItem('team_session_token', data.sessionToken);
|
||||||
|
localStorage.setItem('team_dev_id', devId);
|
||||||
|
localStorage.setItem('team_dev_name', data.devName);
|
||||||
|
localStorage.setItem('team_dev_level', String(data.level));
|
||||||
|
localStorage.setItem('team_dev_channel', data.channel);
|
||||||
|
// API Key 不存前端,后端代理所有 API 调用
|
||||||
|
|
||||||
|
return { success: true, data: data };
|
||||||
|
} else {
|
||||||
|
return { success: false, message: data.message || '开发者编号不存在,请检查。' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: '连接失败,请稍后重试。' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证当前会话
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
async function verifySession() {
|
||||||
|
var token = sessionStorage.getItem('team_session_token');
|
||||||
|
if (!token) return { valid: false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/auth/verify', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销
|
||||||
|
*/
|
||||||
|
async function logout() {
|
||||||
|
var token = sessionStorage.getItem('team_session_token');
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem('team_session_token');
|
||||||
|
localStorage.removeItem('team_dev_id');
|
||||||
|
localStorage.removeItem('team_dev_name');
|
||||||
|
localStorage.removeItem('team_dev_level');
|
||||||
|
localStorage.removeItem('team_dev_channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话 token(用于 API 请求头)
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getSessionToken() {
|
||||||
|
return sessionStorage.getItem('team_session_token') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已登录的开发者信息
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
function getDevInfo() {
|
||||||
|
var devId = localStorage.getItem('team_dev_id');
|
||||||
|
if (!devId) return null;
|
||||||
|
return {
|
||||||
|
devId: devId,
|
||||||
|
name: localStorage.getItem('team_dev_name') || '',
|
||||||
|
level: parseInt(localStorage.getItem('team_dev_level') || '0', 10),
|
||||||
|
channel: localStorage.getItem('team_dev_channel') || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
teamLogin: teamLogin,
|
||||||
|
verifySession: verifySession,
|
||||||
|
logout: logout,
|
||||||
|
getSessionToken: getSessionToken,
|
||||||
|
getDevInfo: getDevInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue