@xiaoyuanhao/yunzhi-cli 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/arguments.cjs +7 -4
  2. package/lib/attendance-clock-entry.cjs +31 -0
  3. package/lib/attendance-configuration-entry.cjs +35 -0
  4. package/lib/attendance-correction-applications.cjs +64 -0
  5. package/lib/attendance-correction-approval.cjs +135 -0
  6. package/lib/attendance-correction-start.cjs +47 -0
  7. package/lib/attendance-leave-enums.cjs +193 -0
  8. package/lib/attendance-leave-ids.cjs +15 -0
  9. package/lib/attendance-leave-list-filters.cjs +21 -0
  10. package/lib/attendance-leave-route.cjs +20 -0
  11. package/lib/attendance-operation-result.cjs +97 -0
  12. package/lib/attendance-report-inbox.cjs +42 -0
  13. package/lib/attendance-report-remarks.cjs +62 -0
  14. package/lib/attendance-statistics.cjs +49 -0
  15. package/lib/attendance.cjs +404 -0
  16. package/lib/auth.cjs +1 -1
  17. package/lib/business-schemas.cjs +3 -1
  18. package/lib/cli.cjs +294 -1
  19. package/lib/command-guidance.cjs +1162 -1
  20. package/lib/command-reference.cjs +54 -0
  21. package/lib/contact-help.cjs +43 -0
  22. package/lib/contact-mobile.cjs +14 -0
  23. package/lib/contact.cjs +85 -0
  24. package/lib/correction-help.cjs +57 -0
  25. package/lib/dormitory-replenishment.cjs +63 -0
  26. package/lib/dormitory-report-detail.cjs +76 -0
  27. package/lib/dormitory-report-id.cjs +8 -0
  28. package/lib/dormitory-report-summary.cjs +66 -0
  29. package/lib/dormitory-report-view.cjs +36 -0
  30. package/lib/dormitory-rooms.cjs +41 -0
  31. package/lib/dormitory-scope.cjs +18 -0
  32. package/lib/dormitory-statistics.cjs +30 -0
  33. package/lib/download.cjs +17 -8
  34. package/lib/gateway.cjs +9 -4
  35. package/lib/leave-approval-receipt.cjs +126 -0
  36. package/lib/leave-attachment-upload.cjs +110 -0
  37. package/lib/leave-form-users.cjs +48 -0
  38. package/lib/leave-start-receipt.cjs +64 -0
  39. package/lib/leave.cjs +457 -0
  40. package/lib/replenishment-records.cjs +39 -0
  41. package/lib/result.cjs +1 -0
  42. package/lib/saved-report-summary.cjs +16 -0
  43. package/lib/school-contacts.cjs +24 -0
  44. package/lib/school-help.cjs +53 -0
  45. package/lib/school-schema.cjs +38 -0
  46. package/lib/student-application-help.cjs +76 -0
  47. package/lib/student-leave-form.cjs +108 -0
  48. package/lib/student-leave-operations.cjs +24 -0
  49. package/lib/student-leave-query.cjs +75 -0
  50. package/lib/student-leave-resumption.cjs +2 -0
  51. package/lib/student-leave-start.cjs +34 -0
  52. package/lib/student-leave.cjs +51 -0
  53. package/lib/student-query-help.cjs +57 -0
  54. package/lib/student-replenishment.cjs +64 -0
  55. package/lib/student-report-detail.cjs +39 -0
  56. package/lib/student-report-summary.cjs +30 -0
  57. package/lib/teacher-approval-help.cjs +132 -0
  58. package/lib/teacher-leave-files.cjs +112 -0
  59. package/lib/teacher-leave-form-check.cjs +185 -0
  60. package/lib/teacher-leave-query.cjs +99 -0
  61. package/lib/teacher-own-leave-help.cjs +134 -0
  62. package/lib/workflow-entry.cjs +42 -0
  63. package/package.json +1 -1
@@ -1,6 +1,60 @@
1
1
  // Optional business guidance, relative to the product Skill root.
2
2
  function reference(command) {
3
3
  const [space, action, operation] = command.split(' ');
4
+ if (space === 'contact') return 'references/contact/SKILL.md';
5
+ if (space === 'school') return 'references/school/SKILL.md';
6
+ if (space === 'dormitory-attendance' && action === 'rooms') return 'references/attendance/dormitory.md#find-dormitory-rooms';
7
+ if (space === 'dormitory-attendance' && action === 'statistics') return 'references/attendance/dormitory.md#read-dormitory-period-statistics';
8
+ if (space === 'attendance' && action === 'correction-approval-entry') return 'references/attendance/correction-approvals.md#approval-continuation';
9
+ if (['leave', 'student-leave'].includes(space) && ['approval-entry', 'resumption-approval-entry'].includes(action)) return `references/${space}/approvals.md#approval-continuation`;
10
+ if (space === 'student-attendance' && action === 'tags') return 'references/attendance/student-gate.md#read-student-attendance-by-tag';
11
+ if (space === 'student-attendance' && action === 'applicable-rule') return 'references/attendance/student-gate.md#read-student-gate-applicable-rule';
12
+ if (space === 'dormitory-attendance' && action === 'applicable-rule') return 'references/attendance/dormitory.md#read-dormitory-applicable-rule';
13
+ if (space === 'attendance' && action === 'applicable-rule') return 'references/attendance/teacher-daily.md#read-current-applicable-rule';
14
+ if (space === 'attendance' && action === 'correction-form-people-search') return 'references/attendance/corrections.md#correction-person-search';
15
+ if (space === 'attendance' && action === 'correction-approval-form-people-search') return 'references/attendance/correction-approvals.md#correction-person-search';
16
+
17
+ if (['leave', 'student-leave'].includes(space) && ['form-people-search', 'resumption-form-people-search'].includes(action)) return `references/${space}/SKILL.md#form-person-search`;
18
+ if (['leave', 'student-leave'].includes(space) && ['approval-form-people-search', 'resumption-approval-form-people-search'].includes(action)) return `references/${space}/approvals.md#form-person-search`;
19
+ if (['attendance', 'student-attendance', 'dormitory-attendance'].includes(space) && action === 'configuration-entry') return 'references/attendance/' + ({ attendance: 'teacher-daily', 'student-attendance': 'student-gate', 'dormitory-attendance': 'dormitory' }[space]) + '.md#open-rule-management-configuration';
20
+ if (space === 'dormitory-attendance' && ['report-group', 'report-room'].includes(action)) return 'references/attendance/dormitory.md#management-group-and-room-dormitory-reports';
21
+ if (space === 'dormitory-attendance' && action === 'report-summary') return 'references/attendance/dormitory.md#saved-school-dormitory-report-summaries';
22
+ if (['student-attendance', 'dormitory-attendance'].includes(space) && ['manual-entries', 'manual-entry-delete'].includes(action)) return 'references/attendance/' + (space === 'student-attendance' ? 'student-gate' : 'dormitory') + '.md#own-replenishment-records-and-deletion';
23
+ if (space === 'dormitory-attendance' && action === 'manual-entry') return 'references/attendance/dormitory.md#dormitory-report-same-day-replenishment';
24
+ if (space === 'student-attendance' && action === 'manual-entry') return 'references/attendance/student-gate.md#student-gate-same-day-replenishment';
25
+ if (space === 'student-attendance' && action === 'report-tag') return 'references/attendance/student-gate.md#label-gate-report-details';
26
+ if (space === 'student-attendance' && action === 'report-class') return 'references/attendance/student-gate.md#class-gate-report-details';
27
+ if (space === 'student-attendance' && action === 'report-summary') return 'references/attendance/student-gate.md#saved-schoolgrade-gate-report-summaries';
28
+ if (['student-attendance', 'dormitory-attendance'].includes(space) && action === 'reports') return 'references/attendance/' + (space === 'student-attendance' ? 'student-gate' : 'dormitory') + '.md#received-attendance-reports';
29
+ if (space === 'student-leave') {
30
+ const section = (['resumption-list', 'resumption-show', 'resumption-remind', 'resumption-withdraw'].includes(action) ? 'read-and-manage-owned-student-resumptions' : ['resumption-approvals', 'resumption-approval', 'approvals', 'approval'].includes(action) ? 'read-the-approval-inbox' : ['resumption-approve', 'resumption-reject', 'approve', 'reject'].includes(action) ? 'execute-a-teachers-approval-decision' : action.startsWith('resumption-') ? 'resume-a-students-leave' : action === 'submit' ? 'submit-student-leave' : action === 'form' ? 'prepare-student-leave' : ['remind', 'withdraw'].includes(action) ? 'manage-owned-student-leave' : ['list', 'show'].includes(action) ? 'read-student-leave-records' : 'locate-managed-students');
31
+ const guide = ['read-the-approval-inbox', 'execute-a-teachers-approval-decision'].includes(section) ? 'student-leave/approvals.md' : 'student-leave/SKILL.md';
32
+ return 'references/' + guide + '#' + section;
33
+ }
34
+ if (space === 'attendance' && action === 'show' || space === 'student-attendance' && action === 'show') return 'references/attendance/' + (space === 'attendance' ? 'teacher-daily' : 'student-gate') + '.md#read-managed-attendance-day';
35
+ if (space === 'dormitory-attendance' && ['report-remark', 'report-remarks'].includes(action)) return 'references/attendance/dormitory.md#legacy-dormitory-report-remarks';
36
+ if (space === 'dormitory-attendance' && action === 'remark') return 'references/attendance/dormitory.md#write-a-dormitory-period-remark';
37
+ if (space === 'dormitory-attendance') return 'references/attendance/dormitory.md#' + (action === 'show' ? 'read-dormitory-attendance-day' : 'read-dormitory-attendance');
38
+ if (space === 'student-attendance' && ['report-remark', 'report-remarks'].includes(action)) return 'references/attendance/student-gate.md#legacy-student-report-remarks';
39
+ if (space === 'student-attendance' && action === 'remark') return 'references/attendance/student-gate.md#write-a-student-gate-period-remark';
40
+ if (space === 'student-attendance' || space === 'attendance' && action === 'list') return 'references/attendance/' + (space === 'attendance' ? 'teacher-daily' : 'student-gate') + '.md#read-managed-attendance';
41
+ if (space === 'attendance' && action === 'statistics') return 'references/attendance/teacher-daily.md#read-own-saved-monthly-statistics';
42
+ if (space === 'attendance' && action === 'report-summary') return 'references/attendance/teacher-daily.md#read-teacher-report-summary';
43
+ if (space === 'attendance' && ['correction-approvals', 'correction-approval', 'correction-approve', 'correction-reject'].includes(action)) return 'references/attendance/correction-approvals.md#review-correction-applications';
44
+
45
+ if (space === 'attendance' && action === 'correction-submit') return 'references/attendance/corrections.md#submit-own-correction-application';
46
+ if (space === 'attendance' && ['correction-list', 'correction-show'].includes(action)) return 'references/attendance/corrections.md#read-own-correction-applications';
47
+ if (space === 'attendance' && action === 'correction-form') return 'references/attendance/corrections.md#read-own-correction-context';
48
+ if (space === 'attendance' && action === 'remark') return 'references/attendance/teacher-daily.md#write-an-own-attendance-remark';
49
+ if (space === 'attendance' && action === 'clock-entry') return 'references/attendance/teacher-daily.md#open-physical-clock-in';
50
+ if (space === 'attendance') return 'references/attendance/teacher-daily.md#interpret-attendance';
51
+ if (space === 'leave') {
52
+ if (['list', 'show', 'resumption-list', 'resumption-show'].includes(action)) return 'references/leave/SKILL.md#read-own-leave-and-resumption';
53
+ const section = (['resumption-approve', 'resumption-reject'].includes(action) ? 'execute-a-teachers-approval-decision' : ['resumption-approvals', 'resumption-approval'].includes(action) ? 'read-the-approval-inbox' : ['resumption-remind', 'resumption-withdraw'].includes(action) ? 'manage-a-pending-leave-application' : action.startsWith('resumption-') ? 'prepare-an-owned-leave-resumption' : action === 'submit' ? 'submit-a-teacher-leave-application' : ['approve', 'reject'].includes(action) ? 'execute-a-teachers-approval-decision' : ['approvals', 'approval'].includes(action) ? 'read-the-approval-inbox' : action === 'form' ? 'read-a-school-leave-form'
54
+ : ['remind', 'withdraw'].includes(action) ? 'manage-a-pending-leave-application' : 'read-own-leave-and-resumption');
55
+ const guide = ['read-the-approval-inbox', 'execute-a-teachers-approval-decision'].includes(section) ? 'leave/approvals.md' : 'leave/SKILL.md';
56
+ return 'references/' + guide + '#' + section;
57
+ }
4
58
  if (space === 'auth') return 'references/auth/SKILL.md#' +
5
59
  (action === 'connect' ? 'connector-connection' : action === 'login' ? 'authorization-flow' : 'identity-and-organization');
6
60
  if (space === 'config') return 'SKILL.md#configuration';
@@ -0,0 +1,43 @@
1
+ const paging = '[--page <n>] [--page-size <1-100>]';
2
+ const usage = {
3
+ 'contact departments search': '--name <text> [--department-id <id>] ' + paging,
4
+ 'contact departments tree': '[--department-id <id>] [--depth <1-20>] ' + paging,
5
+ 'contact departments members': '--department-id <id> [--department-id <id>...] [--include-descendants] [--include-mobile] [--yes] ' + paging,
6
+ 'contact people search': '--name <text> [--department-id <id>...] [--include-descendants] [--include-mobile] [--yes] ' + paging,
7
+ };
8
+ const summaries = {
9
+ contact: '通讯录:找部门、查看组织结构、查询员工',
10
+ 'contact departments': '查找部门、浏览结构和列出成员',
11
+ 'contact people': '按姓名查找员工',
12
+ 'contact departments search': '按名称查找可见的内部部门',
13
+ 'contact departments tree': '查看可见的内部部门结构',
14
+ 'contact departments members': '列出一个或多个部门的可见员工',
15
+ 'contact people search': '按姓名查找可见员工,可限定部门',
16
+ };
17
+ const definitions = Object.fromEntries(Object.keys(usage).map(command => [command, {
18
+ purpose: summaries[command], scope: 'contact:read', errors: 'read',
19
+ }]));
20
+ function render(command) {
21
+ if (!usage[command]) return null;
22
+ const tree = command.endsWith(' tree'), members = command.endsWith(' members');
23
+ const people = members || command === 'contact people search';
24
+ const params = [];
25
+ if (!tree && !members) params.push('--name 必填;' + (people ? '姓名' : '部门名称') + '文字片段,1到50字,按包含匹配;不解释为正则或通配符。');
26
+ params.push(people
27
+ ? '--department-id ' + (members ? '必填' : '可选') + ';可重复指定不同部门,最多200个,合并查询并去重;默认只查直属成员。'
28
+ : '--department-id 可选,只能指定一个;' + (tree ? '作为子树根节点并包含该节点。省略从当前可见顶层开始。' : '在该部门及其下级范围搜索。省略搜索当前学校可见内部部门。'));
29
+ if (people) params.push('--include-descendants 可选开关;包含所选部门的可见下级部门成员,必须同时指定部门。');
30
+ if (people) params.push('--include-mobile 可选;读取本次结果中人员的手机号。仅老师明确要求或用途明确需要时使用。',
31
+ '--yes 读取手机号时必填;先说明对象、范围和用途,取得老师二次确认后才能传入。首次提出需求不算二次确认;单独使用 --yes 报错。');
32
+ if (tree) params.push('--depth 可选,默认2,范围1到20;查询根算第1层,省略根时可见顶层算第1层。');
33
+ params.push('--page 可选,从1开始,默认1。', '--page-size 可选,默认20,范围1到100。',
34
+ people ? '仅 --department-id 可重复传入;重复部门 ID 报错,其他参数不能重复。' : '所有参数均不能重复传入。');
35
+ const result = people
36
+ ? 'items:员工列表,每项含 id、name、departments。\n departments:所属部门,含 id、name、parent_id、path、has_children、level,只展示可见部门。指定部门时只返回本次所选范围内的部门关系。\n 一人多部门只计一次;total 为筛选和权限过滤后的员工人数。\n 默认不返回 mobile;确认展开后增加 mobile,未登记为 null。'
37
+ : 'items:部门列表,每项含 id、name、parent_id、path(可见部门名称数组)、has_children(是否有可见下级)、level。隐藏祖先不返回,parent_id 指向最近可见祖先。' + (tree
38
+ ? '\n 树采用平铺节点列表,按 level、名称、ID 排序;指定根的 parent_id 为 null,level 从查询根开始计数。父节点可能在其他页,完整结构需读取全部分页。total 是当前深度内节点数;depth_limited=true 表示还有更深层节点,可增加深度或用节点 ID 查看子树。'
39
+ : '\n 按名称精确匹配优先,再按名称和 ID 排序;同名部门分别返回。level 为可见目录层数,total 为匹配部门数。');
40
+ const example = tree ? '--depth 2' : members ? '--department-id <教务处ID> --include-descendants' : '--name ' + (people ? '张' : '教务');
41
+ return `${summaries[command]}。\n\n用法:yunzhi-cli ${command} ${usage[command]}\n\n参数:\n ${params.join('\n ')}\n\n示例:\n yunzhi-cli ${command} ${example}\n\n返回:\n ${result}\n tenant_id、operator_id 为当前学校和操作人;name、department_ids 返回本次查询条件${people ? ';include_descendants、include_mobile 表示是否包含下级及手机号。' : '。'}page、page_size、total、total_pages 为分页信息。${people ? (members ? '成员按姓名、ID 排序。' : '姓名精确匹配优先,再按姓名、ID 排序。') : (tree ? 'depth 返回本次查询深度。' : 'depth 为 null,depth_limited 为 false。')}\n\n限制:\n 仅当前学校内部通讯录,员工不全是教师,不包含学生目录。部门可见与成员可见分别校验;指定不可见或不存在的部门整体报错,不静默跳过。权限或数据源失败报错,空结果仅表示当前可见范围无匹配。分页偏移不能超过2147483647。\n ${people ? '手机号缺少 --yes 时退出码10,不发起查询。' : ''}所需权限:contact:read。默认输出 JSON,错误见 error.message/error.hint。\n`;
42
+ }
43
+ module.exports = { usage, summaries, definitions, render };
@@ -0,0 +1,14 @@
1
+ const { confirm } = require('./arguments.cjs');
2
+ const { invalid, Failure } = require('./result.cjs');
3
+
4
+ function confirmMobile(payload) {
5
+ if (payload.yes && !payload.include_mobile) throw invalid('--yes 仅用于确认 --include-mobile 的手机号读取。');
6
+ payload.include_mobile ??= false;
7
+ if (payload.include_mobile) {
8
+ if (!payload.yes) throw new Failure(10, 'confirmation', 'confirmation_required',
9
+ '读取手机号需要老师二次确认。请先说明查询对象和用途,取得再次确认后才添加 --yes。');
10
+ confirm(payload);
11
+ }
12
+ payload.mobile_confirmed = payload.include_mobile;
13
+ }
14
+ module.exports = { confirmMobile };
@@ -0,0 +1,85 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+
5
+ const department = { id: required('string'), name: required('string'), parent_id: nullable('string'),
6
+ path: required([required('string')]), has_children: required('boolean'), level: required('integer') };
7
+
8
+ async function execute(group, action, args, space = 'contact') {
9
+ const school = space === 'school';
10
+ const people = group === 'people' || group === 'students' || action === 'members';
11
+ const schoolSchema = school ? require('./school-schema.cjs') : null;
12
+ const departmentSchema = school ? schoolSchema.summary : department;
13
+ const search = action === 'search';
14
+ const supportsMobile = school ? !people : people;
15
+ const rest = [], ids = [];
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] !== '--department-id') { rest.push(args[i]); continue; }
18
+ const id = args[++i];
19
+ if (!id?.trim() || id.startsWith('--') || id.length > 200) throw invalid('--department-id 需要有效部门 ID,最多200字符。');
20
+ ids.push(id.trim());
21
+ }
22
+ if (ids.length > (people ? 200 : 1) || new Set(ids).size !== ids.length) throw invalid('部门 ID 不能重复;部门搜索和树最多指定一个,成员及人员搜索最多200个。');
23
+ if (action === 'members' && !ids.length) throw invalid('--department-id 为必填参数。');
24
+ const payload = options(rest, ['page', 'page_size', ...(search ? ['name'] : []), ...(action === 'tree' ? ['depth'] : [])],
25
+ [...(people ? ['include_descendants'] : school ? ['include_teachers'] : []),
26
+ ...(supportsMobile ? ['include_mobile', 'yes'] : [])], [], true);
27
+ payload.department_ids = ids;
28
+ if (search) { requireArguments(payload, 'name'); if (payload.name.length > 50) throw invalid('--name 最多50字。'); }
29
+ for (const [key, fallback, maximum] of [['page', 1, 2147483647], ['page_size', 20, 100], ...(action === 'tree' ? [['depth', 2, 20]] : [])]) {
30
+ payload[key] ??= fallback;
31
+ if (!/^[1-9][0-9]*$/.test(String(payload[key])) || !Number.isSafeInteger(+payload[key]) || +payload[key] > maximum)
32
+ throw invalid(`--${key.replaceAll('_', '-')} 必须为1到${maximum}的整数。`);
33
+ payload[key] = +payload[key];
34
+ }
35
+ if ((payload.page - 1) * payload.page_size > 2147483647) throw invalid('分页偏移超出范围。');
36
+ if (people) {
37
+ payload.include_descendants ??= false;
38
+ if (payload.include_descendants && !ids.length) throw invalid('--include-descendants 需要同时指定部门。');
39
+ }
40
+ if (school && !people) payload.include_teachers ??= false;
41
+ if (supportsMobile) require('./contact-mobile.cjs').confirmMobile(payload);
42
+ const result = await gateway.execute(`${space}:read`, `${space}/${group}/${action}`, payload, {
43
+ tenant_id: required('string'), operator_id: required('string'), name: nullable('string'), department_ids: required([required('string')]),
44
+ ...(people ? { include_descendants: required('boolean') } : { depth: nullable('integer'), depth_limited: required('boolean') }),
45
+ ...(school && !people ? { include_teachers: required('boolean') } : {}),
46
+ ...(supportsMobile ? { include_mobile: required('boolean') } : {}),
47
+ items: required([people ? { id: required('string'), name: required('string'), departments: required([departmentSchema]),
48
+ ...(school ? { student_number: nullable('string') } : payload.include_mobile ? { mobile: nullable('string') } : {}) }
49
+ : school ? schoolSchema.departmentWithMobile(payload.include_mobile) : department]),
50
+ page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'),
51
+ });
52
+ const d = result.value.data;
53
+ const fail = () => { throw new Failure(1, 'api', 'invalid_response', '通讯录返回的筛选条件、部门或分页信息不一致。'); };
54
+ if (d.name !== (payload.name ?? null) || JSON.stringify(d.department_ids) !== JSON.stringify(ids)
55
+ || d.page !== payload.page || d.page_size !== payload.page_size || d.total < 0
56
+ || d.total_pages !== Math.ceil(d.total / d.page_size)
57
+ || d.items.length !== Math.min(d.page_size, Math.max(0, d.total - (d.page - 1) * d.page_size))
58
+ || new Set(d.items.map(row => row.id)).size !== d.items.length
59
+ || (people ? d.include_descendants !== payload.include_descendants
60
+ : d.depth !== (payload.depth ?? null) || action !== 'tree' && d.depth_limited)) fail();
61
+ if (supportsMobile && d.include_mobile !== payload.include_mobile) fail();
62
+ if (school && !people && d.include_teachers !== payload.include_teachers) fail();
63
+ const checkDepartment = row => {
64
+ if (!row.path.length || row.path.at(-1) !== row.name || row.level < 1 || row.parent_id === row.id) fail();
65
+ if (school) schoolSchema.check(row, people, payload.include_teachers, fail);
66
+ };
67
+ for (const row of d.items) {
68
+ if (search && !row.name.includes(payload.name)) fail();
69
+ if (people) {
70
+ if (new Set(row.departments.map(dept => dept.id)).size !== row.departments.length || ids.length && !row.departments.length) fail();
71
+ for (const dept of row.departments) {
72
+ checkDepartment(dept);
73
+ if (ids.length && !payload.include_descendants && !ids.includes(dept.id)) fail();
74
+ }
75
+ } else {
76
+ checkDepartment(row);
77
+ if (action === 'tree' && row.level > payload.depth) fail();
78
+ }
79
+ }
80
+ d.message = !d.total ? '当前可见范围内没有匹配结果。' : `本页返回${d.items.length}条,共${d.total}条。`
81
+ + (d.page < d.total_pages ? '完整结果还需继续翻页。' : '')
82
+ + (!people && d.depth_limited ? '仍有更深层的可见部门,可增加深度或指定部门继续查看。' : '');
83
+ return result;
84
+ }
85
+ module.exports = { execute };
@@ -0,0 +1,57 @@
1
+ const { formFields, fieldInput, submissionResult } = require('./teacher-own-leave-help.cjs');
2
+ const correctionFormFields = formFields
3
+ .replace('见 submit Help', '见 correction-submit Help')
4
+ .replace('才支持 form-people-search', '才支持 correction-form-people-search')
5
+ .replace('显示结果读取 form_check,无需 AI 自行执行 visibility。CLI 按任一可见控制题满足即显示、隐藏控制题不触发下游、子表每行独立判断;循环依赖或失效的引用明确报错。',
6
+ '按 form_check.visible_fields 填写,根据 issues 补充或修正,无需自行判断 visibility。');
7
+ function render(command, usage) {
8
+ if (!command.startsWith('attendance correction-')) return null;
9
+ const kind = command.slice('attendance correction-'.length);
10
+ if (!['form', 'form-people-search', 'submit', 'show', 'list'].includes(kind)) return null;
11
+ const target = ['--date 必填;考勤日期 YYYY-MM-DD。', '--result-id 必填;本人考勤详情返回的时段 result_id。'];
12
+ let purpose, params, result, limits, example, details = '';
13
+ const settings = 'correction_enabled 为补卡开关;day_delay 为北京时间自然日期限,0 仅当天,1 今天和昨天。\n times_per_month 为月上限;used_in_month 为目标月份的补卡记录数,跨规则和打卡类型统计,不是申请数、人数或天数。限制值 null 为未知,不表示无限制。';
14
+ if (kind === 'form') {
15
+ purpose = '查询本人所选时段的补卡设置;补卡开启时读取表单并检查当前答案。';
16
+ params = [...target, '--correction-time 可选;实际需要补卡的时间,YYYY-MM-DD HH:mm:ss(Asia/Shanghai),须属于所选日期;提交时必填。', '--field-values 可选;当前完整答案,JSON 数组或 @UTF-8 文件;不保存、不上传、不提交。'];
17
+ example = '--date 2026-10-16 --result-id <result_id>';
18
+ result = 'status=unavailable:当前规则未开启补卡,reason 说明原因;program_code、time_field_key、fields 为 null,不返回 form_check。';
19
+ result += '\n status=ready:已读取表单,reason 为 null;不代表已获准提交。';
20
+ result += '\n ' + settings;
21
+ result += '\n form_check(仅 ready):status=complete 本地检查无问题,needs_input 需补充或修正,blocked 为配置异常或不支持的必需字段;visible_fields 为当前显示字段,issues 给出问题及位置,row_index 从 0 开始。server_validation_required=true,最终校验在提交时执行。';
22
+ result += '\n program_code 为学校选定流程;time_field_key 为可见的补卡时间绑定字段,不可见时为 null。补卡时间使用 --correction-time,不在 field-values 中填写绑定字段。';
23
+ result += '\n tenant_id/user_id/date/result 标明本人和考勤时段;result.status 与 attendance list 使用同一业务状态名称;rule_name/rule_version_time 标明规则来源。';
24
+ details = '\n字段说明(仅 status=ready 时,按需阅读):\n ' + correctionFormFields + '\n';
25
+ limits = '只咨询规则时按设置回答,无需补填表单。次数用尽仍可读取表单,读取不预留次数。权限不足、配置或服务异常会报错,不视为补卡关闭;提交时重新校验计算、期限、次数、人员资格和文件内容。';
26
+ } else if (kind === 'form-people-search') {
27
+ purpose = '填写本人补卡表单中的人员字段时,按姓名查找可选人员。';
28
+ params = [...target, '--field-key 必填;允许自由搜索的可编辑人员字段。', '--keyword 必填;非空姓名线索。'];
29
+ example = '--date 2026-10-16 --result-id <result_id> --field-key <field_key> --keyword 李';
30
+ result = 'candidates 为可见人员及部门信息;填写人员取 tenant_id/user_id。exhaustive=false,最多 10 人,无分页,未知部门为 null。';
31
+ limits = '仅 person_search_supported=true 的可编辑字段支持搜索;固定候选直接使用表单中的选项。';
32
+ } else if (kind === 'submit') {
33
+ purpose = '为本人选定的考勤时段提交补卡申请,附件随提交上传。';
34
+ params = [...target, '--correction-time 必填;YYYY-MM-DD HH:mm:ss(Asia/Shanghai),须属于所选日期。', '--field-values 可选;JSON 数组或 @UTF-8 文件;学校必填项仍须完整。'];
35
+ example = '--date 2026-10-16 --result-id <result_id> --correction-time "2026-10-16 08:00:00" --field-values "@补卡.json"';
36
+ result = 'id 为新补卡申请;date/result_id/correction_time 标明办理目标。\n ' + submissionResult.replace('失败不撤销已确认提交。', '申请已提交、后续处理失败时,分别报告。');
37
+ details = '\n输入格式:\n ' + fieldInput + '\n';
38
+ limits = '提交前自动读取最新设置和表单,无需另行检查。关闭时返回 correction_unavailable;缺项时 error.form_check 汇总问题。两者均在上传前停止,execution_status=NOT_EXECUTED、step=check_form_before_upload。\n 服务端重新检查期限(含最早允许日,不允许未来日期)、次数和资格。真实时间与学校日期字段不兼容时报错,不截断时间。申请提交或审批完成不代表考勤已更新。\n request_id 仅用于查询定位,不是幂等键;结果不确定时先查询核实,不自动重试。';
39
+ } else if (kind === 'list') {
40
+ purpose = '查询当前老师发起的补卡申请。';
41
+ params = ['--start-date + --end-date 可选;拟补卡日期,YYYY-MM-DD,成对提供、包含首尾日期。', '--submitted-start-date + --submitted-end-date 可选;最近一次提交日期,格式同上;从未提交的历史草稿不匹配此条件。', '--status 可选;withdrawn 撤回、returned 已退回、draft 草稿、processing 处理中、completed 完成;省略不过滤。', '--page 可选;默认 1,范围 1..2147483647。', '--page-size 可选;默认 20,范围 1..50。'];
42
+ example = '--status processing';
43
+ result = 'items 返回 id、流程、标题、status(与筛选使用同一业务名称)和创建时间;page/page_size/total/total_pages 为分页。';
44
+ limits = '历史申请查询不要求当前补卡开关开启或有剩余额度;创建时间不是拟补卡时间。两组日期按北京时间自然日,可与状态组合,筛选后计数和分页;未传时不限制对应日期。拟补卡日期读取工作流保存的时间,不要求审批完成或已经生成打卡;完整结论需要读全匹配分页。';
45
+ } else {
46
+ purpose = '读取本人补卡申请、审批进度及附件链接。';
47
+ params = ['--id 必填;提交或 correction-list 返回的申请 ID。'];
48
+ example = '--id <id>';
49
+ result = 'id/program_code/status/status_description 为申请事实,status 与 correction-list 的筛选名称一致;fields 为可见只读字段,progress 为节点、处理人、时间及意见,节点 status 使用 pending/transferred/completed/returned/withdrawn、node_type 使用 start/normal/end。\n attachments 为字段/行位置、文件信息、download_url 和 expires_at;链接最长 15 分钟,直接下载,无需业务认证头,过期重查详情。历史 URL 图片在 fields.value 中原样展示。\n clock_reference 为服务端关联的 date/record_id/clock_record_found;没有可信关联时为 null。';
50
+ limits = '申请完成不证明已产生补卡或考勤更新。核实效果时按 clock_reference 查询考勤详情;不凭相近时间猜关联;关联缺失也不证明补卡失败,不能据此再补一次。附件读取失败明确报错,不作为空列表。';
51
+ }
52
+ const input = kind === 'submit' ? details : '';
53
+ const output = kind === 'submit' ? '' : details;
54
+ if (['form', 'list', 'show'].includes(kind)) result += '\n 已有数据的未知状态返回 unknown,并在 status_code 保留原码;不推断已同意或已完成。';
55
+ return `${purpose}\n\n用法:yunzhi-cli ${command} ${usage}\n参数:\n ${params.join('\n ')}\n\n示例:\n yunzhi-cli ${command} ${example}\n${input}\n返回:\n ${result}\n\n${kind === 'submit' ? '校验与失败处理' : '注意'}:\n ${limits}\n 所需权限:${kind === 'submit' ? 'attendance-leave:read + attendance-leave:write' : 'attendance-leave:read'}。\n${output}`;
56
+ }
57
+ module.exports = { render };
@@ -0,0 +1,63 @@
1
+ const { replenishmentId } = require('./attendance-leave-ids.cjs');
2
+ const { options, required: requireArguments } = require('./arguments.cjs');
3
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
4
+ const gateway = require('./gateway.cjs');
5
+ function positiveLong(value) { return typeof value === 'string' && /^[1-9][0-9]{0,18}$/.test(value) && BigInt(value) <= 9223372036854775807n; }
6
+ function validWindow(value) {
7
+ return typeof value === 'string' && /^[0-9]{1,2}:[0-9]{2}-[0-9]{1,2}:[0-9]{2}$/.test(value)
8
+ && value.split('-').every(time => { const [h, m] = time.split(':').map(Number); return h < 24 && m < 60 || h === 24 && m === 0; });
9
+ }
10
+ function parse(args) {
11
+ const keys = ['target_user_id', 'bedroom_id', 'date'];
12
+ const payload = options(args, [...keys, 'report_id', 'manage_id', 'time_bucket'], [], ['time_bucket'], true);
13
+ const fromRules = payload.report_id === undefined;
14
+ requireArguments(payload, ...keys, ...(fromRules ? ['manage_id'] : []));
15
+ const write = payload.time_bucket !== undefined;
16
+ if (payload.manage_id != null && !positiveLong(payload.manage_id)) throw invalid('--group-id 必须是有效管理组ID,原样使用字符串标识。');
17
+ if (!fromRules && (!positiveLong(payload.report_id) || BigInt(payload.report_id) > 2147483647n) || !positiveLong(payload.bedroom_id)) throw invalid('--report-id 必须是有效报告ID,--room-id 必须是有效房间ID;原样使用字符串标识。');
18
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.date) || !Number.isFinite(Date.parse(payload.date)) || new Date(payload.date).toISOString().slice(0, 10) !== payload.date)
19
+ throw invalid('--date 必须是有效的 YYYY-MM-DD 日期;服务端只允许业务当天补签到。');
20
+ if (write && !validWindow(payload.time_bucket)) throw invalid('--time-window 必须使用原报告或当前规则返回的时段,不能指定任意打卡时间。');
21
+ if (write) payload.direction = '0';
22
+ return payload;
23
+ }
24
+ function invalidReply(result) {
25
+ const failure = new Failure(1, 'api', 'invalid_response', '宿舍补签到返回的学生、报告、房间、日期、时段或执行阶段缺失或不一致。');
26
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; return failure;
27
+ }
28
+ function preserve(failure, payload, write) {
29
+ if (failure instanceof Failure) {
30
+ const error = failure.result.value.error;
31
+ const unknown = write && (error.type === 'network' && error.subtype !== 'connection_failed' || error.type === 'api' && error.subtype === 'invalid_response');
32
+ error.details = { ...error.details, ...payload, ...(unknown ? { execution_status: 'UNCONFIRMED' } : {}) };
33
+ if (unknown) error.message += '宿舍补签到结果待核实,请保留目标、报告和请求标识,按需要查询当天打卡,不要自动重试。';
34
+ }
35
+ throw failure;
36
+ }
37
+ async function execute(args) {
38
+ const payload = parse(args);
39
+ const fromRules = payload.report_id === undefined;
40
+ const write = payload.time_bucket !== undefined;
41
+ try {
42
+ const result = await gateway.execute('attendance-leave:' + (write ? 'write' : 'read'), 'dormitory-attendance/' + (fromRules ? 'rule-' : '') + (write ? 'replenish' : 'replenishment-context'), payload, {
43
+ tenant_id: required('string'), operator_id: required('string'), subject_id: required('string'), ...(!fromRules ? { report_id: required('string') } : {}), bedroom_id: required('string'), date: required('string'), ...(!fromRules || write ? { time_bucket: required('string'), direction: required('string') } : {}),
44
+ ...(write ? { id: nullable('string'), manage_id: nullable('string'), request_id: required('string'), execution_status: required('string'), recorded_time: nullable('string'), handled_at: nullable('integer'), message: required('string') }
45
+ : { subject_name: required('string'), bedroom_name: required('string'), manage_id: required('string'), manage_name: required('string'), ...(fromRules ? { rule_id: required('string'), rule_name: nullable('string'), options: required([{ time_bucket: required('string'), direction: required('string') }]) } : {}) }),
46
+ });
47
+ const d = result.value.data;
48
+ if (payload.manage_id != null && d.manage_id !== payload.manage_id || d.subject_id !== payload.target_user_id || (!fromRules && d.report_id !== payload.report_id || fromRules && d.report_id != null) || d.bedroom_id !== payload.bedroom_id || d.date !== payload.date || (!fromRules || write) && (!validWindow(d.time_bucket) || d.direction !== '0')) throw invalidReply(result);
49
+ if (write) {
50
+ if (d.execution_status === 'COMPLETED' ? !replenishmentId(d.id, '2', payload.date) : d.id != null) throw invalidReply(result);
51
+ if (d.time_bucket !== payload.time_bucket || result.value.request_id && result.value.request_id !== d.request_id || d.manage_id != null && !positiveLong(d.manage_id) || d.handled_at != null && d.handled_at < 0
52
+ || (d.execution_status === 'COMPLETED' ? !positiveLong(d.manage_id) || d.recorded_time !== payload.time_bucket.split('-')[1] + ':00' || d.handled_at == null
53
+ : d.execution_status !== 'UNCONFIRMED' || d.recorded_time != null)) throw invalidReply(result);
54
+ } else if (!positiveLong(d.manage_id) || fromRules && (!positiveLong(d.rule_id) || BigInt(d.rule_id) > 2147483647n || !d.options.length || d.options.some(o => !validWindow(o.time_bucket) || o.direction !== '0'))) throw invalidReply(result);
55
+ if (!write && !fromRules) {
56
+ d.options = [{ time_bucket: d.time_bucket, direction: d.direction }];
57
+ delete d.time_bucket; delete d.direction;
58
+ }
59
+ if (!write) d.message = '已查询' + (fromRules ? '当天规则的可补时段' : '原报表的补签到时段和住宿信息') + ',本次未提交补签到;需要办理时提供 --time-window。';
60
+ return result;
61
+ } catch (failure) { preserve(failure, payload, write); }
62
+ }
63
+ module.exports = { execute };
@@ -0,0 +1,76 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ const { reportSchema, validReportContext } = require('./saved-report-summary.cjs');
5
+ const { counts, counterNames } = require('./dormitory-report-summary.cjs');
6
+ const { currentReport } = require('./dormitory-report-id.cjs');
7
+ const { attendanceResult, validDate, validTime } = require('./attendance.cjs');
8
+ const savedCountSchema = { type: required('string'), id: required('string'), name: nullable('string'),
9
+ ...Object.fromEntries(['total', 'normal', 'late', 'missing', 'leave'].map(key => [key, required('integer')])), updated_at: nullable('integer') };
10
+ const currentRoomShape = { tenant_id: required('string'), user_id: required('string'),
11
+ report: required({ ...reportSchema, snapshot_missing_count: nullable('integer') }), bedroom_id: required('string'), bedroom_name: nullable('string'),
12
+ saved_room: required({ counts: required(savedCountSchema), records: required([{ user_id: required('string'), user_name: nullable('string'),
13
+ date: required('string'), bedroom_id: required('string'), bedroom_name: nullable('string'), department_ids: nullable('string'), department_name: nullable('string'),
14
+ clock_record_id: nullable('string'), clock_method: nullable('integer'), result: required(attendanceResult) }]) }) };
15
+ const rowSchema = { ...Object.fromEntries(['record_id', 'user_name', 'date', 'time_bucket', 'at_time', 'direction', 'site', 'remark',
16
+ 'replenish_at_time', 'replenish_user_role', 'replenish_user_name', 'pic_url', 'video_url', 'bedroom_name', 'building_name'].map(name => [name, nullable('string')])),
17
+ user_id: required('string'), status: required('string'), bedroom_id: required('string'), duty_teachers: nullable([nullable('string')]), leave_ids: nullable(['string']) };
18
+ const positiveLong = value => typeof value === 'string' && /^[1-9][0-9]{0,18}$/.test(value) && BigInt(value) <= 9223372036854775807n;
19
+ const invalidRow = (r, bedroomId) => !r.user_id.trim() || !r.status.trim() || !positiveLong(r.bedroom_id) || bedroomId != null && r.bedroom_id !== bedroomId
20
+ || r.leave_ids != null && r.leave_ids.some(id => !require('./student-leave-query.cjs').validId(id, false));
21
+ function shape(room = false) {
22
+ return {
23
+ tenant_id: required('string'), user_id: required('string'), manage_id: required('string'), manage_name: nullable('string'), building_id: required('string'), building_name: nullable('string'),
24
+ report: required({ ...reportSchema }),
25
+ ...(room ? { bedroom_id: required('string'), bedroom_name: nullable('string'), resident_count: required('integer'), rows: required([rowSchema]) }
26
+ : { retire_number: required('integer'), groups: required([{ type: required('string'), count: required('integer'), rows: required([rowSchema]) }]), bedrooms: required([counts]) }),
27
+ };
28
+ }
29
+ function validateQuery(payload, room = false) {
30
+ requireArguments(payload, ...(room ? ['report_id', 'bedroom_id'] : ['report_id']));
31
+ if (!(room && currentReport(payload.report_id)) && (!/^[1-9][0-9]{0,9}$/.test(payload.report_id) || Number(payload.report_id) > 2147483647)
32
+ || room && !positiveLong(payload.bedroom_id)) throw invalid(room ? '必须提供有效的宿舍报告 --report-id 和所属房间 --room-id。' : '必须提供有效的管理组报告 --report-id。');
33
+ }
34
+ function validate(result, payload, room = false) {
35
+ if (room && currentReport(payload.report_id)) return validateCurrentRoom(result, payload);
36
+ const d = result.value.data;
37
+ if (!validReportContext(d.report, payload.report_id, ['4']) || !positiveLong(d.manage_id) || d.manage_id !== d.report.scope_id || !positiveLong(d.building_id)
38
+ || (room ? d.bedroom_id !== payload.bedroom_id || d.resident_count < 0 || d.rows.some(r => invalidRow(r, payload.bedroom_id))
39
+ : d.retire_number < 0 || !d.groups.length || new Set(d.groups.map(g => g.type)).size !== d.groups.length
40
+ || d.groups.some(g => !g.type.trim() || g.count < 0 || g.rows.some(r => invalidRow(r)))
41
+ || d.bedrooms.some(b => !positiveLong(b.bedroom_id) || counterNames.some(key => b[key] < 0)))) {
42
+ const failure = new Failure(1, 'api', 'invalid_response', '宿舍报告详情返回的报告关联、房间或人员统计缺失或不一致。');
43
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; throw failure;
44
+ }
45
+ return result;
46
+ }
47
+ function validateCurrentRoom(result, payload) {
48
+ const d = result.value.data, report = d.report, counts = d.saved_room.counts, records = d.saved_room.records;
49
+ const contentDate = payload.report_id.slice(17, 25).replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3');
50
+ if (d.bedroom_id !== payload.bedroom_id || report.report_id !== payload.report_id || report.content_date !== contentDate
51
+ || !validDate(report.delivery_date) || !report.time_bucket.trim() || !['school', 'rooms'].includes(report.scope_type)
52
+ || report.scope_id !== null || report.snapshot_missing_count !== null || !['in', 'out'].includes(report.direction)
53
+ || counts.type !== 'room' || counts.id !== payload.bedroom_id
54
+ || ['total', 'normal', 'late', 'missing', 'leave'].some(key => counts[key] < 0)
55
+ || counts.normal + counts.late + counts.missing + counts.leave > counts.total || counts.updated_at != null && counts.updated_at < 0
56
+ || new Set(records.map(row => row.user_id)).size !== records.length || new Set(records.map(row => row.result.result_id)).size !== records.length
57
+ || records.some(row => !row.user_id.trim() || row.bedroom_id !== payload.bedroom_id || row.date !== contentDate
58
+ || !row.result.result_id.trim() || !positiveLong(row.result.rule_id) || row.result.period == null || row.result.period <= 0 || row.result.status == null
59
+ || row.result.in_out_type !== (report.direction === 'in' ? 1 : 2)
60
+ || ['clock_time', 'start_time', 'mark_time', 'end_time', 'created_at', 'updated_at'].some(key => !validTime(row.result[key])))) {
61
+ const failure = new Failure(1, 'api', 'invalid_response', '宿舍报告保存明细的日期、房间、方向或统计不一致。');
62
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; throw failure;
63
+ }
64
+ return result;
65
+ }
66
+ function addContext(failure, payload, room) {
67
+ if (failure instanceof Failure) failure.result.value.error.details = { ...failure.result.value.error.details, report_id: payload.report_id, ...(room ? { bedroom_id: payload.bedroom_id } : {}) };
68
+ }
69
+ async function execute(args, room = false) {
70
+ const payload = options(args, room ? ['report_id', 'bedroom_id'] : ['report_id'], [], [], true); validateQuery(payload, room);
71
+ try { return validate(await gateway.execute('attendance-leave:read', 'dormitory-attendance/report-' + (room ? 'room' : 'group'), payload,
72
+ room && currentReport(payload.report_id) ? currentRoomShape : shape(room)), payload, room); }
73
+ catch (failure) { addContext(failure, payload, room); throw failure; }
74
+ }
75
+
76
+ module.exports = { execute };
@@ -0,0 +1,8 @@
1
+ function currentReport(value) {
2
+ if (typeof value !== 'string') return false;
3
+ const match = /^dormitory_report_([0-9]{4})([0-9]{2})([0-9]{2})_([1-9][0-9]{0,18})$/.exec(value);
4
+ if (!match || Number(match[1]) < 1 || BigInt(match[4]) > 9223372036854775807n) return false;
5
+ const date = `${match[1]}-${match[2]}-${match[3]}`;
6
+ return Number.isFinite(Date.parse(date)) && new Date(date).toISOString().slice(0, 10) === date;
7
+ }
8
+ module.exports = { currentReport };
@@ -0,0 +1,66 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ const { reportSchema, validReport } = require('./saved-report-summary.cjs');
5
+ const { currentReport } = require('./dormitory-report-id.cjs');
6
+ const counterNames = ['retire_number', 'no_record_number', 'leave_number', 'replenish_number'];
7
+ const counts = { bedroom_id: nullable('string'), bedroom_name: nullable('string'), retire_state: nullable('string'), duty_teachers: nullable([nullable('string')]),
8
+ ...Object.fromEntries(counterNames.map(name => [name, required('integer')])) };
9
+ const shape = { tenant_id: required('string'), user_id: required('string'), statistics_time: nullable('integer'), total: required(counts), bedrooms: required([counts]), report: required(reportSchema) };
10
+ function validateQuery(payload) {
11
+ requireArguments(payload, 'report_id');
12
+ if (!currentReport(payload.report_id) && (!/^[1-9][0-9]{0,9}$/.test(payload.report_id) || Number(payload.report_id) > 2147483647)) throw invalid('--report-id 必须是本人收到的有效宿舍报告ID。');
13
+ }
14
+ function validate(result, payload) {
15
+ if (currentReport(payload.report_id)) return validateCurrent(result, payload);
16
+ const data = result.value.data; const report = data.report;
17
+ if (!validReport(report, payload.report_id, ['5']) || data.statistics_time != null && data.statistics_time < 0
18
+ || [data.total, ...data.bedrooms].some(row => counterNames.some(name => row[name] < 0) || row.bedroom_id != null && (!/^[1-9][0-9]{0,18}$/.test(row.bedroom_id) || BigInt(row.bedroom_id) > 9223372036854775807n))) {
19
+ const failure = new Failure(1, 'api', 'invalid_response', '宿舍报告汇总返回的报告关联、统计数或时间缺失或不一致。');
20
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; throw failure;
21
+ }
22
+ return result;
23
+ }
24
+ async function execute(args) {
25
+ const payload = options(args, ['report_id'], [], [], true); validateQuery(payload);
26
+ try { return validate(await gateway.execute('attendance-leave:read', 'dormitory-attendance/report-summary', payload, currentReport(payload.report_id) ? currentShape : shape), payload); }
27
+ catch (failure) { addContext(failure, payload); throw failure; }
28
+ }
29
+
30
+ const currentShape = {
31
+ tenant_id: required('string'), user_id: required('string'),
32
+ report: required({ ...reportSchema, snapshot_missing_count: nullable('integer') }),
33
+ saved_summary: required({ scope: required('string'), rows: required([{
34
+ type: required('string'), id: required('string'), name: nullable('string'), total: required('integer'), normal: required('integer'),
35
+ late: required('integer'), missing: required('integer'), leave: required('integer'), updated_at: nullable('integer'),
36
+ }]) }),
37
+ };
38
+ function validateCurrent(result, payload) {
39
+ const { report, saved_summary: summary } = result.value.data;
40
+ const date = payload.report_id.slice(17, 25);
41
+ const contentDate = date.slice(0, 4) + '-' + date.slice(4, 6) + '-' + date.slice(6, 8);
42
+ const validDate = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
43
+ && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
44
+ const validRoom = value => /^[1-9][0-9]{0,18}$/.test(value) && BigInt(value) <= 9223372036854775807n;
45
+ if (report.report_id !== payload.report_id || report.content_date !== contentDate || !validDate(report.delivery_date)
46
+ || !report.time_bucket.trim() || !['school', 'class', 'rooms'].includes(report.scope_type)
47
+ || !['in', 'out'].includes(report.direction) || report.snapshot_missing_count !== null
48
+ || summary.scope !== (report.scope_type === 'rooms' ? 'room' : report.scope_type) || !summary.rows.length
49
+ || new Set(summary.rows.map(r => r.type + ':' + r.id)).size !== summary.rows.length
50
+ || summary.scope === 'class' && (summary.rows.length !== 1 || summary.rows[0].id !== report.scope_id)
51
+ || summary.scope !== 'class' && report.scope_id !== null
52
+ || summary.rows.some(row => !['class', 'room'].includes(row.type) || summary.scope !== 'school' && summary.scope !== row.type
53
+ || (row.type === 'room' && !validRoom(row.id))
54
+ || ['total', 'normal', 'late', 'missing', 'leave'].some(key => row[key] < 0)
55
+ || row.normal + row.late + row.missing + row.leave > row.total || row.updated_at != null && row.updated_at < 0)) {
56
+ const failure = new Failure(1, 'api', 'invalid_response', '宿舍报告的范围、日期或保存统计不完整或不一致。');
57
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
58
+ throw failure;
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function addContext(failure, payload) {
64
+ if (failure instanceof Failure) failure.result.value.error.details = { ...failure.result.value.error.details, report_id: payload.report_id };
65
+ }
66
+ module.exports = { execute, counts, counterNames };
@@ -0,0 +1,36 @@
1
+ // Normalize source structure after validation and enum projection; preserve distinct business bases.
2
+ function project(data, action) {
3
+ if (!['report-summary', 'report-group', 'report-room'].includes(action)) return;
4
+ const saved = Boolean(data.saved_summary || data.saved_room);
5
+ data.basis = {
6
+ attendance: saved ? '保存的时段考勤结果' : action === 'report-summary' ? '保存的住宿汇总,读取当前统计值' : '按当前住宿名单读取的报表明细',
7
+ leave: saved ? '该时段保存的请假判定' : '当日有请假,不代表该时段免考勤',
8
+ date: data.report.content_date, time_bucket: data.report.time_bucket,
9
+ counts_may_overlap: !saved,
10
+ };
11
+ const savedMetrics = row => ['total', 'normal', 'late', 'missing', 'leave'].map(key => ({
12
+ type: key, label: { total: '总人数', normal: '正常', late: '晚归或晚出', missing: '未归或未出', leave: '时段请假' }[key], count: row[key] }));
13
+ const currentMetrics = row => ['retire_number', 'no_record_number', 'leave_number', 'replenish_number']
14
+ .filter(key => row[key] != null).map(key => ({ type: key.replace('_number', ''),
15
+ label: { retire_number: '已归寝', no_record_number: '无记录', leave_number: '当日请假', replenish_number: '补签到' }[key], count: row[key] }));
16
+ if (data.saved_summary) {
17
+ data.summary = data.saved_summary.rows.map(row => ({ type: row.type, id: row.id, name: row.name,
18
+ metrics: savedMetrics(row), updated_at: row.updated_at }));
19
+ } else if (data.saved_room) {
20
+ const room = data.saved_room;
21
+ data.summary = [{ type: 'room', id: data.bedroom_id, name: data.bedroom_name,
22
+ metrics: savedMetrics(room.counts), updated_at: room.counts.updated_at }];
23
+ data.rows = room.records.map(row => ({ user_id: row.user_id, user_name: row.user_name, date: row.date,
24
+ bedroom_id: row.bedroom_id, bedroom_name: row.bedroom_name, department_ids: row.department_ids, department_name: row.department_name,
25
+ status: row.result.status, direction: row.result.direction, direction_name: row.result.direction_name,
26
+ at_time: row.result.clock_time, time_bucket: data.report.time_bucket, record_id: row.clock_record_id,
27
+ clock_method: row.clock_method, remark: row.result.remark ?? null, result: row.result }));
28
+ } else {
29
+ data.summary = (data.bedrooms || []).map(row => ({ type: 'room', id: row.bedroom_id, name: row.bedroom_name,
30
+ metrics: currentMetrics(row), updated_at: null }));
31
+ if (data.total) data.summary.unshift({ type: 'total', id: null, name: '合计', metrics: currentMetrics(data.total), updated_at: null });
32
+ if (action === 'report-room') data.summary = [{ type: 'room', id: data.bedroom_id, name: data.bedroom_name,
33
+ metrics: [{ type: 'residents', label: '当前住宿人数', count: data.resident_count }], updated_at: null }];
34
+ }
35
+ }
36
+ module.exports = { project };
@@ -0,0 +1,41 @@
1
+ const { options } = require('./arguments.cjs');
2
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ const id = value => typeof value === 'string' && /^[1-9][0-9]{0,18}$/.test(value) && BigInt(value) <= 9223372036854775807n;
5
+ async function execute(args) {
6
+ const payload = options(args, ['name', 'building_name', 'building_id', 'page', 'page_size'], [], [], true);
7
+ for (const key of ['name', 'building_name']) if (payload[key]?.length > 50) throw invalid('名称最多50字。');
8
+ if (payload.building_id != null && (!id(payload.building_id) || payload.building_name != null)) throw invalid('楼栋名称与有效楼栋 ID 二选一。');
9
+ for (const [key, fallback, maximum] of [['page', 1, 2147483647], ['page_size', 20, 50]]) {
10
+ if (payload[key] != null && (!/^[1-9][0-9]*$/.test(payload[key]) || Number(payload[key]) > maximum)) throw invalid('页码从1开始,每页1到50条。');
11
+ payload[key] = payload[key] == null ? fallback : Number(payload[key]);
12
+ }
13
+ if ((payload.page - 1) * payload.page_size > 2147483647) throw invalid('分页偏移不能超过2147483647。');
14
+ const result = await gateway.execute('attendance-leave:read', 'dormitory-attendance/rooms', payload, {
15
+ tenant_id: required('string'), operator_id: required('string'), name: nullable('string'), building_name: nullable('string'), building_id: nullable('string'),
16
+ page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'),
17
+ items: required([{ bedroom_id: required('string'), bedroom_name: required('string'), building_id: required('string'), building_name: required('string'),
18
+ manage_groups: required([{ manage_id: required('string'), manage_name: required('string') }]) }]),
19
+ });
20
+ const d = result.value.data;
21
+ const valid = ['name', 'building_name', 'building_id'].every(key => d[key] === (payload[key] ?? null))
22
+ && d.page === payload.page && d.page_size === payload.page_size && d.total >= 0 && d.total <= 2147483647
23
+ && d.total_pages === Math.ceil(d.total / payload.page_size)
24
+ && d.items.length === Math.min(payload.page_size, Math.max(0, d.total - (payload.page - 1) * payload.page_size))
25
+ && new Set(d.items.map(row => row.bedroom_id)).size === d.items.length
26
+ && d.items.every((row, index) => id(row.bedroom_id) && id(row.building_id) && row.bedroom_name.trim() && row.building_name.trim()
27
+ && (payload.name == null || row.bedroom_name.includes(payload.name))
28
+ && (payload.building_name == null || row.building_name.includes(payload.building_name))
29
+ && (payload.building_id == null || row.building_id === payload.building_id)
30
+ && (index === 0 || BigInt(row.building_id) > BigInt(d.items[index - 1].building_id)
31
+ || row.building_id === d.items[index - 1].building_id && BigInt(row.bedroom_id) > BigInt(d.items[index - 1].bedroom_id))
32
+ && row.manage_groups.every(group => id(group.manage_id) && group.manage_name.trim())
33
+ && new Set(row.manage_groups.map(group => group.manage_id)).size === row.manage_groups.length);
34
+ if (!valid) {
35
+ const failure = new Failure(1, 'api', 'invalid_response', '房间目录的筛选、分页或房间信息不一致。');
36
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
37
+ throw failure;
38
+ }
39
+ return result;
40
+ }
41
+ module.exports = { execute };