weaver-work-cli 0.1.36 → 0.1.38
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/chunks/{archive-XBVH7JU7.js → archive-PDV724YK.js} +5 -4
- package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-4XE4MNXQ.js} +4 -3
- package/dist/chunks/{asset-4XO7UOUW.js → asset-PDL2QE2N.js} +8 -7
- package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-MDDGOE5E.js} +3 -3
- package/dist/chunks/{blog-PGZKOPSC.js → blog-UYR3EUXW.js} +5 -4
- package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-LG2X5NMF.js} +5 -4
- package/dist/chunks/{calendar-MXEMTIO4.js → calendar-FG4BAZHE.js} +4 -3
- package/dist/chunks/{chunk-NCOVUAZK.js → chunk-6FTTSA45.js} +4 -3
- package/dist/chunks/{chunk-HXJQI5NM.js → chunk-6LTFIFLM.js} +3 -2
- package/dist/chunks/{chunk-QYGO5EE2.js → chunk-CORFRD7Q.js} +4 -3
- package/dist/chunks/{chunk-QHFANTX7.js → chunk-DMV2TOMH.js} +1348 -90
- package/dist/chunks/{chunk-TC6MZAO3.js → chunk-ELYDWBZJ.js} +52 -2
- package/dist/chunks/{chunk-JCSN3JGM.js → chunk-FUWVHMVV.js} +7 -6
- package/dist/chunks/{chunk-BPE37FPB.js → chunk-GGJFMNJM.js} +3 -3
- package/dist/chunks/{chunk-6LLJFKJE.js → chunk-IZUDA353.js} +1 -1
- package/dist/chunks/{chunk-XMGSYD6S.js → chunk-J7USS3ED.js} +4 -4
- package/dist/chunks/{chunk-F746NKNF.js → chunk-JSMFPO43.js} +1 -1
- package/dist/chunks/{chunk-T72N74ZT.js → chunk-KKRL3REV.js} +5 -4
- package/dist/chunks/{chunk-VPUTVEMC.js → chunk-NI6GIPIM.js} +98 -3
- package/dist/chunks/{chunk-VPXOPQJI.js → chunk-NR6RKXPA.js} +2 -2
- package/dist/chunks/{chunk-JNG42JKG.js → chunk-OJQUJZQF.js} +4 -6
- package/dist/chunks/{chunk-I6QGIBDM.js → chunk-UJJD4QWF.js} +4 -3
- package/dist/chunks/{chunk-JUXVYKII.js → chunk-UZZBA6PV.js} +174 -14
- package/dist/chunks/{chunk-JYKABYCV.js → chunk-VRYHGG6O.js} +2 -2
- package/dist/chunks/{chunk-WXFPWCXG.js → chunk-XJY7G4DD.js} +3 -3
- package/dist/chunks/{chunk-5DQEEGCC.js → chunk-Z32KWY6G.js} +1 -1
- package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-BQZEQ4SP.js} +2 -2
- package/dist/chunks/{contract-XI2EEIZU.js → contract-XIRAFTEV.js} +4 -3
- package/dist/chunks/{cowork-SCPGLBA4.js → cowork-BLG62BL2.js} +4 -3
- package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-NUKG5QW4.js} +3 -3
- package/dist/chunks/{ehr-EAP77IMT.js → ehr-2STW5ILH.js} +4 -3
- package/dist/chunks/{esb-2TGGXEE7.js → esb-UUDQUNKY.js} +4 -4
- package/dist/chunks/{fna-2X7ZE7BU.js → fna-6RCY4NU5.js} +4 -3
- package/dist/chunks/{host-C6EL2RKL.js → host-7RXEDMAH.js} +3 -3
- package/dist/chunks/{host-XCLDDXKQ.js → host-EJZFROU3.js} +3 -3
- package/dist/chunks/{host-I4NRK4MX.js → host-FWDOSRH5.js} +3 -3
- package/dist/chunks/{host-Q4LZGCRU.js → host-RHM3NPXV.js} +3 -3
- package/dist/chunks/{host-UEI5FGDM.js → host-UFF33TWJ.js} +3 -3
- package/dist/chunks/{hrm-YXERJM76.js → hrm-PKJ6ELSH.js} +3 -3
- package/dist/chunks/{im-A4VZN2TQ.js → im-YJAXS7CO.js} +3 -3
- package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-HWVSXGX6.js} +3 -3
- package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-L7VKILZB.js} +4 -3
- package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NMYEIHRH.js} +4 -4
- package/dist/chunks/{mail-7GZ6CPBJ.js → mail-B42RRU7G.js} +4 -3
- package/dist/chunks/{meeting-3NMGA3MB.js → meeting-CFYVBJSS.js} +13 -12
- package/dist/chunks/{okr-BZ6XUOAG.js → okr-GCLF6V7W.js} +4 -3
- package/dist/chunks/{plan-DR5CMUYP.js → plan-ZVL7GBRI.js} +4 -3
- package/dist/chunks/{project-6J63MK33.js → project-4YWAAE2K.js} +4 -3
- package/dist/chunks/{registry-VRTNC6UR.js → registry-EP4VTHZS.js} +8 -8
- package/dist/chunks/{registry-KDD7DHNK.js → registry-FRJ6H7BT.js} +4 -4
- package/dist/chunks/{registry-TKX2E7C7.js → registry-SG2PGVCF.js} +2 -2
- package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-R3JVUUAC.js} +4 -4
- package/dist/chunks/{schedule-IVBICP32.js → schedule-NIJBTYDO.js} +4 -4
- package/dist/chunks/{workflow-D74NFK3C.js → workflow-BB24DFBY.js} +4 -4
- package/dist/index.js +27 -27
- package/dist/internal/e10/auth-recovery.js +114 -1
- package/dist/internal/e10/client.js +16 -2
- package/dist/internal/e10/request-runtime.js +103 -7
- package/dist/shortcuts/archive/host.js +3 -2
- package/dist/shortcuts/archive-oa/host.js +2 -1
- package/dist/shortcuts/asset/host.js +6 -5
- package/dist/shortcuts/blog/host.js +3 -2
- package/dist/shortcuts/caizhilian/host.js +3 -2
- package/dist/shortcuts/calendar/host.js +2 -1
- package/dist/shortcuts/contract/host.js +2 -1
- package/dist/shortcuts/cowork/host.js +2 -1
- package/dist/shortcuts/ebuilder-form/host.js +3 -1
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +2 -1
- package/dist/shortcuts/hrm/host.js +2 -1
- package/dist/shortcuts/im/host.js +3 -2
- package/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/jiuchuanhui/host.js +2 -1
- package/dist/shortcuts/mail/host.js +2 -1
- package/dist/shortcuts/meeting/host.js +3 -2
- package/dist/shortcuts/meeting/invitation/text.js +71 -0
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
- package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
- package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
- package/dist/shortcuts/meeting/operations/invitation.js +122 -9
- package/dist/shortcuts/okr/host.js +2 -1
- package/dist/shortcuts/plan/host.js +2 -1
- package/dist/shortcuts/project/host.js +2 -1
- package/dist/shortcuts/workflow/operations/form.js +160 -2
- package/package.json +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +250 -301
- package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
import { meetingValidationError } from '../errors.js';
|
|
21
21
|
import { meetingStateDir } from './env.js';
|
|
22
22
|
import { renderDocx } from '../invitation/docx.js';
|
|
23
|
+
import { renderInvitationMarkdown } from '../invitation/text.js';
|
|
23
24
|
import { meetingSuccess, SCHEMA_ID } from './types.js';
|
|
24
25
|
/** 模板随 CLI 包分发(`templates/` 已在 package.json 的 files 白名单内)。 */
|
|
25
26
|
function templatePath() {
|
|
@@ -250,8 +251,80 @@ async function resolveIssuerOrganization(env, creatorId) {
|
|
|
250
251
|
return null;
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
254
|
+
/**
|
|
255
|
+
* 取当前登录人的姓名 —— 会议既没填召集人、又没有创建人名时,用**发起人本人**兜底。
|
|
256
|
+
*
|
|
257
|
+
* 理由:邀请函里「XXX 诚邀您参加…」这句必须有个人名,否则通篇【待补充】没法看。
|
|
258
|
+
* 实际业务上发起建会的人就是召集人,比留空更接近事实(2026-10-31 用户对【待补充】有意见)。
|
|
259
|
+
*/
|
|
260
|
+
async function resolveSessionUserName(env) {
|
|
261
|
+
try {
|
|
262
|
+
const session = await env.host.getSession();
|
|
263
|
+
const userId = String(session?.userId ?? '').trim();
|
|
264
|
+
if (!userId)
|
|
265
|
+
return '';
|
|
266
|
+
const { hrmEmployeeOperations } = await import('../../hrm/operations/employee.js');
|
|
267
|
+
const batch = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.batch');
|
|
268
|
+
if (!batch)
|
|
269
|
+
return '';
|
|
270
|
+
const result = await batch.handler({ ids: [userId] }, env);
|
|
271
|
+
const row = (result?.data?.rows ?? [])[0];
|
|
272
|
+
return String(row?.name ?? '').trim();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 用 `meeting.eb.meeting.detail` 补列表单查里**空着**的字段。
|
|
280
|
+
*
|
|
281
|
+
* 为什么需要(2026-10-31 真机取证):同一个会议
|
|
282
|
+
* · `meeting.eb.meeting.list`(单查)→ `hrm_members = ""`
|
|
283
|
+
* · `meeting.eb.meeting.detail` → `hrm_members = "王丹丹"`
|
|
284
|
+
* 邀请函原先只读 list,于是"明明有参会人却写成【待补充】"—— 这正是用户投诉过的
|
|
285
|
+
* 「参会人员没有带出来」。detail 的价值在于**字段更多且有值**,代价是 `address` 回的是
|
|
286
|
+
* 会议室 id(不是名称),所以这里**只补白名单字段、且只填空值**,绝不覆盖 list 已有的值。
|
|
287
|
+
*/
|
|
288
|
+
const DETAIL_FALLBACK_KEYS = [
|
|
289
|
+
'hrm_members', 'mt_desc', 'total_member', 'caller', 'contacter',
|
|
290
|
+
'begin_datetime', 'end_datetime', 'other_members', 'mt_creator',
|
|
291
|
+
'topic_presenter', 'mt_format', 'mt_type', 'video_url',
|
|
292
|
+
'org_members', 'with_secauth',
|
|
293
|
+
];
|
|
294
|
+
async function supplementRowFromDetail(row, mtId, env) {
|
|
295
|
+
const isBlank = (value) => {
|
|
296
|
+
if (value === undefined || value === null)
|
|
297
|
+
return true;
|
|
298
|
+
if (Array.isArray(value))
|
|
299
|
+
return value.length === 0;
|
|
300
|
+
return String(value).trim() === '';
|
|
301
|
+
};
|
|
302
|
+
const missing = DETAIL_FALLBACK_KEYS.filter((key) => isBlank(row[key]));
|
|
303
|
+
if (missing.length === 0)
|
|
304
|
+
return { row, filled: [] };
|
|
305
|
+
try {
|
|
306
|
+
const { getMeetingOperation } = await import('./registry.js');
|
|
307
|
+
const detailOperation = getMeetingOperation('meeting.eb.meeting.detail');
|
|
308
|
+
const result = await detailOperation.handler({ id: mtId }, env);
|
|
309
|
+
// detail 的字段是**扁平**挂在 `data.data` 下(不是 mainTable),两种都兜一下
|
|
310
|
+
const flat = (result?.data?.data ?? result?.data ?? {});
|
|
311
|
+
const merged = { ...row };
|
|
312
|
+
const filled = [];
|
|
313
|
+
for (const key of missing) {
|
|
314
|
+
const value = flat[key];
|
|
315
|
+
if (!isBlank(value)) {
|
|
316
|
+
merged[key] = value;
|
|
317
|
+
filled.push(key);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { row: merged, filled };
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// detail 读失败不能拖垮邀请函:列表数据照样能出一份(缺的字段仍标【待补充】)
|
|
324
|
+
return { row, filled: [] };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** 组装占位符解析器:派生字段 → 会议标准字段 → extra(租户自定义字段)→ 待补充。 */ export function buildResolver(row, extra, issuerOrg) {
|
|
255
328
|
const begin = parseDateTime(row.begin_datetime);
|
|
256
329
|
const end = parseDateTime(row.end_datetime);
|
|
257
330
|
const derived = {
|
|
@@ -297,6 +370,9 @@ export function buildResolver(row, extra, issuerOrg) {
|
|
|
297
370
|
org_members: orgMembers,
|
|
298
371
|
// 落款公司全称:`extra.company_name` 可覆盖(对外开放时客户传自己的公司名)
|
|
299
372
|
company_name: String(extra.company_name ?? '').trim() || DEFAULT_COMPANY_NAME,
|
|
373
|
+
// 客户单位(「尊敬的 XXX」):先看 extra(建会时由 create.flow 把参会客户名带进来),
|
|
374
|
+
// 再看会议主表的 crm_members(部分租户会存),都没有才留【待补充】
|
|
375
|
+
guest_org: String(extra.guest_org ?? '').trim() || flattenFieldValue(row.crm_members) || '',
|
|
300
376
|
// 单独暴露上级公司,模板需要「公司级落款」时可直接用
|
|
301
377
|
issuer_company: issuerCompany,
|
|
302
378
|
other_members: humanizeMembers(row.other_members),
|
|
@@ -320,8 +396,14 @@ export function buildResolver(row, extra, issuerOrg) {
|
|
|
320
396
|
const lookup = (key) => {
|
|
321
397
|
if (key in derived)
|
|
322
398
|
return derived[key];
|
|
323
|
-
if (key in standard)
|
|
324
|
-
|
|
399
|
+
if (key in standard) {
|
|
400
|
+
const standardValue = standard[key];
|
|
401
|
+
// ⛔ 会议里该字段**空着**时要继续往 extra 找(2026-10-31):
|
|
402
|
+
// 以前 `key in standard` 直接返回空串,导致「会议没填召集人 → extra.caller 也压不上」,
|
|
403
|
+
// 明明调用方补了值还是渲染成【待补充】。空值不算"有值",让 extra 有机会兜底。
|
|
404
|
+
if (String(standardValue ?? '').trim() !== '')
|
|
405
|
+
return standardValue;
|
|
406
|
+
}
|
|
325
407
|
if (key.startsWith('extra.') && key.slice(6) in extra)
|
|
326
408
|
return extra[key.slice(6)];
|
|
327
409
|
if (key in extra)
|
|
@@ -353,7 +435,10 @@ const meetingInvitationRender = {
|
|
|
353
435
|
resource: 'invitation',
|
|
354
436
|
method: 'render',
|
|
355
437
|
action: 'meeting.invitation.render',
|
|
356
|
-
summary: '
|
|
438
|
+
summary: '按会议真实数据生成邀请函(只读,不发送):**同时返回 docx 与对话内可展示的 Markdown 全文**(`text` 字段),'
|
|
439
|
+
+ '会话里应直接把 `text` 贴出来,不要只给文件名让人自己下载;'
|
|
440
|
+
+ '数据取 meeting.eb.meeting.list 单查;议程有议题就按议题自动填、没有则保留模板示例行;'
|
|
441
|
+
+ '取不到的字段渲染成【待补充】(docx 里是黄色高亮);租户自定义字段用 extra 传入',
|
|
357
442
|
capability: 'invitation',
|
|
358
443
|
risk: 'read',
|
|
359
444
|
inputSchema: {
|
|
@@ -395,6 +480,8 @@ const meetingInvitationRender = {
|
|
|
395
480
|
let row;
|
|
396
481
|
/** meeting=按真实会议数据;payload=按调用方传入的申请单字段(会议可能还没生成)。 */
|
|
397
482
|
let dataSource = 'meeting';
|
|
483
|
+
/** 用 detail 补回来的字段名(列表单查会丢「参会人员」,见下方注释)。 */
|
|
484
|
+
let detailSupplementary = [];
|
|
398
485
|
if (!mtId) {
|
|
399
486
|
// ⛔ 邀请函本质是「预置函」:项目人员下载后自己改、自己发(2026-10-30 用户明确要求)。
|
|
400
487
|
// 所以不该卡在"会议必须已生成"——流程还在审批时,用申请单已知信息先生成一份,
|
|
@@ -437,6 +524,21 @@ const meetingInvitationRender = {
|
|
|
437
524
|
if (!row) {
|
|
438
525
|
throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { mtId });
|
|
439
526
|
}
|
|
527
|
+
// 列表单查在真机上会**丢「参会人员」**(2026-10-31 实测:同一会议 list 里 `hrm_members=""`,
|
|
528
|
+
// detail 里是「王丹丹」)⇒ 只对**空字段**用 detail 补,避免邀请函把明明有的人写成【待补充】。
|
|
529
|
+
const supplement = await supplementRowFromDetail(row, mtId, env);
|
|
530
|
+
row = supplement.row;
|
|
531
|
+
detailSupplementary = supplement.filled;
|
|
532
|
+
}
|
|
533
|
+
// 召集人再兜一层:会议没填 caller、也没有创建人名时,用**发起人本人**(当前登录人)。
|
|
534
|
+
// 否则邀请函开头「XXX 诚邀您参加…」整句就是【待补充】,对话里贴出来很难看。
|
|
535
|
+
let callerFromSession = false;
|
|
536
|
+
if (!String(row.caller ?? '').trim() && !String(row.mt_creator ?? '').trim()) {
|
|
537
|
+
const sessionName = await resolveSessionUserName(env);
|
|
538
|
+
if (sessionName) {
|
|
539
|
+
row = { ...row, caller: sessionName };
|
|
540
|
+
callerFromSession = true;
|
|
541
|
+
}
|
|
440
542
|
}
|
|
441
543
|
// 落款单位回落:会议 org_members 为空时,用「发起人(creator 人员 id)的所属组织」
|
|
442
544
|
const creatorId = String(row.creator ?? '').trim();
|
|
@@ -463,6 +565,10 @@ const meetingInvitationRender = {
|
|
|
463
565
|
const rendered = renderDocx(template, resolver, {
|
|
464
566
|
agenda: { headerKeyword: AGENDA_HEADER_KEYWORD, rows: agendaRows },
|
|
465
567
|
});
|
|
568
|
+
// 对话内文本版(2026-10-31 用户要求「直接展示在对话里」):
|
|
569
|
+
// docx 是给打印/外发的成品,但会话里用户看不见内容 ⇒ 同一份 resolver 再渲染一遍 Markdown,
|
|
570
|
+
// 上层(create.flow / 模型回复)可以直接把 text 贴出来,字段与 docx 完全一致。
|
|
571
|
+
const textVersion = renderInvitationMarkdown(resolver, agendaRows);
|
|
466
572
|
const begin = parseDateTime(row.begin_datetime);
|
|
467
573
|
const dateTag = begin ? `${begin.getFullYear()}${pad(begin.getMonth() + 1)}${pad(begin.getDate())}` : '未知日期';
|
|
468
574
|
const fileName = `邀请函-${safeFileName(String(row.mt_name ?? ''))}-${dateTag}.docx`;
|
|
@@ -499,14 +605,21 @@ const meetingInvitationRender = {
|
|
|
499
605
|
meetingName: String(row.mt_name ?? ''),
|
|
500
606
|
// meeting=按真实会议数据;payload=按申请单字段预生成(会议可能还没生成,属预置函)
|
|
501
607
|
dataSource,
|
|
608
|
+
// 列表单查丢字段时,用 detail 补回来的字段名(有值 = 邀请函用的是合并后的数据)
|
|
609
|
+
detailSupplemented: detailSupplementary,
|
|
610
|
+
// 对话内展示用的 Markdown 全文(与 docx 同源)。上层应直接把这段贴进回复正文,
|
|
611
|
+
// ⛔ 不要只丢一个 filePath 让用户自己去本地打开看内容。
|
|
612
|
+
text: textVersion.markdown,
|
|
502
613
|
filledFields: rendered.filled,
|
|
503
614
|
pendingFields: rendered.pending,
|
|
504
615
|
// 落款单位的实际来源,便于调用方判断是否需要人工改写
|
|
505
616
|
orgSource,
|
|
506
|
-
// 召集人来源:meeting.caller=会议自己填的 / meeting.mt_creator=回落到创建人 / none=都没有
|
|
507
|
-
callerSource:
|
|
508
|
-
? '
|
|
509
|
-
: (String(row.
|
|
617
|
+
// 召集人来源:meeting.caller=会议自己填的 / meeting.mt_creator=回落到创建人 / session.user=回落到发起人本人 / none=都没有
|
|
618
|
+
callerSource: callerFromSession
|
|
619
|
+
? 'session.user'
|
|
620
|
+
: (String(row.caller ?? '').trim()
|
|
621
|
+
? 'meeting.caller'
|
|
622
|
+
: (String(row.mt_creator ?? '').trim() ? 'meeting.mt_creator' : 'none')),
|
|
510
623
|
// 议程来源:meeting.topic=按议题自动填 / extra=调用方传入 / template=保留模板示例行
|
|
511
624
|
agendaSource,
|
|
512
625
|
agendaRows: agendaRows.length,
|
|
@@ -6,6 +6,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
|
|
|
6
6
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
7
7
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
8
8
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
9
|
+
import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
|
|
9
10
|
import { okrStateRoot } from './continuation.js';
|
|
10
11
|
import { OkrCliError, okrAuthError, okrNetworkError, okrValidationError } from './errors.js';
|
|
11
12
|
function inferProfile(ctx) {
|
|
@@ -143,7 +144,7 @@ export function createOkrHost(ctx, dependencies = {}) {
|
|
|
143
144
|
catch (error) {
|
|
144
145
|
mapRuntimeNetworkError(error);
|
|
145
146
|
}
|
|
146
|
-
if (response
|
|
147
|
+
if (isE10SessionExpiredResponse(response)) {
|
|
147
148
|
throw okrAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
148
149
|
}
|
|
149
150
|
if (response.parseError) {
|
|
@@ -6,6 +6,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
|
|
|
6
6
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
7
7
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
8
8
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
9
|
+
import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
|
|
9
10
|
import { planStateRoot } from './continuation.js';
|
|
10
11
|
import { PlanCliError, planAuthError, planNetworkError, planValidationError } from './errors.js';
|
|
11
12
|
function inferProfile(ctx) {
|
|
@@ -150,7 +151,7 @@ export function createPlanHost(ctx, dependencies = {}) {
|
|
|
150
151
|
catch (error) {
|
|
151
152
|
mapRuntimeNetworkError(error);
|
|
152
153
|
}
|
|
153
|
-
if (response
|
|
154
|
+
if (isE10SessionExpiredResponse(response)) {
|
|
154
155
|
throw planAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
155
156
|
}
|
|
156
157
|
if (response.parseError) {
|
|
@@ -2,6 +2,7 @@ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js
|
|
|
2
2
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
3
3
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
4
4
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
5
|
+
import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
|
|
5
6
|
import { ProjectCliError, projectApiError, projectAuthError, projectNetworkError, projectValidationError, } from './errors.js';
|
|
6
7
|
const PROJECT_PATH_PREFIXES = [
|
|
7
8
|
'/api/ebuilder/form/formdata/v2/',
|
|
@@ -133,7 +134,7 @@ export function createProjectHost(ctx, dependencies = {}) {
|
|
|
133
134
|
catch (error) {
|
|
134
135
|
mapRuntimeNetworkError(error);
|
|
135
136
|
}
|
|
136
|
-
if (response
|
|
137
|
+
if (isE10SessionExpiredResponse(response)) {
|
|
137
138
|
throw projectAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, {
|
|
138
139
|
httpStatus: response.status,
|
|
139
140
|
});
|
|
@@ -593,7 +593,14 @@ function extractFields(layoutData, requiredMap, disableEditMap, displayMap) {
|
|
|
593
593
|
// (Python 的 `bool({})` 为 False)——直接照抄 Python 的 `bool(browser_type)` 会让**每个字段**
|
|
594
594
|
// 都被判成浏览按钮字段,文本/富文本字段随之走 name→ID 解析并报 BROWSER NOT_FOUND,
|
|
595
595
|
// 内容永远写不进去(2026-09-16「内部留言」留言内容字段实证)。必须判"非空对象"。
|
|
596
|
-
|
|
596
|
+
// 组件名有两种写法:eBuilder 表单里常带 `Component` 后缀(`CustomerComponent` /
|
|
597
|
+
// `EmployeeComponent`),标准流程表单里是不带的(`customer` / `employee`)。
|
|
598
|
+
// ⛔ 只认前者会漏判:2026-10-31 会议申请流程的「参会客户」是 `CustomerComponent`,
|
|
599
|
+
// 被当成普通文本字段 ⇒ 提交 `[{id,name}]` 报「文本字段只接受字符串、数字或布尔值」。
|
|
600
|
+
const componentBase = normalized.replace(/component$/, '');
|
|
601
|
+
const isBrowser = Object.keys(browserType).length > 0
|
|
602
|
+
|| BROWSER_COMPONENTS.has(normalized)
|
|
603
|
+
|| BROWSER_COMPONENTS.has(componentBase);
|
|
597
604
|
const isSelect = SELECT_COMPONENTS.has(normalized) || options.length > 0;
|
|
598
605
|
result.push({
|
|
599
606
|
id: fieldId,
|
|
@@ -2140,6 +2147,31 @@ async function buildCreateFormData(workflowId, env, options = {}) {
|
|
|
2140
2147
|
continue;
|
|
2141
2148
|
visibleFields.push(f);
|
|
2142
2149
|
}
|
|
2150
|
+
/**
|
|
2151
|
+
* 明细表**整表被判不可见**时的显式告警(2026-10-31)。
|
|
2152
|
+
*
|
|
2153
|
+
* 场景:明细表被摆在了页面网格的**隐藏行 / 隐藏列**上(`pageLayouts[0].config.workbook.Sheet1.rows[n].visible=false`),
|
|
2154
|
+
* 于是它的列字段全部被 `hiddenCellIds` 过滤掉。此前是**静默丢弃** —— 模型只看到"表单里没有这张明细表",
|
|
2155
|
+
* 分不清「表单确实没配」和「配了但摆在隐藏行上」,只能回一句"没有这张表",用户无从下手。
|
|
2156
|
+
* 这里按 subFormId 汇总,整表字段全灭的才告警(部分列隐藏属正常设计)。
|
|
2157
|
+
*/
|
|
2158
|
+
const hiddenBySubform = new Map();
|
|
2159
|
+
for (const f of fields) {
|
|
2160
|
+
const fid = String(f.id ?? '');
|
|
2161
|
+
const sid = String(f.subFormId ?? '').trim();
|
|
2162
|
+
if (!sid || !hiddenCellIds.has(fid))
|
|
2163
|
+
continue;
|
|
2164
|
+
hiddenBySubform.set(sid, (hiddenBySubform.get(sid) || 0) + 1);
|
|
2165
|
+
}
|
|
2166
|
+
const survivingSubforms = new Set(visibleFields.map((f) => String(f.subFormId ?? '').trim()).filter(Boolean));
|
|
2167
|
+
const invisibleSubformWarnings = [];
|
|
2168
|
+
for (const [sid, count] of hiddenBySubform) {
|
|
2169
|
+
if (survivingSubforms.has(sid))
|
|
2170
|
+
continue;
|
|
2171
|
+
invisibleSubformWarnings.push(`明细表 ${sid} 的 ${count} 个字段全部被判为「不可见」(该明细表所在的行/列在表单布局里是隐藏的),`
|
|
2172
|
+
+ '因此没有出现在字段清单里。要么在表单设计器里把明细表挪到可见行,要么取消隐藏它所在的行/列,然后重新载入表单。');
|
|
2173
|
+
}
|
|
2174
|
+
let layoutWarning = invisibleSubformWarnings.length ? invisibleSubformWarnings.join(';') : null;
|
|
2143
2175
|
const defaults = normalizeDefaults(layoutData.defaultDetails);
|
|
2144
2176
|
// 计算必填/可编辑/明细必填(按默认值 + onLoadForm 联动)
|
|
2145
2177
|
const systemFieldValues = _normalizeSystemFieldValues(layoutData.businessOperatorObj);
|
|
@@ -2204,6 +2236,52 @@ async function buildCreateFormData(workflowId, env, options = {}) {
|
|
|
2204
2236
|
}
|
|
2205
2237
|
}
|
|
2206
2238
|
const folderOptionValues = _mainValueMap(defaults);
|
|
2239
|
+
/**
|
|
2240
|
+
* 「条件显示」的明细表(2026-10-31,会议申请流程「参会客户明细」实证)
|
|
2241
|
+
*
|
|
2242
|
+
* 表单可以给明细表配**显示联动**:触发字段有值时 `show`、为空时 `hideLine`。
|
|
2243
|
+
* 会议申请流程的「参会客户明细」(subFormId `1332370499762806784`)就是这样 ——
|
|
2244
|
+
* 触发字段是「参会客户」(`crm_members`),空着时整表隐藏。
|
|
2245
|
+
*
|
|
2246
|
+
* 旧行为把它与「明细表被摆在隐藏行」一视同仁 ⇒ 整表字段从 `fields` 里消失,
|
|
2247
|
+
* 建会链路既解析不到明细表也写不进明细,模型只能回「表单里没有这张表」。
|
|
2248
|
+
* 现在:布局上确实存在、且有成对 `show` 规则的表,按**条件可见**处理 ——
|
|
2249
|
+
* 字段照常下发(标 `conditional: true`),并在 `layoutWarning` 里说明触发字段。
|
|
2250
|
+
*/
|
|
2251
|
+
const conditionalHiddenSubforms = new Set();
|
|
2252
|
+
const conditionalMainFields = new Set();
|
|
2253
|
+
const conditionalHints = collectDetailShowLinkages(layoutData.viewLinkages, fields);
|
|
2254
|
+
/**
|
|
2255
|
+
* 主表字段同理:「参会客户」(`crm_members`) 自己也有成对的 `show` / `hideLine` 规则
|
|
2256
|
+
* (空值时连自己都隐藏)。若不这样处理,建会链路的 `crm_members` 永远映射不到字段。
|
|
2257
|
+
*/
|
|
2258
|
+
const fieldShowIds = collectFieldShowLinkageIds(layoutData.viewLinkages);
|
|
2259
|
+
for (const fid of Array.from(mainHidden)) {
|
|
2260
|
+
if (!fieldShowIds.has(fid))
|
|
2261
|
+
continue;
|
|
2262
|
+
if (hiddenCellIds.has(fid))
|
|
2263
|
+
continue;
|
|
2264
|
+
const target = visibleFields.find((f) => String(f.id ?? '') === fid);
|
|
2265
|
+
if (!target)
|
|
2266
|
+
continue;
|
|
2267
|
+
conditionalMainFields.add(fid);
|
|
2268
|
+
mainHidden.delete(fid);
|
|
2269
|
+
invisibleSubformWarnings.push(`字段「${target.name}」(${fid}) 因显示联动隐藏(该字段本身是触发字段),已照常下发并标 conditional: true`);
|
|
2270
|
+
}
|
|
2271
|
+
for (const sid of Array.from(hiddenSubforms)) {
|
|
2272
|
+
const hint = conditionalHints.get(sid);
|
|
2273
|
+
if (!hint)
|
|
2274
|
+
continue;
|
|
2275
|
+
const usable = visibleFields.some((f) => String(f.subFormId ?? '') === sid && !hiddenCellIds.has(String(f.id ?? '')));
|
|
2276
|
+
if (!usable)
|
|
2277
|
+
continue;
|
|
2278
|
+
conditionalHiddenSubforms.add(sid);
|
|
2279
|
+
hiddenSubforms.delete(sid);
|
|
2280
|
+
invisibleSubformWarnings.push(`明细表 ${sid}${hint.tableName ? `(${hint.tableName})` : ''} 因显示联动隐藏,触发字段:`
|
|
2281
|
+
+ `${hint.triggerNames.join('、') || hint.triggerIds.join('、')}。`
|
|
2282
|
+
+ '该表字段已照常下发(标 conditional: true),把触发字段填上后表单里就会显示;不要回「表单里没有这张明细表」。');
|
|
2283
|
+
}
|
|
2284
|
+
layoutWarning = invisibleSubformWarnings.length ? invisibleSubformWarnings.join(';') : null;
|
|
2207
2285
|
for (const field of visibleFields) {
|
|
2208
2286
|
const fieldId = String(field.id ?? '');
|
|
2209
2287
|
const subFormId = String(field.subFormId ?? '');
|
|
@@ -2303,6 +2381,11 @@ async function buildCreateFormData(workflowId, env, options = {}) {
|
|
|
2303
2381
|
else if (slimSubFormId) {
|
|
2304
2382
|
item.subFormId = slimSubFormId;
|
|
2305
2383
|
}
|
|
2384
|
+
// 条件可见的明细表(见 conditionalHiddenSubforms 注释):字段可用,但当前触发字段没值时表单里不显示。
|
|
2385
|
+
if (slimSubFormId && conditionalHiddenSubforms.has(slimSubFormId))
|
|
2386
|
+
item.conditional = true;
|
|
2387
|
+
if (!slimSubFormId && conditionalMainFields.has(String(field.id ?? '')))
|
|
2388
|
+
item.conditional = true;
|
|
2306
2389
|
item.componentKey = field.componentKey;
|
|
2307
2390
|
// 表单初始化时就有的值(OA 预填 + onLoadForm 联动带出)——有才出现该键;**没有默认值的字段不带**。
|
|
2308
2391
|
// CLI 拼 `data` 时会自动并入,模型**不必显式传**;用户本次明确改写了它才按用户说的覆盖(见
|
|
@@ -2461,6 +2544,8 @@ async function buildCreateFormData(workflowId, env, options = {}) {
|
|
|
2461
2544
|
},
|
|
2462
2545
|
systemFieldWarning: minorWarning,
|
|
2463
2546
|
requiredInfoWarning: requiredInfoWarningText(context),
|
|
2547
|
+
// 明细表整表不可见时的告警(见 layoutWarning 注释):有值时**不要**回"表单里没有这张明细表"。
|
|
2548
|
+
layoutWarning,
|
|
2464
2549
|
// 原 Python 把完整 context 落盘;此处通过 continuation 承载,返回结构保持文档契约。
|
|
2465
2550
|
continuation: token,
|
|
2466
2551
|
},
|
|
@@ -3962,6 +4047,74 @@ function mergeLinkageDetails(merged, linkageDetails) {
|
|
|
3962
4047
|
function emptyLinkageVisibility() {
|
|
3963
4048
|
return [new Set(), {}, {}, {}];
|
|
3964
4049
|
}
|
|
4050
|
+
/**
|
|
4051
|
+
* 收集主表字段的 `show` 联动字段 id(`linkageObj=field` 且 `linkageProp=show`)。
|
|
4052
|
+
* 这类字段的隐藏是**条件性**的,按条件可见处理,见 `buildCreateFormData` 的 `conditionalMainFields`。
|
|
4053
|
+
*/
|
|
4054
|
+
export function collectFieldShowLinkageIds(linkages) {
|
|
4055
|
+
const out = new Set();
|
|
4056
|
+
if (!Array.isArray(linkages))
|
|
4057
|
+
return out;
|
|
4058
|
+
for (const rule of linkages) {
|
|
4059
|
+
const details = Array.isArray(rule?.linkageDetails) ? rule.linkageDetails : [];
|
|
4060
|
+
for (const detail of details) {
|
|
4061
|
+
if (!detail || typeof detail !== 'object')
|
|
4062
|
+
continue;
|
|
4063
|
+
if (String(detail.linkageObj ?? '') !== 'field')
|
|
4064
|
+
continue;
|
|
4065
|
+
if (String(detail.linkageProp ?? '') !== 'show')
|
|
4066
|
+
continue;
|
|
4067
|
+
const fid = String(detail.linkageValue ?? '').trim();
|
|
4068
|
+
if (fid)
|
|
4069
|
+
out.add(fid);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
return out;
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* 收集「明细表的显示联动」——`linkageObj=detail` 且 `linkageProp=show` 的规则。
|
|
4076
|
+
*
|
|
4077
|
+
* 用途见 `buildCreateFormData` 里 `conditionalHiddenSubforms` 的注释:配了 `show` 规则的明细表
|
|
4078
|
+
* 是**条件可见**(另有配套的 `hideLine` 规则),不能因为当期条件不成立就把整表字段丢掉。
|
|
4079
|
+
* 返回 `subFormId → { triggerIds, triggerNames, tableName }`,triggerNames 供告警文本直接引用。
|
|
4080
|
+
*/
|
|
4081
|
+
export function collectDetailShowLinkages(linkages, fields) {
|
|
4082
|
+
const out = new Map();
|
|
4083
|
+
if (!Array.isArray(linkages))
|
|
4084
|
+
return out;
|
|
4085
|
+
const list = Array.isArray(fields) ? fields : [];
|
|
4086
|
+
const nameOf = (id) => {
|
|
4087
|
+
for (const f of list) {
|
|
4088
|
+
if (String(f?.id ?? '') === id)
|
|
4089
|
+
return String(f?.name ?? id);
|
|
4090
|
+
}
|
|
4091
|
+
return id;
|
|
4092
|
+
};
|
|
4093
|
+
for (const rule of linkages) {
|
|
4094
|
+
const details = Array.isArray(rule?.linkageDetails) ? rule.linkageDetails : [];
|
|
4095
|
+
for (const detail of details) {
|
|
4096
|
+
if (!detail || typeof detail !== 'object')
|
|
4097
|
+
continue;
|
|
4098
|
+
if (String(detail.linkageObj ?? '') !== 'detail')
|
|
4099
|
+
continue;
|
|
4100
|
+
if (String(detail.linkageProp ?? '') !== 'show')
|
|
4101
|
+
continue;
|
|
4102
|
+
const sid = String(detail.linkageValue ?? '').trim();
|
|
4103
|
+
if (!sid)
|
|
4104
|
+
continue;
|
|
4105
|
+
const conds = Array.isArray(rule?.condition?.conditionDetails) ? rule.condition.conditionDetails : [];
|
|
4106
|
+
const triggerIds = conds
|
|
4107
|
+
.map((c) => String(c?.targetId ?? '').trim())
|
|
4108
|
+
.filter(Boolean);
|
|
4109
|
+
out.set(sid, {
|
|
4110
|
+
triggerIds,
|
|
4111
|
+
triggerNames: triggerIds.map(nameOf),
|
|
4112
|
+
tableName: nameOf(sid) === sid ? '' : nameOf(sid),
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
return out;
|
|
4117
|
+
}
|
|
3965
4118
|
function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues, fields) {
|
|
3966
4119
|
const hiddenSubforms = new Set();
|
|
3967
4120
|
const rowHidden = {};
|
|
@@ -5067,7 +5220,12 @@ async function runCreateRequest(args, env, options = {}) {
|
|
|
5067
5220
|
* (`buildCreateFormData`,那边的联动结果要用来算字段的必填 / 可编辑清单);这里两者都不跑 ——
|
|
5068
5221
|
* 本档既不做必填判定、也不发写请求。非小e 环境行为不变。
|
|
5069
5222
|
*/
|
|
5070
|
-
|
|
5223
|
+
// ⛔ 2026-10-31 真机回归:调用方可以显式要求「小e 环境也走服务端提交」(meeting 建会直提用)。
|
|
5224
|
+
// 起因:小e 的浏览器填单页只给**文本类**字段赋值,浏览按钮(参会人员 / 参会客户)传过去整段是空 ——
|
|
5225
|
+
// 用户看到的就是"填了但表单里这两个字段没有值"。服务端提交这条路上这些字段走标准 JSON,
|
|
5226
|
+
// 能完整落库,所以建会这类「字段必须完整」的场景应走直提,浏览器填单只留给需要人工核对/补填的场合。
|
|
5227
|
+
const forceServerSubmit = args.forceServerSubmit === true;
|
|
5228
|
+
const isXiaoE = forceServerSubmit ? false : hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
|
|
5071
5229
|
const linkageDetails = isXiaoE
|
|
5072
5230
|
? []
|
|
5073
5231
|
: await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false, context.defaults || []);
|