weaver-work-cli 0.1.32 → 0.1.36
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-XBVH7JU7.js} +2 -2
- package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
- package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
- package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
- package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
- package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
- package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
- package/dist/chunks/chunk-3SO6QL6U.js +73 -0
- package/dist/chunks/chunk-5DQEEGCC.js +229 -0
- package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
- package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
- package/dist/chunks/chunk-BYATLLEF.js +68 -0
- package/dist/chunks/chunk-BYBSCN4X.js +932 -0
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
- package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
- package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
- package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
- package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
- package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
- package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
- package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
- package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
- package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
- package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
- package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
- package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
- package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
- package/dist/chunks/chunk-WXFPWCXG.js +774 -0
- package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
- package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
- package/dist/chunks/continuation-RN6PZN2M.js +11 -0
- package/dist/chunks/contract-XI2EEIZU.js +9110 -0
- package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
- package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
- package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
- package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
- package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
- package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
- package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
- package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
- package/dist/chunks/host-UEI5FGDM.js +23 -0
- package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
- package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
- package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
- package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
- package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
- package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
- package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
- package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
- package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
- package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
- package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
- package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
- package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
- package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
- package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
- package/dist/index.js +35 -25
- package/dist/internal/e10/auth-recovery.js +14 -4
- package/dist/internal/e10/fetch-timing.js +31 -0
- package/dist/internal/e10/request-runtime.js +5 -0
- 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 +180 -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 +39 -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 +43 -16
- package/dist/shortcuts/ehr/operations/recruit.js +182 -10
- package/dist/shortcuts/ehr/operations/registry.js +6 -0
- package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
- 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 +10 -0
- package/dist/shortcuts/mail/host.js +12 -1
- package/dist/shortcuts/mail/operations/types.js +17 -2
- package/dist/shortcuts/meeting/host.js +27 -34
- package/dist/shortcuts/meeting/index.js +79 -1
- package/dist/shortcuts/meeting/manifest.js +10 -0
- package/dist/shortcuts/meeting/operations/create.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
- package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
- package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
- package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
- package/dist/shortcuts/meeting/operations/env.js +99 -46
- package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
- package/dist/shortcuts/meeting/operations/invitation.js +79 -20
- 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/endpoints.js +8 -3
- package/dist/shortcuts/workflow/index.js +110 -1
- package/dist/shortcuts/workflow/operations/form.js +204 -36
- 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/dist/shortcuts/workflow/operations.js +23 -1
- package/docs/e10-auth.md +1 -1
- package/docs/ehr.md +1 -1
- package/docs/fna.md +2 -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/business-info.json +18 -0
- package/skill-template/domains/cowork.md +14 -0
- 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-cowork/SKILL.md +101 -0
- package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
- package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
- package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
- 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 +38 -14
- package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
- package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
- package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
- package/skills/weaver-e10-jucailin/references/salary.md +15 -11
- package/skills/weaver-e10-mail/SKILL.md +17 -14
- package/skills/weaver-e10-meeting/SKILL.md +442 -46
- package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
- 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-qiyecheng/SKILL.md +5 -2
- package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
- package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
- 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
|
@@ -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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { imValidationError } from '../errors.js';
|
|
2
2
|
import { schemaOf, WRITE, } from './types.js';
|
|
3
|
-
import { assertImOk, compact, splitIds, str, toUint, unwrapInner } from './shared.js';
|
|
3
|
+
import { assertImOk, compact, extractGroupMembers, isSystemMember, splitIds, str, toUint, unwrapInner } from './shared.js';
|
|
4
4
|
import { makeWritePair } from './write-util.js';
|
|
5
5
|
/** 解析 uid 列表为 {cid,uid} 成员,支持 failed 移除。 */
|
|
6
6
|
async function resolveMembers(args, env) {
|
|
@@ -59,7 +59,7 @@ const groupCreatePair = makeWritePair({
|
|
|
59
59
|
}
|
|
60
60
|
const { users, failed, names: resolvedNames } = await resolveMembers(args, env);
|
|
61
61
|
if (failed.length && args.removeFailed !== true) {
|
|
62
|
-
throw imValidationError('members_resolve_failed', `以下成员信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`,
|
|
62
|
+
throw imValidationError('members_resolve_failed', `以下成员信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`, failed);
|
|
63
63
|
}
|
|
64
64
|
if (users.length === 0)
|
|
65
65
|
throw imValidationError('members_empty', '没有可入群的成员');
|
|
@@ -98,6 +98,8 @@ const groupCreatePair = makeWritePair({
|
|
|
98
98
|
members: users.map((u) => u.uid),
|
|
99
99
|
adminCount: admins.length,
|
|
100
100
|
removeFailed: args.removeFailed === true ? failed.map((f) => f.uid) : [],
|
|
101
|
+
/** 服务端可选能力:建群时会自动补入系统成员(cid=10002),实际成员数可能比 users 多 1。 */
|
|
102
|
+
systemMemberNote: '建群后服务端可能自动补入 1 个系统成员(cid=10002,群内系统消息载体,租户侧可选开启);它不是真实员工,也不是本次传入的成员,不需要也不建议单独踢出。',
|
|
101
103
|
},
|
|
102
104
|
snapshot: { name, users, admins, failed, removeFailed: args.removeFailed === true },
|
|
103
105
|
};
|
|
@@ -194,28 +196,29 @@ const groupExitPair = makeWritePair({
|
|
|
194
196
|
if (!groupId || groupId === '0')
|
|
195
197
|
throw imValidationError('id_invalid', 'groupId 必须非 0');
|
|
196
198
|
/**
|
|
197
|
-
*
|
|
199
|
+
* 只读回查群主与真实人数:实测「群主/最后一个真实成员退群」后该群会变成无主空群,
|
|
198
200
|
* 之后 join / destroy 都会被服务端拒绝(1210),必须由客户端或管理员处理。
|
|
199
201
|
* 因此这两种情形要求调用方显式传 acknowledgeOrphanRisk:true 才签发 continuation。
|
|
202
|
+
*
|
|
203
|
+
* 群主判定只认 **成员列表的 role=3**(getGroupUsers):group/queryGroupInfo 的 host_uid
|
|
204
|
+
* 是「群创建者」,转让群主后不会更新,用它判群主会误判(实测已复现)。
|
|
200
205
|
*/
|
|
201
206
|
let memberCount;
|
|
202
207
|
let isOwner = false;
|
|
203
208
|
try {
|
|
204
|
-
const
|
|
209
|
+
const usersRes = await env.host.request({
|
|
205
210
|
method: 'POST',
|
|
206
|
-
path: '/api/em/msg/executeIm/group/
|
|
207
|
-
body: {
|
|
208
|
-
operation: 'im.group.exit.plan.
|
|
211
|
+
path: '/api/em/msg/executeIm/group/getGroupUsers',
|
|
212
|
+
body: { group_id: groupId, type: 0, sync_type: 0, member_mask: 142, client_uc: '0' },
|
|
213
|
+
operation: 'im.group.exit.plan.members',
|
|
209
214
|
});
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
isOwner = hostUid === String(self.uid);
|
|
218
|
-
}
|
|
215
|
+
const members = extractGroupMembers(unwrapInner(usersRes));
|
|
216
|
+
if (members.length) {
|
|
217
|
+
const self = await env.host.getSelfIdentity();
|
|
218
|
+
const selfRow = members.find((m) => str(m?.uid) === String(self.uid));
|
|
219
|
+
isOwner = Number(selfRow?.role_type ?? selfRow?.role ?? -1) === 3;
|
|
220
|
+
// 系统成员(cid=10002,产品可选能力)不是真实成员,不能算进「是否只剩自己」
|
|
221
|
+
memberCount = members.filter((m) => !isSystemMember(m)).length;
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
catch {
|
|
@@ -223,7 +226,7 @@ const groupExitPair = makeWritePair({
|
|
|
223
226
|
}
|
|
224
227
|
const orphanRisk = isOwner || (memberCount !== undefined && memberCount <= 1);
|
|
225
228
|
if (orphanRisk && args.acknowledgeOrphanRisk !== true) {
|
|
226
|
-
throw imValidationError('owner_exit_ack_required', `退群风险确认缺失:${isOwner ? '你是该群群主' : '你是该群最后一个成员'},退群后该群会变成无主空群,之后 join/destroy 都会被服务端拒绝(1210),只能由客户端或管理员处理。确认仍要退群请传 acknowledgeOrphanRisk:true`, {
|
|
229
|
+
throw imValidationError('owner_exit_ack_required', `退群风险确认缺失:${isOwner ? '你是该群群主' : '你是该群最后一个成员'},退群后该群会变成无主空群,之后 join/destroy 都会被服务端拒绝(1210),只能由客户端或管理员处理。确认仍要退群请传 acknowledgeOrphanRisk:true`, { groupId, isOwner, memberCount });
|
|
227
230
|
}
|
|
228
231
|
return {
|
|
229
232
|
summary: {
|
|
@@ -537,12 +540,16 @@ const groupAnnounceModifyPair = makeWritePair({
|
|
|
537
540
|
};
|
|
538
541
|
},
|
|
539
542
|
});
|
|
540
|
-
function buildWriteDefinition(name, method, summary, inputProps, pair) {
|
|
543
|
+
function buildWriteDefinition(name, method, summary, inputProps, pair, condition = {}) {
|
|
541
544
|
const requiredInput = Object.keys(inputProps).filter((k) => {
|
|
542
545
|
const prop = inputProps[k];
|
|
543
546
|
return prop?.required === true;
|
|
544
547
|
});
|
|
545
|
-
const prepareSchema = schemaOf({ ...inputProps, removeFailed: { type: 'boolean', description: '确认移除解析失败成员' } }, {
|
|
548
|
+
const prepareSchema = schemaOf({ ...inputProps, removeFailed: { type: 'boolean', description: '确认移除解析失败成员' } }, {
|
|
549
|
+
...condition,
|
|
550
|
+
required: [...new Set([...(condition.required || []), ...requiredInput])],
|
|
551
|
+
description: summary,
|
|
552
|
+
});
|
|
546
553
|
return [
|
|
547
554
|
{
|
|
548
555
|
name: `${name}.prepare`,
|
|
@@ -579,30 +586,30 @@ const groupWriteOps = [
|
|
|
579
586
|
name: { type: 'string', description: '群名(缺省自动生成)' },
|
|
580
587
|
names: { type: 'array', items: { type: 'string' }, description: '与 users 顺序对应姓名(用于默认群名)' },
|
|
581
588
|
admins: { type: 'array', items: { type: 'string' }, description: '管理员 uid' },
|
|
582
|
-
}, groupCreatePair),
|
|
589
|
+
}, groupCreatePair, { required: ['users'] }),
|
|
583
590
|
...buildWriteDefinition('im.group.invite', 'group.invite', '邀请成员入群', {
|
|
584
591
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
585
592
|
users: { type: 'array', items: { type: 'string' }, description: '被邀请 uid' },
|
|
586
|
-
}, groupInvitePair),
|
|
593
|
+
}, groupInvitePair, { required: ['groupId', 'users'] }),
|
|
587
594
|
...buildWriteDefinition('im.group.kick', 'group.kick', '踢出群成员', {
|
|
588
595
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
589
596
|
users: { type: 'array', items: { type: 'string' }, description: '被踢 uid' },
|
|
590
|
-
}, groupKickPair),
|
|
597
|
+
}, groupKickPair, { required: ['groupId', 'users'] }),
|
|
591
598
|
...buildWriteDefinition('im.group.exit', 'group.exit', '退出群聊', {
|
|
592
599
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
593
600
|
acknowledgeOrphanRisk: {
|
|
594
601
|
type: 'boolean',
|
|
595
602
|
description: '群主或最后一个成员退群时必须显式传 true(退群后该群会变成无主空群,无法再 join/destroy)',
|
|
596
603
|
},
|
|
597
|
-
}, groupExitPair),
|
|
604
|
+
}, groupExitPair, { required: ['groupId'] }),
|
|
598
605
|
...buildWriteDefinition('im.group.destroy', 'group.destroy', '解散群聊(仅群主,不可恢复)', {
|
|
599
606
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
600
|
-
}, groupDestroyPair),
|
|
607
|
+
}, groupDestroyPair, { required: ['groupId'] }),
|
|
601
608
|
...buildWriteDefinition('im.group.join', 'group.join', '申请加入群聊', {
|
|
602
609
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id(token 二选一)' },
|
|
603
610
|
token: { type: 'string', description: '群 token/二维码加入' },
|
|
604
611
|
mark: { type: 'string', maxLength: 100, description: '申请理由' },
|
|
605
|
-
}, groupJoinPair),
|
|
612
|
+
}, groupJoinPair, { requiredAny: ['groupId', 'token'] }),
|
|
606
613
|
...buildWriteDefinition('im.group.modify', 'group.modify', '批量修改群属性(batModGroupInfo)', {
|
|
607
614
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
608
615
|
num: { type: 'integer', description: '最大人数' },
|
|
@@ -614,7 +621,7 @@ const groupWriteOps = [
|
|
|
614
621
|
creator: { type: 'string', description: '转让群主(单 uid)' },
|
|
615
622
|
copyGroupids: { type: 'array', items: { type: 'string' }, description: '拷贝群列表' },
|
|
616
623
|
gtId: { type: 'string', description: '关联群类型 id' },
|
|
617
|
-
}, groupModifyPair),
|
|
624
|
+
}, groupModifyPair, { required: ['groupId'] }),
|
|
618
625
|
...buildWriteDefinition('im.group.rename', 'group.rename', '修改群信息(一次一项:群名/历史/群主/显示/打扰/标记/gtid)', {
|
|
619
626
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
620
627
|
name: { type: 'string', description: '改群名' },
|
|
@@ -624,7 +631,7 @@ const groupWriteOps = [
|
|
|
624
631
|
msgSetting: { type: 'integer', description: '消息打扰设置' },
|
|
625
632
|
mark: { type: 'integer', description: '标记位' },
|
|
626
633
|
gtid: { type: 'string', description: 'GTID' },
|
|
627
|
-
}, groupRenamePair),
|
|
634
|
+
}, groupRenamePair, { required: ['groupId'] }),
|
|
628
635
|
...buildWriteDefinition('im.group.announce.modify', 'group.announce.modify', '新增/修改/删除群公告', {
|
|
629
636
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
630
637
|
annouce: { type: 'string', description: '公告内容(删除时不需要)' },
|
|
@@ -633,6 +640,6 @@ const groupWriteOps = [
|
|
|
633
640
|
notice: { type: 'integer', enum: [0, 1, 2], description: '0 发通知(默认)/1 不发/2 发并@全体' },
|
|
634
641
|
fmark: { type: 'integer', enum: [0, 1], description: '新人必看' },
|
|
635
642
|
related: { type: 'string', description: '关联附件 id' },
|
|
636
|
-
}, groupAnnounceModifyPair),
|
|
643
|
+
}, groupAnnounceModifyPair, { required: ['groupId'] }),
|
|
637
644
|
];
|
|
638
645
|
export const groupWriteOperations = groupWriteOps;
|
|
@@ -2,6 +2,7 @@ import { imValidationError } from '../errors.js';
|
|
|
2
2
|
import { READ, schemaOf, imSuccess, } from './types.js';
|
|
3
3
|
import { assertImOk, clampTimeRangeTo7Days, compact, DEFAULT_PAGE_SIZE, formatFullTime, makeMinMaxIdByTm, paginationMeta, parseMsg, parseTimeToSec, pullPaged, rangeShrinkMeta, resolveAuthUid, sortMessagesNewestFirst, str, todayRange, unwrapInner, } from './shared.js';
|
|
4
4
|
import { collapseNewlines, collectI18nIds, collectPersonIds, extractFiles, extractImgs, extractVideos, parseContent, senderName, } from './msg-parse.js';
|
|
5
|
+
import { extractQuoteSummary } from './quote-mention.js';
|
|
5
6
|
const MAX_MSG_ID = String((1n << 63n) - 1n);
|
|
6
7
|
/**
|
|
7
8
|
* 时间/最新/显式 id 计算 start/end(与源 skill 一致):
|
|
@@ -151,6 +152,7 @@ async function enrichMessages(rawRows, host, options = {}) {
|
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
const sender = senderName(msgObj || {}, row.from_uid, options.authUid, nameMap);
|
|
155
|
+
const quoteSummary = extractQuoteSummary(msgObj);
|
|
154
156
|
enriched.push({
|
|
155
157
|
msgid: str(row.ser_msgid),
|
|
156
158
|
sender,
|
|
@@ -161,6 +163,9 @@ async function enrichMessages(rawRows, host, options = {}) {
|
|
|
161
163
|
typeName: parsed.typeName,
|
|
162
164
|
title: options.titleFirst ? parsed.title : undefined,
|
|
163
165
|
content: collapseNewlines(parsed.content),
|
|
166
|
+
...(quoteSummary
|
|
167
|
+
? { quote: quoteSummary, quoteTime: Number(quoteSummary.msgtime) > 0 ? formatFullTime(Math.floor(Number(quoteSummary.msgtime) / 1000)) : undefined }
|
|
168
|
+
: {}),
|
|
164
169
|
must: parsed.must,
|
|
165
170
|
fileName: parsed.fileName,
|
|
166
171
|
media,
|
|
@@ -401,7 +406,7 @@ const msgReadSingle = {
|
|
|
401
406
|
toCid: { type: 'string', pattern: '^\\d+$', description: '对方 cid' },
|
|
402
407
|
msgids: { type: 'array', items: { type: 'string' }, description: '自己发出的消息 id(最多 50)' },
|
|
403
408
|
recvMsgids: { type: 'array', items: { type: 'string' }, description: '自己接收的消息 id(最多 50)' },
|
|
404
|
-
}, { required: ['toUid', 'toCid'], description: '查询单聊阅读状态(msgids/recvMsgids 至少其一)' }),
|
|
409
|
+
}, { required: ['toUid', 'toCid'], requiredAny: ['msgids', 'recvMsgids'], description: '查询单聊阅读状态(msgids / recvMsgids 至少其一)' }),
|
|
405
410
|
handler: async (args, { host }) => {
|
|
406
411
|
const hasSent = Array.isArray(args.msgids) && args.msgids.length > 0;
|
|
407
412
|
const hasRecv = Array.isArray(args.recvMsgids) && args.recvMsgids.length > 0;
|
|
@@ -449,7 +454,7 @@ const msgReadGroup = {
|
|
|
449
454
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
450
455
|
msgids: { type: 'array', items: { type: 'string' }, description: '自己发出的消息 id(最多 50)' },
|
|
451
456
|
recvMsgids: { type: 'array', items: { type: 'string' }, description: '别人发出的消息 id(最多 50)' },
|
|
452
|
-
}, { required: ['groupId'], description: '
|
|
457
|
+
}, { required: ['groupId'], requiredAny: ['msgids', 'recvMsgids'], description: '查询群消息阅读状态(msgids / recvMsgids 至少其一)' }),
|
|
453
458
|
handler: async (args, { host }) => {
|
|
454
459
|
const hasSent = Array.isArray(args.msgids) && args.msgids.length > 0;
|
|
455
460
|
const hasRecv = Array.isArray(args.recvMsgids) && args.recvMsgids.length > 0;
|
|
@@ -3,6 +3,8 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { imValidationError } from '../errors.js';
|
|
4
4
|
import { schemaOf, } from './types.js';
|
|
5
5
|
import { assertImOk, compact, extractGroupMembers, str } from './shared.js';
|
|
6
|
+
import { buildAtMsgJson, buildAtText, hasAtInput, resolveAtMentions } from './at-mention.js';
|
|
7
|
+
import { hasQuoteInput, resolveQuotedRef } from './quote-mention.js';
|
|
6
8
|
import { fileObjToFile, fileObjToImg, makeWritePair } from './write-util.js';
|
|
7
9
|
/** 校验本地文件存在,返回绝对路径。 */
|
|
8
10
|
function assertLocalFile(pathValue, field) {
|
|
@@ -37,7 +39,18 @@ const sendSinglePair = makeWritePair({
|
|
|
37
39
|
if (!toUid || !toCid || toUid === '0' || toCid === '0') {
|
|
38
40
|
throw imValidationError('id_invalid', 'toUid/toCid 必须非 0');
|
|
39
41
|
}
|
|
42
|
+
if (hasAtInput(args)) {
|
|
43
|
+
throw imValidationError('at_not_supported', '@ 提及仅群聊消息支持(单聊发送不接受 atAll/atUids)');
|
|
44
|
+
}
|
|
45
|
+
if (args.replyMsgId !== undefined && args.replyMsgId !== null && args.replyMsgId !== '') {
|
|
46
|
+
throw imValidationError('reply_not_supported', '单聊仅支持引用(quoteMsgId),不支持回复(replyMsgId)');
|
|
47
|
+
}
|
|
40
48
|
const content = normalizeContent(args);
|
|
49
|
+
// 引用(仅单聊·文本):prepare 阶段在单聊会话内定位被引消息并组装 ref
|
|
50
|
+
const quote = hasQuoteInput(args) ? await resolveQuotedRef(args, env, { toUid, toCid }) : null;
|
|
51
|
+
if (quote && !content.txt) {
|
|
52
|
+
throw imValidationError('quote_requires_txt', 'quoteMsgId 仅支持文本消息(txt)');
|
|
53
|
+
}
|
|
41
54
|
const summary = {
|
|
42
55
|
action: '发送单聊消息',
|
|
43
56
|
toUid,
|
|
@@ -51,6 +64,11 @@ const sendSinglePair = makeWritePair({
|
|
|
51
64
|
snapshot.txt = content.txt;
|
|
52
65
|
summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
|
|
53
66
|
}
|
|
67
|
+
if (quote) {
|
|
68
|
+
snapshot.quoteKind = quote.kind;
|
|
69
|
+
snapshot.quoteRef = quote.ref;
|
|
70
|
+
summary.quote = { kind: quote.kind, quotedMsgId: quote.quotedMsgId, fromName: quote.fromName, textPreview: quote.text.length > 40 ? `${quote.text.slice(0, 40)}…` : quote.text };
|
|
71
|
+
}
|
|
54
72
|
if (content.imgPath) {
|
|
55
73
|
const abs = assertLocalFile(content.imgPath, 'img');
|
|
56
74
|
snapshot.imgPath = abs;
|
|
@@ -65,8 +83,15 @@ const sendSinglePair = makeWritePair({
|
|
|
65
83
|
},
|
|
66
84
|
async execute(plan, env) {
|
|
67
85
|
const body = { reciver: { cid: plan.toCid, uid: plan.toUid } };
|
|
68
|
-
|
|
86
|
+
const quoteRef = plan.quoteRef ?? null;
|
|
87
|
+
if (quoteRef) {
|
|
88
|
+
// 引用:完整原生 msg JSON 走 msg 字段(与群聊同款承载方式,实测单聊同样生效)
|
|
89
|
+
const self = await env.host.getSelfIdentity();
|
|
90
|
+
body.msg = buildAtMsgJson(String(plan.txt ?? ''), [], self, { ref: quoteRef });
|
|
91
|
+
}
|
|
92
|
+
else if (plan.txt !== undefined) {
|
|
69
93
|
body.txt = plan.txt;
|
|
94
|
+
}
|
|
70
95
|
if (plan.imgPath) {
|
|
71
96
|
const fileObj = await env.host.uploadFile(plan.imgPath, { shareUsers: plan.toUid });
|
|
72
97
|
body.img = fileObjToImg(fileObj);
|
|
@@ -93,12 +118,31 @@ const sendGroupPair = makeWritePair({
|
|
|
93
118
|
if (!groupId || groupId === '0')
|
|
94
119
|
throw imValidationError('id_invalid', 'groupId 必须非 0');
|
|
95
120
|
const content = normalizeContent(args);
|
|
121
|
+
// @ 提及(仅群聊):@具体人必须在群内(prepare 阶段逐个核实),@所有人走服务端固定 id=88
|
|
122
|
+
const atEntries = await resolveAtMentions(args, env, groupId);
|
|
123
|
+
if (atEntries.length && !content.txt) {
|
|
124
|
+
throw imValidationError('at_requires_txt', 'atAll/atUids 仅支持文本消息(txt)');
|
|
125
|
+
}
|
|
126
|
+
// 引用/回复(仅群聊文本):prepare 阶段定位被引消息并组装 ref/rmsg
|
|
127
|
+
const quote = hasQuoteInput(args) ? await resolveQuotedRef(args, env, { groupId }) : null;
|
|
128
|
+
if (quote && !content.txt) {
|
|
129
|
+
throw imValidationError('quote_requires_txt', 'quoteMsgId/replyMsgId 仅支持文本消息(txt)');
|
|
130
|
+
}
|
|
96
131
|
const summary = { action: '发送群聊消息', groupId, hasTxt: Boolean(content.txt), hasImg: Boolean(content.imgPath), hasFile: Boolean(content.filePath) };
|
|
97
132
|
const snapshot = { groupId };
|
|
98
133
|
if (content.txt) {
|
|
99
134
|
snapshot.txt = content.txt;
|
|
100
135
|
summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
|
|
101
136
|
}
|
|
137
|
+
if (atEntries.length) {
|
|
138
|
+
snapshot.atEntries = atEntries;
|
|
139
|
+
summary.at = atEntries.map((e) => (e.t === '2' ? { target: 'ALL', name: e.nm, id: e.id } : { uid: e.id, cid: e.cid, name: e.nm }));
|
|
140
|
+
}
|
|
141
|
+
if (quote) {
|
|
142
|
+
snapshot.quoteKind = quote.kind;
|
|
143
|
+
snapshot.quoteRef = quote.ref;
|
|
144
|
+
summary.quote = { kind: quote.kind, quotedMsgId: quote.quotedMsgId, fromName: quote.fromName, textPreview: quote.text.length > 40 ? `${quote.text.slice(0, 40)}…` : quote.text };
|
|
145
|
+
}
|
|
102
146
|
if (content.imgPath) {
|
|
103
147
|
const abs = assertLocalFile(content.imgPath, 'img');
|
|
104
148
|
snapshot.imgPath = abs;
|
|
@@ -113,8 +157,17 @@ const sendGroupPair = makeWritePair({
|
|
|
113
157
|
},
|
|
114
158
|
async execute(plan, env) {
|
|
115
159
|
const body = { groupId: plan.groupId };
|
|
116
|
-
|
|
160
|
+
const atEntries = Array.isArray(plan.atEntries) ? plan.atEntries : [];
|
|
161
|
+
const quoteRef = plan.quoteRef ?? null;
|
|
162
|
+
if (atEntries.length || quoteRef) {
|
|
163
|
+
// @ / 引用 / 回复:完整原生 msg JSON 走 msg 字段(txt 字段无法携带 aus/ref,实测会被当纯文本展示)
|
|
164
|
+
const { v, aus } = buildAtText(String(plan.txt ?? ''), atEntries);
|
|
165
|
+
const self = await env.host.getSelfIdentity();
|
|
166
|
+
body.msg = buildAtMsgJson(v, aus, self, quoteRef ? { [plan.quoteKind === 'reply' ? 'rmsg' : 'ref']: quoteRef } : {});
|
|
167
|
+
}
|
|
168
|
+
else if (plan.txt !== undefined) {
|
|
117
169
|
body.txt = plan.txt;
|
|
170
|
+
}
|
|
118
171
|
if (plan.imgPath) {
|
|
119
172
|
const fileObj = await env.host.uploadFile(plan.imgPath, { shareGroups: plan.groupId });
|
|
120
173
|
body.img = fileObjToImg(fileObj);
|
|
@@ -516,7 +569,7 @@ const dingSendPair = makeWritePair({
|
|
|
516
569
|
return { id: str(res?.data?.id ?? ''), msg: res?.data?.msg ?? '' };
|
|
517
570
|
},
|
|
518
571
|
});
|
|
519
|
-
function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
|
|
572
|
+
function buildMsgWriteDefinition(name, method, summary, inputProps, pair, condition = {}) {
|
|
520
573
|
const requiredInput = Object.keys(inputProps).filter((k) => {
|
|
521
574
|
const prop = inputProps[k];
|
|
522
575
|
return prop?.required === true;
|
|
@@ -531,7 +584,7 @@ function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
|
|
|
531
584
|
capability: 'msg',
|
|
532
585
|
risk: 'read-before-write',
|
|
533
586
|
requiresConfirmation: true,
|
|
534
|
-
inputSchema: schemaOf(inputProps, { required: requiredInput, description: summary }),
|
|
587
|
+
inputSchema: schemaOf(inputProps, { required: requiredInput, ...condition, description: summary }),
|
|
535
588
|
handler: pair.prepare,
|
|
536
589
|
},
|
|
537
590
|
{
|
|
@@ -552,19 +605,27 @@ function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
|
|
|
552
605
|
];
|
|
553
606
|
}
|
|
554
607
|
const msgWriteOps = [
|
|
555
|
-
...buildMsgWriteDefinition('im.msg.send.single', 'msg.send.single', '
|
|
608
|
+
...buildMsgWriteDefinition('im.msg.send.single', 'msg.send.single', '发送单聊消息(文本/图片/文件,文本支持引用)', {
|
|
556
609
|
toUid: { type: 'string', pattern: '^\\d+$', description: '接收人 uid' },
|
|
557
610
|
toCid: { type: 'string', pattern: '^\\d+$', description: '接收人 cid' },
|
|
558
611
|
txt: { type: 'string', description: '文本内容' },
|
|
612
|
+
quoteMsgId: { type: 'string', pattern: '^\\d+$', description: '引用该消息(单聊仅支持引用,不支持回复;仅 txt 文本消息);传单聊会话内消息 id(ser_msgid),prepare 阶段自动定位并组装 ref' },
|
|
559
613
|
img: { type: 'string', description: '本地图片路径(发送前会上传到 E10 文件服务)' },
|
|
560
614
|
file: { type: 'string', description: '本地文件路径' },
|
|
561
|
-
}, sendSinglePair
|
|
562
|
-
|
|
615
|
+
}, sendSinglePair, {
|
|
616
|
+
required: ['toUid', 'toCid'],
|
|
617
|
+
requiredAny: ['txt', 'img', 'file'],
|
|
618
|
+
}),
|
|
619
|
+
...buildMsgWriteDefinition('im.msg.send.group', 'msg.send.group', '发送群聊消息(文本/图片/文件,文本支持 @ 提及与引用/回复)', {
|
|
563
620
|
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
564
|
-
txt: { type: 'string', description: '
|
|
621
|
+
txt: { type: 'string', description: '文本内容;@ 位置直接写 @<姓名>(如 @张三)或 @全体成员/@所有人' },
|
|
622
|
+
atAll: { type: 'boolean', description: '@全体成员(服务端固定 id=88),仅群聊、仅 txt 文本消息生效' },
|
|
623
|
+
atUids: { type: 'array', items: { type: 'string' }, description: '@具体人 uid 列表,仅群聊;prepare 阶段会逐个核实是否在群内,不在群内则拒绝发送' },
|
|
624
|
+
quoteMsgId: { type: 'string', pattern: '^\\d+$', description: '引用该消息(与 replyMsgId 互斥,仅群聊、仅 txt 文本消息);传消息 id(ser_msgid),prepare 阶段自动定位被引消息并组装 ref' },
|
|
625
|
+
replyMsgId: { type: 'string', pattern: '^\\d+$', description: '回复该消息(与 quoteMsgId 互斥,仅群聊、仅 txt 文本消息);传消息 id(ser_msgid),prepare 阶段自动定位被引消息并组装 rmsg' },
|
|
565
626
|
img: { type: 'string', description: '本地图片路径' },
|
|
566
627
|
file: { type: 'string', description: '本地文件路径' },
|
|
567
|
-
}, sendGroupPair),
|
|
628
|
+
}, sendGroupPair, { required: ['groupId'], requiredAny: ['txt', 'img', 'file'] }),
|
|
568
629
|
...buildMsgWriteDefinition('im.msg.withdraw', 'msg.withdraw', '撤回消息(单聊/群聊,限本人 2 分钟内)', {
|
|
569
630
|
type: { type: 'string', enum: ['single', 'group'], description: 'single 或 group' },
|
|
570
631
|
msgid: { type: 'string', pattern: '^\\d+$', description: '消息 id(ser_msgid)' },
|
|
@@ -588,6 +649,11 @@ const msgWriteOps = [
|
|
|
588
649
|
toUids: { type: 'array', items: { type: 'string' }, description: '指定接收人 uid' },
|
|
589
650
|
sendMsg: { type: 'integer', enum: [0, 1], description: '发短信(默认 1 发)' },
|
|
590
651
|
showType: { type: 'integer', enum: [0, 1], description: '0 全体可见(默认)/1 仅发送人可见' },
|
|
591
|
-
}, dingSendPair
|
|
652
|
+
}, dingSendPair, {
|
|
653
|
+
allOf: [
|
|
654
|
+
{ anyOf: [{ required: ['groupId'] }, { required: ['toUid'] }] },
|
|
655
|
+
{ anyOf: [{ required: ['txt'] }, { required: ['convertMsgid'] }] },
|
|
656
|
+
],
|
|
657
|
+
}),
|
|
592
658
|
];
|
|
593
659
|
export const msgWriteOperations = msgWriteOps;
|