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.
@@ -135,10 +135,11 @@ export function createInvoiceHost(ctx, dependencies = {}) {
135
135
  if (typeof token !== 'string' || !token.trim()) {
136
136
  throw invoiceAuthError('invoice_token_missing', '未取得业票通业务 Token');
137
137
  }
138
- const saved = await tokenStore.save(token);
139
- if (!saved) {
140
- throw invoiceAuthError('invoice_token_cache_failed', '发票 Token 安全缓存写入失败,已停止业务请求');
141
- }
138
+ // Headless Web/Linux hosts may not provide a system keyring (for example,
139
+ // Secret Service over D-Bus). Persistence is only an optimization: keep
140
+ // the token in this process so the current business request can proceed,
141
+ // without falling back to an insecure plaintext cache.
142
+ await tokenStore.save(token);
142
143
  invoiceToken = token;
143
144
  return token;
144
145
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * EB 建会后的**关联数据补建**(占用 / 回执 / 签到)。
3
+ *
4
+ * 为什么必须补(2026-09-21 真机确认,应用设置 `open_mt_approve=0` + `only_flow_create=0`):
5
+ * eBuilder 的**自定义接口** `create_meeting` 只写会议主表,**不会**像页面建会那样
6
+ * 自动生成占用视图 / 回执 / 签到记录;而这三样恰好是后续能力的硬依赖:
7
+ *
8
+ * - 没有**占用**(roomusage)⇒ 会议室冲突检测(`query_roomusage_list`)查不到这场会,
9
+ * 以后别人在同一时段订同一间会议室不会被拦住;
10
+ * - 没有**回执**(receipt)⇒ 参会人冲突检测(以回执表 member_id 为准)对本场会议失效;
11
+ * - 没有**签到**记录(sign)⇒ `meeting.eb.sign.mark`(补签)报「未找到签到记录」。
12
+ *
13
+ * 因此建会成功后由本模块按「会议 + 参会人」逐条补齐,语义与页面建会一致。
14
+ *
15
+ * 失败语义(重要):主写入(建会)**已经成功**,这里任何一步失败都**不抛错**,
16
+ * 只把失败原因汇总进 `warnings` 并标明后果 —— 否则调用方会以为建会失败而重试,导致重复建会。
17
+ */
18
+ import { EB_BASE, ebBusinessSuccess, normalizeEbWrite, writeBody } from './factory.js';
19
+ import { EB_ENDPOINTS } from './catalog.js';
20
+ /** 只在建会后补建关联数据;其他写操作直接跳过。 */
21
+ const CREATE_MEETING_PK = 'create_meeting';
22
+ /** 回执人员类型:1 = 参会人员(参会人冲突检测只统计该类型)。 */
23
+ const MEMBER_TYPE_ATTENDEE = '1';
24
+ function requireEndpoint(interfacePk) {
25
+ const endpoint = EB_ENDPOINTS.find((item) => item.interfacePk === interfacePk);
26
+ if (!endpoint)
27
+ throw new Error(`EB catalog 缺少 ${interfacePk},请检查 catalog.ts 声明`);
28
+ return endpoint;
29
+ }
30
+ /** 逗号分隔 id 串(`hrm_members` 等)→ 去重去空后的数组。 */
31
+ function splitIds(value) {
32
+ if (value === undefined || value === null)
33
+ return [];
34
+ const text = String(value).trim();
35
+ if (text === '')
36
+ return [];
37
+ const seen = new Set();
38
+ for (const part of text.split(',')) {
39
+ const id = part.trim();
40
+ if (id !== '')
41
+ seen.add(id);
42
+ }
43
+ return [...seen];
44
+ }
45
+ function errorText(error) {
46
+ return error instanceof Error ? error.message : String(error);
47
+ }
48
+ /**
49
+ * 单条 EB 写入。**不抛错**:返回失败原因交由调用方汇总(主写入已成功,不可中断)。
50
+ * 复用 `normalizeEbWrite` 以保证字段白名单 / 必填 / 日期格式与手动调用完全一致。
51
+ */
52
+ async function writeOne(env, interfacePk, args) {
53
+ try {
54
+ const session = await env.host.getSession();
55
+ const payload = normalizeEbWrite(requireEndpoint(interfacePk), args, session.userId);
56
+ const response = await env.host.request({
57
+ method: 'POST',
58
+ path: `${EB_BASE}/saveFormData/${interfacePk}`,
59
+ operation: 'meeting.eb.meeting.create.apply',
60
+ body: writeBody(session.userId, payload),
61
+ acceptBusiness: ebBusinessSuccess,
62
+ });
63
+ if (response?.datajson?.status === false) {
64
+ return { ok: false, error: `${interfacePk} 返回 status=false` };
65
+ }
66
+ return { ok: true };
67
+ }
68
+ catch (error) {
69
+ return { ok: false, error: `${interfacePk}: ${errorText(error)}` };
70
+ }
71
+ }
72
+ export const ebMeetingAssociation = async (ctx) => {
73
+ if (ctx.endpoint.interfacePk !== CREATE_MEETING_PK)
74
+ return undefined;
75
+ const mtId = ctx.dataIds[0];
76
+ if (!mtId) {
77
+ return {
78
+ status: 'SKIPPED',
79
+ reason: 'create_meeting 未返回会议 id,无法补建占用/回执/签到,请到会议详情页确认。',
80
+ };
81
+ }
82
+ const main = ctx.payload.mainTable;
83
+ const warnings = [];
84
+ const created = {};
85
+ // ① 会议室占用:仅在指定了会议室时写入。
86
+ const address = main.address === undefined || main.address === null ? '' : String(main.address).trim();
87
+ if (address === '') {
88
+ warnings.push('会议未指定会议室(address),已跳过占用写入。');
89
+ }
90
+ else {
91
+ const result = await writeOne(ctx.env, 'create_roomusage', {
92
+ mt_id: mtId,
93
+ room_id: address,
94
+ startdate: main.begin_datetime,
95
+ enddate: main.end_datetime,
96
+ });
97
+ if (result.ok)
98
+ created.roomUsage = [address];
99
+ else {
100
+ warnings.push(`会议室占用写入失败(${result.error}):该会议不会被会议室冲突检测识别,`
101
+ + '建议到会议详情页确认,或按需重新创建占用。');
102
+ }
103
+ }
104
+ // ② 回执 + ③ 签到:按 hrm_members 逐人补齐。
105
+ const members = splitIds(main.hrm_members);
106
+ if (members.length === 0) {
107
+ warnings.push('会议未提供具体参会人员(hrm_members 为空),已跳过回执与签到写入:'
108
+ + '参会人冲突检测与签到/补签都依赖这些记录;若通过 org_members 指定了部门,本 CLI 不展开部门成员,'
109
+ + '请到会议详情页确认参会人后按需补建。');
110
+ }
111
+ else {
112
+ const receiptFailed = [];
113
+ const signFailed = [];
114
+ for (const memberId of members) {
115
+ const receipt = await writeOne(ctx.env, 'create_meeting_receipt', {
116
+ mt_id: mtId,
117
+ member_type: MEMBER_TYPE_ATTENDEE,
118
+ member_id: memberId,
119
+ member_manager: memberId,
120
+ });
121
+ if (receipt.ok)
122
+ (created.receipt ??= []).push(memberId);
123
+ else
124
+ receiptFailed.push(`${memberId}(${receipt.error})`);
125
+ const sign = await writeOne(ctx.env, 'create_meeting_sign', {
126
+ mt_id: mtId,
127
+ user_id: memberId,
128
+ });
129
+ if (sign.ok)
130
+ (created.sign ??= []).push(memberId);
131
+ else
132
+ signFailed.push(`${memberId}(${sign.error})`);
133
+ }
134
+ if (receiptFailed.length > 0) {
135
+ warnings.push(`回执写入失败 ${receiptFailed.length}/${members.length} 人:${receiptFailed.join('、')}。`
136
+ + '参会人冲突检测以回执表为准,缺失将导致该会议的参会人冲突漏检。');
137
+ }
138
+ if (signFailed.length > 0) {
139
+ warnings.push(`签到记录写入失败 ${signFailed.length}/${members.length} 人:${signFailed.join('、')}。`
140
+ + '这些人将无法通过 meeting.eb.sign.mark 补签(无签到记录可定位)。');
141
+ }
142
+ }
143
+ return {
144
+ status: warnings.length === 0 ? 'COMPLETE' : 'PARTIAL',
145
+ mtId,
146
+ created,
147
+ ...(warnings.length > 0 ? { warnings } : {}),
148
+ note: 'eBuilder 自定义接口 create_meeting 不会像页面建会那样自动生成占用/回执/签到,'
149
+ + '本步骤由 CLI 按「会议 + 参会人」补齐,供会议室/参会人冲突检测与签到补签使用。',
150
+ };
151
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 签到 / 回执列表的**出席情况汇总**(供 `meeting.eb.sign.list` 与 `meeting.eb.receipt.list`
3
+ * 声明式挂载,见 catalog 的 `summarizeRows`)。机制与 `task-progress.ts` 一致:
4
+ * 只**增**一个 `data.progress` 字段,原始 `rows` 原样返回。
5
+ *
6
+ * 真机确立的取值口径(2026-09-21/22,实现前务必看清):
7
+ * - `is_sign` / `attend_status` 回的是**数字字符串**(`"1"` / `""`),不是中文渲染值;
8
+ * - **"未签到 / 未回执" = 空字符串**,而 `is_sign="0"` 是**缺席**(是"已表态"而非"没签")——
9
+ * 两者语义不同,混在一起会得出错误的出席率;
10
+ * - `sign_out_status`:`0` 已签退 / `1` 未签退 / `2` 早退 / 空 = 未签到;
11
+ * - ⚠️ **两张表的 `mt_id` 语义不同**:签到表回的是**会议名称文本**,回执表回的是**会议 id** ——
12
+ * 故汇总里统一命名为 `mtRef`(原样透出),不硬称为 id 或 name。
13
+ */
14
+ function toText(value) {
15
+ return value === undefined || value === null ? '' : String(value).trim();
16
+ }
17
+ /** 百分比字符串(保留 1 位小数);分母为 0 时返回 "0.0%"。 */
18
+ function rate(part, whole) {
19
+ if (whole <= 0)
20
+ return '0.0%';
21
+ return `${((part / whole) * 100).toFixed(1)}%`;
22
+ }
23
+ /**
24
+ * `is_sign` 归一化。
25
+ *
26
+ * 数字字符串为主(实测 `"1"` / `""`),同时兼容中文渲染值与"已签退但未填 is_sign"的兜底:
27
+ * **空值一律算「未签到」**(不是缺席)。
28
+ */
29
+ export function normalizeSignState(isSign, signTime) {
30
+ const text = toText(isSign);
31
+ if (text === '') {
32
+ // is_sign 为空:只要 sign_time 有值说明实际签过,按正常处理,避免误判成"未签到"。
33
+ return toText(signTime) === '' ? 'notSigned' : 'normal';
34
+ }
35
+ if (text.includes('正常'))
36
+ return 'normal';
37
+ if (text.includes('请假'))
38
+ return 'leave';
39
+ if (text.includes('迟到早退'))
40
+ return 'lateAndEarlyLeave';
41
+ if (text.includes('迟到未签退'))
42
+ return 'lateAndNotSignedOut';
43
+ if (text.includes('迟到'))
44
+ return 'late';
45
+ if (text.includes('早退'))
46
+ return 'earlyLeave';
47
+ if (text.includes('未签退'))
48
+ return 'notSignedOut';
49
+ if (text === '0' || text.includes('缺席'))
50
+ return 'absent';
51
+ switch (text) {
52
+ case '1': return 'normal';
53
+ case '2': return 'leave';
54
+ case '3': return 'late';
55
+ case '4': return 'earlyLeave';
56
+ case '5': return 'notSignedOut';
57
+ case '6': return 'lateAndNotSignedOut';
58
+ case '7': return 'lateAndEarlyLeave';
59
+ default: return 'unknown';
60
+ }
61
+ }
62
+ const SIGN_STATES = [
63
+ 'normal', 'leave', 'late', 'earlyLeave', 'notSignedOut', 'lateAndNotSignedOut',
64
+ 'lateAndEarlyLeave', 'absent', 'notSigned', 'unknown',
65
+ ];
66
+ /** `meeting.eb.sign.list` 的结果后处理。 */
67
+ export function summarizeSignRows(rows) {
68
+ const byState = Object.fromEntries(SIGN_STATES.map((key) => [key, 0]));
69
+ const items = rows.map((row) => {
70
+ const main = (row?.mainTable ?? row ?? {});
71
+ const state = normalizeSignState(main.is_sign, main.sign_time);
72
+ byState[state] += 1;
73
+ return {
74
+ id: toText(main.id),
75
+ userId: toText(main.user_id),
76
+ mtRef: toText(main.mt_id),
77
+ state,
78
+ signTime: toText(main.sign_time),
79
+ signBackTime: toText(main.signback_time),
80
+ signOutStatus: toText(main.sign_out_status),
81
+ };
82
+ });
83
+ const total = items.length;
84
+ const notSigned = byState.notSigned;
85
+ const signed = total - notSigned;
86
+ return {
87
+ summary: {
88
+ total,
89
+ signed,
90
+ notSigned,
91
+ signedOut: items.filter((item) => item.signOutStatus === '0').length,
92
+ // 出席率按"已签到"口径(未签到 = 没来),比把"缺席"也算作出席更直观。
93
+ attendanceRate: rate(signed, total),
94
+ byState,
95
+ },
96
+ items,
97
+ };
98
+ }
99
+ /** `attend_status` 归一化:空 = 未回执;`0` = 不参加;`1` = 参加。 */
100
+ export function normalizeReceiptState(raw) {
101
+ const text = toText(raw);
102
+ if (text === '')
103
+ return 'noReply';
104
+ if (text === '1' || text.includes('参加') && !text.includes('不参加'))
105
+ return 'attend';
106
+ if (text === '0' || text.includes('不参加'))
107
+ return 'decline';
108
+ return 'unknown';
109
+ }
110
+ /** `meeting.eb.receipt.list` 的结果后处理。 */
111
+ export function summarizeReceiptRows(rows) {
112
+ const byMemberType = { attendee: 0, customer: 0, other: 0, unknown: 0 };
113
+ const items = rows.map((row) => {
114
+ const main = (row?.mainTable ?? row ?? {});
115
+ const memberType = toText(main.member_type);
116
+ if (memberType === '1')
117
+ byMemberType.attendee += 1;
118
+ else if (memberType === '2')
119
+ byMemberType.customer += 1;
120
+ else if (memberType === '3')
121
+ byMemberType.other += 1;
122
+ else
123
+ byMemberType.unknown += 1;
124
+ return {
125
+ id: toText(main.id),
126
+ memberId: toText(main.member_id),
127
+ mtRef: toText(main.mt_id),
128
+ state: normalizeReceiptState(main.attend_status),
129
+ memberType,
130
+ attendStatus: toText(main.attend_status),
131
+ bookRoom: toText(main.book_room),
132
+ bookTicket: toText(main.book_ticket),
133
+ };
134
+ });
135
+ const total = items.length;
136
+ const attend = items.filter((item) => item.state === 'attend').length;
137
+ const decline = items.filter((item) => item.state === 'decline').length;
138
+ const replied = attend + decline;
139
+ return {
140
+ summary: {
141
+ total,
142
+ attend,
143
+ decline,
144
+ noReply: total - replied,
145
+ replyRate: rate(replied, total),
146
+ attendRate: rate(attend, total),
147
+ byMemberType,
148
+ bookRoom: items.filter((item) => item.bookRoom !== '').length,
149
+ bookTicket: items.filter((item) => item.bookTicket !== '').length,
150
+ },
151
+ items,
152
+ };
153
+ }
@@ -13,6 +13,8 @@
13
13
  * - 详情 / 写:只用 body(详情 body 顶层 mainTable;写操作 body 用 datas 数组包裹 mainTable)。
14
14
  * - 写操作日期字段统一 `yyyy-MM-dd HH:mm`(不带秒);create 不传 id,update/delete 必传 id。
15
15
  */
16
+ import { summarizeReceiptRows, summarizeSignRows } from './attendance-summary.js';
17
+ import { summarizeTaskProgress } from './task-progress.js';
16
18
  const MEETING_FIELDS_CREATE = [
17
19
  'mt_creator', 'mt_name', 'begin_datetime', 'end_datetime', 'mt_status', 'operate_type',
18
20
  'is_decision', 'mt_type', 'caller', 'contacter', 'mt_format', 'mt_desc', 'mt_hours',
@@ -27,6 +29,20 @@ const MEETING_FIELDS_CHANGE = [
27
29
  'next_occur_date', 'repeat_status', 'repeat_id', 'request_id', 'mt_status',
28
30
  'attend_user', 'reporting_user',
29
31
  ];
32
+ /**
33
+ * 签到字段(2026-09-21 按最新接口文档 `RestfulDocumentation (6)` 补全)。
34
+ * 原清单缺 `map_typeback` / `signback_longitude` / `signback_latitude` / `external_*` 7 个字段,
35
+ * 会导致签退经纬度与外部人员信息写不进去。
36
+ */
37
+ const SIGN_FIELDS_CREATE = [
38
+ 'mt_id', 'hylx', 'site', 'sign_flag', 'user_id', 'attend_type', 'sign_time', 'sign_reson',
39
+ 'is_sign', 'longitude', 'latitude', 'sign_remark', 'signback_time', 'backsite', 'map_type', 'map_typeback',
40
+ 'signback_longitude', 'signback_latitude', 'allow_late_time', 'sign_out_status',
41
+ 'external_flag', 'external_name', 'external_email', 'external_phone', 'external_remark',
42
+ 'sub_dept', 'department',
43
+ ];
44
+ /** 更新签到:在 create 字段基础上加 id(必填)。 */
45
+ const SIGN_FIELDS_UPDATE = ['id', ...SIGN_FIELDS_CREATE];
30
46
  const ROOM_FIELDS = [
31
47
  'id', 'room_name', 'hrm_ids', 'org_id', 'status', 'mr_type', 'place', 'room_attr',
32
48
  'allow_minnum', 'allow_maxnum', 'rs_style', 'room_desc', 'equipments', 'dsporder', 'images',
@@ -82,6 +98,8 @@ export const EB_ENDPOINTS = [
82
98
  fields: MEETING_FIELDS_CHANGE,
83
99
  requiredFields: ['id', 'operate_type'],
84
100
  dateFields: ['begin_datetime', 'end_datetime', 'next_occur_date'],
101
+ // 主键必须走 URL(updateFormData 端点统一要求,2026-09-21 真机实测)
102
+ pkQueryParam: 'id',
85
103
  summary: 'EB 版变更会议(change_meeting):必传 id + operate_type;取消会议也用本接口',
86
104
  notes: ['变更用 CHANGE;取消用 CANCEL 并置 mt_status=4。'],
87
105
  },
@@ -126,6 +144,8 @@ export const EB_ENDPOINTS = [
126
144
  interfacePk: 'update_room',
127
145
  fields: ROOM_FIELDS,
128
146
  requiredFields: ['id'],
147
+ // 主键必须走 URL(updateFormData 端点统一要求)
148
+ pkQueryParam: 'id',
129
149
  summary: 'EB 版更新会议室(update_room):仅 id 必填',
130
150
  },
131
151
  {
@@ -176,6 +196,8 @@ export const EB_ENDPOINTS = [
176
196
  countPk: 'query_meeting_sign_count',
177
197
  queryParams: ['mt_id', 'signMember', 'sign_status', 'sign_back_status'],
178
198
  summary: 'EB 版签到列表(query_meeting_sign_list + count)',
199
+ // 附带出席汇总(data.progress):各状态人数 / 已签退 / 出席率;原始 rows 原样保留。
200
+ summarizeRows: summarizeSignRows,
179
201
  notes: [
180
202
  'sign_status:0缺席/1正常/2请假/3迟到/4早退/5未签退/6迟到未签退/7迟到早退;sign_back_status:0已签退/1未签退/2早退。',
181
203
  '未签到判断:sign_time 为空或查不到记录。',
@@ -194,28 +216,33 @@ export const EB_ENDPOINTS = [
194
216
  operation: 'meeting.eb.sign.create',
195
217
  kind: 'create',
196
218
  interfacePk: 'create_meeting_sign',
197
- fields: [
198
- 'mt_id', 'hylx', 'site', 'sign_flag', 'user_id', 'attend_type', 'sign_time', 'sign_reson',
199
- 'is_sign', 'longitude', 'latitude', 'sign_remark', 'signback_time', 'backsite', 'map_type',
200
- 'allow_late_time', 'external_flag', 'sub_dept', 'department', 'sign_out_status',
201
- ],
219
+ fields: SIGN_FIELDS_CREATE,
202
220
  requiredFields: ['mt_id'],
203
- dateFields: ['sign_time', 'signback_time', 'allow_late_time'],
221
+ // 签到表时间字段**必须带秒**(真机实测:传 yyyy-MM-dd HH:mm 被拒,要求 yyyy-MM-dd HH:mm:ss)
222
+ secondDateFields: ['sign_time', 'signback_time', 'allow_late_time'],
204
223
  summary: 'EB 版新建签到记录(create_meeting_sign):必填 mt_id',
224
+ notes: [
225
+ '仅用于「建会后初始化签到记录」等**该会议尚无签到记录**的场景。',
226
+ '⚠️ **补签不是本接口**:已有记录再 create 会产生重复签到数据,补签请用 meeting.eb.sign.mark。',
227
+ ],
205
228
  },
206
229
  {
207
230
  domain: 'sign',
208
231
  operation: 'meeting.eb.sign.update',
209
232
  kind: 'update',
210
233
  interfacePk: 'update_meeting_sign_info',
211
- fields: [
212
- 'id', 'mt_id', 'hylx', 'site', 'sign_flag', 'user_id', 'attend_type', 'sign_time', 'sign_reson',
213
- 'is_sign', 'longitude', 'latitude', 'sign_remark', 'signback_time', 'backsite', 'map_type',
214
- 'allow_late_time', 'external_flag', 'sub_dept', 'department', 'sign_out_status',
215
- ],
234
+ fields: SIGN_FIELDS_UPDATE,
216
235
  requiredFields: ['id', 'mt_id'],
217
- dateFields: ['sign_time', 'signback_time', 'allow_late_time'],
236
+ // 同 create:签到时间字段必须带秒(yyyy-MM-dd HH:mm:ss)
237
+ secondDateFields: ['sign_time', 'signback_time', 'allow_late_time'],
238
+ // 主键必须走 URL(真机实测:只在 body 里传 id 会报「主键传入不能为空」)
239
+ pkQueryParam: 'id',
218
240
  summary: 'EB 版更新签到记录(update_meeting_sign_info):必传 id + mt_id',
241
+ notes: [
242
+ '**补签 / 补签退的正确入口**(`meeting.eb.sign.mark` 即封装本接口)。',
243
+ '必传 id = **签到记录 id**(从 meeting.eb.sign.list 的 rows[].mainTable.id 取),不是会议 id;mt_id 也要带。',
244
+ 'is_sign:0缺席/1正常/2请假/3迟到/4早退/5未签退/6迟到未签退/7迟到早退;sign_out_status 对应签退状态。',
245
+ ],
219
246
  },
220
247
  {
221
248
  domain: 'receipt',
@@ -225,7 +252,9 @@ export const EB_ENDPOINTS = [
225
252
  countPk: 'query_meeting_receipt_count',
226
253
  queryParams: ['mt_id', 'hrmmember', 'is_attend'],
227
254
  summary: 'EB 版回执列表(query_meeting_receipt_list + count)',
228
- notes: ['is_attend:0否/1是。'],
255
+ // 附带出席汇总(data.progress):参加 / 不参加 / 未回执、回执率、人员类型分布;原始 rows 原样保留。
256
+ summarizeRows: summarizeReceiptRows,
257
+ notes: ['is_attend:0否/1是(URL query 筛选);`attend_status` 空 = 未回执,`0` = 不参加、`1` = 参加。'],
229
258
  },
230
259
  {
231
260
  domain: 'receipt',
@@ -263,6 +292,8 @@ export const EB_ENDPOINTS = [
263
292
  ],
264
293
  requiredFields: ['id', 'mt_id', 'member_id', 'attend_status'],
265
294
  dateFields: ['arrive_date_time', 'return_date_time'],
295
+ // 主键必须走 URL(updateFormData 端点统一要求)
296
+ pkQueryParam: 'id',
266
297
  summary: 'EB 版更新回执(update_meeting_receipt):attend_status 0否/1是',
267
298
  },
268
299
  {
@@ -324,6 +355,8 @@ export const EB_ENDPOINTS = [
324
355
  interfacePk: 'update_mt_topicType_info',
325
356
  fields: ['id', 'name', 'topic_type_num', 'topic_type_name', 'data_status'],
326
357
  requiredFields: ['id'],
358
+ // 主键必须走 URL(updateFormData 端点统一要求)
359
+ pkQueryParam: 'id',
327
360
  summary: 'EB 版更新议题类型(update_mt_topicType_info):必传 id',
328
361
  },
329
362
  {
@@ -362,6 +395,8 @@ export const EB_ENDPOINTS = [
362
395
  ],
363
396
  requiredFields: ['id'],
364
397
  dateFields: ['start_time', 'end_time'],
398
+ // 主键必须走 URL(updateFormData 端点统一要求)
399
+ pkQueryParam: 'id',
365
400
  summary: 'EB 版更新会议议题(update_mt_topic_info):必传 id',
366
401
  notes: ['topic_status:0待开始/1进行中/2已结束。'],
367
402
  },
@@ -383,6 +418,143 @@ export const EB_ENDPOINTS = [
383
418
  '据此判断 EB 版建会是否需走流程(审批开启 → 走 workflow,不由 CLI 直接建会)。',
384
419
  ],
385
420
  },
421
+ // ---------------------------------------------------------------------------
422
+ // 会议决议(决议概述表 + 决议任务表)—— 2026-09-21 新增
423
+ // 链路:create_mt_decision 写「概述 + 明细」→ 用户配置的 ESB 联动按明细生成决议任务
424
+ // → update_mt_isdecision 把会议标记为已决议(is_decision=2 + decision_Id + mt_status)
425
+ // → 后续用 decisionTask.* 查进度、更新完成状态
426
+ // ---------------------------------------------------------------------------
427
+ {
428
+ domain: 'decision',
429
+ operation: 'meeting.eb.decision.list',
430
+ kind: 'list',
431
+ interfacePk: 'query_mt_decision_list',
432
+ countPk: 'query_mt_decision_count',
433
+ queryParams: ['mt_id', 'name', 'creator', 'data_status'],
434
+ summary: 'EB 版会议决议列表(query_mt_decision_list + count)',
435
+ notes: [
436
+ '一个会议**只有一个**决议概述(按 mt_id 查即可)。',
437
+ '主表为概述(mt_content 为决议概述文本),detail1 为决议明细。',
438
+ ],
439
+ },
440
+ {
441
+ domain: 'decision',
442
+ operation: 'meeting.eb.decision.detail',
443
+ kind: 'detail',
444
+ interfacePk: 'query_mt_decision_info',
445
+ summary: 'EB 版会议决议详情(query_mt_decision_info):含 detail1 决议明细',
446
+ },
447
+ {
448
+ domain: 'decision',
449
+ operation: 'meeting.eb.decision.create',
450
+ kind: 'create',
451
+ interfacePk: 'create_mt_decision',
452
+ // ⚠️ data_status **不在**写入字段内:2026-09-21 真机实测该接口拒绝它
453
+ // (DataError:dataKey:data_status,该字段不存在);提交状态由系统自动置为 1(回查列表 data_status=1)。
454
+ fields: ['mt_id', 'mt_content'],
455
+ requiredFields: ['mt_id', 'mt_content'],
456
+ summary: 'EB 版创建会议决议(create_mt_decision):主表概述 + detail1 决议明细',
457
+ notes: [
458
+ 'mt_id 关联会议;mt_content 为概述文本(由调用方结合议题 / 纪要 / 材料汇总后传入)。',
459
+ '**不要传 data_status**:该接口不接受该字段(真机报 DataError:该字段不存在),提交状态由系统自动置 1。',
460
+ 'detail1 至少 1 条:ESB 按明细逐条生成决议任务;`rummager`(检查人)已改为非必填。',
461
+ 'decision_status 默认 0(未完成)、related_type 默认 0(未关联);executors 必须是**员工 id**。',
462
+ ],
463
+ details: [
464
+ {
465
+ node: 'detail1',
466
+ fields: [
467
+ 'sort_number', 'decision_title', 'executors', 'rummager', 'begin_datetime', 'end_datetime',
468
+ 'related_type', 'relate_agenda', 'decision_status', 'relate_task',
469
+ ],
470
+ requiredFields: ['decision_title', 'executors', 'begin_datetime', 'end_datetime', 'decision_status'],
471
+ dateFields: ['begin_datetime', 'end_datetime'],
472
+ // 0未完成 / 1已完成 / 2已延期(非法值 EB 常静默忽略,故入口拦下)
473
+ enumFields: { decision_status: ['0', '1', '2'] },
474
+ minRows: 1,
475
+ purpose: '决议明细:每条 = 一项待办任务',
476
+ },
477
+ ],
478
+ },
479
+ {
480
+ domain: 'decision',
481
+ operation: 'meeting.eb.decision.delete',
482
+ kind: 'delete',
483
+ interfacePk: 'delete_mt_decision',
484
+ summary: 'EB 版删除会议决议(delete_mt_decision):仅传 id(概述 id)',
485
+ notes: ['删除概述**不会**自动回收已生成的决议任务,需要时另行处理。'],
486
+ },
487
+ {
488
+ domain: 'decisionTask',
489
+ operation: 'meeting.eb.decisionTask.list',
490
+ kind: 'list',
491
+ interfacePk: 'query_mt_decision_task_list',
492
+ countPk: 'query_mt_decision_task_count',
493
+ queryParams: ['mt_id', 'relate_decision', 'decision_status', 'executors', 'decision_title'],
494
+ summary: 'EB 版决议任务列表(query_mt_decision_task_list + count):一条任务 = 一条决议明细',
495
+ // 声明式进度汇总:结果进 `data.progress`(原始 rows 原样保留),实现见 task-progress.ts。
496
+ summarizeRows: summarizeTaskProgress,
497
+ notes: [
498
+ '★ **`data.progress` 已直接给出进度与延误**,不需要自己解析 detail1:`progress.summary` 是整页汇总(total/done/overdue/inProgress/pending);`progress.items[]` 逐任务给出 `progress.status`(done 全员完成 / overdue 有人延期 / inProgress 部分完成 / pending 尚未开始)与 `members[]`(每人 executorId / executorName / status / finishDate)。',
499
+ '任务由用户配置的 **ESB 联动**生成:一条**决议明细**生成**一个任务**;明细里写多个执行人 ⇒ 该任务**多人共同跟进**(真机实测:1 明细 + 2 执行人 → 仅 1 个任务)。',
500
+ '看某会议的全部任务 → 传 mt_id;看某人的任务 → 传 executors(员工 id);分页用 pageNo/pageSize(分页必须走 body.pageInfo,CLI 已处理)。',
501
+ '★ **读进度请看 `detail1`**:每条 = 一个执行人的跟进记录,含 `finish_status`(未完成/已完成/已延期)、`finish_date`、`executor`;**同一执行人重复更新会覆盖其记录,不同执行人是追加**(真机实测互不覆盖)。',
502
+ '⚠️ **`mainTable.decision_status` 不能作为进度依据**:真机实测它**始终为「未完成」**,既不随 detail1 变化,也无法经 `update_mt_decision_task` 写入(该接口只映射 detail1),故 catalog 未暴露该字段。',
503
+ '任务由明细带出 `begin_datetime`/`end_datetime`/`decision_title`,可用于展示计划周期。',
504
+ ],
505
+ },
506
+ {
507
+ domain: 'decisionTask',
508
+ operation: 'meeting.eb.decisionTask.update',
509
+ kind: 'update',
510
+ interfacePk: 'update_mt_decision_task',
511
+ // ⚠️ 只开放 id:真机实测即使传主表 `decision_status`,接口返回成功但**值不生效**
512
+ // (该接口只映射 detail1 明细),故不暴露它以免误导。
513
+ fields: ['id'],
514
+ requiredFields: ['id'],
515
+ // 主键必须走 URL(updateFormData 端点统一要求)
516
+ pkQueryParam: 'id',
517
+ summary: 'EB 版更新决议任务完成状态(update_mt_decision_task):按执行人更新 detail1',
518
+ notes: [
519
+ 'id = **任务 id**(decisionTask.list 的 rows[].mainTable.id),不是会议 id、不是决议 id。',
520
+ 'detail1 传 executor(员工 id)+ finish_status(0未完成/1已完成/2已延期)+ finish_date(**带秒**:yyyy-MM-dd HH:mm:ss)。',
521
+ '⚠️ **finish_date 是服务端必填**(真机:不传报「存为未填写的必填字段: finish_date」);把任务改回「未完成」时传一个**空格**即可显式清空(CLI 会落库为空串,与页面生成的未完成记录一致)。',
522
+ '不传 operationinfo.operateType 时为「更新追加」,不会覆盖其他执行人的记录 —— 真机实测:同一任务上两人分别跟进后,detail1 保留**两条**记录。',
523
+ '⚠️ **任务主表的 decision_status 不随 detail1 变化**(真机实测始终为「未完成」);查任务进度请看 detail1 的 finish_status。',
524
+ '写入后建议回查 decisionTask.list 确认;EB 写接口报错**不代表没有写入**。',
525
+ ],
526
+ details: [
527
+ {
528
+ node: 'detail1',
529
+ fields: ['executor', 'finish_status', 'finish_date'],
530
+ // finish_date 服务端也是必填;「未完成」时传空格显式清空(见 factory 的 normalizeDetailRow)。
531
+ requiredFields: ['executor', 'finish_status', 'finish_date'],
532
+ // 0未完成 / 1已完成 / 2已延期(非法值 EB 常静默忽略,故入口拦下并提示可选值)
533
+ enumFields: { finish_status: ['0', '1', '2'] },
534
+ // finish_date 必须**带秒**:真机实测传 "2026-09-21 18:30" 被拒(要求 yyyy-MM-dd HH:mm:ss)。
535
+ secondDateFields: ['finish_date'],
536
+ minRows: 1,
537
+ purpose: '执行人跟进:同一任务的每个执行人分别记录完成状态',
538
+ },
539
+ ],
540
+ },
541
+ {
542
+ domain: 'meeting',
543
+ operation: 'meeting.eb.meeting.markDecided',
544
+ kind: 'update',
545
+ interfacePk: 'update_mt_isdecision',
546
+ fields: ['id', 'decision_Id', 'is_decision', 'mt_status'],
547
+ requiredFields: ['id', 'is_decision'],
548
+ // 主键必须走 URL(updateFormData 端点统一要求)
549
+ pkQueryParam: 'id',
550
+ summary: 'EB 版把会议标记为已决议(update_mt_isdecision):写回关联决议 + 决议状态 + 会议状态',
551
+ notes: [
552
+ 'is_decision:0未决议 / 1决议临时保存 / **2决议提交完成**。',
553
+ 'decision_Id:关联的决议概述 id(decision.create 的 dataIds[0])。',
554
+ 'mt_status:**8结束**(产生决议即视为会议结束),也可按需传 9进行中 等。',
555
+ 'detail1(汇报人)/ detail2(服务项目)已由接口配置清空,**无需传**。',
556
+ ],
557
+ },
386
558
  ];
387
559
  /** 写操作(create/update/delete)需要确认链。 */
388
560
  export function isWriteKind(kind) {