weaver-work-cli 0.1.32 → 0.1.33
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/README.md +2 -2
- package/dist/chunks/{archive-UMGX72K6.js → archive-FVFY6GZN.js} +2 -2
- package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-NW3OD4UV.js} +2 -2
- package/dist/chunks/{asset-GLEGKFG5.js → asset-7QJKMIDV.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
- package/dist/chunks/{blog-E44OTVDA.js → blog-I7SDPNW4.js} +2 -2
- package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-O37ZIQNY.js} +2 -2
- package/dist/chunks/{calendar-JKPIRKFD.js → calendar-7KXE4ED3.js} +2 -2
- package/dist/chunks/{chunk-4YC43QQ4.js → chunk-2FLWXFVS.js} +4 -4
- package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
- package/dist/chunks/chunk-3SO6QL6U.js +73 -0
- package/dist/chunks/{chunk-DW6AUUKS.js → chunk-6ATGWRZM.js} +6 -4
- package/dist/chunks/chunk-BYATLLEF.js +68 -0
- package/dist/chunks/chunk-BYBSCN4X.js +932 -0
- package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-D223PTXU.js} +1 -0
- package/dist/chunks/{chunk-H7J4DC5R.js → chunk-EEWPQE3O.js} +2 -2
- package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-GFPXQIY2.js} +2 -2
- package/dist/chunks/{chunk-C6R3B2ML.js → chunk-HEL5XTYI.js} +12 -6
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
- package/dist/chunks/{chunk-YRVDW4WN.js → chunk-ILMBNVSG.js} +2 -2
- package/dist/chunks/{chunk-G3QJULO2.js → chunk-KAS45KNR.js} +1 -1
- package/dist/chunks/{chunk-H6XSTSLM.js → chunk-MEEFGWTM.js} +54 -196
- package/dist/chunks/{chunk-GCG2NMWR.js → chunk-MQ2I5AA3.js} +4 -2
- package/dist/chunks/chunk-NHA62QD7.js +774 -0
- package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-OCU4RLHC.js} +641 -309
- package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-PRHJGPMA.js} +2 -2
- package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-Q6LWB5HX.js} +2 -2
- package/dist/chunks/{chunk-IURUFGZK.js → chunk-RBGOMK4Y.js} +50 -33
- package/dist/chunks/chunk-VBCFFFGT.js +226 -0
- package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-WRRJ4GG2.js} +22 -1148
- package/dist/chunks/{chunk-E4Z23GHM.js → chunk-WY74UBJ6.js} +264 -750
- package/dist/chunks/conflict-SFNEVJY5.js +21 -0
- package/dist/chunks/continuation-RN6PZN2M.js +11 -0
- package/dist/chunks/cowork-CKWYHXYB.js +2508 -0
- package/dist/chunks/ebuilder-form-KOLJRVZD.js +121 -0
- package/dist/chunks/{ehr-HMFY6VIL.js → ehr-WFOC3B4D.js} +1174 -20
- package/dist/chunks/{esb-ZYSVB7SG.js → esb-LJZTN663.js} +4 -4
- package/dist/chunks/{fna-B7RKPAZD.js → fna-F7QRIIZC.js} +2 -2
- package/dist/chunks/{host-45WED2KK.js → host-BCGUVEWX.js} +3 -3
- package/dist/chunks/host-BQBRHVR3.js +23 -0
- package/dist/chunks/{host-I4Z6BFYX.js → host-DGE2TZD4.js} +3 -3
- package/dist/chunks/{host-KNZ52LJZ.js → host-KXPTEWFS.js} +3 -3
- package/dist/chunks/{host-OCH2BCCV.js → host-VQ6JJXGQ.js} +3 -3
- package/dist/chunks/{hrm-2KAZGUHI.js → hrm-NI6XAKW5.js} +3 -3
- package/dist/chunks/{im-ACJJWH4U.js → im-7TRTF3KT.js} +88 -12
- package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-AOXWJKYT.js} +3 -3
- package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-7NQWEZAW.js} +2 -2
- package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-RNS4EE4S.js} +4 -4
- package/dist/chunks/{mail-S2HDDXO2.js → mail-VDHJAB4O.js} +3 -3
- package/dist/chunks/{meeting-QVN3I37L.js → meeting-GA7XCNNP.js} +214 -43
- package/dist/chunks/{okr-QNG4VC2Q.js → okr-TQIRERPQ.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A25EMW6U.js → plan-4RY4LN2P.js} +2 -2
- package/dist/chunks/{project-G6XV66DZ.js → project-D6XT2OC5.js} +2 -2
- package/dist/chunks/{registry-DC6KR3NH.js → registry-JHRIDQ3A.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-TI3CIUXF.js → registry-QKMI43Q6.js} +8 -6
- package/dist/chunks/{registry-7C5UMIPL.js → registry-VOGKFFTR.js} +2 -2
- package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-D4EUOOWY.js} +4 -3
- package/dist/chunks/{schedule-KFIITGSK.js → schedule-XPFBHPCV.js} +4 -3
- package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-HLBZRWIY.js} +172 -6
- package/dist/index.js +30 -25
- package/dist/internal/e10/auth-recovery.js +14 -4
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/cowork/continuation.js +70 -0
- package/dist/shortcuts/cowork/errors.js +59 -0
- package/dist/shortcuts/cowork/host.js +172 -0
- package/dist/shortcuts/cowork/index.js +166 -0
- package/dist/shortcuts/cowork/manifest.js +65 -0
- package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
- package/dist/shortcuts/cowork/operations/item-write.js +380 -0
- package/dist/shortcuts/cowork/operations/member-write.js +283 -0
- package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
- package/dist/shortcuts/cowork/operations/read.js +423 -0
- package/dist/shortcuts/cowork/operations/registry.js +39 -0
- package/dist/shortcuts/cowork/operations/review-write.js +157 -0
- package/dist/shortcuts/cowork/operations/shared.js +215 -0
- package/dist/shortcuts/cowork/operations/types.js +24 -0
- package/dist/shortcuts/cowork/operations.js +19 -0
- package/dist/shortcuts/ehr/host.js +4 -1
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
- package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
- package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
- package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
- package/dist/shortcuts/ehr/operations/recruit.js +35 -10
- package/dist/shortcuts/ehr/operations/registry.js +6 -0
- package/dist/shortcuts/im/diagnostics.js +97 -0
- package/dist/shortcuts/im/host.js +58 -31
- package/dist/shortcuts/im/index.js +7 -2
- package/dist/shortcuts/im/operations/at-mention.js +127 -0
- package/dist/shortcuts/im/operations/file-user.js +10 -2
- package/dist/shortcuts/im/operations/group-read.js +26 -5
- package/dist/shortcuts/im/operations/group-write.js +35 -28
- package/dist/shortcuts/im/operations/msg-sync.js +7 -2
- package/dist/shortcuts/im/operations/msg-write.js +76 -10
- package/dist/shortcuts/im/operations/quote-mention.js +119 -0
- package/dist/shortcuts/im/operations/session.js +1 -1
- package/dist/shortcuts/im/operations/shared.js +10 -0
- package/dist/shortcuts/im/operations/types.js +13 -0
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/mail/host.js +4 -1
- package/dist/shortcuts/meeting/host.js +27 -34
- package/dist/shortcuts/meeting/operations/create.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
- package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
- package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
- package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
- package/dist/shortcuts/meeting/operations/env.js +93 -46
- package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
- package/dist/shortcuts/meeting/operations.js +16 -0
- package/dist/shortcuts/workflow/index.js +110 -1
- package/dist/shortcuts/workflow/operations/form.js +24 -3
- package/dist/shortcuts/workflow/operations/read.js +104 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
- package/docs/e10-auth.md +1 -1
- package/docs/im.md +8 -1
- package/docs/todo-smart-convert/README.md +1 -1
- package/docs/todo-smart-convert/data-contract.md +2 -2
- package/docs/todo-smart-convert/subgroup-rules.md +6 -0
- package/package.json +1 -1
- package/skill-template/domains/yimiaoban.md +3 -3
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +5 -5
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
- package/skills/weaver-e10-calendar/SKILL.md +5 -5
- package/skills/weaver-e10-esb/SKILL.md +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +5 -5
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
- package/skills/weaver-e10-jucailin/SKILL.md +18 -12
- package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
- package/skills/weaver-e10-mail/SKILL.md +5 -5
- package/skills/weaver-e10-meeting/SKILL.md +171 -25
- package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
- package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +5 -5
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +5 -5
- package/skills/weaver-e10-shared/SKILL.md +12 -12
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
- package/skills/weaver-e10-workflow/SKILL.md +29 -5
- package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
- package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
- package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
- package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
- package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
- package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
|
@@ -42,7 +42,7 @@ const LIST_KEYWORDS = {
|
|
|
42
42
|
biz_config: ['业务配置映射表'],
|
|
43
43
|
};
|
|
44
44
|
/** 列表类型 -> 缓存键 + 表单逻辑名(candidate.list 四列表映射,同 recruit-list.mjs TYPES) */
|
|
45
|
-
const LIST_TYPES = {
|
|
45
|
+
export const LIST_TYPES = {
|
|
46
46
|
demand: { listKey: 'demand', formType: 'demand_manage', title: '招聘需求' },
|
|
47
47
|
position: { listKey: 'position', formType: 'position_manage', title: '招聘职位' },
|
|
48
48
|
entry: { listKey: 'entry', formType: 'entry_manage', title: '入职管理' },
|
|
@@ -58,7 +58,7 @@ const INTERNAL_KEYS = new Set([
|
|
|
58
58
|
const DISPLAY_UI_KEYS = new Set([
|
|
59
59
|
...INTERNAL_KEYS, 'operate', 'isToDo', 'todoPropSpan', 'flow_status',
|
|
60
60
|
]);
|
|
61
|
-
function randHex32() {
|
|
61
|
+
export function randHex32() {
|
|
62
62
|
return randomUUID().replace(/-/g, '');
|
|
63
63
|
}
|
|
64
64
|
function randHex6() {
|
|
@@ -419,7 +419,7 @@ export function createRecruitContextLoader() {
|
|
|
419
419
|
return { load };
|
|
420
420
|
}
|
|
421
421
|
/** EB 列表通用查询体(同 recruit-list.mjs baseBody) */
|
|
422
|
-
function buildListBody(params) {
|
|
422
|
+
export function buildListBody(params) {
|
|
423
423
|
return {
|
|
424
424
|
current: params.current ?? 1,
|
|
425
425
|
pageSize: params.pageSize ?? 10,
|
|
@@ -444,7 +444,7 @@ function buildListBody(params) {
|
|
|
444
444
|
};
|
|
445
445
|
}
|
|
446
446
|
/** 列表 + 计数并行;displayData 优先(同 recruit-search/recruit-list 的显示层策略) */
|
|
447
|
-
async function fetchListRows(host, objId, listId, body) {
|
|
447
|
+
export async function fetchListRows(host, objId, listId, body) {
|
|
448
448
|
const countBody = { ...body };
|
|
449
449
|
delete countBody.current;
|
|
450
450
|
delete countBody.pageSize;
|
|
@@ -466,8 +466,33 @@ async function fetchListRows(host, objId, listId, body) {
|
|
|
466
466
|
const hasDisplay = Array.isArray(displayRows) && displayRows.length > 0;
|
|
467
467
|
const rows = hasDisplay ? displayRows : rawRows;
|
|
468
468
|
const countData = countResponse ? normalizePaged(countResponse).data : null;
|
|
469
|
-
const
|
|
470
|
-
|
|
469
|
+
const pageSize = typeof body.pageSize === 'number'
|
|
470
|
+
? body.pageSize
|
|
471
|
+
: (typeof body.pageSize === 'string' ? (Number(body.pageSize) || rows.length) : rows.length);
|
|
472
|
+
// 总数取值(不输出假值,对齐 ehr recruit-list.mjs 三态降级):
|
|
473
|
+
// ① 计数接口可用(200 且 total 为数字)→ 精确;
|
|
474
|
+
// ② 计数不可用但本页未满(rows < pageSize,即已到末页)→ 总数=已取回行数(精确);
|
|
475
|
+
// ③ 计数不可用且本页已满 → total=null(总数不可知),附 note 提示加大 pageSize。
|
|
476
|
+
// 背景:部分租户/列表 getListDataCount 恒 500,且 getListDatas 的 total 不可信,唯一可靠判据是分页本身。
|
|
477
|
+
const countUsable = countData !== null && typeof countData.total === 'number';
|
|
478
|
+
let total;
|
|
479
|
+
let totalExact;
|
|
480
|
+
let totalNote;
|
|
481
|
+
if (countUsable) {
|
|
482
|
+
total = countData.total;
|
|
483
|
+
totalExact = true;
|
|
484
|
+
}
|
|
485
|
+
else if (rows.length < pageSize) {
|
|
486
|
+
total = rows.length;
|
|
487
|
+
totalExact = true;
|
|
488
|
+
totalNote = '计数接口不可用,本页未满(已到末页),总数按已取回行数得出';
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
total = null;
|
|
492
|
+
totalExact = false;
|
|
493
|
+
totalNote = '计数接口不可用且本页已满,总数不可知(可调大 pageSize 取回全部行数)';
|
|
494
|
+
}
|
|
495
|
+
return { rows, displayRows: hasDisplay ? displayRows : null, total, totalExact, totalNote };
|
|
471
496
|
}
|
|
472
497
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
473
498
|
// operations
|
|
@@ -537,7 +562,7 @@ const candidateSearch = {
|
|
|
537
562
|
pageSize: toNumber(args.pageSize, 'pageSize') ?? 10,
|
|
538
563
|
conditions,
|
|
539
564
|
});
|
|
540
|
-
const { rows, displayRows, total } = await fetchListRows(env.host, talentObjId, talentList.listId, body);
|
|
565
|
+
const { rows, displayRows, total, totalExact, totalNote } = await fetchListRows(env.host, talentObjId, talentList.listId, body);
|
|
541
566
|
// 显示层优先:displayData 已内联人员/字典中文,零额外请求;
|
|
542
567
|
// 缺失时回退原始层:字典映射 + 纯 ID 经 hrm getEmployeeByIds 批量反查(兜底,行为不变)
|
|
543
568
|
let creatorNameMap = {};
|
|
@@ -590,7 +615,7 @@ const candidateSearch = {
|
|
|
590
615
|
createTime: row?.create_time || null,
|
|
591
616
|
};
|
|
592
617
|
});
|
|
593
|
-
return ehrSuccess('ehr.recruit.candidate.search', { total, rows: dataRows });
|
|
618
|
+
return ehrSuccess('ehr.recruit.candidate.search', { total, totalExact, ...(totalNote ? { totalNote } : {}), rows: dataRows });
|
|
594
619
|
},
|
|
595
620
|
};
|
|
596
621
|
const candidateList = {
|
|
@@ -632,7 +657,7 @@ const candidateList = {
|
|
|
632
657
|
current: toPositiveInt(args.current, 'current') ?? 1,
|
|
633
658
|
pageSize: toNumber(args.pageSize, 'pageSize') ?? 10,
|
|
634
659
|
});
|
|
635
|
-
const { rows, displayRows, total } = await fetchListRows(env.host, formObjId, listParam.listId, body);
|
|
660
|
+
const { rows, displayRows, total, totalExact, totalNote } = await fetchListRows(env.host, formObjId, listParam.listId, body);
|
|
636
661
|
const outRows = rows.map((row) => {
|
|
637
662
|
const cleaned = { form_data_id: row?.form_data_id || row?.id };
|
|
638
663
|
for (const [key, value] of Object.entries(row || {})) {
|
|
@@ -669,7 +694,7 @@ const candidateList = {
|
|
|
669
694
|
return cleaned;
|
|
670
695
|
});
|
|
671
696
|
return ehrSuccess('ehr.recruit.candidate.list', {
|
|
672
|
-
type, total, current: body.current, pageSize: body.pageSize, rows: outRows,
|
|
697
|
+
type, total, totalExact, ...(totalNote ? { totalNote } : {}), current: body.current, pageSize: body.pageSize, rows: outRows,
|
|
673
698
|
});
|
|
674
699
|
},
|
|
675
700
|
};
|
|
@@ -6,6 +6,9 @@ import { hrDataSourceOperations } from './hr-ds.js';
|
|
|
6
6
|
import { hrReportOperations } from './hr-report.js';
|
|
7
7
|
import { recruitOperations } from './recruit.js';
|
|
8
8
|
import { recruitWriteOperations } from './recruit-write.js';
|
|
9
|
+
import { recruitStatsOperations } from './recruit-stats.js';
|
|
10
|
+
import { recruitPositionOperations } from './recruit-position.js';
|
|
11
|
+
import { recruitCandidateImportOperations } from './recruit-candidate-import.js';
|
|
9
12
|
import { salaryOperations } from './salary.js';
|
|
10
13
|
function buildRegistry() {
|
|
11
14
|
const registry = new Map();
|
|
@@ -18,6 +21,9 @@ function buildRegistry() {
|
|
|
18
21
|
...hrReportOperations,
|
|
19
22
|
...recruitOperations,
|
|
20
23
|
...recruitWriteOperations,
|
|
24
|
+
...recruitStatsOperations,
|
|
25
|
+
...recruitPositionOperations,
|
|
26
|
+
...recruitCandidateImportOperations,
|
|
21
27
|
];
|
|
22
28
|
for (const definition of definitions) {
|
|
23
29
|
if (registry.has(definition.name)) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 易秒办(im)域内的诊断能力:**只动本模块目录,不改公共 core/cmd/internal**。
|
|
3
|
+
*
|
|
4
|
+
* 两个能力都通过公共已有的扩展点实现:
|
|
5
|
+
* - 失败信封双写:`im` 自己的 catch 分支里写 stdout + stderr,不扩 `Output` 类;
|
|
6
|
+
* - 请求级追踪:走 `HookManager.registerE10RequestInterceptor()`,不碰 `E10RequestRuntime`。
|
|
7
|
+
*/
|
|
8
|
+
const TRACE_ENV = 'WEAVER_WORK_CLI_TRACE';
|
|
9
|
+
/** 同一进程内只注册一次拦截器(`forCommand()` 复用同一个 HookManager 实例)。 */
|
|
10
|
+
const registeredHooks = new WeakSet();
|
|
11
|
+
function truthy(value) {
|
|
12
|
+
const raw = String(value ?? '').toLowerCase();
|
|
13
|
+
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
14
|
+
}
|
|
15
|
+
/** 从当前命令向上找 `im --trace`:命令级开关,不占用全局选项槽位。 */
|
|
16
|
+
function traceFromCommand(ctx) {
|
|
17
|
+
let current = ctx.activeCommand;
|
|
18
|
+
for (let depth = 0; current && depth <= 3; depth += 1) {
|
|
19
|
+
const opts = typeof current.opts === 'function' ? current.opts() : undefined;
|
|
20
|
+
if (opts?.trace === true)
|
|
21
|
+
return true;
|
|
22
|
+
current = current.parent ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function imTraceEnabled(ctx) {
|
|
27
|
+
if (truthy(ctx.env?.[TRACE_ENV]) || truthy(process.env[TRACE_ENV]))
|
|
28
|
+
return true;
|
|
29
|
+
try {
|
|
30
|
+
return traceFromCommand(ctx);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// 单测里的 ctx 可能是最小 mock:追踪开关不得影响请求本身
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** 按 path 记录在途请求的起始时间;同 path 并发时按 FIFO 配对。 */
|
|
38
|
+
const inflight = new Map();
|
|
39
|
+
function remember(path) {
|
|
40
|
+
const list = inflight.get(path) ?? [];
|
|
41
|
+
list.push(Date.now());
|
|
42
|
+
inflight.set(path, list);
|
|
43
|
+
}
|
|
44
|
+
function forget(path) {
|
|
45
|
+
const list = inflight.get(path);
|
|
46
|
+
if (!list?.length)
|
|
47
|
+
return undefined;
|
|
48
|
+
const startedAt = list.shift();
|
|
49
|
+
if (!list.length)
|
|
50
|
+
inflight.delete(path);
|
|
51
|
+
return startedAt;
|
|
52
|
+
}
|
|
53
|
+
function label(request) {
|
|
54
|
+
const operation = request.operation || '-';
|
|
55
|
+
return `${request.method} ${request.path} op=${operation}`;
|
|
56
|
+
}
|
|
57
|
+
function traceInterceptor() {
|
|
58
|
+
return {
|
|
59
|
+
beforeRequest({ request }) {
|
|
60
|
+
remember(request.path);
|
|
61
|
+
},
|
|
62
|
+
afterResponse({ request, response }) {
|
|
63
|
+
const startedAt = forget(request.path);
|
|
64
|
+
const ms = startedAt === undefined ? '-' : `${Date.now() - startedAt}ms`;
|
|
65
|
+
process.stderr.write(`[trace] ${label(request)} status=${response.status} ${ms}\n`);
|
|
66
|
+
},
|
|
67
|
+
onError({ request, error }) {
|
|
68
|
+
const startedAt = forget(request.path);
|
|
69
|
+
const ms = startedAt === undefined ? '-' : `${Date.now() - startedAt}ms`;
|
|
70
|
+
const code = error?.subtype
|
|
71
|
+
|| error?.code
|
|
72
|
+
|| 'unknown';
|
|
73
|
+
// 只打方法/路径/operation/错误码/耗时,绝不打印请求头与请求体(可能含凭证)
|
|
74
|
+
process.stderr.write(`[trace] ${label(request)} status=ERR code=${code} ${ms}\n`);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 按需注册追踪拦截器:未开启 trace 时完全零开销。 */
|
|
79
|
+
export function registerImTrace(ctx) {
|
|
80
|
+
if (!imTraceEnabled(ctx))
|
|
81
|
+
return;
|
|
82
|
+
const hooks = ctx.hooks;
|
|
83
|
+
if (!hooks || registeredHooks.has(hooks))
|
|
84
|
+
return;
|
|
85
|
+
registeredHooks.add(hooks);
|
|
86
|
+
ctx.hooks.registerE10RequestInterceptor(traceInterceptor());
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 失败信封:**stdout 与 stderr 各写一份**(内容相同)。
|
|
90
|
+
* stdout 是主通道 —— 只读 stdout 的调用方也能拿到结构化错误;
|
|
91
|
+
* stderr 保留一份,兼容「stderr 有内容 = 失败」的历史判断。
|
|
92
|
+
*/
|
|
93
|
+
export function printImFailure(envelope) {
|
|
94
|
+
const text = `${JSON.stringify(envelope, null, 2)}\n`;
|
|
95
|
+
process.stdout.write(text);
|
|
96
|
+
process.stderr.write(text);
|
|
97
|
+
}
|
|
@@ -94,6 +94,7 @@ export function createImHost(ctx, dependencies = {}) {
|
|
|
94
94
|
return createE10RequestRuntime(ctx, await ensureSession(), {
|
|
95
95
|
interceptors: dependencies.interceptors,
|
|
96
96
|
fetch: dependencies.fetch,
|
|
97
|
+
recoverAuthentication: dependencies.recoverAuthentication,
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
async function request(request) {
|
|
@@ -144,41 +145,53 @@ export function createImHost(ctx, dependencies = {}) {
|
|
|
144
145
|
}
|
|
145
146
|
async function requestBinary(url, headers = {}) {
|
|
146
147
|
const session = await ensureSession();
|
|
147
|
-
const
|
|
148
|
-
|
|
148
|
+
const baseUrl = new URL(session.getBaseUrl());
|
|
149
|
+
let target;
|
|
149
150
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
target = new URL(url, baseUrl);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw imValidationError('download_url_invalid', '下载地址不是合法 URL');
|
|
155
|
+
}
|
|
156
|
+
if (target.origin !== baseUrl.origin) {
|
|
157
|
+
throw imValidationError('download_url_cross_origin', '下载地址不允许跨 E10 环境');
|
|
158
|
+
}
|
|
159
|
+
assertPath(target.pathname);
|
|
160
|
+
const query = {};
|
|
161
|
+
for (const [key, value] of target.searchParams) {
|
|
162
|
+
const previous = query[key];
|
|
163
|
+
if (previous === undefined)
|
|
164
|
+
query[key] = value;
|
|
165
|
+
else if (Array.isArray(previous))
|
|
166
|
+
previous.push(value);
|
|
167
|
+
else
|
|
168
|
+
query[key] = [previous, value];
|
|
169
|
+
}
|
|
170
|
+
let response;
|
|
171
|
+
try {
|
|
172
|
+
response = await (await runtime()).requestBinary({
|
|
173
|
+
module: 'im',
|
|
174
|
+
operation: 'im.file.download',
|
|
175
|
+
method: 'GET',
|
|
176
|
+
path: target.pathname,
|
|
177
|
+
query,
|
|
178
|
+
headers,
|
|
179
|
+
timeoutMs: 60_000,
|
|
161
180
|
});
|
|
162
|
-
if (resp.status === 401 || resp.status === 302) {
|
|
163
|
-
throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
|
|
164
|
-
}
|
|
165
|
-
if (resp.status !== 200) {
|
|
166
|
-
throw new ImCliError('api', 'download_failed', `下载失败 HTTP ${resp.status}`, {
|
|
167
|
-
code: resp.status,
|
|
168
|
-
exitCode: 1,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return Buffer.from(await resp.arrayBuffer());
|
|
172
181
|
}
|
|
173
182
|
catch (error) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
throw
|
|
183
|
+
mapRuntimeNetworkError(error);
|
|
184
|
+
}
|
|
185
|
+
if (response.status === 401 || response.status === 302) {
|
|
186
|
+
throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
|
|
178
187
|
}
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
if (response.status !== 200) {
|
|
189
|
+
throw new ImCliError('api', 'download_failed', `下载失败 HTTP ${response.status}`, {
|
|
190
|
+
code: response.status,
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
});
|
|
181
193
|
}
|
|
194
|
+
return response.content;
|
|
182
195
|
}
|
|
183
196
|
/** teamsCheck:解析当前登录人的 imUid/imCid/name(IM uid 语义 = 员工 id)。 */
|
|
184
197
|
async function getSelfIdentity() {
|
|
@@ -248,11 +261,25 @@ export function createImHost(ctx, dependencies = {}) {
|
|
|
248
261
|
timeoutMs: 15_000,
|
|
249
262
|
});
|
|
250
263
|
const preData = preRes?.data || {};
|
|
264
|
+
/**
|
|
265
|
+
* 秒传分支:服务端按 MD5 命中已有文件时**不再返回 preUploadId**,而是直接给出 fileObj
|
|
266
|
+
* (`uploadType: "second"`)。实测两种结构:
|
|
267
|
+
* { uploadType: 'second', fileObj: {...} }
|
|
268
|
+
* { uploadType: 'second', imUploadInfoDto: { uploadType: 'second', fileObj: {...}, transcode: {...} } }
|
|
269
|
+
* 旧实现只认 preUploadId,导致「同一张图/同一个文件第二次发送」必定失败(upload_check_failed,
|
|
270
|
+
* 但响应本身是 code:200 status:true)。命中秒传时直接复用 fileObj,跳过 valid/upload。
|
|
271
|
+
*/
|
|
272
|
+
const preInfo = preData.imUploadInfoDto || preData;
|
|
273
|
+
const preFileObj = preInfo?.fileObj && (preInfo.fileObj.id || preInfo.fileObj.fileid) ? preInfo.fileObj : null;
|
|
274
|
+
const preUploadType = String(preData.uploadType || preInfo?.uploadType || '');
|
|
275
|
+
if (preFileObj && (!preData.preUploadId || preUploadType === 'second')) {
|
|
276
|
+
return preFileObj;
|
|
277
|
+
}
|
|
251
278
|
if (!preData.preUploadId) {
|
|
252
|
-
throw new ImCliError('api', 'upload_check_failed', `preUploadCheck
|
|
279
|
+
throw new ImCliError('api', 'upload_check_failed', `preUploadCheck 失败(未返回 preUploadId,也没有可复用的秒传 fileObj)`, { details: preRes, exitCode: 1 });
|
|
253
280
|
}
|
|
254
281
|
const preUploadId = String(preData.preUploadId);
|
|
255
|
-
const uploadType =
|
|
282
|
+
const uploadType = preUploadType || 'common';
|
|
256
283
|
// 第 2 步:实传
|
|
257
284
|
const upForm = new FormData();
|
|
258
285
|
upForm.append('data', new Blob([data], { type: mime }), name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { ExitCode, isCliError, toErrorEnvelope } from '../../core/errors.js';
|
|
3
3
|
import { createImContinuationSigner } from './continuation.js';
|
|
4
|
+
import { printImFailure, registerImTrace } from './diagnostics.js';
|
|
4
5
|
import { imValidationError } from './errors.js';
|
|
5
6
|
import { createImHost } from './host.js';
|
|
6
7
|
import { imManifest } from './manifest.js';
|
|
@@ -81,6 +82,8 @@ function failureEnvelope(error) {
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
async function executeAndPrint(ctx, operation, input) {
|
|
85
|
+
// 排障/接口计数:`im --trace` 或 WEAVER_WORK_CLI_TRACE=1(实现完全在 im 域内,不改公共 runtime)
|
|
86
|
+
registerImTrace(ctx);
|
|
84
87
|
try {
|
|
85
88
|
const result = await executeImOperation(operation, input, {
|
|
86
89
|
ctx,
|
|
@@ -90,7 +93,7 @@ async function executeAndPrint(ctx, operation, input) {
|
|
|
90
93
|
ctx.output.write(successEnvelope(operation, result), { json: true });
|
|
91
94
|
}
|
|
92
95
|
catch (error) {
|
|
93
|
-
|
|
96
|
+
printImFailure(failureEnvelope(error));
|
|
94
97
|
process.exitCode = isCliError(error) ? error.exitCode : ExitCode.GeneralError;
|
|
95
98
|
}
|
|
96
99
|
}
|
|
@@ -110,7 +113,9 @@ export const imShortcut = {
|
|
|
110
113
|
description: '泛微E10易秒办(即时通讯)CLI',
|
|
111
114
|
category: 'shortcut',
|
|
112
115
|
register({ program, action }) {
|
|
113
|
-
const im = program.command('im')
|
|
116
|
+
const im = program.command('im')
|
|
117
|
+
.description('泛微E10易秒办:消息/群组/会话/系统消息')
|
|
118
|
+
.option('--trace', '每个 E10 请求在 stderr 打一行 [trace](method/path/op/status/耗时,不含凭证)');
|
|
114
119
|
im.command('schema')
|
|
115
120
|
.description('Print im operation manifest for agents')
|
|
116
121
|
.action(action((ctx) => {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { imValidationError } from '../errors.js';
|
|
2
|
+
import { extractGroupMembers, isSystemMember, str, unwrapInner } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* 群聊 @ 提及(@所有人 / @具体人)。
|
|
5
|
+
*
|
|
6
|
+
* 服务端契约(2026-10-29 实测 yunteams,群 1793244595701000005):
|
|
7
|
+
* - 只能通过 `sendGroupMsg` 的 **`msg` 字段**(完整原生 msg JSON 字符串)携带 @;
|
|
8
|
+
* `txt` 字段传 JSON 字符串会被服务端原样塞进 `dt[0].txt.v`,客户端直接显示 JSON 原文(踩坑确认)。
|
|
9
|
+
* - 原生结构:`{ bgc:'', cnt:'0', dev:5, display:'', dt:[{txt:{aus, ft:'china', next:'0', v}}],
|
|
10
|
+
* guid:'0', idx:'0', opb:['received','reply'], scid, sname, suid, sysId:'', type:'1', ver:'1.0', weaOaId:'' }`
|
|
11
|
+
* - `txt.aus` 条目:@具体人 `{ cid:<人cid>, fd:'@', id:<人uid>, nm:<姓名>, t:'1' }`;
|
|
12
|
+
* @所有人固定 `{ cid:'10000', fd:'@', id:'88', nm:'全体成员', t:'2' }`。
|
|
13
|
+
* - `txt.v` 中每个 @ 位置写占位符 `:@<id>;`(冒号+@+id+分号),其余文本原样保留;
|
|
14
|
+
* 客户端渲染时用 aus 把占位符还原为 `@<nm>`。
|
|
15
|
+
*/
|
|
16
|
+
/** @全体成员 的服务端固定条目。 */
|
|
17
|
+
export const AT_ALL_ENTRY = { cid: '10000', fd: '@', id: '88', nm: '全体成员', t: '2' };
|
|
18
|
+
function truthy(value) {
|
|
19
|
+
return value === true || value === 'true' || value === '1';
|
|
20
|
+
}
|
|
21
|
+
function uidList(value) {
|
|
22
|
+
if (value === undefined || value === null || value === '')
|
|
23
|
+
return [];
|
|
24
|
+
const raw = Array.isArray(value)
|
|
25
|
+
? value.map((x) => str(x))
|
|
26
|
+
: String(value).split(/[,,;;\s]+/);
|
|
27
|
+
return raw.map((x) => x.trim()).filter((x) => x && x !== '0');
|
|
28
|
+
}
|
|
29
|
+
/** 调用方是否带了任何 @ 入参(含显式无效值,用于单聊报错)。 */
|
|
30
|
+
export function hasAtInput(args) {
|
|
31
|
+
return args.atAll !== undefined || args.atUids !== undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 解析 @ 入参为 aus 条目列表。
|
|
35
|
+
* @具体人必须逐个核实已在目标群(getGroupUsers,member_mask=6 只取 uid+cid),
|
|
36
|
+
* 不在群内直接拒绝(错误码 at_member_not_in_group);姓名一律走 hrm 解析(不用 IM 返回的 name)。
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveAtMentions(args, env, groupId) {
|
|
39
|
+
const atAll = truthy(args.atAll);
|
|
40
|
+
const atUids = uidList(args.atUids);
|
|
41
|
+
if (!atAll && !atUids.length)
|
|
42
|
+
return [];
|
|
43
|
+
const entries = [];
|
|
44
|
+
if (atAll)
|
|
45
|
+
entries.push({ ...AT_ALL_ENTRY });
|
|
46
|
+
if (atUids.length) {
|
|
47
|
+
let members = [];
|
|
48
|
+
try {
|
|
49
|
+
const res = await env.host.request({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/api/em/msg/executeIm/group/getGroupUsers',
|
|
52
|
+
body: { group_id: groupId, type: 0, sync_type: 0, member_mask: 6, client_uc: '0' },
|
|
53
|
+
operation: 'im.msg.send.group.atMembers',
|
|
54
|
+
});
|
|
55
|
+
members = extractGroupMembers(unwrapInner(res));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw imValidationError('at_members_unavailable', `无法核实 @ 目标是否在群内:拉取群成员失败(${error instanceof Error ? error.message : String(error)})`, { groupId, atUids });
|
|
59
|
+
}
|
|
60
|
+
const inGroup = new Map();
|
|
61
|
+
for (const m of members) {
|
|
62
|
+
if (isSystemMember(m))
|
|
63
|
+
continue;
|
|
64
|
+
inGroup.set(str(m.uid), str(m.cid));
|
|
65
|
+
}
|
|
66
|
+
const missing = atUids.filter((uid) => !inGroup.has(uid));
|
|
67
|
+
if (missing.length) {
|
|
68
|
+
throw imValidationError('at_member_not_in_group', `以下 @ 目标不在群 ${groupId} 内,已拒绝发送:${missing.join(', ')}`, { groupId, missing, atUids });
|
|
69
|
+
}
|
|
70
|
+
const personMap = await env.host.resolveUsersByIds([...new Set(atUids)]);
|
|
71
|
+
for (const uid of atUids) {
|
|
72
|
+
entries.push({ cid: inGroup.get(uid) || '', fd: '@', id: uid, nm: personMap[uid]?.name || uid, t: '1' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return entries;
|
|
76
|
+
}
|
|
77
|
+
function escapeRegExp(value) {
|
|
78
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 把 @ 入参落进文本:文本中出现的 `@<姓名>`(@全体成员兼认 `@所有人`)替换为 `:@<id>;` 占位符;
|
|
82
|
+
* 未在文本中出现的提及按传入顺序前置(`:@<id>;` 两空格分隔,仿客户端格式)。
|
|
83
|
+
* 返回原生 txt 需要的 `v`(占位文本)与 `aus`(按 v 中出现顺序)。
|
|
84
|
+
*/
|
|
85
|
+
export function buildAtText(text, entries) {
|
|
86
|
+
let v = String(text ?? '');
|
|
87
|
+
const used = new Set();
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const names = entry.t === '2' ? ['全体成员', '所有人'] : [entry.nm];
|
|
90
|
+
for (const nm of names) {
|
|
91
|
+
const re = new RegExp(`@${escapeRegExp(nm)}`, 'g');
|
|
92
|
+
if (!re.test(v))
|
|
93
|
+
continue;
|
|
94
|
+
v = v.replace(re, () => `:@${entry.id};`);
|
|
95
|
+
used.add(entry);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const rest = entries.filter((e) => !used.has(e));
|
|
100
|
+
if (rest.length) {
|
|
101
|
+
const prefix = rest.map((e) => `:@${e.id};`).join(' ');
|
|
102
|
+
v = v.trim() ? `${prefix} ${v}` : prefix;
|
|
103
|
+
}
|
|
104
|
+
const aus = [...used, ...rest];
|
|
105
|
+
return { v, aus };
|
|
106
|
+
}
|
|
107
|
+
/** 组装 sendGroupMsg 的 `msg` 字段值(完整原生 msg JSON 字符串)。 */
|
|
108
|
+
export function buildAtMsgJson(v, aus, self, extra = {}) {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
bgc: '',
|
|
111
|
+
cnt: '0',
|
|
112
|
+
dev: 5,
|
|
113
|
+
display: '',
|
|
114
|
+
dt: [{ txt: { aus, ft: 'china', next: '0', v } }],
|
|
115
|
+
guid: '0',
|
|
116
|
+
idx: '0',
|
|
117
|
+
opb: ['received', 'reply'],
|
|
118
|
+
scid: self.cid,
|
|
119
|
+
sname: self.name,
|
|
120
|
+
suid: self.uid,
|
|
121
|
+
sysId: '',
|
|
122
|
+
type: '1',
|
|
123
|
+
ver: '1.0',
|
|
124
|
+
weaOaId: '',
|
|
125
|
+
...extra,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -72,7 +72,11 @@ const filePreview = {
|
|
|
72
72
|
toUid: { type: 'string', description: '单聊场景对方 uid' },
|
|
73
73
|
toCid: { type: 'string', description: '单聊场景对方 cid' },
|
|
74
74
|
imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small)' },
|
|
75
|
-
}, {
|
|
75
|
+
}, {
|
|
76
|
+
required: ['fileId', 'msgid'],
|
|
77
|
+
anyOf: [{ required: ['groupId'] }, { required: ['toUid', 'toCid'] }],
|
|
78
|
+
description: '媒体预览 URL(groupId 群聊 或 toUid+toCid 单聊,二选一)',
|
|
79
|
+
}),
|
|
76
80
|
handler: async (args, { host }) => {
|
|
77
81
|
const kind = String(args.kind ?? 'img') === 'video' ? 'video' : 'img';
|
|
78
82
|
const fileId = String(args.fileId);
|
|
@@ -137,7 +141,11 @@ const fileDownload = {
|
|
|
137
141
|
toCid: { type: 'string', description: '单聊场景对方 cid(与 groupId 二选一)' },
|
|
138
142
|
imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small;要原图传 original)' },
|
|
139
143
|
output: { type: 'string', minLength: 1, description: '本地输出文件路径(父目录必须存在,已存在则拒绝)' },
|
|
140
|
-
}, {
|
|
144
|
+
}, {
|
|
145
|
+
required: ['fileId', 'msgid', 'output'],
|
|
146
|
+
anyOf: [{ required: ['groupId'] }, { required: ['toUid', 'toCid'] }],
|
|
147
|
+
description: '媒体/文件落地到本地(groupId 群聊 或 toUid+toCid 单聊,二选一)',
|
|
148
|
+
}),
|
|
141
149
|
handler: async (args, { host }) => {
|
|
142
150
|
const fileId = String(args.fileId ?? '');
|
|
143
151
|
const msgid = String(args.msgid ?? '');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { imValidationError } from '../errors.js';
|
|
2
2
|
import { READ, schemaOf, imSuccess, } from './types.js';
|
|
3
|
-
import { assertImOk, compact, extractGroupMembers, formatFullTime, splitIds, str, toPositiveInt, toUint, unwrapInner, } from './shared.js';
|
|
3
|
+
import { assertImOk, compact, extractGroupMembers, formatFullTime, isSystemMember, splitIds, str, toPositiveInt, toUint, unwrapInner, } from './shared.js';
|
|
4
4
|
const SEARCH_TYPE_MASK = {
|
|
5
5
|
type: 1,
|
|
6
6
|
key: 2,
|
|
@@ -153,7 +153,7 @@ const groupInfo = {
|
|
|
153
153
|
inputSchema: schemaOf({
|
|
154
154
|
groupIds: { type: 'array', items: { type: 'string' }, description: '群 id 列表(逗号串或数组)' },
|
|
155
155
|
groupId: { type: 'string', description: '单群 id(groupIds 的便捷形式)' },
|
|
156
|
-
}, { description: 'groupIds / groupId
|
|
156
|
+
}, { requiredAny: ['groupIds', 'groupId'], description: '按群 id 查基础信息(groupIds / groupId 至少其一)' }),
|
|
157
157
|
handler: async (args, { host }) => {
|
|
158
158
|
let ids = [];
|
|
159
159
|
if (Array.isArray(args.groupIds))
|
|
@@ -186,12 +186,23 @@ const groupInfo = {
|
|
|
186
186
|
maskName: maskTypeName(Number(g.mask ?? 0)),
|
|
187
187
|
userNum: g.user_num,
|
|
188
188
|
maxNum: g.max_num,
|
|
189
|
-
|
|
189
|
+
/** 群创建者 uid(服务端字段 host_uid);**不是群主**,群主以 group.users 的 role=3 为准。 */
|
|
190
|
+
creatorUid: str(g.host_uid ?? g.hostUid ?? ''),
|
|
191
|
+
/** 群创建者 cid(服务端字段 host_cid),与 creatorUid 配对。 */
|
|
192
|
+
creatorCid: str(g.host_cid ?? g.hostCid ?? ''),
|
|
190
193
|
state: Number(g.state ?? -1),
|
|
191
194
|
stateName: stateName(Number(g.state ?? -1)),
|
|
192
195
|
createTime: formatFullTime(g.create_time ?? g.createTime),
|
|
193
196
|
}));
|
|
194
|
-
return imSuccess('im.group.info', {
|
|
197
|
+
return imSuccess('im.group.info', {
|
|
198
|
+
total: groups.length,
|
|
199
|
+
groups,
|
|
200
|
+
fieldNotes: {
|
|
201
|
+
creatorUid: '群创建者(host_uid),不是群主;创建后转让群主也不会变。',
|
|
202
|
+
creatorCid: '群创建者 cid(host_cid)。',
|
|
203
|
+
owner: '群主请用 im.group.users 的 role=3(或 im.group.admins)获取,不要用 creatorUid 判断。',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
195
206
|
},
|
|
196
207
|
};
|
|
197
208
|
/** 群成员列表。 */
|
|
@@ -233,7 +244,11 @@ const groupUsers = {
|
|
|
233
244
|
const hasCid = (memberMask & 2) !== 0;
|
|
234
245
|
const hasUid = (memberMask & 4) !== 0;
|
|
235
246
|
const roleNames = { 0: '不可用', 1: '普通成员', 2: '管理员', 3: '群主', 4: '客户', 5: '负责人', 6: '协助人员' };
|
|
236
|
-
|
|
247
|
+
// 系统成员没有 hrm 资料,不必进解析批次(省一次无效 id,也避免被当成「解析失败」)
|
|
248
|
+
const personIds = members
|
|
249
|
+
.filter((m) => !isSystemMember(m))
|
|
250
|
+
.map((m) => String(m?.uid ?? ''))
|
|
251
|
+
.filter((x) => x && x !== '0');
|
|
237
252
|
// 姓名一律走 hrm 解析(不看 member_mask 的 name 位,也不使用 IM 返回的 name 字段)
|
|
238
253
|
const personMap = personIds.length ? await host.resolveUsersByIds([...new Set(personIds)]) : {};
|
|
239
254
|
const rows = members.map((m) => ({
|
|
@@ -243,10 +258,16 @@ const groupUsers = {
|
|
|
243
258
|
role: hasRole ? Number(m.role_type ?? m.role ?? -1) : undefined,
|
|
244
259
|
roleName: hasRole ? roleNames[Number(m.role_type ?? m.role ?? -1)] ?? '-' : undefined,
|
|
245
260
|
addTime: hasAddTime ? formatFullTime(m.add_time ?? m.addTime) : undefined,
|
|
261
|
+
/** 服务端自动补入的系统成员(可选功能,不是真实员工),统计真实成员时排除。 */
|
|
262
|
+
isSystem: hasCid ? isSystemMember(m) : undefined,
|
|
246
263
|
}));
|
|
264
|
+
const personRows = rows.filter((row) => row.isSystem !== true);
|
|
247
265
|
return imSuccess('im.group.users', {
|
|
248
266
|
groupId,
|
|
267
|
+
/** 成员总数(含系统成员,即服务端返回的原始条数) */
|
|
249
268
|
totalNum: inner?.total_num ?? rows.length,
|
|
269
|
+
/** 真实成员数(排除系统成员 cid=10002),判断「只剩自己」请用这个 */
|
|
270
|
+
personNum: personRows.length,
|
|
250
271
|
endFlag: inner?.end_flag,
|
|
251
272
|
serverUc: str(inner?.server_uc ?? ''),
|
|
252
273
|
members: rows,
|