Agent/packages/shared/module-registry.js

368 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
// ============================================================
// 模块注册表 —— SOURCE OF TRUTHmigration-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,
};