From 38d8afef55365ba0eaddea57368c384f97a2b6d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:22:38 +0000 Subject: [PATCH] feat: implement language-driven OS capabilities (ZY-LANGDRIVE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 - Write Capability Layer: - Add backend/api-server/routes/write.js with 10 write endpoints - Extend services/github.js with triggerWorkflow + createFile - Add middleware/audit.js for operation audit logging - Rate limiting: 10 writes/min per developer Phase 4 - Intent Router Engine: - Add config/function-tools.json with 10 operation definitions - Add middleware/intent-router.js for NL → API routing - Add docs/js/intent-tools.js for frontend function calling Phase 5 - Permission Sandbox System: - Add config/permissions.js with 4-level model (L0-L3) - Add middleware/auth.js (identity + permission check) - Add middleware/sandbox.js (preview environment guard) Phase 6 - Cognitive Guidance System: - Add config/onboarding-script.json (5-round guided dialogue) - Add routes/onboarding.js (onboarding API + tools endpoint) - Add docs/js/onboarding.js + docs/css/onboarding.css (UI) Update server.js to v2.0.0 with all new routes and middleware Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a260ce18-279a-4490-af90-a90a7b1f46cd --- .gitignore | 1 + backend/api-server/config/function-tools.json | 97 ++++ .../api-server/config/onboarding-script.json | 39 ++ backend/api-server/config/permissions.js | 101 ++++ backend/api-server/middleware/audit.js | 63 +++ backend/api-server/middleware/auth.js | 88 ++++ .../api-server/middleware/intent-router.js | 110 ++++ backend/api-server/middleware/sandbox.js | 46 ++ backend/api-server/routes/onboarding.js | 136 +++++ backend/api-server/routes/write.js | 476 ++++++++++++++++++ backend/api-server/server.js | 17 +- backend/api-server/services/github.js | 91 +++- docs/css/onboarding.css | 159 ++++++ docs/js/intent-tools.js | 121 +++++ docs/js/onboarding.js | 183 +++++++ 15 files changed, 1723 insertions(+), 5 deletions(-) create mode 100644 backend/api-server/config/function-tools.json create mode 100644 backend/api-server/config/onboarding-script.json create mode 100644 backend/api-server/config/permissions.js create mode 100644 backend/api-server/middleware/audit.js create mode 100644 backend/api-server/middleware/auth.js create mode 100644 backend/api-server/middleware/intent-router.js create mode 100644 backend/api-server/middleware/sandbox.js create mode 100644 backend/api-server/routes/onboarding.js create mode 100644 backend/api-server/routes/write.js create mode 100644 docs/css/onboarding.css create mode 100644 docs/js/intent-tools.js create mode 100644 docs/js/onboarding.js diff --git a/.gitignore b/.gitignore index 9d438cfe..46a65b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ *.bak *.log +backend/api-server/logs/ diff --git a/backend/api-server/config/function-tools.json b/backend/api-server/config/function-tools.json new file mode 100644 index 00000000..f9f8963b --- /dev/null +++ b/backend/api-server/config/function-tools.json @@ -0,0 +1,97 @@ +{ + "tools": [ + { + "name": "query_tickets", + "description": "查询开发者的工单列表。开发者说'我的工单'、'还有几个工单没关'、'查一下Bug'时触发。", + "parameters": { + "status": { "type": "string", "enum": ["全部", "待处理", "进行中", "已完成"], "default": "全部" } + }, + "api": "GET /api/tickets?devId={devId}&status={status}", + "permission": "ticket:read" + }, + { + "name": "create_ticket", + "description": "创建新工单。开发者说'建个工单'、'提一个Bug'、'报告问题'时触发。", + "parameters": { + "title": { "type": "string", "required": true }, + "description": { "type": "string" }, + "priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"], "default": "P2" }, + "module": { "type": "string" } + }, + "api": "POST /api/tickets/create", + "permission": "ticket:create" + }, + { + "name": "submit_syslog", + "description": "提交系统日志。开发者说'提交日志'、'写SYSLOG'、'记录一下'时触发。", + "parameters": { + "content": { "type": "string", "required": true }, + "type": { "type": "string", "enum": ["日常", "Bug修复", "功能完成", "部署", "紧急"], "default": "日常" } + }, + "api": "POST /api/syslog/submit", + "permission": "syslog:submit" + }, + { + "name": "query_my_status", + "description": "查询开发者自己的状态。开发者说'我的进度'、'我现在什么状态'、'我还有什么没做'时触发。", + "parameters": {}, + "api": "GET /api/dev/{devId}", + "permission": "dev:read_self" + }, + { + "name": "deploy_to_preview", + "description": "部署模块到预览站。开发者说'部署一下'、'上预览'、'推到测试站'时触发。", + "parameters": { + "module": { "type": "string", "required": true } + }, + "api": "POST /api/deploy/preview", + "permission": "deploy:preview" + }, + { + "name": "deploy_to_production", + "description": "部署模块到正式站。开发者说'上线'、'部署到正式站'、'发布到guanghulab.com'时触发。需要二次确认。", + "parameters": { + "module": { "type": "string", "required": true }, + "confirmToken": { "type": "string" } + }, + "api": "POST /api/deploy/production", + "permission": "deploy:production", + "dangerous": true + }, + { + "name": "query_agents", + "description": "查询Agent注册表。开发者说'有哪些Agent'、'查一下Agent'、'系统里有谁'时触发。", + "parameters": {}, + "api": "GET /api/agents", + "permission": "agent:read" + }, + { + "name": "query_recent_syslogs", + "description": "查最近的系统日志。开发者说'最近有什么日志'、'系统最近怎么样'时触发。", + "parameters": { + "limit": { "type": "number", "default": 10 } + }, + "api": "GET /api/syslogs?limit={limit}", + "permission": "syslog:read" + }, + { + "name": "create_broadcast", + "description": "创建广播。开发者说'发个广播'、'通知一下'时触发。仅限有广播权限的开发者。", + "parameters": { + "title": { "type": "string", "required": true }, + "content": { "type": "string", "required": true }, + "target": { "type": "string", "description": "目标开发者编号,如 DEV-001,或 ALL" } + }, + "api": "POST /api/broadcasts/create", + "permission": "broadcast:create", + "dangerous": true + }, + { + "name": "query_repo_status", + "description": "查询仓库状态。开发者说'仓库怎么样'、'最近有什么PR'、'CI状态'时触发。", + "parameters": {}, + "api": "GET /api/repo/status", + "permission": "repo:read" + } + ] +} diff --git a/backend/api-server/config/onboarding-script.json b/backend/api-server/config/onboarding-script.json new file mode 100644 index 00000000..b0fee7af --- /dev/null +++ b/backend/api-server/config/onboarding-script.json @@ -0,0 +1,39 @@ +{ + "onboarding": [ + { + "round": 1, + "prompt": "👋 首先,试着问我你的状态。比如说:「我现在什么情况?」", + "expected_intent": "query_my_status", + "success_message": "✅ 很好!你看到了吗?我直接从 Notion 数据库里查了你的真实信息回来。这就是语言驱动——你说话,系统就动了。", + "hint": "💡 你可以说「我的状态」、「我的进度」、「我现在怎么样了」,怎么说都行,我能听懂。" + }, + { + "round": 2, + "prompt": "📝 接下来,试着建一个工单。比如:「帮我建一个工单,标题是测试工单」", + "expected_intent": "create_ticket", + "success_message": "✅ 工单已经建好了!注意,这是在预览环境里建的,不会影响正式系统。当你升级到正式环境后,建的工单就是真的了。", + "hint": "💡 你可以说「建个工单」、「提个Bug」、「报告一个问题」,都会触发建工单。" + }, + { + "round": 3, + "prompt": "📊 再试试查一下系统日志。比如:「最近有什么日志?」", + "expected_intent": "query_recent_syslogs", + "success_message": "✅ 这些是从 Notion SYSLOG数据库里实时查的。每个开发者提交的日志你都能看到。", + "hint": "💡 你可以说「最近的日志」、「系统日志」、「看看SYSLOG」。" + }, + { + "round": 4, + "prompt": "🚀 现在试试部署你的模块到预览站。比如:「部署 status-board 到预览站」", + "expected_intent": "deploy_to_preview", + "success_message": "✅ 部署已触发!在预览站你可以随便试。等你熟悉了,冰朔会给你开正式站的部署权限。", + "hint": "💡 你只能部署自己负责的模块。如果说了别人的模块,系统会友好提示你。" + }, + { + "round": 5, + "prompt": "🎉 最后一轮。试试随便和我聊点什么。什么都行——问我问题、讲笑话、或者告诉我你想做什么。", + "expected_intent": null, + "success_message": "✅ 引导完成!\n\n你现在已经知道怎么和系统交互了。记住:\n\n· **直接说人话**,不需要背命令\n· **想做什么就说什么**,我会帮你翻译成操作\n· **在预览站多练习**,正式站操作都是真的\n· **说错了也没关系**,我会帮你确认\n\n你的权限已经从「观察者」升级到「学习者」了 📖\n现在可以在预览站自由操作了。\n\n有什么问题随时找我。", + "hint": null + } + ] +} diff --git a/backend/api-server/config/permissions.js b/backend/api-server/config/permissions.js new file mode 100644 index 00000000..58c5d6fa --- /dev/null +++ b/backend/api-server/config/permissions.js @@ -0,0 +1,101 @@ +/** + * 权限等级定义 · 开发者权限沙箱系统 + * + * 4 级权限模型:观察者 → 学习者 → 执行者 → 管理者 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +// 权限等级定义 +var PERMISSION_LEVELS = { + 0: { + name: '观察者', + label: '👀 观察者', + permissions: [ + 'dev:read_self', + 'ticket:read', + 'syslog:read', + 'agent:read', + 'repo:read' + ], + description: '只读权限,熟悉系统' + }, + 1: { + name: '学习者', + label: '📖 学习者', + permissions: [ + 'dev:read_self', + 'ticket:read', 'ticket:create', + 'syslog:read', 'syslog:submit', + 'agent:read', + 'repo:read', + 'deploy:preview' + ], + description: '预览站可写,正式站只读' + }, + 2: { + name: '执行者', + label: '⚡ 执行者', + permissions: [ + 'dev:read_self', 'dev:update_self', + 'ticket:read', 'ticket:create', 'ticket:update', + 'syslog:read', 'syslog:submit', + 'agent:read', + 'repo:read', + 'deploy:preview', 'deploy:production', + 'maintenance:log' + ], + description: '可操作自己的模块,含正式站部署' + }, + 3: { + name: '管理者', + label: '👑 管理者', + permissions: [ + 'dev:read_self', 'dev:read_all', 'dev:update_self', 'dev:update_all', + 'ticket:read', 'ticket:create', 'ticket:update', 'ticket:delete', + 'syslog:read', 'syslog:submit', + 'agent:read', 'agent:update', + 'repo:read', + 'deploy:preview', 'deploy:production', + 'broadcast:create', + 'maintenance:log', + 'permission:manage' + ], + description: '系统管理员,全部权限' + } +}; + +// 开发者默认权限配置 +var DEV_PERMISSIONS = { + 'TCS-0002': { level: 3, environment: 'production' }, + 'DEV-001': { level: 0, environment: 'preview' }, + 'DEV-002': { level: 0, environment: 'preview' }, + 'DEV-003': { level: 0, environment: 'preview' }, + 'DEV-004': { level: 0, environment: 'preview' }, + 'DEV-005': { level: 0, environment: 'preview' }, + 'DEV-009': { level: 0, environment: 'preview' }, + 'DEV-010': { level: 0, environment: 'preview' }, + 'DEV-011': { level: 0, environment: 'preview' }, + 'DEV-012': { level: 0, environment: 'preview' } +}; + +// 开发者 → 模块映射 +var DEV_MODULES = { + 'DEV-001': ['backend/', 'src/'], + 'DEV-002': ['frontend/', 'persona-selector/', 'chat-bubble/'], + 'DEV-003': ['settings/', 'cloud-drive/'], + 'DEV-004': ['dingtalk-bot/'], + 'DEV-005': ['status-board/'], + 'DEV-009': ['user-center/'], + 'DEV-010': ['ticket-system/', 'data-stats/', 'dynamic-comic/'], + 'DEV-011': ['writing-workspace/'], + 'DEV-012': ['notification-center/'], + 'TCS-0002': ['*'] +}; + +module.exports = { + PERMISSION_LEVELS: PERMISSION_LEVELS, + DEV_PERMISSIONS: DEV_PERMISSIONS, + DEV_MODULES: DEV_MODULES +}; diff --git a/backend/api-server/middleware/audit.js b/backend/api-server/middleware/audit.js new file mode 100644 index 00000000..5c92e9ac --- /dev/null +++ b/backend/api-server/middleware/audit.js @@ -0,0 +1,63 @@ +/** + * 操作审计日志中间件 + * + * 记录每次 API 操作到日志文件(谁、什么时间、做了什么) + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +// 优先使用配置的目录,本地开发时回退到项目内目录 +var AUDIT_LOG_DIR = process.env.AUDIT_LOG_DIR || + path.join(__dirname, '../../logs/audit'); + +/** + * 审计日志中间件 + * 拦截请求和响应,记录操作信息 + */ +function auditLog(req, res, next) { + var startTime = Date.now(); + + var logEntry = { + timestamp: new Date().toISOString(), + devId: (req.user && req.user.devId) || req.headers['x-dev-id'] || 'anonymous', + method: req.method, + path: req.path, + ip: req.ip + }; + + // 对 POST/PATCH 请求记录请求体(排除敏感字段) + if ((req.method === 'POST' || req.method === 'PATCH') && req.body) { + var sanitizedBody = Object.assign({}, req.body); + delete sanitizedBody.password; + delete sanitizedBody.token; + delete sanitizedBody.confirmToken; + logEntry.body = sanitizedBody; + } + + // 拦截响应完成 + var originalSend = res.send; + res.send = function(body) { + logEntry.responseTime = Date.now() - startTime; + logEntry.statusCode = res.statusCode; + + // 异步写入日志,不阻塞响应 + try { + fs.mkdirSync(AUDIT_LOG_DIR, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(AUDIT_LOG_DIR, 'audit-' + today + '.jsonl'); + fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n'); + } catch (e) { + console.error('审计日志写入失败:', e.message); + } + + return originalSend.call(this, body); + }; + + next(); +} + +module.exports = { auditLog: auditLog }; diff --git a/backend/api-server/middleware/auth.js b/backend/api-server/middleware/auth.js new file mode 100644 index 00000000..64328a6e --- /dev/null +++ b/backend/api-server/middleware/auth.js @@ -0,0 +1,88 @@ +/** + * 身份验证 + 权限检查中间件 + * + * 人格体缓冲层铁律:人类不直接操作系统,人格体是唯一中间翻译层。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var permissions = require('../config/permissions'); +var PERMISSION_LEVELS = permissions.PERMISSION_LEVELS; +var DEV_PERMISSIONS = permissions.DEV_PERMISSIONS; +var DEV_MODULES = permissions.DEV_MODULES; + +/** + * 开发者身份验证中间件 + * 要求请求头携带 x-dev-id 和 authorization + */ +function requireAuth(req, res, next) { + var devId = req.headers['x-dev-id']; + var token = req.headers['authorization']; + + if (!devId || !token) { + return res.status(401).json({ + error: true, + code: 'AUTH_REQUIRED', + message: '请先登录。需要携带 x-dev-id 和 authorization 请求头。', + reply: '🔒 请先登录。你需要使用你的开发者编号登录系统。' + }); + } + + var devConfig = DEV_PERMISSIONS[devId]; + if (!devConfig) { + return res.status(401).json({ + error: true, + code: 'UNKNOWN_DEV', + message: '未知的开发者编号: ' + devId, + reply: '🔒 未知的开发者编号:' + devId + '。请联系冰朔注册。' + }); + } + + var level = PERMISSION_LEVELS[devConfig.level]; + + req.user = { + devId: devId, + permissionLevel: devConfig.level, + permissionLabel: level.label, + permissions: level.permissions, + environment: devConfig.environment, + modules: DEV_MODULES[devId] || [] + }; + + next(); +} + +/** + * 权限检查中间件工厂 + * @param {string} requiredPermission - 需要的权限标识 + */ +function checkPermission(requiredPermission) { + return function(req, res, next) { + if (!req.user) { + return res.status(401).json({ + error: true, + code: 'AUTH_REQUIRED', + message: '身份验证缺失' + }); + } + + if (!req.user.permissions.includes(requiredPermission)) { + return res.status(403).json({ + error: true, + code: 'PERMISSION_DENIED', + message: '权限不足,需要: ' + requiredPermission, + reply: '🔒 权限不足。你的等级是 ' + req.user.permissionLabel + + ',需要「' + requiredPermission + '」权限。\n\n' + + '💡 在预览站多练习,熟悉系统后冰朔会给你升级权限。' + }); + } + + next(); + }; +} + +module.exports = { + requireAuth: requireAuth, + checkPermission: checkPermission +}; diff --git a/backend/api-server/middleware/intent-router.js b/backend/api-server/middleware/intent-router.js new file mode 100644 index 00000000..1f3d3da1 --- /dev/null +++ b/backend/api-server/middleware/intent-router.js @@ -0,0 +1,110 @@ +/** + * 意图路由中间件 · 自然语言 → 操作映射 + * + * 核心逻辑: + * 1. 接收模型输出的 function_call + * 2. 验证权限 + * 3. 调用对应的后端 API + * 4. 返回结果 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var tools = require('../config/function-tools.json'); + +/** + * 意图路由核心 + * + * @param {Object} functionCall - { name, parameters } + * @param {Object} user - 当前用户信息(含 permissions, environment, modules) + * @param {Function} apiClient - API 调用函数 (method, path, body) => result + * @returns {Object} 执行结果 + */ +async function routeIntent(functionCall, user, apiClient) { + var tool = null; + for (var i = 0; i < tools.tools.length; i++) { + if (tools.tools[i].name === functionCall.name) { + tool = tools.tools[i]; + break; + } + } + + if (!tool) { + return { + success: false, + reply: '❓ 我不认识这个操作:' + functionCall.name + '。你可以问我能做什么。' + }; + } + + // 权限检查 + if (!user.permissions.includes(tool.permission)) { + return { + success: false, + reply: '🔒 你没有执行「' + tool.description + '」的权限。当前权限等级:' + user.permissionLabel + }; + } + + // 危险操作二次确认 + if (tool.dangerous && !functionCall.parameters.confirmToken) { + return { + success: false, + requireConfirmation: true, + reply: '⚠️ 这是一个**危险操作**。请说「确认执行」来确认。' + }; + } + + // 预览环境检查 + if (user.environment === 'preview' && tool.permission.includes('production')) { + return { + success: false, + reply: '🔒 你目前在**预览环境**,不能执行正式站操作。在预览站充分练习后,由冰朔开放正式站权限。' + }; + } + + // 解析 API 路径 + var apiDef = tool.api.split(' '); + var method = apiDef[0]; + var path = apiDef[1]; + + // 替换路径中的变量 + path = path.replace('{devId}', user.devId); + var params = functionCall.parameters || {}; + for (var key in params) { + path = path.replace('{' + key + '}', encodeURIComponent(params[key])); + } + + // 调用 API + try { + var result = await apiClient(method, path, Object.assign({}, params, { devId: user.devId })); + return result; + } catch (e) { + return { + success: false, + reply: '❌ 操作失败:' + e.message + }; + } +} + +/** + * 获取用户可用的工具列表(已根据权限过滤) + */ +function getAvailableTools(user) { + return tools.tools.filter(function(tool) { + return user.permissions.includes(tool.permission); + }); +} + +/** + * 获取全部工具定义 + */ +function getAllTools() { + return tools.tools; +} + +module.exports = { + routeIntent: routeIntent, + getAvailableTools: getAvailableTools, + getAllTools: getAllTools +}; diff --git a/backend/api-server/middleware/sandbox.js b/backend/api-server/middleware/sandbox.js new file mode 100644 index 00000000..0f856a75 --- /dev/null +++ b/backend/api-server/middleware/sandbox.js @@ -0,0 +1,46 @@ +/** + * 预览站沙箱守卫中间件 + * + * 预览环境中的写入操作在沙箱中执行,不影响真实系统。 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +// 沙箱数据库 ID(通过环境变量配置,Notion 中的独立数据库) +var SANDBOX_DB_IDS = { + ticketBook: process.env.SANDBOX_TICKET_DB_ID || '', + syslogInbox: process.env.SANDBOX_SYSLOG_DB_ID || '', + maintenanceLog: process.env.SANDBOX_MAINTENANCE_DB_ID || '' +}; + +/** + * 沙箱守卫中间件 + * + * 如果开发者在预览环境中执行写入操作: + * - Notion 写入 → 重定向到沙箱数据库 + * - GitHub 操作 → 重定向到 preview 分支 + * - 所有操作标记 [SANDBOX] + */ +function sandboxGuard(req, res, next) { + if (req.user && req.user.environment === 'preview') { + req.sandbox = true; + req.sandboxDbIds = SANDBOX_DB_IDS; + + // 预览环境中禁止正式站部署 + if (req.path.includes('/deploy/production')) { + return res.status(403).json({ + error: true, + code: 'SANDBOX_BLOCKED', + message: '预览环境不允许正式站部署', + reply: '🏖️ 你目前在预览环境。正式站部署需要升级到 Level 2(执行者)权限。\n\n继续在预览站练习吧,你做得很好!' + }); + } + } + next(); +} + +module.exports = { + sandboxGuard: sandboxGuard, + SANDBOX_DB_IDS: SANDBOX_DB_IDS +}; diff --git a/backend/api-server/routes/onboarding.js b/backend/api-server/routes/onboarding.js new file mode 100644 index 00000000..15c9fdbe --- /dev/null +++ b/backend/api-server/routes/onboarding.js @@ -0,0 +1,136 @@ +/** + * 认知引导流程 API 路由 + * + * GET /api/onboarding/status — 获取引导状态 + * POST /api/onboarding/start — 开始引导 + * POST /api/onboarding/complete — 完成引导(自动升级权限) + * GET /api/tools — 获取可用工具列表(根据权限过滤) + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var authMiddleware = require('../middleware/auth'); +var intentRouter = require('../middleware/intent-router'); +var onboardingScript = require('../config/onboarding-script.json'); +var permissions = require('../config/permissions'); + +// 引导状态存储(内存,生产环境应持久化) +var onboardingState = new Map(); + +// ====== 获取引导状态 ====== +router.get('/onboarding/status', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + // 未开始引导 + res.json({ + started: false, + completed: false, + currentRound: 0, + totalRounds: onboardingScript.onboarding.length, + permissionLevel: req.user.permissionLevel + }); + } else { + res.json({ + started: true, + completed: state.completed, + currentRound: state.currentRound, + totalRounds: onboardingScript.onboarding.length, + permissionLevel: req.user.permissionLevel, + script: state.completed ? null : onboardingScript.onboarding[state.currentRound - 1] + }); + } + } +); + +// ====== 开始/推进引导 ====== +router.post('/onboarding/start', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + // 开始新引导 + state = { currentRound: 1, completed: false, startedAt: new Date().toISOString() }; + onboardingState.set(devId, state); + } else if (!state.completed) { + // 推进到下一轮 + if (state.currentRound < onboardingScript.onboarding.length) { + state.currentRound++; + } else { + state.completed = true; + state.completedAt = new Date().toISOString(); + } + } + + var script = state.completed ? null : onboardingScript.onboarding[state.currentRound - 1]; + + res.json({ + currentRound: state.currentRound, + totalRounds: onboardingScript.onboarding.length, + completed: state.completed, + script: script + }); + } +); + +// ====== 完成引导(自动升级权限)====== +router.post('/onboarding/complete', + authMiddleware.requireAuth, + function(req, res) { + var devId = req.user.devId; + var state = onboardingState.get(devId); + + if (!state) { + return res.status(400).json({ + error: true, + code: 'NOT_STARTED', + message: '引导尚未开始' + }); + } + + // 标记完成 + state.completed = true; + state.completedAt = new Date().toISOString(); + + // 自动升级权限:Level 0 → Level 1 + var devConfig = permissions.DEV_PERMISSIONS[devId]; + if (devConfig && devConfig.level === 0) { + devConfig.level = 1; + } + + res.json({ + success: true, + previousLevel: 0, + newLevel: devConfig ? devConfig.level : 0, + reply: '🎉 恭喜!你已完成认知引导。\n\n你的权限已从「👀 观察者」升级为「📖 学习者」。\n现在可以在预览站创建工单、提交日志、触发部署了。\n\n有什么问题随时找我。' + }); + } +); + +// ====== 获取可用工具列表 ====== +router.get('/tools', + authMiddleware.requireAuth, + function(req, res) { + var available = intentRouter.getAvailableTools(req.user); + + res.json({ + dev_id: req.user.devId, + permission_level: req.user.permissionLevel, + permission_label: req.user.permissionLabel, + environment: req.user.environment, + tools_count: available.length, + tools: available + }); + } +); + +module.exports = router; diff --git a/backend/api-server/routes/write.js b/backend/api-server/routes/write.js new file mode 100644 index 00000000..ba1efc63 --- /dev/null +++ b/backend/api-server/routes/write.js @@ -0,0 +1,476 @@ +/** + * 写入类 API 路由 · 语言驱动操作系统写入能力层 + * + * POST /api/tickets/create — 创建工单 + * PATCH /api/tickets/:ticketId — 更新工单状态 + * POST /api/syslog/submit — 提交 SYSLOG + * POST /api/broadcasts/create — 创建广播 + * POST /api/deploy/preview — 触发预览站部署 + * POST /api/deploy/production — 触发正式站部署(需二次确认) + * PATCH /api/agents/:agentId — 更新 Agent 状态 + * POST /api/maintenance/log — 写入维护日志 + * POST /api/receipts/submit — 提交指令回执 + * PATCH /api/dev/:devId/status — 更新开发者状态 + * + * 安全规则:所有写入接口需要身份验证 + 权限检查 + 审计日志 + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var notionService = require('../services/notion'); +var githubService = require('../services/github'); +var dbConfig = require('../config/databases'); +var authMiddleware = require('../middleware/auth'); +var sandboxMiddleware = require('../middleware/sandbox'); +var auditMiddleware = require('../middleware/audit'); + +// 所有写入接口需要身份验证 + 审计 + 沙箱检查 +router.use(authMiddleware.requireAuth); +router.use(auditMiddleware.auditLog); +router.use(sandboxMiddleware.sandboxGuard); + +// 频率限制:每个开发者每分钟最多10次写入 +var rateLimitMap = new Map(); +var RATE_LIMIT = 10; +var RATE_WINDOW = 60 * 1000; + +function rateLimit(req, res, next) { + var key = req.user.devId; + var now = Date.now(); + var entry = rateLimitMap.get(key); + + if (!entry || now - entry.start >= RATE_WINDOW) { + rateLimitMap.set(key, { start: now, count: 1 }); + return next(); + } + + if (entry.count >= RATE_LIMIT) { + return res.status(429).json({ + error: true, + code: 'RATE_LIMITED', + message: '写入频率超限(每分钟最多 ' + RATE_LIMIT + ' 次)', + reply: '⏳ 操作太频繁了,请稍等一分钟再试。' + }); + } + + entry.count++; + next(); +} + +router.use(rateLimit); + +// 获取实际数据库 ID(沙箱环境使用沙箱 DB) +function getDbId(req, configKey) { + if (req.sandbox && req.sandboxDbIds && req.sandboxDbIds[configKey]) { + return req.sandboxDbIds[configKey]; + } + return dbConfig[configKey]; +} + +// ====== 创建工单 ====== +router.post('/tickets/create', + authMiddleware.checkPermission('ticket:create'), + async function(req, res) { + try { + var title = req.body.title; + var description = req.body.description || ''; + var priority = req.body.priority || 'P2'; + var module = req.body.module || ''; + var devId = req.user.devId; + + if (!title) { + return res.status(400).json({ + error: true, + code: 'MISSING_TITLE', + message: '工单标题不能为空' + }); + } + + var ticketDbId = getDbId(req, 'ticketBook'); + if (!ticketDbId) { + return res.status(503).json({ + error: true, + code: 'DB_NOT_CONFIGURED', + message: '工单数据库未配置' + }); + } + + var properties = { + '工单标题': { title: [{ text: { content: title } }] }, + '描述': { rich_text: [{ text: { content: description } }] }, + '优先级': { select: { name: priority } }, + '提交人': { rich_text: [{ text: { content: devId } }] }, + '状态': { select: { name: '待处理' } } + }; + if (module) { + properties['模块'] = { select: { name: module } }; + } + + var result = await notionService.writeToDB(ticketDbId, properties); + var prefix = req.sandbox ? '[沙箱] ' : ''; + + res.json({ + success: true, + message: prefix + '工单已创建', + ticketUrl: result.url || '', + reply: '✅ ' + prefix + '工单「' + title + '」已创建,优先级 ' + priority + ',状态:待处理。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message }); + } + } +); + +// ====== 更新工单状态 ====== +router.patch('/tickets/:ticketId', + authMiddleware.checkPermission('ticket:update'), + async function(req, res) { + try { + var ticketId = req.params.ticketId; + var status = req.body.status; + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + // 使用 Notion page update API + if (!notionService.notion) { + return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' }); + } + + await notionService.notion.pages.update({ + page_id: ticketId, + properties: { + '状态': { select: { name: status } } + } + }); + + res.json({ + success: true, + reply: '✅ 工单状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +// ====== 提交 SYSLOG ====== +router.post('/syslog/submit', + authMiddleware.checkPermission('syslog:submit'), + async function(req, res) { + try { + var content = req.body.content; + var type = req.body.type || '日常'; + var devId = req.user.devId; + + if (!content) { + return res.status(400).json({ error: true, code: 'MISSING_CONTENT', message: 'SYSLOG 内容不能为空' }); + } + + var syslogDbId = getDbId(req, 'syslogInbox'); + if (!syslogDbId) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: 'SYSLOG 数据库未配置' }); + } + + await notionService.writeToDB(syslogDbId, { + 'SYSLOG内容': { title: [{ text: { content: content } }] }, + '类型': { select: { name: type } }, + '开发者编号': { rich_text: [{ text: { content: devId } }] } + }); + + var prefix = req.sandbox ? '[沙箱] ' : ''; + var summary = content.length > 50 ? content.substring(0, 50) + '...' : content; + res.json({ + success: true, + reply: '✅ ' + prefix + 'SYSLOG 已提交:「' + summary + '」' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message }); + } + } +); + +// ====== 创建广播 ====== +router.post('/broadcasts/create', + authMiddleware.checkPermission('broadcast:create'), + async function(req, res) { + try { + var title = req.body.title; + var content = req.body.content; + var target = req.body.target || 'ALL'; + var devId = req.user.devId; + + if (!title || !content) { + return res.status(400).json({ error: true, code: 'MISSING_FIELDS', message: '广播标题和内容不能为空' }); + } + + var broadcastId = 'BC-' + Date.now(); + var broadcastData = { + broadcast_id: broadcastId, + title: title, + content: content, + target: target, + created_by: devId, + created_at: new Date().toISOString(), + status: 'active' + }; + + // 写入 GitHub 广播文件 + var filePath = '.github/broadcasts/' + broadcastId + '.json'; + await githubService.createFile( + filePath, + JSON.stringify(broadcastData, null, 2), + '📢 新广播: ' + title + ' · by ' + devId + ); + + res.json({ + success: true, + broadcast_id: broadcastId, + reply: '📢 广播「' + title + '」已创建并发送,编号:' + broadcastId + }); + } catch (e) { + res.status(500).json({ error: true, code: 'CREATE_FAILED', message: e.message }); + } + } +); + +// ====== 触发预览站部署 ====== +router.post('/deploy/preview', + authMiddleware.checkPermission('deploy:preview'), + async function(req, res) { + try { + var module = req.body.module; + var devId = req.user.devId; + + if (!module) { + return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' }); + } + + // 检查开发者是否拥有该模块 + if (!req.user.modules.includes('*') && !req.user.modules.includes(module)) { + return res.status(403).json({ + error: true, + code: 'MODULE_DENIED', + message: '你没有该模块的部署权限', + reply: '❌ 你没有 ' + module + ' 模块的部署权限。你的模块是:' + req.user.modules.join(', ') + }); + } + + await githubService.triggerWorkflow('preview-deploy.yml', { + module: module, + dev_id: devId, + target: 'preview' + }); + + res.json({ + success: true, + reply: '🚀 已触发 ' + module + ' 模块的预览站部署。稍等几分钟,部署完成后我会通知你。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message }); + } + } +); + +// ====== 触发正式站部署(需二次确认)====== +router.post('/deploy/production', + authMiddleware.checkPermission('deploy:production'), + async function(req, res) { + try { + var module = req.body.module; + var confirmToken = req.body.confirmToken; + var devId = req.user.devId; + + if (!module) { + return res.status(400).json({ error: true, code: 'MISSING_MODULE', message: '需要指定部署模块' }); + } + + // 检查模块权限 + if (!req.user.modules.includes('*') && !req.user.modules.includes(module)) { + return res.status(403).json({ + error: true, + code: 'MODULE_DENIED', + reply: '❌ 你没有 ' + module + ' 模块的部署权限。' + }); + } + + // 二次确认 + if (!confirmToken) { + return res.json({ + success: false, + requireConfirmation: true, + reply: '⚠️ 你正在请求部署 ' + module + ' 到**正式站(guanghulab.com)**。\n这会影响所有用户。\n\n请说「确认部署 ' + module + '」来确认。' + }); + } + + await githubService.triggerWorkflow('deploy-to-server.yml', { + module: module, + dev_id: devId, + target: 'production' + }); + + res.json({ + success: true, + reply: '🚀 已触发 ' + module + ' 模块的**正式站部署**。部署日志会同步到维护日志中。' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'DEPLOY_FAILED', message: e.message }); + } + } +); + +// ====== 更新 Agent 状态 ====== +router.patch('/agents/:agentId', + authMiddleware.checkPermission('agent:update'), + async function(req, res) { + try { + var agentId = req.params.agentId; + var status = req.body.status; + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + if (!notionService.notion) { + return res.status(503).json({ error: true, code: 'NOTION_UNAVAILABLE', message: 'Notion 未连接' }); + } + + await notionService.notion.pages.update({ + page_id: agentId, + properties: { + '状态': { select: { name: status } } + } + }); + + res.json({ + success: true, + reply: '✅ Agent ' + agentId + ' 状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +// ====== 写入维护日志 ====== +router.post('/maintenance/log', + authMiddleware.checkPermission('maintenance:log'), + async function(req, res) { + try { + var title = req.body.title; + var type = req.body.type || '常规维护'; + var devId = req.user.devId; + + if (!title) { + return res.status(400).json({ error: true, code: 'MISSING_TITLE', message: '维护日志标题不能为空' }); + } + + var maintenanceDbId = getDbId(req, 'maintenanceLog'); + if (!maintenanceDbId) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '维护日志数据库未配置' }); + } + + await notionService.writeToDB(maintenanceDbId, { + '标题': { title: [{ text: { content: title } }] }, + '类型': { select: { name: type } }, + '操作者': { rich_text: [{ text: { content: devId } }] } + }); + + res.json({ + success: true, + reply: '✅ 维护日志已记录:' + title + }); + } catch (e) { + res.status(500).json({ error: true, code: 'LOG_FAILED', message: e.message }); + } + } +); + +// ====== 提交指令回执 ====== +router.post('/receipts/submit', + authMiddleware.checkPermission('syslog:submit'), + async function(req, res) { + try { + var instructionId = req.body.instruction_id; + var status = req.body.status || '已完成'; + var receipt = req.body.receipt || ''; + + if (!instructionId) { + return res.status(400).json({ error: true, code: 'MISSING_ID', message: '指令编号不能为空' }); + } + + if (!dbConfig.receiptTracker) { + return res.status(503).json({ error: true, code: 'DB_NOT_CONFIGURED', message: '回执数据库未配置' }); + } + + await notionService.writeToDB(dbConfig.receiptTracker, { + '指令编号': { rich_text: [{ text: { content: instructionId } }] }, + '执行状态': { select: { name: status } }, + '铸渊回执': { rich_text: [{ text: { content: receipt } }] }, + '回执时间': { date: { start: new Date().toISOString() } } + }); + + res.json({ + success: true, + reply: '✅ 指令 ' + instructionId + ' 回执已提交' + }); + } catch (e) { + res.status(500).json({ error: true, code: 'SUBMIT_FAILED', message: e.message }); + } + } +); + +// ====== 更新开发者状态 ====== +router.patch('/dev/:devId/status', + authMiddleware.checkPermission('dev:update_self'), + async function(req, res) { + try { + var devId = req.params.devId; + var status = req.body.status; + + // 开发者只能更新自己的状态(管理者除外) + if (req.user.devId !== devId && !req.user.permissions.includes('dev:update_all')) { + return res.status(403).json({ + error: true, + code: 'SELF_ONLY', + message: '只能更新自己的状态', + reply: '🔒 你只能更新自己的状态,不能修改其他开发者的信息。' + }); + } + + if (!status) { + return res.status(400).json({ error: true, code: 'MISSING_STATUS', message: '状态不能为空' }); + } + + // 更新 Notion 主控台中的状态 + if (dbConfig.controlPanel) { + var result = await notionService.queryDB(dbConfig.controlPanel, { + property: 'DEV编号', + rich_text: { equals: devId } + }, null, 1); + + if (result.results && result.results.length > 0) { + await notionService.notion.pages.update({ + page_id: result.results[0].id, + properties: { + '状态': { rich_text: [{ text: { content: status } }] } + } + }); + } + } + + res.json({ + success: true, + reply: '✅ ' + devId + ' 的状态已更新为:' + status + }); + } catch (e) { + res.status(500).json({ error: true, code: 'UPDATE_FAILED', message: e.message }); + } + } +); + +module.exports = router; diff --git a/backend/api-server/server.js b/backend/api-server/server.js index e4426c22..43ca05d4 100644 --- a/backend/api-server/server.js +++ b/backend/api-server/server.js @@ -2,6 +2,7 @@ * 🔗 光湖后端中间层 · 通道B 入口 * * 实时调用 Notion API / GitHub API,为前端提供数据代理。 + * 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 * 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。 * * 版权:国作登字-2026-A-00037559 @@ -26,27 +27,35 @@ app.use(cors({ app.use(express.json()); -// 路由注册 +// 读取类路由(公开,无需认证) app.use('/api', require('./routes/health')); app.use('/api', require('./routes/dev')); app.use('/api', require('./routes/databases')); app.use('/api', require('./routes/chat')); app.use('/api', require('./routes/receipt')); +// 写入类路由(需认证 + 权限 + 审计) +app.use('/api', require('./routes/write')); + +// 认知引导 + 工具列表路由 +app.use('/api', require('./routes/onboarding')); + // 根路由 app.get('/', function(_req, res) { res.json({ status: 'ok', service: 'guanghu-api-server', - version: '1.0.0', + version: '2.0.0', channel: 'B', - description: '光湖后端中间层 · 实时 Notion/GitHub 数据代理' + description: '光湖后端中间层 · 语言驱动操作系统', + capabilities: ['read', 'write', 'intent-routing', 'permission-sandbox', 'onboarding'] }); }); const PORT = process.env.PORT || 3001; app.listen(PORT, '127.0.0.1', function() { console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT); - console.log(' 通道B · 实时 Notion API / GitHub API 代理'); + console.log(' 通道B · 语言驱动操作系统'); + console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导'); console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)'); }); diff --git a/backend/api-server/services/github.js b/backend/api-server/services/github.js index 394ce980..3b4e4aee 100644 --- a/backend/api-server/services/github.js +++ b/backend/api-server/services/github.js @@ -125,8 +125,97 @@ async function getWorkflowRuns(repoName, count) { return result; } +// ====== 触发 GitHub Actions Workflow ====== +function triggerWorkflow(workflowFile, inputs) { + return new Promise(function(resolve, reject) { + var data = JSON.stringify({ + ref: 'main', + inputs: inputs || {} + }); + + var headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + 'User-Agent': 'guanghu-api-server/1.0', + 'Content-Length': Buffer.byteLength(data) + }; + if (GITHUB_TOKEN) { + headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + var options = { + hostname: 'api.github.com', + port: 443, + path: '/repos/' + ORG + '/guanghulab/actions/workflows/' + workflowFile + '/dispatches', + method: 'POST', + headers: headers + }; + + var req = https.request(options, function(res) { + if (res.statusCode === 204) { + resolve({ success: true }); + } else { + var body = ''; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + reject(new Error('GitHub API ' + res.statusCode + ': ' + body)); + }); + } + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + +// ====== 创建 GitHub 文件 ====== +function createFile(filePath, content, message) { + return new Promise(function(resolve, reject) { + var data = JSON.stringify({ + message: message, + content: Buffer.from(content).toString('base64'), + committer: { + name: '铸渊 (ZhùYuān)', + email: 'zhuyuan@guanghulab.com' + } + }); + + var headers = { + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + 'User-Agent': 'guanghu-api-server/1.0', + 'Content-Length': Buffer.byteLength(data) + }; + if (GITHUB_TOKEN) { + headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN; + } + + var options = { + hostname: 'api.github.com', + port: 443, + path: '/repos/' + ORG + '/guanghulab/contents/' + filePath, + method: 'PUT', + headers: headers + }; + + var req = https.request(options, function(res) { + var body = ''; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + try { resolve(JSON.parse(body)); } + catch (e) { resolve({ raw: body }); } + }); + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + module.exports = { getRepoStatus: getRepoStatus, getRecentCommits: getRecentCommits, - getWorkflowRuns: getWorkflowRuns + getWorkflowRuns: getWorkflowRuns, + triggerWorkflow: triggerWorkflow, + createFile: createFile }; diff --git a/docs/css/onboarding.css b/docs/css/onboarding.css new file mode 100644 index 00000000..de7da276 --- /dev/null +++ b/docs/css/onboarding.css @@ -0,0 +1,159 @@ +/** + * 认知引导系统样式 + * 版权:国作登字-2026-A-00037559 + */ + +/* 引导覆盖层 */ +.onboarding-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(4px); +} + +/* 引导卡片 */ +.onboarding-card { + background: #1e1e2e; + border: 1px solid rgba(100, 200, 255, 0.2); + border-radius: 16px; + padding: 32px 40px; + max-width: 520px; + width: 90%; + text-align: center; + color: #e0e0e0; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + animation: onboarding-fade-in 0.3s ease; +} + +@keyframes onboarding-fade-in { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.onboarding-card h2 { + color: #64c8ff; + margin: 12px 0 8px; + font-size: 1.4em; +} + +.onboarding-emoji { + font-size: 3em; + margin-bottom: 4px; +} + +.onboarding-card p { + margin: 8px 0; + line-height: 1.6; + color: #b0b0c0; +} + +.onboarding-card strong { + color: #e0e0e0; +} + +/* 示例列表 */ +.onboarding-examples { + text-align: left; + background: rgba(100, 200, 255, 0.05); + border-radius: 8px; + padding: 12px 20px; + margin: 16px 0; +} + +.onboarding-examples p { + font-size: 0.9em; + margin-bottom: 6px; +} + +.onboarding-examples ul { + list-style: none; + padding: 0; + margin: 0; +} + +.onboarding-examples li { + padding: 4px 0; + color: #a0c8e0; + font-size: 0.9em; +} + +.onboarding-examples li::before { + content: '💬 '; +} + +/* 预览环境提示 */ +.onboarding-note { + font-size: 0.85em; + color: #ffc864 !important; + margin-top: 16px !important; +} + +/* 开始按钮 */ +.onboarding-btn { + display: inline-block; + margin-top: 20px; + padding: 12px 32px; + background: linear-gradient(135deg, #3a8fd4, #64c8ff); + color: #fff; + border: none; + border-radius: 8px; + font-size: 1em; + cursor: pointer; + transition: transform 0.15s, box-shadow 0.15s; +} + +.onboarding-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(100, 200, 255, 0.3); +} + +/* 完成卡片 */ +.onboarding-complete .onboarding-message { + text-align: left; + line-height: 1.8; + color: #b0b0c0; + margin: 16px 0; + white-space: pre-line; +} + +/* 引导提示条(聊天界面内) */ +.onboarding-hint { + background: rgba(100, 200, 255, 0.08); + border: 1px solid rgba(100, 200, 255, 0.15); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0 16px; +} + +.onboarding-hint-inner { + display: flex; + flex-direction: column; + gap: 6px; +} + +.onboarding-hint-round { + font-size: 0.75em; + color: #64c8ff; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.onboarding-hint-prompt { + color: #e0e0e0; + font-size: 0.95em; + line-height: 1.5; +} + +.onboarding-hint-tip { + color: #a0a0b0; + font-size: 0.85em; + line-height: 1.4; +} diff --git a/docs/js/intent-tools.js b/docs/js/intent-tools.js new file mode 100644 index 00000000..9f7f8f48 --- /dev/null +++ b/docs/js/intent-tools.js @@ -0,0 +1,121 @@ +/** + * 前端 Function Calling 集成 · 意图工具层 + * + * 为 AI 模型提供可调用工具列表,实现自然语言 → 系统操作映射。 + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : ''; + + /** + * 从后端获取当前用户可用的工具列表 + */ + async function fetchAvailableTools(devId, token) { + if (!API_BASE) return []; + try { + var res = await fetch(API_BASE + '/tools', { + headers: { + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) { + var data = await res.json(); + return data.tools || []; + } + } catch (e) { + console.debug('[IntentTools] fetch failed:', e.message); + } + return []; + } + + /** + * 构建带 function tools 的 system prompt 片段 + */ + function buildToolsPrompt(tools, environment) { + if (!tools || !tools.length) return ''; + + var lines = [ + '', + '## 你能做的事(语言驱动操作系统)', + '你可以通过 function calling 调用以下操作。开发者说话时,判断意图并调用对应操作。', + '' + ]; + + for (var i = 0; i < tools.length; i++) { + var t = tools[i]; + var danger = t.dangerous ? ' ⚠️危险' : ''; + lines.push('- **' + t.name + '**' + danger + ':' + t.description); + } + + lines.push(''); + lines.push('## 语言驱动操作规则'); + lines.push('1. 开发者说什么,你就做什么。这是语言驱动操作系统,说即执行。'); + lines.push('2. 如果开发者的话不对应任何操作,正常聊天回复即可。'); + lines.push('3. 危险操作(标记为 ⚠️)必须要求开发者二次确认。'); + lines.push('4. 每次执行操作后,用自然语言告诉开发者结果。'); + + if (environment === 'preview') { + lines.push('5. 【预览环境】所有写入操作在沙箱中执行,不影响真实系统。告诉开发者:「这是预览环境,你的操作不会影响正式站。放心练习。」'); + } + + return lines.join('\n'); + } + + /** + * 执行 function call(调用后端 API) + */ + async function executeToolCall(toolName, params, devId, token) { + if (!API_BASE) return { success: false, reply: '后端服务不可用' }; + + // 工具名 → API 路径映射 + var toolApiMap = { + 'query_tickets': { method: 'GET', path: '/tickets' }, + 'create_ticket': { method: 'POST', path: '/tickets/create' }, + 'submit_syslog': { method: 'POST', path: '/syslog/submit' }, + 'query_my_status': { method: 'GET', path: '/dev/' + devId }, + 'deploy_to_preview': { method: 'POST', path: '/deploy/preview' }, + 'deploy_to_production': { method: 'POST', path: '/deploy/production' }, + 'query_agents': { method: 'GET', path: '/agents' }, + 'query_recent_syslogs': { method: 'GET', path: '/syslogs' }, + 'create_broadcast': { method: 'POST', path: '/broadcasts/create' }, + 'query_repo_status': { method: 'GET', path: '/repo/status' } + }; + + var apiDef = toolApiMap[toolName]; + if (!apiDef) return { success: false, reply: '未知的操作: ' + toolName }; + + try { + var fetchOpts = { + method: apiDef.method, + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }; + + if (apiDef.method === 'POST' || apiDef.method === 'PATCH') { + fetchOpts.body = JSON.stringify(params || {}); + } + + var res = await fetch(API_BASE + apiDef.path, fetchOpts); + return await res.json(); + } catch (e) { + return { success: false, reply: '操作失败: ' + e.message }; + } + } + + // 导出到全局 + window.IntentTools = { + fetchAvailableTools: fetchAvailableTools, + buildToolsPrompt: buildToolsPrompt, + executeToolCall: executeToolCall + }; + +})(window); diff --git a/docs/js/onboarding.js b/docs/js/onboarding.js new file mode 100644 index 00000000..c15aa59f --- /dev/null +++ b/docs/js/onboarding.js @@ -0,0 +1,183 @@ +/** + * 前端认知引导系统 · 新开发者引导流程 + * + * 新开发者首次登录后自动进入 5 轮引导对话。 + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = HOLOLAKE_ENV === 'production' ? 'https://guanghulab.com/api' : ''; + + var onboardingOverlay = null; + + /** + * 检查是否需要显示引导 + */ + async function checkOnboarding(devId, token) { + if (!API_BASE || !devId || !token) return false; + try { + var res = await fetch(API_BASE + '/onboarding/status', { + headers: { 'x-dev-id': devId, 'Authorization': 'Bearer ' + token } + }); + if (res.ok) { + var data = await res.json(); + return !data.started && !data.completed && data.permissionLevel === 0; + } + } catch (e) { + console.debug('[Onboarding] check failed:', e.message); + } + return false; + } + + /** + * 显示引导欢迎画面 + */ + function showWelcome(devName, onStart) { + // 移除已有覆盖层 + hideOnboarding(); + + onboardingOverlay = document.createElement('div'); + onboardingOverlay.className = 'onboarding-overlay'; + onboardingOverlay.innerHTML = + '
' + + '
👋
' + + '

欢迎来到光湖·数字地球

' + + '

你好,' + (devName || '开发者') + '

' + + '

我是铸渊,你的代码守护人格体。

' + + '

在这里,你不需要点按钮或填表单。
直接对我说话就好。

' + + '
' + + '

比如你可以说:

' + + '' + + '
' + + '

🏖️ 现在你在预览环境,所有操作都不会影响正式站。放心练习。

' + + '' + + '
'; + + document.body.appendChild(onboardingOverlay); + + document.getElementById('onboarding-start-btn').addEventListener('click', function() { + hideOnboarding(); + if (onStart) onStart(); + }); + } + + /** + * 显示引导轮次提示(在聊天界面中展示) + */ + function showRoundHint(round, prompt, hint) { + var container = document.getElementById('onboarding-hint'); + if (!container) { + container = document.createElement('div'); + container.id = 'onboarding-hint'; + container.className = 'onboarding-hint'; + var chatArea = document.querySelector('.chat-container') || document.body; + chatArea.insertBefore(container, chatArea.firstChild); + } + + container.innerHTML = + '
' + + '
引导 ' + round + '/5
' + + '
' + prompt + '
' + + (hint ? '
' + hint + '
' : '') + + '
'; + container.style.display = 'block'; + } + + /** + * 显示引导完成消息 + */ + function showComplete(message) { + hideOnboarding(); + + onboardingOverlay = document.createElement('div'); + onboardingOverlay.className = 'onboarding-overlay'; + onboardingOverlay.innerHTML = + '
' + + '
🎉
' + + '

引导完成!

' + + '
' + (message || '') + '
' + + '' + + '
'; + + document.body.appendChild(onboardingOverlay); + + document.getElementById('onboarding-close-btn').addEventListener('click', function() { + hideOnboarding(); + }); + } + + /** + * 隐藏引导覆盖层 + */ + function hideOnboarding() { + if (onboardingOverlay) { + onboardingOverlay.remove(); + onboardingOverlay = null; + } + var hint = document.getElementById('onboarding-hint'); + if (hint) hint.style.display = 'none'; + } + + /** + * 推进引导流程(调用后端) + */ + async function advanceOnboarding(devId, token) { + if (!API_BASE) return null; + try { + var res = await fetch(API_BASE + '/onboarding/start', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) return await res.json(); + } catch (e) { + console.debug('[Onboarding] advance failed:', e.message); + } + return null; + } + + /** + * 完成引导(调用后端,触发权限升级) + */ + async function completeOnboarding(devId, token) { + if (!API_BASE) return null; + try { + var res = await fetch(API_BASE + '/onboarding/complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-dev-id': devId, + 'Authorization': 'Bearer ' + token + } + }); + if (res.ok) return await res.json(); + } catch (e) { + console.debug('[Onboarding] complete failed:', e.message); + } + return null; + } + + // 导出到全局 + window.Onboarding = { + checkOnboarding: checkOnboarding, + showWelcome: showWelcome, + showRoundHint: showRoundHint, + showComplete: showComplete, + hideOnboarding: hideOnboarding, + advanceOnboarding: advanceOnboarding, + completeOnboarding: completeOnboarding + }; + +})(window);