weaver-work-cli 0.1.21 → 0.1.22
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/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
- package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
- package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
- package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
- package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
- package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
- package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
- package/dist/shortcuts/meeting/operations/env.js +78 -61
- package/dist/shortcuts/meeting/operations/link.js +125 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/shared.js +13 -0
- package/docs/e10-auth.md +4 -0
- package/docs/invoice.md +3 -0
- package/package.json +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +26 -10
- package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
- package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
- package/skills/weaver-e10-meeting.zip +0 -0
|
@@ -20,8 +20,8 @@ import { EB_BASE, ebBusinessSuccess, extractEbRows, queryBody } from './factory.
|
|
|
20
20
|
* 占用视图与回执只对有效状态生成,故无需再额外过滤;
|
|
21
21
|
* - 命中后**严格按应用设置**处理,CLI 不额外加码:
|
|
22
22
|
* - `1` = **禁止提交** → 抛 `conflict_rejected`,怎么都不允许提交(不询问、不新建);
|
|
23
|
-
* - `0` = **仅提醒** → 只写入 warnings
|
|
24
|
-
*
|
|
23
|
+
* - `0` = **仅提醒** → 只写入 warnings(如实告知冲突、建议换时间/换地点、决定权交用户);
|
|
24
|
+
* 服务端与 CLI 都不阻止提交,但 CLI **不主动建议忽略冲突**、不替用户做决定。
|
|
25
25
|
*
|
|
26
26
|
* 失败语义与标准版(create.ts 的 runMandatoryConflictChecks)对齐:
|
|
27
27
|
* - 应用设置读取失败:非致命,按「开启检测 + 仅提醒」继续,并在 warnings 中如实说明;
|
|
@@ -41,12 +41,16 @@ function errorText(error) {
|
|
|
41
41
|
/** 单页 EB 列表查询(list,不计数;筛选走 URL query,body 带 datajson)。 */
|
|
42
42
|
async function requestEbList(env, interfacePk, query, operation) {
|
|
43
43
|
const session = await env.host.getSession();
|
|
44
|
+
// ⚠️ 分页必须进 body.pageInfo(EB 只认 body):仅放 URL query 时 EB 恒定返回首页 10 条,
|
|
45
|
+
// 于是 listAllEbRows 会在第一页误判「末页」⇒ 冲突检测只比对前 10 条(漏检,2026-09-21 修复)。
|
|
46
|
+
const pageNo = typeof query.pageNo === 'number' ? query.pageNo : 1;
|
|
47
|
+
const pageSize = typeof query.pageSize === 'number' ? query.pageSize : PAGE_SIZE;
|
|
44
48
|
const response = await env.host.request({
|
|
45
49
|
method: 'POST',
|
|
46
50
|
path: `${EB_BASE}/getFormDataList/${interfacePk}`,
|
|
47
51
|
operation,
|
|
48
52
|
query,
|
|
49
|
-
body: queryBody(session.userId),
|
|
53
|
+
body: queryBody(session.userId, { pageNo, pageSize }),
|
|
50
54
|
acceptBusiness: ebBusinessSuccess,
|
|
51
55
|
});
|
|
52
56
|
// 行数据可能是 { mainTable: {...} } 包裹,统一摊平(与 member-conflict.mjs 同口径)。
|
|
@@ -184,7 +188,7 @@ export async function runEbConflictCheck(input, env, operation, options = {}) {
|
|
|
184
188
|
* 命中冲突的处置(**fail-fast**):
|
|
185
189
|
* - 禁止提交 → **立即抛错并结束**,**不再继续后续检测** —— 结论已定局为拒绝,
|
|
186
190
|
* 继续检测只会多花请求,也不会改变结果;
|
|
187
|
-
* - 仅提醒 → 记入 warnings
|
|
191
|
+
* - 仅提醒 → 记入 warnings,**不阻止提交**(如实告知 + 建议改期改址,是否继续由用户决定)。
|
|
188
192
|
* `throwOnReject=false`(只查不建的 operation)时,禁止项改为写入 warnings 而不抛错。
|
|
189
193
|
*/
|
|
190
194
|
const handleHit = (action, message, details) => {
|
|
@@ -195,8 +199,8 @@ export async function runEbConflictCheck(input, env, operation, options = {}) {
|
|
|
195
199
|
warnings.push(text);
|
|
196
200
|
return;
|
|
197
201
|
}
|
|
198
|
-
warnings.push(`${message};应用设置仅提醒(action=0
|
|
199
|
-
+ '
|
|
202
|
+
warnings.push(`${message};应用设置仅提醒(action=0):本次不阻止提交,请如实告知用户该冲突并建议更换时间或地点,`
|
|
203
|
+
+ '由用户决定是否继续(不要主动建议忽略冲突,也不要替用户做决定)');
|
|
200
204
|
};
|
|
201
205
|
// ---- 1) 会议室冲突 ----
|
|
202
206
|
if (input.address) {
|
|
@@ -270,12 +274,14 @@ export async function runEbConflictCheck(input, env, operation, options = {}) {
|
|
|
270
274
|
export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env }) => {
|
|
271
275
|
if (endpoint.interfacePk !== 'create_meeting' && endpoint.interfacePk !== 'change_meeting')
|
|
272
276
|
return [];
|
|
273
|
-
|
|
274
|
-
const
|
|
277
|
+
// 载荷自 2026-09-21 起为结构化(mainTable + details),冲突检测只关心主表字段。
|
|
278
|
+
const main = payload.mainTable;
|
|
279
|
+
const begin = main.begin_datetime;
|
|
280
|
+
const end = main.end_datetime;
|
|
275
281
|
if (typeof begin !== 'string' || typeof end !== 'string' || !begin || !end)
|
|
276
282
|
return [];
|
|
277
|
-
const address = typeof
|
|
278
|
-
const hrmMembers = typeof
|
|
283
|
+
const address = typeof main.address === 'string' && main.address ? main.address : undefined;
|
|
284
|
+
const hrmMembers = typeof main.hrm_members === 'string' && main.hrm_members ? main.hrm_members : undefined;
|
|
279
285
|
if (!address && !hrmMembers)
|
|
280
286
|
return [];
|
|
281
287
|
const outcome = await runEbConflictCheck({
|
|
@@ -283,7 +289,7 @@ export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env })
|
|
|
283
289
|
endDatetime: end,
|
|
284
290
|
...(address ? { address } : {}),
|
|
285
291
|
...(hrmMembers ? { hrmMembers } : {}),
|
|
286
|
-
...(typeof
|
|
292
|
+
...(typeof main.org_members === 'string' && main.org_members ? { orgMembers: main.org_members } : {}),
|
|
287
293
|
...(typeof args.id === 'string' || typeof args.id === 'number' ? { excludeMtId: String(args.id) } : {}),
|
|
288
294
|
}, env, endpoint.operation === 'meeting.eb.meeting.change' ? 'meeting.eb.meeting.change.prepare' : 'meeting.eb.meeting.create.prepare');
|
|
289
295
|
return outcome.warnings;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会议决议(决议概述 + 决议任务)与签到补签的**语义化操作**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要封装(2026-09-21 新增,需求:把会议内容汇总成决议任务):
|
|
5
|
+
* EB 原生接口是「多张表 + 多个写接口」的拼装式 API,直接暴露给调用方有三个坑:
|
|
6
|
+
*
|
|
7
|
+
* 1. `create_mt_decision` 只写「决议概述 + 明细」,**不会**改会议自身状态;必须再用
|
|
8
|
+
* `update_mt_isdecision` 写回 `is_decision=2` + `decision_Id` + `mt_status`,
|
|
9
|
+
* 否则会议页仍显示"未决议"、用户会以为决议没生效 —— 而这一步极易被漏掉(真实场景高频)。
|
|
10
|
+
* 2. `sign.create` 与 `sign.update` 字段几乎一致,**补签用错就会新增一条签到记录**(用户实测反馈)。
|
|
11
|
+
* 3. 决议明细一次可能十几条,若不指名"第几条、缺哪个字段",调用方无法定位。
|
|
12
|
+
*
|
|
13
|
+
* 故提供两个语义化入口,把多步串成一次调用(仍走 prepare → 用户确认 → apply 的高风险写入链):
|
|
14
|
+
* - `meeting.eb.decision.publish`:写决议(概述 + 明细)→ 自动写回会议为已决议(含部分失败语义);
|
|
15
|
+
* - `meeting.eb.sign.mark`:按「会议 + 参会人」自动定位签到记录,走 update 补签 / 补签退。
|
|
16
|
+
*/
|
|
17
|
+
import { EB_BASE, ebBusinessSuccess, extractEbRows, fingerprint, normalizeEbWrite, queryBody, requireConfirm, verifyLease, writeBody, } from './factory.js';
|
|
18
|
+
import { EB_ENDPOINTS } from './catalog.js';
|
|
19
|
+
import { meetingSuccess } from '../types.js';
|
|
20
|
+
import { meetingPartialError, meetingValidationError } from '../../errors.js';
|
|
21
|
+
import { toIdString } from '../shared.js';
|
|
22
|
+
const CONTINUATION_TTL_SECONDS = 600;
|
|
23
|
+
const DATE_PATTERN = '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$';
|
|
24
|
+
/** 决议状态:0未决议 / 1决议临时保存 / 2决议提交完成。 */
|
|
25
|
+
const DECISION_DONE = '2';
|
|
26
|
+
/** 会议状态「结束」:产生决议后会议即视为结束。 */
|
|
27
|
+
const MT_STATUS_OVER = '8';
|
|
28
|
+
function requireEndpoint(interfacePk) {
|
|
29
|
+
const endpoint = EB_ENDPOINTS.find((item) => item.interfacePk === interfacePk);
|
|
30
|
+
if (!endpoint)
|
|
31
|
+
throw new Error(`EB catalog 缺少 ${interfacePk},请检查 catalog.ts 声明`);
|
|
32
|
+
return endpoint;
|
|
33
|
+
}
|
|
34
|
+
function businessFailed(response, interfacePk) {
|
|
35
|
+
const datajson = response?.datajson ?? {};
|
|
36
|
+
if (datajson.status === false) {
|
|
37
|
+
throw meetingValidationError('eb_write_not_applied', `${interfacePk} 返回 status=false,服务端未生效,请核对字段与权限后重试`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 决议明细最少 1 条:没有明细就不会有决议任务,等于白建。 */
|
|
41
|
+
function normalizeDecisionRows(input) {
|
|
42
|
+
if (input === undefined || input === null) {
|
|
43
|
+
throw meetingValidationError('missing_required_field', '缺少决议明细 detail1:至少 1 条(每条决议明细会由 ESB 联动生成一条决议任务)', { node: 'detail1', minRows: 1 });
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(input)) {
|
|
46
|
+
throw meetingValidationError('invalid_detail', 'detail1 必须是数组,每个元素为一条决议明细', { node: 'detail1' });
|
|
47
|
+
}
|
|
48
|
+
return input.map((row, index) => {
|
|
49
|
+
const source = (row && typeof row === 'object' ? row : {});
|
|
50
|
+
return {
|
|
51
|
+
// 明细序号与两个状态字段给默认值:调用方通常不关心,缺了会让列表顺序不稳/状态为空。
|
|
52
|
+
sort_number: String(index + 1),
|
|
53
|
+
decision_status: '0',
|
|
54
|
+
related_type: '0',
|
|
55
|
+
...source,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const decisionPublishPrepare = {
|
|
60
|
+
name: 'meeting.eb.decision.publish.prepare',
|
|
61
|
+
resource: 'eb',
|
|
62
|
+
method: 'meeting.eb.decision.publish.prepare',
|
|
63
|
+
action: 'meeting.eb.decision.publish.prepare',
|
|
64
|
+
summary: '准备发布会议决议:写「决议概述 + 明细」,并把会议标记为已决议(生成确认预览,不写入)',
|
|
65
|
+
capability: 'eb.decision',
|
|
66
|
+
risk: 'read-before-write',
|
|
67
|
+
requiresConfirmation: true,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
properties: {
|
|
72
|
+
mt_id: { type: 'string', description: '会议 id(必填)' },
|
|
73
|
+
mt_content: { type: 'string', description: '决议概述文本(必填):由调用方结合会议议题 / 纪要 / 用户提供的材料汇总' },
|
|
74
|
+
detail1: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
minItems: 1,
|
|
77
|
+
description: '决议明细(至少 1 条):每条 = 一项待办任务,ESB 联动据此生成决议任务;executors 必须传**员工 id**',
|
|
78
|
+
items: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
decision_title: { type: 'string', description: '决议标题(必填)' },
|
|
83
|
+
executors: { type: 'string', description: '执行人**员工 id**(必填,多人用逗号分隔)' },
|
|
84
|
+
begin_datetime: { type: 'string', pattern: DATE_PATTERN, description: '开始时间(必填,yyyy-MM-dd HH:mm)' },
|
|
85
|
+
end_datetime: { type: 'string', pattern: DATE_PATTERN, description: '结束时间(必填,yyyy-MM-dd HH:mm)' },
|
|
86
|
+
rummager: { type: 'string', description: '检查人**员工 id**(非必填)' },
|
|
87
|
+
sort_number: { type: 'string', description: '明细序号(省略时按数组顺序自动编号)' },
|
|
88
|
+
decision_status: { type: 'string', description: '决议状态(默认 0 未完成;0未完成/1已完成/2已延期)' },
|
|
89
|
+
related_type: { type: 'string', description: '关联类型(默认 0 未关联;0未关联/1日程/2任务)' },
|
|
90
|
+
relate_task: { type: 'string', description: '关联任务' },
|
|
91
|
+
relate_agenda: { type: 'string', description: '关联日程' },
|
|
92
|
+
},
|
|
93
|
+
required: ['decision_title', 'executors', 'begin_datetime', 'end_datetime'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
mt_status: { type: 'string', description: `写回会议状态,默认 ${MT_STATUS_OVER}(结束);产生决议即视为会议结束` },
|
|
97
|
+
},
|
|
98
|
+
required: ['mt_id', 'mt_content', 'detail1'],
|
|
99
|
+
},
|
|
100
|
+
handler: async (args, env) => {
|
|
101
|
+
const session = await env.host.getSession();
|
|
102
|
+
const create = normalizeEbWrite(requireEndpoint('create_mt_decision'), {
|
|
103
|
+
mt_id: args.mt_id,
|
|
104
|
+
mt_content: args.mt_content,
|
|
105
|
+
// 注意:**不要传 data_status** —— 2026-09-21 真机实测该接口拒绝该字段
|
|
106
|
+
// (DataError:dataKey:data_status,该字段不存在),提交状态由系统自动置 1。
|
|
107
|
+
detail1: normalizeDecisionRows(args.detail1),
|
|
108
|
+
}, session.userId);
|
|
109
|
+
const mtStatus = args.mt_status === undefined || args.mt_status === null || args.mt_status === ''
|
|
110
|
+
? MT_STATUS_OVER
|
|
111
|
+
: String(args.mt_status);
|
|
112
|
+
const payload = { create, mtId: toIdString(args.mt_id, 'mt_id'), mtStatus };
|
|
113
|
+
const token = await env.continuation.issue({
|
|
114
|
+
operation: 'meeting.eb.decision.publish.apply',
|
|
115
|
+
baseUrl: session.baseUrl,
|
|
116
|
+
profile: session.profile,
|
|
117
|
+
userId: session.userId,
|
|
118
|
+
payload,
|
|
119
|
+
fingerprint: fingerprint(payload),
|
|
120
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
121
|
+
return meetingSuccess('meeting.eb.decision.publish.prepare', {
|
|
122
|
+
status: 'AWAITING_CONFIRMATION',
|
|
123
|
+
preview: {
|
|
124
|
+
steps: [
|
|
125
|
+
{
|
|
126
|
+
step: 1,
|
|
127
|
+
interfacePk: 'create_mt_decision',
|
|
128
|
+
path: `${EB_BASE}/saveFormData/create_mt_decision`,
|
|
129
|
+
mainTable: create.mainTable,
|
|
130
|
+
detail1: create.details.detail1 ?? [],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
step: 2,
|
|
134
|
+
interfacePk: 'update_mt_isdecision',
|
|
135
|
+
mainTable: { id: payload.mtId, decision_Id: '<第 1 步返回的 dataIds[0]>', is_decision: DECISION_DONE, mt_status: mtStatus },
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
decisionCount: (create.details.detail1 ?? []).length,
|
|
139
|
+
},
|
|
140
|
+
continuation: token,
|
|
141
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
142
|
+
}, {
|
|
143
|
+
meta: {
|
|
144
|
+
notes: [
|
|
145
|
+
'第 1 步写入决议概述与明细,第 2 步把会议写回为「已决议(is_decision=2)」并置会议状态。',
|
|
146
|
+
'决议任务由 ESB 联动按明细生成,本操作不直接写任务表。',
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const decisionPublishApply = {
|
|
153
|
+
name: 'meeting.eb.decision.publish.apply',
|
|
154
|
+
resource: 'eb',
|
|
155
|
+
method: 'meeting.eb.decision.publish.apply',
|
|
156
|
+
action: 'meeting.eb.decision.publish.apply',
|
|
157
|
+
summary: '提交会议决议:写入决议概述 + 明细,并把会议标记为已决议;必须用户明确确认后调用',
|
|
158
|
+
capability: 'eb.decision',
|
|
159
|
+
risk: 'high-risk-write',
|
|
160
|
+
requiresConfirmation: true,
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
properties: {
|
|
165
|
+
confirm: { const: true },
|
|
166
|
+
continuation: { type: 'string', minLength: 1 },
|
|
167
|
+
},
|
|
168
|
+
required: ['confirm', 'continuation'],
|
|
169
|
+
},
|
|
170
|
+
handler: async (args, env) => {
|
|
171
|
+
const applyName = 'meeting.eb.decision.publish.apply';
|
|
172
|
+
requireConfirm(args, applyName);
|
|
173
|
+
const lease = await verifyLease(args, applyName, env);
|
|
174
|
+
const payload = lease.payload;
|
|
175
|
+
if (lease.fingerprint !== fingerprint(payload)) {
|
|
176
|
+
throw meetingValidationError('target_changed', '提交内容与 prepare 时的快照不一致,请重新执行 prepare', {
|
|
177
|
+
expected: lease.payload,
|
|
178
|
+
actual: payload,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const session = await env.host.getSession();
|
|
182
|
+
const { create, mtId, mtStatus } = payload;
|
|
183
|
+
// 第 1 步:写决议概述 + 明细
|
|
184
|
+
// ⚠️ 真机教训(2026-09-21):EB 写接口**报错不等于没写库** —— 实测一次因字段校验失败
|
|
185
|
+
// (data_status 不被接受)的 create_mt_decision,主表与明细其实已经落库;调用方若把"报错"
|
|
186
|
+
// 当"没写入"并重试,就会产生**重复决议 + 重复任务**(本次实测因此出现了 4 条任务)。
|
|
187
|
+
// 所以这里只要拿不到明确的成功证据,一律按「可能已写入」处理(抛 partial,提示先回查)。
|
|
188
|
+
let createResponse;
|
|
189
|
+
try {
|
|
190
|
+
createResponse = await env.host.request({
|
|
191
|
+
method: 'POST',
|
|
192
|
+
path: `${EB_BASE}/saveFormData/create_mt_decision`,
|
|
193
|
+
operation: applyName,
|
|
194
|
+
body: writeBody(session.userId, create),
|
|
195
|
+
acceptBusiness: ebBusinessSuccess,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw meetingPartialError(`决议写入请求失败:${error instanceof Error ? error.message : String(error)}。`
|
|
200
|
+
+ '注意:EB 接口报错**不代表没有写入**(实测校验失败时主表也可能已落库)。'
|
|
201
|
+
+ `请先用 meeting.eb.decision.list({ mt_id: "${mtId}" }) 回查该会议是否已生成决议,`
|
|
202
|
+
+ '再决定是重试还是删除,**不要盲目重试以免产生重复决议与重复任务**。', { mtId, attempted: { interfacePk: 'create_mt_decision', body: create } }, { stage: applyName });
|
|
203
|
+
}
|
|
204
|
+
const createData = createResponse?.datajson ?? {};
|
|
205
|
+
if (createData.status === false) {
|
|
206
|
+
throw meetingPartialError('create_mt_decision 返回 status=false(服务端未确认成功)。'
|
|
207
|
+
+ 'EB 接口报错/返回 false **不代表没有写入**,请先用 meeting.eb.decision.list({ mt_id: "'
|
|
208
|
+
+ `${mtId}" }) 回查是否已生成决议,**不要盲目重试**。`, { mtId, attempted: { interfacePk: 'create_mt_decision', body: create }, response: createResponse }, { stage: applyName });
|
|
209
|
+
}
|
|
210
|
+
const rawIds = createData.dataIds ?? createData.ids;
|
|
211
|
+
const dataIds = Array.isArray(rawIds) ? rawIds : [];
|
|
212
|
+
const decisionId = dataIds.length > 0 ? String(dataIds[0]) : undefined;
|
|
213
|
+
if (!decisionId) {
|
|
214
|
+
// 决议可能已写入但拿不到 id → 无法关联会议。**不能**在这里重试创建(会重复)。
|
|
215
|
+
throw meetingPartialError('决议概述可能已写入,但接口未返回 dataIds(拿不到决议 id),无法自动写回会议。'
|
|
216
|
+
+ `请先用 meeting.eb.decision.list({ mt_id: "${mtId}" }) 回查该会议是否已生成决议,再决定是否单独调用 meeting.eb.meeting.markDecided 标记;**不要**重复创建决议。`, { mtId, attempted: { interfacePk: 'create_mt_decision', body: create } }, { stage: applyName });
|
|
217
|
+
}
|
|
218
|
+
// 第 2 步:写回会议(关联决议 + 决议状态 + 会议状态)
|
|
219
|
+
const markMainTable = { id: mtId, decision_Id: decisionId, is_decision: DECISION_DONE, mt_status: mtStatus };
|
|
220
|
+
const markPayload = { mainTable: markMainTable, details: {}, operateType: {} };
|
|
221
|
+
try {
|
|
222
|
+
const markResponse = await env.host.request({
|
|
223
|
+
method: 'POST',
|
|
224
|
+
path: `${EB_BASE}/updateFormData/update_mt_isdecision`,
|
|
225
|
+
operation: applyName,
|
|
226
|
+
// 主键必须走 URL(updateFormData 端点统一要求,2026-09-21 真机实测)
|
|
227
|
+
query: { id: mtId },
|
|
228
|
+
body: writeBody(session.userId, markPayload),
|
|
229
|
+
acceptBusiness: ebBusinessSuccess,
|
|
230
|
+
});
|
|
231
|
+
businessFailed(markResponse, 'update_mt_isdecision');
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// 决议已经建成、只是"会议标记"没写上 —— 必须说清楚,否则用户会重复建决议。
|
|
235
|
+
throw meetingPartialError(`决议已创建成功(decision_Id=${decisionId}),但把会议标记为「已决议」失败:`
|
|
236
|
+
+ `${error instanceof Error ? error.message : String(error)}。`
|
|
237
|
+
+ '请**不要**重复创建决议;可稍后单独调用 meeting.eb.meeting.markDecided 重试标记,或到会议详情页确认。', { mtId, decisionId, attempted: { interfacePk: 'update_mt_isdecision', body: markMainTable } }, { stage: applyName });
|
|
238
|
+
}
|
|
239
|
+
return meetingSuccess(applyName, {
|
|
240
|
+
status: 'COMPLETE',
|
|
241
|
+
mtId,
|
|
242
|
+
decisionId,
|
|
243
|
+
isDecision: DECISION_DONE,
|
|
244
|
+
mtStatus,
|
|
245
|
+
result: createResponse,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const signMarkPrepare = {
|
|
250
|
+
name: 'meeting.eb.sign.mark.prepare',
|
|
251
|
+
resource: 'eb',
|
|
252
|
+
method: 'meeting.eb.sign.mark.prepare',
|
|
253
|
+
action: 'meeting.eb.sign.mark.prepare',
|
|
254
|
+
summary: '准备补签 / 补签退:按「会议 + 参会人」自动定位签到记录,生成确认预览(不写入)',
|
|
255
|
+
capability: 'eb.sign',
|
|
256
|
+
risk: 'read-before-write',
|
|
257
|
+
requiresConfirmation: true,
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
properties: {
|
|
262
|
+
mt_id: { type: 'string', description: '会议 id(必填)' },
|
|
263
|
+
user_id: { type: 'string', description: '被补签人的**员工 id**(必填)' },
|
|
264
|
+
is_sign: { type: 'string', description: '签到状态:0缺席/1正常/2请假/3迟到/4早退;补签一般传 1' },
|
|
265
|
+
sign_time: { type: 'string', pattern: DATE_PATTERN, description: '签到时间(补签时必给,否则记录为空)' },
|
|
266
|
+
sign_out_status: { type: 'string', description: '签退状态:0已签退/1未签退/2早退' },
|
|
267
|
+
signback_time: { type: 'string', pattern: DATE_PATTERN, description: '签退时间(补签退时给)' },
|
|
268
|
+
sign_remark: { type: 'string', description: '签到日志 / 说明' },
|
|
269
|
+
attend_type: { type: 'string', description: '参会人员类型:0外部/1内部/2召集人/3联系人/4创建人' },
|
|
270
|
+
},
|
|
271
|
+
required: ['mt_id', 'user_id'],
|
|
272
|
+
},
|
|
273
|
+
handler: async (args, env) => {
|
|
274
|
+
const session = await env.host.getSession();
|
|
275
|
+
const mtId = toIdString(args.mt_id, 'mt_id');
|
|
276
|
+
const userId = toIdString(args.user_id, 'user_id');
|
|
277
|
+
const listResponse = await env.host.request({
|
|
278
|
+
method: 'POST',
|
|
279
|
+
path: `${EB_BASE}/getFormDataList/query_meeting_sign_list`,
|
|
280
|
+
operation: 'meeting.eb.sign.mark.prepare',
|
|
281
|
+
query: { mt_id: mtId, signMember: userId, pageNo: 1, pageSize: 50 },
|
|
282
|
+
// 分页必须进 body.pageInfo(EB 只认 body):否则恒返回 10 条,参会人多的会议会找不到签到记录。
|
|
283
|
+
body: queryBody(session.userId, { pageNo: 1, pageSize: 50 }),
|
|
284
|
+
acceptBusiness: ebBusinessSuccess,
|
|
285
|
+
});
|
|
286
|
+
const { rows } = extractEbRows(listResponse);
|
|
287
|
+
const record = rows
|
|
288
|
+
.map((row) => row?.mainTable ?? row)
|
|
289
|
+
.find((main) => String(main?.user_id ?? '') === userId);
|
|
290
|
+
const recordId = record?.id === undefined || record?.id === null ? undefined : String(record.id);
|
|
291
|
+
if (!recordId) {
|
|
292
|
+
// 找不到记录时必须明确停手:改走 create 会新增一条,造成"同一人两条签到"。
|
|
293
|
+
throw meetingValidationError('sign_record_not_found', `未在该会议(${mtId})中找到员工 ${userId} 的签到记录,无法补签。`
|
|
294
|
+
+ '请先用 meeting.eb.sign.list 确认该会议是否已生成签到数据(若会议尚无签到记录,属建会初始化问题,需先用 meeting.eb.sign.create 初始化后再补签)。', { mtId, userId });
|
|
295
|
+
}
|
|
296
|
+
const update = normalizeEbWrite(requireEndpoint('update_meeting_sign_info'), { ...args, id: recordId, mt_id: mtId }, session.userId);
|
|
297
|
+
const payload = { mtId, signRecordId: recordId, update };
|
|
298
|
+
const token = await env.continuation.issue({
|
|
299
|
+
operation: 'meeting.eb.sign.mark.apply',
|
|
300
|
+
baseUrl: session.baseUrl,
|
|
301
|
+
profile: session.profile,
|
|
302
|
+
userId: session.userId,
|
|
303
|
+
payload,
|
|
304
|
+
fingerprint: fingerprint(payload),
|
|
305
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
306
|
+
return meetingSuccess('meeting.eb.sign.mark.prepare', {
|
|
307
|
+
status: 'AWAITING_CONFIRMATION',
|
|
308
|
+
preview: {
|
|
309
|
+
interfacePk: 'update_meeting_sign_info',
|
|
310
|
+
// 主键必须走 URL(真机实测:只在 body 里传 id 会报「主键传入不能为空」)
|
|
311
|
+
path: `${EB_BASE}/updateFormData/update_meeting_sign_info?id=${recordId}`,
|
|
312
|
+
signRecordId: recordId,
|
|
313
|
+
before: { sign_time: record?.sign_time ?? '', is_sign: record?.is_sign ?? '', signback_time: record?.signback_time ?? '' },
|
|
314
|
+
after: update.mainTable,
|
|
315
|
+
},
|
|
316
|
+
continuation: token,
|
|
317
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
318
|
+
}, {
|
|
319
|
+
meta: {
|
|
320
|
+
notes: [
|
|
321
|
+
'走的是**更新**(update_meeting_sign_info),不会新增签到记录。',
|
|
322
|
+
'未传 sign_time 时签到时间仍为空,请按需补上。',
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
const signMarkApply = {
|
|
329
|
+
name: 'meeting.eb.sign.mark.apply',
|
|
330
|
+
resource: 'eb',
|
|
331
|
+
method: 'meeting.eb.sign.mark.apply',
|
|
332
|
+
action: 'meeting.eb.sign.mark.apply',
|
|
333
|
+
summary: '提交补签 / 补签退(update_meeting_sign_info);必须用户明确确认后调用',
|
|
334
|
+
capability: 'eb.sign',
|
|
335
|
+
risk: 'high-risk-write',
|
|
336
|
+
requiresConfirmation: true,
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
additionalProperties: false,
|
|
340
|
+
properties: {
|
|
341
|
+
confirm: { const: true },
|
|
342
|
+
continuation: { type: 'string', minLength: 1 },
|
|
343
|
+
},
|
|
344
|
+
required: ['confirm', 'continuation'],
|
|
345
|
+
},
|
|
346
|
+
handler: async (args, env) => {
|
|
347
|
+
const applyName = 'meeting.eb.sign.mark.apply';
|
|
348
|
+
requireConfirm(args, applyName);
|
|
349
|
+
const lease = await verifyLease(args, applyName, env);
|
|
350
|
+
const payload = lease.payload;
|
|
351
|
+
if (lease.fingerprint !== fingerprint(payload)) {
|
|
352
|
+
throw meetingValidationError('target_changed', '提交内容与 prepare 时的快照不一致,请重新执行 prepare', {
|
|
353
|
+
expected: lease.payload,
|
|
354
|
+
actual: payload,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const session = await env.host.getSession();
|
|
358
|
+
const response = await env.host.request({
|
|
359
|
+
method: 'POST',
|
|
360
|
+
path: `${EB_BASE}/updateFormData/update_meeting_sign_info`,
|
|
361
|
+
operation: applyName,
|
|
362
|
+
// 主键走 URL query(真机实测要求);body 里仍保留 id 以满足文档「id 必填」
|
|
363
|
+
query: { id: payload.signRecordId },
|
|
364
|
+
body: writeBody(session.userId, payload.update),
|
|
365
|
+
acceptBusiness: ebBusinessSuccess,
|
|
366
|
+
});
|
|
367
|
+
businessFailed(response, 'update_meeting_sign_info');
|
|
368
|
+
return meetingSuccess(applyName, {
|
|
369
|
+
status: 'COMPLETE',
|
|
370
|
+
mtId: payload.mtId,
|
|
371
|
+
signRecordId: payload.signRecordId,
|
|
372
|
+
applied: payload.update.mainTable,
|
|
373
|
+
result: response,
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
/** 决议 / 补签的语义化 operations。 */
|
|
378
|
+
export const meetingEbDecisionOperations = [
|
|
379
|
+
decisionPublishPrepare,
|
|
380
|
+
decisionPublishApply,
|
|
381
|
+
signMarkPrepare,
|
|
382
|
+
signMarkApply,
|
|
383
|
+
];
|
|
384
|
+
/** 供测试与调用方复用的常量(避免魔法值散落)。 */
|
|
385
|
+
export const EB_DECISION_CONSTANTS = { DECISION_DONE, MT_STATUS_OVER };
|