feat: implement language-driven OS capabilities (ZY-LANGDRIVE)
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
This commit is contained in:
parent
726b05bc0b
commit
38d8afef55
|
|
@ -2,3 +2,4 @@
|
|||
node_modules/
|
||||
*.bak
|
||||
*.log
|
||||
backend/api-server/logs/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 + '(仅本机访问)');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 =
|
||||
'<div class="onboarding-card">' +
|
||||
'<div class="onboarding-emoji">👋</div>' +
|
||||
'<h2>欢迎来到光湖·数字地球</h2>' +
|
||||
'<p>你好,<strong>' + (devName || '开发者') + '</strong>!</p>' +
|
||||
'<p>我是铸渊,你的代码守护人格体。</p>' +
|
||||
'<p>在这里,你不需要点按钮或填表单。<br><strong>直接对我说话就好。</strong></p>' +
|
||||
'<div class="onboarding-examples">' +
|
||||
'<p>比如你可以说:</p>' +
|
||||
'<ul>' +
|
||||
'<li>「我的工单还有几个没关?」</li>' +
|
||||
'<li>「帮我建一个P2的Bug工单」</li>' +
|
||||
'<li>「我想部署模块到预览站」</li>' +
|
||||
'<li>「最近系统有什么日志?」</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<p class="onboarding-note">🏖️ 现在你在<strong>预览环境</strong>,所有操作都不会影响正式站。放心练习。</p>' +
|
||||
'<button class="onboarding-btn" id="onboarding-start-btn">开始体验 →</button>' +
|
||||
'</div>';
|
||||
|
||||
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 =
|
||||
'<div class="onboarding-hint-inner">' +
|
||||
'<div class="onboarding-hint-round">引导 ' + round + '/5</div>' +
|
||||
'<div class="onboarding-hint-prompt">' + prompt + '</div>' +
|
||||
(hint ? '<div class="onboarding-hint-tip">' + hint + '</div>' : '') +
|
||||
'</div>';
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示引导完成消息
|
||||
*/
|
||||
function showComplete(message) {
|
||||
hideOnboarding();
|
||||
|
||||
onboardingOverlay = document.createElement('div');
|
||||
onboardingOverlay.className = 'onboarding-overlay';
|
||||
onboardingOverlay.innerHTML =
|
||||
'<div class="onboarding-card onboarding-complete">' +
|
||||
'<div class="onboarding-emoji">🎉</div>' +
|
||||
'<h2>引导完成!</h2>' +
|
||||
'<div class="onboarding-message">' + (message || '') + '</div>' +
|
||||
'<button class="onboarding-btn" id="onboarding-close-btn">开始使用 →</button>' +
|
||||
'</div>';
|
||||
|
||||
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);
|
||||
Loading…
Reference in New Issue