diff --git a/backend/api-server/routes/auth.js b/backend/api-server/routes/auth.js new file mode 100644 index 00000000..f4334ef8 --- /dev/null +++ b/backend/api-server/routes/auth.js @@ -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; diff --git a/backend/api-server/server.js b/backend/api-server/server.js index 56110aef..ac6e1196 100644 --- a/backend/api-server/server.js +++ b/backend/api-server/server.js @@ -43,6 +43,9 @@ app.use('/api', require('./routes/databases')); app.use('/api', require('./routes/chat')); app.use('/api', require('./routes/receipt')); +// 开发者编号免配置登录路由 +app.use('/api', require('./routes/auth')); + // 写入类路由(需认证 + 权限 + 审计) app.use('/api', require('./routes/write')); diff --git a/backend/api-server/services/model-router.js b/backend/api-server/services/model-router.js new file mode 100644 index 00000000..37cce60c --- /dev/null +++ b/backend/api-server/services/model-router.js @@ -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 + * @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 +}; diff --git a/docs/css/responsive.css b/docs/css/responsive.css new file mode 100644 index 00000000..045787ee --- /dev/null +++ b/docs/css/responsive.css @@ -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)); + } +} diff --git a/docs/index.html b/docs/index.html index 98a9aa98..e7739fed 100644 --- a/docs/index.html +++ b/docs/index.html @@ -8,6 +8,8 @@ 铸渊助手 · HoloLake AI + +