@xiaoyuanhao/yunzhi-cli 2.0.0 → 2.1.0
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.
- package/lib/arguments.cjs +7 -4
- package/lib/attendance-clock-entry.cjs +31 -0
- package/lib/attendance-configuration-entry.cjs +35 -0
- package/lib/attendance-correction-applications.cjs +64 -0
- package/lib/attendance-correction-approval.cjs +135 -0
- package/lib/attendance-correction-start.cjs +47 -0
- package/lib/attendance-leave-enums.cjs +193 -0
- package/lib/attendance-leave-ids.cjs +15 -0
- package/lib/attendance-leave-list-filters.cjs +21 -0
- package/lib/attendance-leave-route.cjs +20 -0
- package/lib/attendance-operation-result.cjs +97 -0
- package/lib/attendance-report-inbox.cjs +42 -0
- package/lib/attendance-report-remarks.cjs +62 -0
- package/lib/attendance-statistics.cjs +49 -0
- package/lib/attendance.cjs +404 -0
- package/lib/auth.cjs +1 -1
- package/lib/business-schemas.cjs +3 -1
- package/lib/cli.cjs +294 -1
- package/lib/command-guidance.cjs +1162 -1
- package/lib/command-reference.cjs +54 -0
- package/lib/contact-help.cjs +43 -0
- package/lib/contact-mobile.cjs +14 -0
- package/lib/contact.cjs +85 -0
- package/lib/correction-help.cjs +57 -0
- package/lib/dormitory-replenishment.cjs +63 -0
- package/lib/dormitory-report-detail.cjs +76 -0
- package/lib/dormitory-report-id.cjs +8 -0
- package/lib/dormitory-report-summary.cjs +66 -0
- package/lib/dormitory-report-view.cjs +36 -0
- package/lib/dormitory-rooms.cjs +41 -0
- package/lib/dormitory-scope.cjs +18 -0
- package/lib/dormitory-statistics.cjs +30 -0
- package/lib/download.cjs +17 -8
- package/lib/gateway.cjs +9 -4
- package/lib/leave-approval-receipt.cjs +126 -0
- package/lib/leave-attachment-upload.cjs +110 -0
- package/lib/leave-form-users.cjs +48 -0
- package/lib/leave-start-receipt.cjs +64 -0
- package/lib/leave.cjs +457 -0
- package/lib/replenishment-records.cjs +39 -0
- package/lib/result.cjs +1 -0
- package/lib/saved-report-summary.cjs +16 -0
- package/lib/school-contacts.cjs +24 -0
- package/lib/school-help.cjs +53 -0
- package/lib/school-schema.cjs +38 -0
- package/lib/student-application-help.cjs +76 -0
- package/lib/student-leave-form.cjs +108 -0
- package/lib/student-leave-operations.cjs +24 -0
- package/lib/student-leave-query.cjs +74 -0
- package/lib/student-leave-resumption.cjs +2 -0
- package/lib/student-leave-start.cjs +34 -0
- package/lib/student-leave.cjs +51 -0
- package/lib/student-query-help.cjs +57 -0
- package/lib/student-replenishment.cjs +64 -0
- package/lib/student-report-detail.cjs +39 -0
- package/lib/student-report-summary.cjs +30 -0
- package/lib/teacher-approval-help.cjs +132 -0
- package/lib/teacher-leave-files.cjs +112 -0
- package/lib/teacher-leave-form-check.cjs +185 -0
- package/lib/teacher-leave-query.cjs +99 -0
- package/lib/teacher-own-leave-help.cjs +134 -0
- package/lib/workflow-entry.cjs +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
const { options, required: requireArguments } = require('./arguments.cjs');
|
|
2
|
+
const { required, nullable, present, invalid, Failure } = require('./result.cjs');
|
|
3
|
+
const gateway = require('./gateway.cjs');
|
|
4
|
+
|
|
5
|
+
const attendanceResult = { result_id: required('string'), rule_id: nullable('string'),
|
|
6
|
+
status: nullable('integer'), status_name: nullable('string'), period: nullable('integer'),
|
|
7
|
+
in_out_type: nullable('integer'), clock_time: nullable('string'), start_time: nullable('string'),
|
|
8
|
+
mark_time: nullable('string'), end_time: nullable('string'), remark: nullable('string'),
|
|
9
|
+
created_at: nullable('string'), updated_at: nullable('string') };
|
|
10
|
+
|
|
11
|
+
const ruleShape = { rule_id: required('string'), name: nullable('string'), version_time: nullable('string'),
|
|
12
|
+
no_check: required('boolean'), shifts: required([{ period: nullable('integer'),
|
|
13
|
+
clock_in_required: nullable('boolean'), clock_out_required: nullable('boolean'),
|
|
14
|
+
clock_in_start_time: nullable('string'), clock_in_mark_time: nullable('string'), clock_in_end_time: nullable('string'),
|
|
15
|
+
clock_out_start_time: nullable('string'), clock_out_mark_time: nullable('string'), clock_out_end_time: nullable('string') }]) };
|
|
16
|
+
|
|
17
|
+
const dormitoryRuleShape = { rule_id: required('string'), name: nullable('string'), version_time: nullable('string'),
|
|
18
|
+
no_check: required('boolean'), shifts: required([{ period: nullable('integer'), in_out_type: nullable('integer'),
|
|
19
|
+
start_time: nullable('string'), end_time: nullable('string'), polling: nullable('boolean') }]) };
|
|
20
|
+
|
|
21
|
+
const dayShape = {
|
|
22
|
+
date: required('string'),
|
|
23
|
+
clock_records: required([{ record_id: required('string'), clock_time: nullable('string'),
|
|
24
|
+
in_out_type: nullable('string'), source: nullable('integer'), source_name: nullable('string'),
|
|
25
|
+
location: nullable('string'), device_id: nullable('string') }]),
|
|
26
|
+
attendance_results: required([attendanceResult]),
|
|
27
|
+
rules: required([ruleShape]),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function teacherQuery(args) {
|
|
31
|
+
const payload = options(args, ['date', 'scope', 'target_user_id'], [], [], true);
|
|
32
|
+
requireArguments(payload, 'date');
|
|
33
|
+
payload.scope ??= 'mine';
|
|
34
|
+
if (!['mine', 'managed'].includes(payload.scope)) throw invalid('--scope 只支持 mine 或 managed。');
|
|
35
|
+
if (payload.scope === 'managed') requireArguments(payload, 'target_user_id');
|
|
36
|
+
else if (payload.target_user_id != null) throw invalid('--scope mine 不能提供 --target-user-id。');
|
|
37
|
+
if (!validDate(payload.date)) throw invalid('--date 必须为有效的 YYYY-MM-DD 日期。');
|
|
38
|
+
return payload;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function execute(args) {
|
|
42
|
+
const payload = teacherQuery(args);
|
|
43
|
+
const result = await gateway.execute('attendance-leave:read', 'attendance/show', payload, {
|
|
44
|
+
scope: required('string'), tenant_id: required('string'), operator_id: required('string'),
|
|
45
|
+
subject_id: required('string'), subject_name: nullable('string'), ...dayShape,
|
|
46
|
+
});
|
|
47
|
+
const data = result.value.data;
|
|
48
|
+
if (data.scope !== payload.scope || data.subject_id !== (payload.scope === 'mine' ? data.operator_id : payload.target_user_id)
|
|
49
|
+
|| data.date !== payload.date) {
|
|
50
|
+
const failure = new Failure(1, 'api', 'invalid_response', '考勤返回的查询范围、人员或日期与请求不一致。');
|
|
51
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
52
|
+
throw failure;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value))
|
|
58
|
+
&& new Date(value).toISOString().slice(0, 10) === value;
|
|
59
|
+
const positiveId = value => typeof value === 'string' && /^[1-9][0-9]*$/.test(value) && BigInt(value) <= 9223372036854775807n;
|
|
60
|
+
const month = value => +value.slice(0, 4) * 12 + +value.slice(5, 7);
|
|
61
|
+
const validTime = value => value == null || /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)
|
|
62
|
+
&& Number.isFinite(Date.parse(value.replace(' ', 'T') + 'Z'))
|
|
63
|
+
&& new Date(value.replace(' ', 'T') + 'Z').toISOString().slice(0, 19).replace('T', ' ') === value;
|
|
64
|
+
|
|
65
|
+
async function executeApplicableRule(args, context = 'own') {
|
|
66
|
+
const teacher = context === 'own';
|
|
67
|
+
const domain = context === 'dormitory' ? 'dormitory-attendance' : context === 'student' ? 'student-attendance' : 'attendance';
|
|
68
|
+
const payload = teacher ? teacherQuery(args) : options(args, ['date', 'target_user_id'], [], [], true);
|
|
69
|
+
requireArguments(payload, 'date', ...(!teacher ? ['target_user_id'] : []));
|
|
70
|
+
if (!validDate(payload.date)) throw invalid('--date 必须为有效的 YYYY-MM-DD 日期。');
|
|
71
|
+
const result = await gateway.execute('attendance-leave:read', domain + '/applicable-rule', payload, {
|
|
72
|
+
...(teacher ? { scope: required('string') } : {}),
|
|
73
|
+
tenant_id: required('string'), operator_id: required('string'), subject_id: required('string'), subject_name: nullable('string'),
|
|
74
|
+
date: required('string'), read_at: required('string'), rule: present(context === 'dormitory' ? dormitoryRuleShape : ruleShape),
|
|
75
|
+
});
|
|
76
|
+
const data = result.value.data;
|
|
77
|
+
if (teacher && data.scope !== payload.scope || data.subject_id !== (teacher && payload.scope === 'mine' ? data.operator_id : payload.target_user_id) || data.date !== payload.date || !validTime(data.read_at) || !Object.hasOwn(data, 'rule')
|
|
78
|
+
|| data.rule != null && !positiveId(data.rule.rule_id)) {
|
|
79
|
+
const failure = new Failure(1, 'api', 'invalid_response', '适用考勤规则的日期、读取时间或规则标识缺失或无效。');
|
|
80
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
81
|
+
throw failure;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function managedQuery(args, domain, paged = true) {
|
|
87
|
+
const students = domain === 'student-attendance';
|
|
88
|
+
const dormitory = domain === 'dormitory-attendance';
|
|
89
|
+
const payload = options(args, ['start_date', 'end_date', 'target_user_id', 'name', 'rule_id', 'status', ...(paged ? ['page', 'page_size'] : []),
|
|
90
|
+
...(students || dormitory ? ['class_id'] : ['scope']), ...(students ? ['tag_id'] : []), ...(dormitory ? ['manage_id', 'period'] : [])], paged || dormitory ? ['abnormal'] : [], [], true);
|
|
91
|
+
return validateManagedQuery(payload, domain, paged);
|
|
92
|
+
}
|
|
93
|
+
function validateManagedQuery(payload, domain, paged) {
|
|
94
|
+
if (domain === 'attendance') {
|
|
95
|
+
payload.scope ??= 'mine';
|
|
96
|
+
if (!['mine', 'managed'].includes(payload.scope)) throw invalid('--scope 只支持 mine 或 managed。');
|
|
97
|
+
if (payload.scope === 'mine' && (payload.target_user_id != null || payload.name != null))
|
|
98
|
+
throw invalid('mine 范围不能提供 --target-user-id 或 --name;查询其他教师请使用 --scope managed。');
|
|
99
|
+
}
|
|
100
|
+
if (payload.abnormal != null && (!['attendance', 'student-attendance', 'dormitory-attendance'].includes(domain) || !paged && domain !== 'dormitory-attendance' || typeof payload.abnormal !== 'boolean'))
|
|
101
|
+
throw invalid('--abnormal 仅用于考勤列表或宿舍考勤统计。');
|
|
102
|
+
if (payload.abnormal && payload.status != null) throw invalid('--abnormal 与 --status 不能同时提供。');
|
|
103
|
+
const dormitory = domain === 'dormitory-attendance';
|
|
104
|
+
requireArguments(payload, 'start_date', 'end_date');
|
|
105
|
+
if (!validDate(payload.start_date) || !validDate(payload.end_date) || payload.start_date > payload.end_date
|
|
106
|
+
|| (dormitory ? (Date.parse(payload.end_date) - Date.parse(payload.start_date)) / 86400000 >= 31
|
|
107
|
+
: month(payload.end_date) - month(payload.start_date) > 1))
|
|
108
|
+
throw invalid('--start-date 和 --end-date 必须为有效的YYYY-MM-DD,开始不能晚于结束,' + (dormitory ? '含首尾最多31天。' : '最多跨两个自然月。'));
|
|
109
|
+
if (paged) for (const [key, fallback, maximum] of [['page', 1, 2147483647], ['page_size', 20, 50]]) {
|
|
110
|
+
const value = payload[key] ?? fallback;
|
|
111
|
+
if (!/^[0-9]+$/.test(String(value)) || +value < 1 || +value > maximum) throw invalid(key + ' 必须为1到' + maximum + '的整数。');
|
|
112
|
+
payload[key] = +value;
|
|
113
|
+
}
|
|
114
|
+
if (paged && (payload.page - 1) * payload.page_size > 2147483647) throw invalid('分页偏移不能超过2147483647,请缩小查询条件。');
|
|
115
|
+
if (payload.tag_id != null && (domain !== 'student-attendance' || !paged || !payload.tag_id.trim() || payload.tag_id.length > 200)) throw invalid('--tag-id 须为 tags 返回的标签 ID,仅用于学生校门考勤列表。');
|
|
116
|
+
if (payload.rule_id != null && !positiveId(payload.rule_id)) throw invalid('--rule-id 必须为有效的规则ID。');
|
|
117
|
+
if (payload.name != null && payload.name.length > 50 || payload.class_id?.includes(',')) throw invalid('姓名最多50字,班级标识不能包含逗号。');
|
|
118
|
+
if (dormitory) {
|
|
119
|
+
if (payload.manage_id != null && (!positiveId(payload.manage_id) || payload.class_id != null)) throw invalid('--group-id 必须为真实管理组ID,不能与--class-id同时提供。');
|
|
120
|
+
if (payload.period != null) {
|
|
121
|
+
if (!/^[1-9][0-9]*$/.test(payload.period) || +payload.period > 2147483647 || payload.rule_id == null) throw invalid('--period 必须为正整数,并同时提供--rule-id。');
|
|
122
|
+
payload.period = +payload.period;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (payload.status != null) {
|
|
126
|
+
const enums = require('./attendance-leave-enums.cjs');
|
|
127
|
+
payload.status = enums.encode(dormitory ? enums.dormitoryStatus : enums.gateStatus, payload.status, '--status');
|
|
128
|
+
}
|
|
129
|
+
return payload;
|
|
130
|
+
}
|
|
131
|
+
async function executeManaged(args, domain) {
|
|
132
|
+
const students = domain === 'student-attendance';
|
|
133
|
+
const dormitory = domain === 'dormitory-attendance';
|
|
134
|
+
const payload = managedQuery(args, domain);
|
|
135
|
+
const mine = domain === 'attendance' && payload.scope === 'mine';
|
|
136
|
+
const result = await gateway.execute('attendance-leave:read', domain + '/list', payload, {
|
|
137
|
+
tenant_id: required('string'), operator_id: required('string'), attendance_type: required('string'),
|
|
138
|
+
start_date: required('string'), end_date: required('string'), school_wide: required('boolean'),
|
|
139
|
+
scope_classes: required([{ class_id: required('string'), class_name: required('string') }]),
|
|
140
|
+
...(domain === 'attendance' ? { scope: required('string') } : {}),
|
|
141
|
+
...(students ? { tag_id: nullable('string'), tag_name: nullable('string') } : {}),
|
|
142
|
+
...(dormitory ? { abnormal: required('boolean'), scope_manage_groups: required([{ manage_id: required('string'), manage_name: required('string'), building_id: nullable('string'),
|
|
143
|
+
bedrooms: required([{ bedroom_id: required('string'), bedroom_name: required('string') }]) }]) } : {}),
|
|
144
|
+
items: required([{ user_id: required('string'), user_name: nullable('string'), date: required('string'),
|
|
145
|
+
department_ids: nullable('string'), department_name: nullable('string'),
|
|
146
|
+
clock_record_id: nullable('string'), clock_method: nullable('integer'),
|
|
147
|
+
...(dormitory ? { bedroom_id: nullable('string'), bedroom_name: nullable('string') } : {}), result: required(attendanceResult) }]),
|
|
148
|
+
page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'),
|
|
149
|
+
});
|
|
150
|
+
const data = result.value.data;
|
|
151
|
+
const classes = new Set(data.scope_classes.map(row => row.class_id));
|
|
152
|
+
const ids = new Set(data.items.map(row => row.result.result_id));
|
|
153
|
+
const groups = dormitory ? data.scope_manage_groups : [];
|
|
154
|
+
const groupIds = new Set(groups.map(row => row.manage_id));
|
|
155
|
+
const rooms = new Set(groups.filter(row => payload.manage_id == null || row.manage_id === payload.manage_id).flatMap(row => row.bedrooms.map(room => room.bedroom_id)));
|
|
156
|
+
const valid = data.attendance_type === (dormitory ? '2' : students ? '1' : '4') && data.start_date === payload.start_date && data.end_date === payload.end_date
|
|
157
|
+
&& (!students || data.tag_id === (payload.tag_id ?? null) && (payload.tag_id == null ? data.tag_name == null : !!data.tag_name?.trim()))
|
|
158
|
+
&& data.page === payload.page && data.page_size === payload.page_size && data.total >= 0 && data.total_pages === Math.ceil(data.total / data.page_size)
|
|
159
|
+
&& data.items.length === Math.min(data.page_size, Math.max(0, data.total - (data.page - 1) * data.page_size))
|
|
160
|
+
&& (students || dormitory || data.scope === payload.scope && data.school_wide === !mine)
|
|
161
|
+
&& (data.school_wide || mine ? classes.size === 0 : classes.size > 0 || dormitory && groups.length > 0)
|
|
162
|
+
&& (!dormitory || data.abnormal === Boolean(payload.abnormal) && groupIds.size === groups.length && (payload.manage_id == null || groupIds.has(payload.manage_id))
|
|
163
|
+
&& groups.every(group => positiveId(group.manage_id) && (group.building_id == null || positiveId(group.building_id))
|
|
164
|
+
&& new Set(group.bedrooms.map(room => room.bedroom_id)).size === group.bedrooms.length && group.bedrooms.every(room => positiveId(room.bedroom_id))))
|
|
165
|
+
&& classes.size === data.scope_classes.length && data.scope_classes.every(row => !row.class_id.includes(',')) && ids.size === data.items.length
|
|
166
|
+
&& (data.school_wide || payload.class_id == null || classes.has(payload.class_id))
|
|
167
|
+
&& data.items.every(row => {
|
|
168
|
+
const departments = row.department_ids?.split(',') || [];
|
|
169
|
+
return validDate(row.date) && row.date >= payload.start_date && row.date <= payload.end_date
|
|
170
|
+
&& (dormitory ? (row.bedroom_id == null || positiveId(row.bedroom_id))
|
|
171
|
+
&& (payload.manage_id == null && (data.school_wide || classes.size > 0) || rooms.has(row.bedroom_id))
|
|
172
|
+
: (mine || data.school_wide || departments.some(id => classes.has(id))) && (payload.class_id == null || departments.includes(payload.class_id)))
|
|
173
|
+
&& (!mine || row.user_id === data.operator_id)
|
|
174
|
+
&& (payload.target_user_id == null || row.user_id === payload.target_user_id)
|
|
175
|
+
&& (payload.name == null || row.user_name?.includes(payload.name)) && (payload.status == null || row.result.status === payload.status)
|
|
176
|
+
&& (!payload.abnormal || (dormitory ? [13, 14, 15, 16] : [-1, -2, -3]).includes(row.result.status))
|
|
177
|
+
&& (payload.period == null || row.result.period === payload.period)
|
|
178
|
+
&& (row.result.rule_id == null || positiveId(row.result.rule_id)) && (payload.rule_id == null || row.result.rule_id === payload.rule_id)
|
|
179
|
+
&& ['clock_time', 'start_time', 'mark_time', 'end_time', 'created_at', 'updated_at'].every(key => validTime(row.result[key]));
|
|
180
|
+
});
|
|
181
|
+
if (!valid) {
|
|
182
|
+
const failure = new Failure(1, 'api', 'invalid_response', '考勤返回的类型、日期、职责范围、筛选或分页信息不一致。');
|
|
183
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
184
|
+
throw failure;
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function executeTags(args) {
|
|
190
|
+
const payload = options(args, ['name'], [], [], true);
|
|
191
|
+
requireArguments(payload, 'name');
|
|
192
|
+
if (!payload.name.trim() || payload.name.length > 50) throw invalid('--name 须为1到50字的标签名称。');
|
|
193
|
+
const result = await gateway.execute('attendance-leave:read', 'student-attendance/tags', payload, {
|
|
194
|
+
tenant_id: required('string'), operator_id: required('string'), name: required('string'),
|
|
195
|
+
items: required([{ id: required('string'), name: required('string'), type: required('string') }]),
|
|
196
|
+
});
|
|
197
|
+
const data = result.value.data;
|
|
198
|
+
if (data.name !== payload.name || new Set(data.items.map(row => row.id)).size !== data.items.length
|
|
199
|
+
|| data.items.some(row => !row.id.trim() || row.id.length > 200 || !row.name.includes(payload.name) || !['general', 'system'].includes(row.type))) {
|
|
200
|
+
const failure = new Failure(1, 'api', 'invalid_response', '标签查询返回的名称、标识或类型不一致。');
|
|
201
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
202
|
+
throw failure;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function executeManagedDay(args, domain) {
|
|
208
|
+
const students = domain === 'student-attendance', dormitory = domain === 'dormitory-attendance';
|
|
209
|
+
const payload = options(args, ['target_user_id', 'date'], [], [], true);
|
|
210
|
+
requireArguments(payload, 'target_user_id', 'date');
|
|
211
|
+
if (!validDate(payload.date)) throw invalid('--date 必须为有效的 YYYY-MM-DD 日期。');
|
|
212
|
+
const result = await gateway.execute('attendance-leave:read', domain + '/show', payload, {
|
|
213
|
+
tenant_id: required('string'), operator_id: required('string'), subject_id: required('string'), subject_name: nullable('string'),
|
|
214
|
+
attendance_type: required('string'), day: required(dormitory ? { ...dayShape,
|
|
215
|
+
rules: required([dormitoryRuleShape]),
|
|
216
|
+
} : dayShape),
|
|
217
|
+
});
|
|
218
|
+
const data = result.value.data;
|
|
219
|
+
if (data.subject_id !== payload.target_user_id || data.attendance_type !== (dormitory ? '2' : students ? '1' : '4') || data.day.date !== payload.date) {
|
|
220
|
+
const failure = new Failure(1, 'api', 'invalid_response', '考勤返回的目标人员、业务类型或日期与请求不一致。');
|
|
221
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
222
|
+
throw failure;
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
async function executeRemark(args, kind = 'own') {
|
|
227
|
+
const student = kind === 'student', dormitory = kind === 'dormitory', teacher = !student && !dormitory;
|
|
228
|
+
const domain = dormitory ? 'dormitory-attendance' : student ? 'student-attendance' : 'attendance';
|
|
229
|
+
const payload = options(args, ['date', 'result_id', 'remark', 'target_user_id', ...(teacher ? ['scope'] : [])], ['clear'], ['remark'], true);
|
|
230
|
+
if (teacher) {
|
|
231
|
+
payload.scope ??= 'mine';
|
|
232
|
+
if (!['mine', 'managed'].includes(payload.scope)) throw invalid('--scope 只支持 mine 或 managed。');
|
|
233
|
+
if (payload.scope === 'mine' && payload.target_user_id != null) throw invalid('--scope mine 不能提供 --target-user-id。');
|
|
234
|
+
}
|
|
235
|
+
const managed = !teacher || payload.scope === 'managed';
|
|
236
|
+
requireArguments(payload, 'date', 'result_id', ...(managed ? ['target_user_id'] : []));
|
|
237
|
+
if (!validDate(payload.date) || (payload.remark != null) === (payload.clear === true))
|
|
238
|
+
throw invalid('--date 必须为有效的 YYYY-MM-DD;--remark <text> 与 --clear 必须且只能选择一项。');
|
|
239
|
+
if (payload.clear) payload.remark = '';
|
|
240
|
+
delete payload.clear;
|
|
241
|
+
try {
|
|
242
|
+
const result = await gateway.execute('attendance-leave:write', domain + '/remark', payload, {
|
|
243
|
+
tenant_id: required('string'), operator_id: required('string'), subject_id: required('string'),
|
|
244
|
+
...(teacher ? { scope: required('string') } : {}),
|
|
245
|
+
date: required('string'), result_id: required('string'),
|
|
246
|
+
request_id: required('string'), execution_status: required('string'), message: required('string'),
|
|
247
|
+
});
|
|
248
|
+
const data = result.value.data;
|
|
249
|
+
if (data.date !== payload.date || data.result_id !== payload.result_id || data.subject_id !== (managed ? payload.target_user_id : data.operator_id) || teacher && data.scope !== payload.scope
|
|
250
|
+
|| !['COMPLETED', 'NOT_EXECUTED', 'UNCONFIRMED'].includes(data.execution_status)
|
|
251
|
+
|| result.value.request_id && result.value.request_id !== data.request_id) {
|
|
252
|
+
const failure = new Failure(1, 'api', 'invalid_response', '考勤备注回执与目标、日期、请求或执行阶段不一致,结果待核实;请查询考勤详情,不要自动重试。');
|
|
253
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
254
|
+
throw failure;
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
} catch (failure) {
|
|
258
|
+
if (failure instanceof Failure) {
|
|
259
|
+
const error = failure.result.value.error;
|
|
260
|
+
const unknown = error.type === 'network' && error.subtype !== 'connection_failed'
|
|
261
|
+
|| error.type === 'api' && error.subtype === 'invalid_response';
|
|
262
|
+
error.details = { ...error.details, ...(teacher ? { scope: payload.scope } : {}), ...(managed ? { target_user_id: payload.target_user_id } : {}), date: payload.date, result_id: payload.result_id,
|
|
263
|
+
...(unknown ? { execution_status: 'UNCONFIRMED' } : {}) };
|
|
264
|
+
if (unknown && !error.message.includes('不要自动重试')) error.message += dormitory ? '请查询目标学生宿舍考勤详情核实备注,不要自动重试。' : student ? '请查询目标学生校门考勤详情核实备注,不要自动重试。' : managed ? '请查询目标教师考勤详情核实备注,不要自动重试。' : '请查询本人考勤详情核实备注,不要自动重试。';
|
|
265
|
+
}
|
|
266
|
+
throw failure;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function executeCorrectionForm(args) {
|
|
270
|
+
const payload = options(args, ['date', 'result_id', 'correction_time', 'field_values'], [], [], true);
|
|
271
|
+
requireArguments(payload, 'date', 'result_id');
|
|
272
|
+
require('./leave.cjs').parseFieldValues(payload);
|
|
273
|
+
if (payload.correction_time !== undefined) {
|
|
274
|
+
require('./leave.cjs').validateResumptionTime(payload.correction_time);
|
|
275
|
+
if (payload.correction_time.slice(0, 10) !== payload.date) throw invalid('补卡时间必须属于所选考勤日期。');
|
|
276
|
+
}
|
|
277
|
+
if (!validDate(payload.date)) throw invalid('--date 必须为有效的 YYYY-MM-DD 日期。');
|
|
278
|
+
try {
|
|
279
|
+
const result = await gateway.execute('attendance-leave:read', 'attendance/correction-form', { date: payload.date, result_id: payload.result_id }, {
|
|
280
|
+
tenant_id: required('string'), user_id: required('string'), date: required('string'), result: required(attendanceResult),
|
|
281
|
+
rule_name: nullable('string'), rule_version_time: nullable('string'), correction_enabled: required('boolean'),
|
|
282
|
+
times_per_month: nullable('integer'), day_delay: nullable('integer'), used_in_month: required('integer'),
|
|
283
|
+
status: required('string'), reason: nullable('string'), program_code: nullable('string'),
|
|
284
|
+
time_field_key: nullable('string'), fields: nullable([require('./leave.cjs').teacherFormField]),
|
|
285
|
+
});
|
|
286
|
+
const data = result.value.data;
|
|
287
|
+
if (data.date !== payload.date || data.result.result_id !== payload.result_id || !positiveId(data.result.rule_id)
|
|
288
|
+
|| data.used_in_month < 0 || !data.correction_enabled && (data.times_per_month != null || data.day_delay != null)) {
|
|
289
|
+
const failure = new Failure(1, 'api', 'invalid_response', '补卡查询返回的日期、时段、规则、开关或用量不一致。');
|
|
290
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
291
|
+
throw failure;
|
|
292
|
+
}
|
|
293
|
+
if (data.status === 'unavailable') {
|
|
294
|
+
if (data.correction_enabled || !data.reason?.trim() || data.program_code != null || data.time_field_key != null || data.fields != null) {
|
|
295
|
+
const failure = new Failure(1, 'api', 'invalid_response', '补卡关闭状态与原因或表单字段不一致。');
|
|
296
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
297
|
+
throw failure;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
const timeFields = data.time_field_key == null ? [] : (data.fields || []).filter(field => field.field_key === data.time_field_key);
|
|
301
|
+
if (data.status !== 'ready' || !data.correction_enabled || !data.program_code?.trim() || data.reason != null || !validCorrectionFields(data.fields)
|
|
302
|
+
|| data.time_field_key != null && (timeFields.length !== 1
|
|
303
|
+
|| !['date', 'datetime'].includes(timeFields[0].type))) {
|
|
304
|
+
const failure = new Failure(1, 'api', 'invalid_response', '本人补卡表单的开关、流程或可见时间字段不完整或不一致。');
|
|
305
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
306
|
+
throw failure;
|
|
307
|
+
}
|
|
308
|
+
data.form_check = require('./teacher-leave-form-check.cjs').evaluate({ ...data,
|
|
309
|
+
field_mappings: data.time_field_key ? { correctionTime: data.time_field_key } : {} }, payload.field_values, payload);
|
|
310
|
+
if (!payload.correction_time) {
|
|
311
|
+
data.form_check.issues.push({ code: 'missing_required', parameter: 'correction_time', message: '请补充实际需要补卡的时间。' });
|
|
312
|
+
if (data.form_check.status === 'complete') data.form_check.status = 'needs_input';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
} catch (failure) {
|
|
317
|
+
if (failure instanceof Failure) {
|
|
318
|
+
const error = failure.result.value.error;
|
|
319
|
+
error.details = { ...error.details, date: payload.date, result_id: payload.result_id };
|
|
320
|
+
}
|
|
321
|
+
throw failure;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function checkCorrectionBeforeSubmit(payload) {
|
|
326
|
+
try {
|
|
327
|
+
const args = ['--date', payload.date, '--result-id', payload.result_id, '--correction-time', payload.correction_time];
|
|
328
|
+
if (payload.field_values !== undefined) args.push('--field-values', JSON.stringify(payload.field_values));
|
|
329
|
+
const form = (await executeCorrectionForm(args)).value.data;
|
|
330
|
+
if (form.status === 'unavailable') throw new Failure(2, 'validation', 'correction_unavailable', form.reason);
|
|
331
|
+
if (form.form_check.status !== 'complete') throw new Failure(2, 'validation', 'form_input_required',
|
|
332
|
+
'补卡表单还有需要处理的信息,尚未上传文件或提交申请。', { form_check: form.form_check });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const details = error.result?.value?.error;
|
|
335
|
+
if (details) {
|
|
336
|
+
details.execution_status = 'NOT_EXECUTED'; details.step = 'check_form_before_upload';
|
|
337
|
+
if (['auth_required', 'missing_scope'].includes(details.subtype)) details.required_scopes = ['attendance-leave:read', 'attendance-leave:write'];
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function validCorrectionFields(fields) {
|
|
343
|
+
return Array.isArray(fields) && fields.every(field => field.field_key.trim() && field.type.trim()
|
|
344
|
+
&& typeof field.editable === 'boolean' && (field.groups == null || field.groups.every(group => validCorrectionFields(group.fields))));
|
|
345
|
+
}
|
|
346
|
+
async function executeReportSummary(args) {
|
|
347
|
+
const payload = options(args, ['month', 'rule_id', 'keyword', 'page', 'page_size'], [], [], true);
|
|
348
|
+
for (const [key, fallback, maximum] of [['page', 1, 2147483647], ['page_size', 20, 100]]) {
|
|
349
|
+
if (payload[key] == null) payload[key] = fallback;
|
|
350
|
+
else if (!/^[1-9][0-9]*$/.test(payload[key]) || Number(payload[key]) > maximum) throw invalid('--' + key.replace('_', '-') + ' 超出有效分页范围。');
|
|
351
|
+
else payload[key] = Number(payload[key]);
|
|
352
|
+
}
|
|
353
|
+
validateReportQuery(payload);
|
|
354
|
+
const result = await gateway.execute('attendance-leave:read', 'attendance/report-summary', payload, reportShape);
|
|
355
|
+
validateReportSummary(result, payload);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
const reportRowShape = {
|
|
359
|
+
user_id: required('string'), user_name: nullable('string'), department: nullable('string'), date: required('string'),
|
|
360
|
+
rule_id: required('string'), rule_name: nullable('string'), has_approved_leave: required('boolean'), result: required('string'), reasons: required(['string']),
|
|
361
|
+
slots: required([{ required: required('boolean'), period: required('integer'), direction: required('string'), start: required('integer'), threshold: required('integer'), end: required('integer') }]),
|
|
362
|
+
leaves: required([{ start: required('integer'), end: required('integer'), daily_start: required('integer'), daily_end: required('integer') }]),
|
|
363
|
+
events: required([{ time: required('integer'), direction: required('string') }]),
|
|
364
|
+
};
|
|
365
|
+
const reportShape = {
|
|
366
|
+
tenant_id: required('string'), operator_id: required('string'), month: required('string'), rule_id: nullable('string'), keyword: nullable('string'),
|
|
367
|
+
all_count: required('integer'), normal_count: required('integer'), abnormal_count: required('integer'), leave_count: required('integer'),
|
|
368
|
+
generated_at: required('integer'), scope_note: required('string'), rules: required([{ rule_id: required('string'), name: nullable('string') }]),
|
|
369
|
+
page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'), items: required([reportRowShape]),
|
|
370
|
+
};
|
|
371
|
+
function validateReportQuery(payload) {
|
|
372
|
+
requireArguments(payload, 'month');
|
|
373
|
+
if (!validDate(payload.month + '-01') || payload.month < '2000-01') throw invalid('--month 必须为有效的 YYYY-MM,不能早于2000-01。');
|
|
374
|
+
if (payload.rule_id != null && !positiveId(payload.rule_id)) throw invalid('--rule-id 必须为有效的规则ID。');
|
|
375
|
+
if (payload.keyword != null && payload.keyword.length > 100) throw invalid('--keyword 最多100个字符。');
|
|
376
|
+
}
|
|
377
|
+
function validateReportSummary(result, payload) {
|
|
378
|
+
const data = result.value.data;
|
|
379
|
+
const ids = new Set(data.rules.map(rule => rule.rule_id));
|
|
380
|
+
const timestamp = value => Number.isSafeInteger(value) && value > 0;
|
|
381
|
+
const detailsValid = data.page === payload.page && data.page_size === payload.page_size && data.total === data.all_count
|
|
382
|
+
&& data.total_pages === Math.ceil(data.total / data.page_size)
|
|
383
|
+
&& data.items.length === Math.min(data.page_size, Math.max(0, data.total - (data.page - 1) * data.page_size))
|
|
384
|
+
&& new Set(data.items.map(row => JSON.stringify([row.user_id, row.date, row.rule_id]))).size === data.items.length
|
|
385
|
+
&& data.items.every(row => row.user_id.trim() && validDate(row.date) && row.date.startsWith(payload.month + '-')
|
|
386
|
+
&& ids.has(row.rule_id) && (payload.rule_id == null || row.rule_id === payload.rule_id)
|
|
387
|
+
&& ['normal', 'abnormal', 'leave', 'pending'].includes(row.result) && row.has_approved_leave === (row.leaves.length > 0)
|
|
388
|
+
&& row.slots.every(slot => slot.period > 0 && ['in', 'out'].includes(slot.direction)
|
|
389
|
+
&& timestamp(slot.start) && timestamp(slot.threshold) && timestamp(slot.end) && slot.start <= slot.end)
|
|
390
|
+
&& row.leaves.every(leave => [leave.start, leave.end, leave.daily_start, leave.daily_end].every(timestamp)
|
|
391
|
+
&& leave.start <= leave.daily_start && leave.daily_start <= leave.daily_end && leave.daily_end <= leave.end)
|
|
392
|
+
&& row.events.every(event => timestamp(event.time) && ['in', 'out', 'unknown'].includes(event.direction)));
|
|
393
|
+
if (!detailsValid || data.month !== payload.month || data.rule_id !== (payload.rule_id ?? null) || data.keyword !== (payload.keyword ?? null)
|
|
394
|
+
|| ['all_count', 'normal_count', 'abnormal_count', 'leave_count'].some(key => data[key] < 0)
|
|
395
|
+
|| data.normal_count + data.abnormal_count > data.all_count || data.leave_count > data.all_count || data.generated_at <= 0
|
|
396
|
+
|| !data.scope_note.trim() || data.rules.some(rule => !positiveId(rule.rule_id)) || ids.size !== data.rules.length
|
|
397
|
+
|| data.all_count > 0 && ids.size === 0 || payload.rule_id != null && !ids.has(payload.rule_id)) {
|
|
398
|
+
const failure = new Failure(1, 'api', 'invalid_response', '教师月报返回的月份、筛选、统计或分页明细不完整或不一致。');
|
|
399
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
400
|
+
throw failure;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
module.exports = { attendanceResult, validTime, validDate, executeTags, checkCorrectionBeforeSubmit, managedQuery, validateManagedQuery, execute, executeApplicableRule, executeManaged, executeManagedDay, executeRemark, executeCorrectionForm, executeReportSummary };
|
package/lib/auth.cjs
CHANGED
|
@@ -53,7 +53,7 @@ async function login(args, signal) {
|
|
|
53
53
|
const failure = invalidAuthorization('Agent Gateway 返回了无效授权会话。', result);
|
|
54
54
|
throw urlError ? attach(failure, urlError) : failure;
|
|
55
55
|
}
|
|
56
|
-
output.value.data.message = '
|
|
56
|
+
output.value.data.message = '授权会话已创建。请将 authorization_url 展示为可点击的 Markdown 链接:[完整 URL](完整 URL),两处保留完整原始地址,不使用行内代码或代码块。用用户的语言和业务名称说明所需权限,不直接展示权限代码。等待用户明确确认网页授权完成后,使用 device_code 兑换登录态。';
|
|
57
57
|
return output;
|
|
58
58
|
} catch (error) {
|
|
59
59
|
if (token && error.result?.value.error.type === 'authentication' && error.result.value.error.subtype === 'auth_required') {
|
package/lib/business-schemas.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { optionalFields, nullableFields, nullable, required } = require('./result.cjs');
|
|
1
|
+
const { optionalFields, nullableFields, nullable, required, fields } = require('./result.cjs');
|
|
2
2
|
const { snippet } = require('./lesson-prep.cjs');
|
|
3
3
|
const resource = { ...optionalFields({ type: 'string', file_id: 'string', kind: 'string', space: 'string', name: 'string',
|
|
4
4
|
path: 'string', file_ext: 'string', file_size: 'integer', label_ids: ['string'], updated_at: 'string', web_url: 'string' }), category_id: nullable('string') };
|
|
@@ -6,6 +6,8 @@ const folder = optionalFields({ event: 'string', folder_id: 'string', name: 'str
|
|
|
6
6
|
const textbook = optionalFields({ event: 'string', textbook_id: 'string', folder_id: 'string', name: 'string', path: 'string' });
|
|
7
7
|
module.exports = {
|
|
8
8
|
errors: {
|
|
9
|
+
resumption_operation_unconfirmed: (value, path) => fields(value, { [Object.hasOwn(value, 'id') ? 'id' : 'application_id']: required('string'), action: required('string') }, path),
|
|
10
|
+
leave_operation_unconfirmed: { id: required('string'), action: required('string') },
|
|
9
11
|
...require('./report-schemas.cjs').errors,
|
|
10
12
|
invalid_pattern: { syntax: ['string'], examples: ['string'] },
|
|
11
13
|
file_report_pending: { report_id: 'string', report_status: 'string' },
|