368 lines
13 KiB
JavaScript
368 lines
13 KiB
JavaScript
'use strict';
|
||
|
||
// ============================================================
|
||
// 模块注册表 —— SOURCE OF TRUTH(migration-plan §5.4)
|
||
// 服务端 modules 表仅承载 status/override;本文件为唯一权威源。
|
||
// 新增模块:在此声明 + DDL + Precomputer,参考 §15 扩展指南。
|
||
// ============================================================
|
||
|
||
/**
|
||
* @typedef {Object} ModuleDescriptor
|
||
* @property {string} key
|
||
* @property {string} label
|
||
* @property {string} rawTable
|
||
* @property {{ l1Monthly?: string, l1Detail?: string, l2Yearly?: string }} statsTables
|
||
* @property {'monthly'|'full'|'none'} uploadMode
|
||
* @property {string[]} uploadDimensions
|
||
* @property {string} [uploadParser]
|
||
* @property {string} [precomputer] 有 statsTables 时必填(见 validateRegistry)
|
||
* @property {Array<'month'|'quarter'|'year'>} statsGrain
|
||
* @property {string[]} statsDimensions
|
||
* @property {string} rbacModule
|
||
* @property {'full_mirror'|'dictionary'|'monthly'|'none'} cacheStrategy
|
||
* @property {string[]} syncTables
|
||
* @property {Array<'details'|'extras'>} extensibleFields
|
||
* @property {{ icon: string, menuGroup: string, sortOrder: number, route: string, pageComponent: string }} ui
|
||
* @property {boolean} mcpExposed
|
||
* @property {string[]} [mcpTools]
|
||
*/
|
||
|
||
/** @type {ModuleDescriptor[]} */
|
||
const MODULE_REGISTRY = [
|
||
{
|
||
key: 'online_exams',
|
||
label: '在线考试',
|
||
rawTable: 'online_exams',
|
||
statsTables: {
|
||
l1Monthly: 'online_exam_stats_monthly',
|
||
l1Detail: 'online_exam_question_stats_monthly',
|
||
l2Yearly: 'online_exam_stats_yearly',
|
||
},
|
||
uploadMode: 'incremental',
|
||
uploadDimensions: ['exam_uuid', 'source_machine', 'source_row_id'],
|
||
uploadParser: 'OnlineExamParser',
|
||
precomputer: 'OnlineExamsPrecomputer',
|
||
statsGrain: ['month', 'year'],
|
||
statsDimensions: ['team', 'paper_class'],
|
||
rbacModule: 'online_exams',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: [
|
||
'online_exams',
|
||
'online_exam_stats_monthly',
|
||
'online_exam_question_stats_monthly',
|
||
'online_exam_stats_yearly',
|
||
'online_exam_category_stats_monthly',
|
||
'online_exam_category_stats_yearly',
|
||
],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'laptop', menuGroup: 'exams', sortOrder: 10, route: '/online-exams', pageComponent: 'pages/online-exams.js' },
|
||
mcpExposed: true,
|
||
mcpTools: [
|
||
'profile_query_online_exam_stats_monthly',
|
||
'profile_query_online_exam_stats_yearly',
|
||
'profile_query_online_exam_question_stats',
|
||
'profile_query_online_exam_trend',
|
||
'profile_get_pass_rate_ranking',
|
||
],
|
||
},
|
||
{
|
||
key: 'practical_exams',
|
||
label: '实操考试',
|
||
rawTable: 'practical_exams',
|
||
statsTables: {
|
||
l1Monthly: 'practical_exam_stats_monthly',
|
||
l1Detail: 'practical_exam_step_stats_monthly',
|
||
l2Yearly: 'practical_exam_stats_yearly',
|
||
},
|
||
uploadMode: 'monthly',
|
||
uploadDimensions: ['year', 'month'],
|
||
uploadParser: 'PracticalExamParser',
|
||
precomputer: 'PracticalExamsPrecomputer',
|
||
statsGrain: ['month', 'year'],
|
||
statsDimensions: ['employee_id', 'team', 'all', 'paper_id', 'step_index'],
|
||
rbacModule: 'practical_exams',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: [
|
||
'practical_exams',
|
||
'papers',
|
||
'paper_steps',
|
||
'practical_exam_stats_monthly',
|
||
'practical_exam_step_stats_monthly',
|
||
'practical_exam_person_stats_monthly',
|
||
'practical_exam_scope_stats_monthly',
|
||
'practical_exam_stats_yearly',
|
||
'practical_exam_person_stats_yearly',
|
||
'practical_exam_scope_stats_yearly',
|
||
],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'tools', menuGroup: 'exams', sortOrder: 20, route: '/practical-exams', pageComponent: 'pages/practical-exams.js' },
|
||
mcpExposed: true,
|
||
mcpTools: [
|
||
'profile_query_practical_exam_stats_monthly',
|
||
'profile_query_practical_exam_stats_yearly',
|
||
'profile_query_practical_exam_step_error_rate',
|
||
'profile_query_practical_exam_top_errors',
|
||
'profile_query_paper_list',
|
||
],
|
||
},
|
||
{
|
||
key: 'kilometers',
|
||
label: '公里数',
|
||
rawTable: 'kilometers_records',
|
||
statsTables: {
|
||
l1Monthly: 'kilometers_stats_team_monthly',
|
||
l1Detail: 'kilometers_stats_employee_monthly',
|
||
l2Yearly: 'kilometers_stats_team_yearly',
|
||
},
|
||
uploadMode: 'monthly',
|
||
uploadDimensions: ['year', 'month'],
|
||
uploadParser: 'KilometersParser',
|
||
precomputer: 'KilometersPrecomputer',
|
||
statsGrain: ['month', 'year'],
|
||
statsDimensions: ['team', 'employee_id'],
|
||
rbacModule: 'kilometers',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: [
|
||
'kilometers_records',
|
||
'kilometers_stats_employee_monthly',
|
||
'kilometers_stats_team_monthly',
|
||
'kilometers_stats_overall_monthly',
|
||
'kilometers_control_stats_monthly',
|
||
'kilometers_stats_employee_yearly',
|
||
'kilometers_stats_team_yearly',
|
||
'kilometers_stats_overall_yearly',
|
||
],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'road', menuGroup: 'operation', sortOrder: 30, route: '/kilometers', pageComponent: 'pages/kilometers.js' },
|
||
mcpExposed: true,
|
||
mcpTools: [
|
||
'profile_query_kilometers_team_monthly',
|
||
'profile_query_kilometers_team_yearly',
|
||
'profile_query_kilometers_employee',
|
||
'profile_query_kilometers_top_drivers',
|
||
],
|
||
},
|
||
{
|
||
key: 'employees',
|
||
label: '员工档案',
|
||
rawTable: 'employees',
|
||
statsTables: {},
|
||
uploadMode: 'full',
|
||
uploadDimensions: [],
|
||
uploadParser: 'EmployeeParser',
|
||
statsGrain: [],
|
||
statsDimensions: [],
|
||
rbacModule: 'employees',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['employees'],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'users', menuGroup: 'base', sortOrder: 1, route: '/employees', pageComponent: 'pages/employees.js' },
|
||
mcpExposed: true, // 脱敏后
|
||
mcpTools: [
|
||
'profile_query_employees',
|
||
'profile_get_employee_by_id',
|
||
'profile_query_team_roster',
|
||
'profile_query_employee_history',
|
||
],
|
||
},
|
||
{
|
||
key: 'question_banks',
|
||
label: '题库',
|
||
rawTable: 'question_banks',
|
||
statsTables: {},
|
||
uploadMode: 'full',
|
||
uploadDimensions: [],
|
||
uploadParser: 'QuestionBankParser',
|
||
statsGrain: [],
|
||
statsDimensions: [],
|
||
rbacModule: 'questions',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['question_banks'],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'book', menuGroup: 'base', sortOrder: 5, route: '/question-banks', pageComponent: 'pages/question-banks.js' },
|
||
mcpExposed: true,
|
||
mcpTools: ['profile_query_question_bank', 'profile_get_paper_steps', 'profile_query_question_categories'],
|
||
},
|
||
{
|
||
key: 'missing_exams',
|
||
label: '缺考',
|
||
rawTable: 'missing_exams',
|
||
statsTables: {},
|
||
uploadMode: 'monthly',
|
||
uploadDimensions: ['year', 'month'],
|
||
uploadParser: 'MissingExamParser',
|
||
statsGrain: [],
|
||
statsDimensions: [],
|
||
rbacModule: 'online_exams', // 缺考归在线考试权限域(§4.1.4.4)
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['missing_exams'],
|
||
extensibleFields: ['details', 'extras'],
|
||
ui: { icon: 'user-x', menuGroup: 'exams', sortOrder: 15, route: '/missing-exams', pageComponent: 'pages/missing-exams.js' },
|
||
mcpExposed: true,
|
||
mcpTools: ['profile_query_missing_exams', 'profile_get_missing_summary'],
|
||
},
|
||
{
|
||
key: 'performance_assessments',
|
||
label: '绩效考核',
|
||
rawTable: 'performance_assessments',
|
||
statsTables: {},
|
||
uploadMode: 'full',
|
||
uploadDimensions: [],
|
||
uploadParser: 'MonthlyMaterialsParser',
|
||
statsGrain: [],
|
||
statsDimensions: ['team', 'employee_id'],
|
||
rbacModule: 'performance',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['performance_assessments'],
|
||
extensibleFields: [],
|
||
ui: { icon: 'award', menuGroup: 'operation', sortOrder: 40, route: '/performance-assessments', pageComponent: 'pages/imported-records.js' },
|
||
mcpExposed: true,
|
||
mcpTools: ['profile_query_performance_assessments'],
|
||
},
|
||
{
|
||
key: 'fault_disposals',
|
||
label: '故障处置',
|
||
rawTable: 'fault_disposals',
|
||
statsTables: {},
|
||
uploadMode: 'full',
|
||
uploadDimensions: [],
|
||
uploadParser: 'MonthlyMaterialsParser',
|
||
statsGrain: [],
|
||
statsDimensions: ['fault_type', 'employee_id'],
|
||
rbacModule: 'faults',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['fault_disposals'],
|
||
extensibleFields: [],
|
||
ui: { icon: 'alert-triangle', menuGroup: 'operation', sortOrder: 41, route: '/fault-disposals', pageComponent: 'pages/imported-records.js' },
|
||
mcpExposed: true,
|
||
mcpTools: ['profile_query_fault_disposals'],
|
||
},
|
||
{
|
||
key: 'incident_events',
|
||
label: '事故事件',
|
||
rawTable: 'incident_events',
|
||
statsTables: {},
|
||
uploadMode: 'full',
|
||
uploadDimensions: [],
|
||
uploadParser: 'MonthlyMaterialsParser',
|
||
statsGrain: [],
|
||
statsDimensions: ['fault_category', 'employee_id'],
|
||
rbacModule: 'incidents',
|
||
cacheStrategy: 'full_mirror',
|
||
syncTables: ['incident_events'],
|
||
extensibleFields: [],
|
||
ui: { icon: 'shield-alert', menuGroup: 'operation', sortOrder: 42, route: '/incident-events', pageComponent: 'pages/imported-records.js' },
|
||
mcpExposed: true,
|
||
mcpTools: ['profile_query_incident_events'],
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 客户端镜像表中文短名(同步进度展示用)
|
||
* 键为逻辑表名;未收录时回退为原英文名。
|
||
*/
|
||
const SYNC_TABLE_LABELS = {
|
||
employees: '员工档案',
|
||
question_banks: '题库',
|
||
missing_exams: '缺考明细',
|
||
performance_assessments: '绩效考核',
|
||
fault_disposals: '故障处置',
|
||
incident_events: '事故事件',
|
||
online_exams: '在线考试明细',
|
||
online_exam_stats_monthly: '在线考试月度统计',
|
||
online_exam_question_stats_monthly: '在线考试题目月度统计',
|
||
online_exam_stats_yearly: '在线考试年度统计',
|
||
online_exam_category_stats_monthly: '在线考试分类月度统计',
|
||
online_exam_category_stats_yearly: '在线考试分类年度统计',
|
||
practical_exams: '实操考试明细',
|
||
papers: '实操试卷',
|
||
paper_steps: '实操试卷步骤',
|
||
practical_exam_stats_monthly: '实操考试月度统计',
|
||
practical_exam_step_stats_monthly: '实操步骤月度统计',
|
||
practical_exam_person_stats_monthly: '实操个人月度统计',
|
||
practical_exam_scope_stats_monthly: '实操范围月度统计',
|
||
practical_exam_stats_yearly: '实操考试年度统计',
|
||
practical_exam_person_stats_yearly: '实操个人年度统计',
|
||
practical_exam_scope_stats_yearly: '实操范围年度统计',
|
||
kilometers_records: '公里数明细',
|
||
kilometers_stats_employee_monthly: '员工公里数月度统计',
|
||
kilometers_stats_team_monthly: '班组公里数月度统计',
|
||
kilometers_stats_overall_monthly: '整体公里数月度统计',
|
||
kilometers_control_stats_monthly: '公里数控制图月度统计',
|
||
kilometers_stats_employee_yearly: '员工公里数年度统计',
|
||
kilometers_stats_team_yearly: '班组公里数年度统计',
|
||
kilometers_stats_overall_yearly: '整体公里数年度统计',
|
||
dashboard_summary: '首页看板摘要',
|
||
crew_metrics_employee_yearly: '乘务画像年度指标',
|
||
crew_metrics_employee_monthly: '乘务画像月度指标',
|
||
practical_exam_steps: '实操步骤明细',
|
||
modules: '模块元数据',
|
||
settings: '系统配置',
|
||
upload_batches: '上传批次',
|
||
sync_table_versions: '同步版本',
|
||
sync_log: '同步日志',
|
||
};
|
||
|
||
/**
|
||
* @param {string} table
|
||
* @returns {string}
|
||
*/
|
||
function syncTableLabel(table) {
|
||
return SYNC_TABLE_LABELS[table] || table;
|
||
}
|
||
|
||
/**
|
||
* 全部参与同步的表(去重),供 SyncModule 白名单使用
|
||
* @returns {string[]}
|
||
*/
|
||
function allSyncTables() {
|
||
const set = new Set();
|
||
for (const m of MODULE_REGISTRY) {
|
||
for (const t of m.syncTables) set.add(t);
|
||
}
|
||
set.add('dashboard_summary');
|
||
set.add('crew_metrics_employee_yearly');
|
||
set.add('crew_metrics_employee_monthly');
|
||
return [...set];
|
||
}
|
||
|
||
/**
|
||
* 注册表结构校验(启动时调用)。
|
||
* 规则(review v3.x N5 定稿):statsTables 非空 ⇒ precomputer 必填。
|
||
* @param {ModuleDescriptor[]} [registry]
|
||
* @returns {string[]} 错误列表(空数组表示通过)
|
||
*/
|
||
function validateRegistry(registry = MODULE_REGISTRY) {
|
||
const errors = [];
|
||
const seen = new Set();
|
||
for (const m of registry) {
|
||
if (seen.has(m.key)) errors.push(`duplicate module key: ${m.key}`);
|
||
seen.add(m.key);
|
||
|
||
const hasStats = Object.values(m.statsTables || {}).some(Boolean);
|
||
if (hasStats && !m.precomputer) {
|
||
errors.push(`module ${m.key}: has statsTables but no precomputer`);
|
||
}
|
||
if (m.uploadMode === 'monthly' && (!m.uploadDimensions || m.uploadDimensions.length === 0)) {
|
||
errors.push(`module ${m.key}: uploadMode=monthly requires uploadDimensions`);
|
||
}
|
||
if (m.mcpExposed && (!m.mcpTools || m.mcpTools.length === 0)) {
|
||
errors.push(`module ${m.key}: mcpExposed but no mcpTools declared`);
|
||
}
|
||
for (const t of m.mcpTools || []) {
|
||
if (!/^profile_(query|get|list)_[a-z0-9_]+$/.test(t)) {
|
||
errors.push(`module ${m.key}: mcp tool "${t}" violates naming (§16.4.1, 只读动词)`);
|
||
}
|
||
}
|
||
}
|
||
return errors;
|
||
}
|
||
|
||
module.exports = {
|
||
MODULE_REGISTRY,
|
||
SYNC_TABLE_LABELS,
|
||
syncTableLabel,
|
||
allSyncTables,
|
||
validateRegistry,
|
||
};
|