weaver-work-cli 0.1.31 → 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-U766FTFH.js → archive-FVFY6GZN.js} +2 -2
- package/dist/chunks/{archive-oa-DMWEKX3L.js → archive-oa-NW3OD4UV.js} +2 -2
- package/dist/chunks/{asset-TCR7VDEV.js → asset-7QJKMIDV.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
- package/dist/chunks/{blog-EVZWGDVM.js → blog-I7SDPNW4.js} +2 -2
- package/dist/chunks/{caizhilian-JOWS6SL5.js → caizhilian-O37ZIQNY.js} +2 -2
- package/dist/chunks/{calendar-5WNCV66Q.js → calendar-7KXE4ED3.js} +2 -2
- package/dist/chunks/{chunk-VZTXRG6X.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-H3WDBYJW.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-324YPTC2.js → chunk-D223PTXU.js} +11 -2
- package/dist/chunks/{chunk-R2IJAK7T.js → chunk-EEWPQE3O.js} +2 -2
- package/dist/chunks/{chunk-J7RJTZSV.js → chunk-GFPXQIY2.js} +2 -2
- package/dist/chunks/{chunk-AC7KJWC5.js → chunk-HEL5XTYI.js} +12 -6
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
- package/dist/chunks/{chunk-R7O5PUWO.js → chunk-ILMBNVSG.js} +2 -2
- package/dist/chunks/{chunk-6V7ZLF34.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-2DUV44UX.js → chunk-OCU4RLHC.js} +641 -309
- package/dist/chunks/{chunk-65QMVWHS.js → chunk-PRHJGPMA.js} +2 -2
- package/dist/chunks/{chunk-WHD4XMRP.js → chunk-Q6LWB5HX.js} +2 -2
- package/dist/chunks/{chunk-D4LTJSJD.js → chunk-RBGOMK4Y.js} +50 -33
- package/dist/chunks/chunk-VBCFFFGT.js +226 -0
- package/dist/chunks/{ebuilder-form-NJ4YK6CC.js → chunk-WRRJ4GG2.js} +22 -1148
- package/dist/chunks/{chunk-RCGYCOMG.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-MMYN36VA.js → ehr-WFOC3B4D.js} +1174 -20
- package/dist/chunks/{esb-HIWHOU7Q.js → esb-LJZTN663.js} +4 -4
- package/dist/chunks/{fna-KBACQTA5.js → fna-F7QRIIZC.js} +2 -2
- package/dist/chunks/{host-3OHS5QUX.js → host-BCGUVEWX.js} +3 -3
- package/dist/chunks/host-BQBRHVR3.js +23 -0
- package/dist/chunks/{host-KQL5N4HO.js → host-DGE2TZD4.js} +3 -3
- package/dist/chunks/{host-LLSZQCWR.js → host-KXPTEWFS.js} +3 -3
- package/dist/chunks/{host-IG6KEH5Q.js → host-VQ6JJXGQ.js} +3 -3
- package/dist/chunks/{hrm-LYEC67ZQ.js → hrm-NI6XAKW5.js} +3 -3
- package/dist/chunks/{im-RN2UKAEU.js → im-7TRTF3KT.js} +88 -12
- package/dist/chunks/{invoice-EEPHLAF6.js → invoice-AOXWJKYT.js} +3 -3
- package/dist/chunks/{jiuchuanhui-WVVZYSJE.js → jiuchuanhui-7NQWEZAW.js} +2 -2
- package/dist/chunks/{lazy-dispatch-MDOFFL2S.js → lazy-dispatch-RNS4EE4S.js} +4 -4
- package/dist/chunks/{mail-LNQLTF6L.js → mail-VDHJAB4O.js} +3 -3
- package/dist/chunks/{meeting-XSD3BDVQ.js → meeting-GA7XCNNP.js} +214 -43
- package/dist/chunks/{okr-TMWTIU2K.js → okr-TQIRERPQ.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A2FSFA7T.js → plan-4RY4LN2P.js} +2 -2
- package/dist/chunks/{project-T26QLBQF.js → project-D6XT2OC5.js} +2 -2
- package/dist/chunks/{registry-JCFEYXOE.js → registry-JHRIDQ3A.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-ZJMJVWFA.js → registry-QKMI43Q6.js} +8 -6
- package/dist/chunks/{registry-Q7OQJ3SU.js → registry-VOGKFFTR.js} +2 -2
- package/dist/chunks/{room-recommend-UR4ZDZS6.js → room-recommend-D4EUOOWY.js} +4 -3
- package/dist/chunks/{schedule-5L2PM4M6.js → schedule-XPFBHPCV.js} +4 -3
- package/dist/chunks/{workflow-TR6PG2SL.js → workflow-HLBZRWIY.js} +172 -6
- package/dist/core/package-paths.js +16 -2
- 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
|
@@ -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;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { imValidationError } from '../errors.js';
|
|
2
|
+
import { assertImOk, str, unwrapInner } from './shared.js';
|
|
3
|
+
import { parseContent, parseMsg } from './msg-parse.js';
|
|
4
|
+
/** 调用方是否带了引用/回复入参。 */
|
|
5
|
+
export function hasQuoteInput(args) {
|
|
6
|
+
return args.quoteMsgId !== undefined || args.replyMsgId !== undefined;
|
|
7
|
+
}
|
|
8
|
+
/** 解析被引消息 id(纯数字、非 0)。 */
|
|
9
|
+
function quotedId(value) {
|
|
10
|
+
const id = str(value);
|
|
11
|
+
if (!id || !/^\d+$/.test(id) || id === '0') {
|
|
12
|
+
throw imValidationError('quote_id_invalid', 'quoteMsgId/replyMsgId 必须是非 0 的纯数字消息 id');
|
|
13
|
+
}
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
/** 从消息对象提取被引内容纯文本(txt 渲染 / [图片] / [文件]名)。 */
|
|
17
|
+
function quotedContentText(msgObj) {
|
|
18
|
+
if (!msgObj)
|
|
19
|
+
return '';
|
|
20
|
+
const parsed = parseContent(msgObj);
|
|
21
|
+
return str(parsed.content).trim();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* prepare 阶段解析引用/回复入参:定位被引消息并组装 ref/rmsg 对象。
|
|
25
|
+
* - quoteMsgId 与 replyMsgId 互斥(quote_conflict);
|
|
26
|
+
* - 被引消息拉不到/找不到时报 quote_msg_not_found;
|
|
27
|
+
* - 被引人姓名走 hrm(resolveUsersByIds),回落消息内嵌 sname。
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveQuotedRef(args, env, target) {
|
|
30
|
+
const hasQuote = args.quoteMsgId !== undefined && args.quoteMsgId !== null && args.quoteMsgId !== '';
|
|
31
|
+
const hasReply = args.replyMsgId !== undefined && args.replyMsgId !== null && args.replyMsgId !== '';
|
|
32
|
+
if (hasQuote && hasReply) {
|
|
33
|
+
throw imValidationError('quote_conflict', 'quoteMsgId(引用)与 replyMsgId(回复)只能二选一');
|
|
34
|
+
}
|
|
35
|
+
if (!hasQuote && !hasReply)
|
|
36
|
+
return null;
|
|
37
|
+
const kind = hasQuote ? 'quote' : 'reply';
|
|
38
|
+
const msgId = quotedId(hasQuote ? args.quoteMsgId : args.replyMsgId);
|
|
39
|
+
const isSingle = !target.groupId;
|
|
40
|
+
if (isSingle && (!target.toUid || !target.toCid)) {
|
|
41
|
+
throw imValidationError('quote_target_required', '单聊引用需要 toUid + toCid 才能定位被引消息');
|
|
42
|
+
}
|
|
43
|
+
const scopeLabel = isSingle ? `与 ${target.toUid} 的单聊` : `群 ${target.groupId}`;
|
|
44
|
+
let rows = [];
|
|
45
|
+
try {
|
|
46
|
+
// 单条定位:id ± 1 秒(id 为纳秒级时间戳,开边界语义)
|
|
47
|
+
const idBig = BigInt(msgId);
|
|
48
|
+
const window = { start: String(idBig + 1000000000n), end: String(idBig - 1000000000n), max_num: 5 };
|
|
49
|
+
const res = isSingle
|
|
50
|
+
? await env.host.request({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/api/em/msg/executeIm/msg/syncSingleMsg',
|
|
53
|
+
body: { trans_id: 0, dev: 4, enc_type: 0, datas: [{ from_cid: String(target.toCid), from_uid: String(target.toUid), ...window }] },
|
|
54
|
+
operation: 'im.msg.send.single.quoteResolve',
|
|
55
|
+
})
|
|
56
|
+
: await env.host.request({
|
|
57
|
+
method: 'POST',
|
|
58
|
+
path: '/api/em/msg/executeIm/chat/syncGroupMsg',
|
|
59
|
+
body: { trans_id: 0, dev: 4, type: 0, enc_type: 0, datas: [{ group_id: String(target.groupId), ...window }] },
|
|
60
|
+
operation: 'im.msg.send.group.quoteResolve',
|
|
61
|
+
});
|
|
62
|
+
assertImOk(res, '定位被引消息');
|
|
63
|
+
const outer = unwrapInner(res)?.datas || [];
|
|
64
|
+
for (const group of outer)
|
|
65
|
+
rows.push(...(group?.datas || group?.data || []));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw imValidationError('quote_msg_not_found', `无法定位被引消息 ${msgId}:${error instanceof Error ? error.message : String(error)}`, { ...target, quotedMsgId: msgId });
|
|
69
|
+
}
|
|
70
|
+
const row = rows.find((r) => str(r?.ser_msgid) === msgId);
|
|
71
|
+
if (!row) {
|
|
72
|
+
throw imValidationError('quote_msg_not_found', `消息 ${msgId} 不在${scopeLabel}内(或已被撤回),无法引用`, {
|
|
73
|
+
...target,
|
|
74
|
+
quotedMsgId: msgId,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const msgObj = parseMsg(row.msg);
|
|
78
|
+
const fromUid = str(row.from_uid) || str(msgObj?.suid);
|
|
79
|
+
const fromCid = str(row.from_cid) || str(msgObj?.scid);
|
|
80
|
+
let fromName = str(msgObj?.sname);
|
|
81
|
+
if (fromUid) {
|
|
82
|
+
try {
|
|
83
|
+
const personMap = await env.host.resolveUsersByIds([fromUid]);
|
|
84
|
+
fromName = personMap[fromUid]?.name || fromName;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// hrm 失败时回落消息内嵌姓名
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const ref = {
|
|
91
|
+
id: msgId,
|
|
92
|
+
t: str(msgObj?.type ?? row.kind ?? '1') || '1',
|
|
93
|
+
f: '0',
|
|
94
|
+
time: Number(row.msgtime) > 0 ? Number(row.msgtime) * 1000 : Date.now(),
|
|
95
|
+
fname: fromName || fromUid || '',
|
|
96
|
+
fuser: { cid: fromCid, uid: fromUid },
|
|
97
|
+
rcon: quotedContentText(msgObj),
|
|
98
|
+
display: '',
|
|
99
|
+
...(kind === 'reply' ? { oid: msgId } : {}),
|
|
100
|
+
};
|
|
101
|
+
return { kind, quotedMsgId: msgId, fromName: ref.fname, fromUid, text: ref.rcon, ref };
|
|
102
|
+
}
|
|
103
|
+
/** 拉取侧:从消息对象提取顶层引用/回复摘要(msg.msg.ref / msg.msg.rmsg)。 */
|
|
104
|
+
export function extractQuoteSummary(msgObj) {
|
|
105
|
+
const raw = msgObj ? (msgObj.rmsg ?? msgObj.ref) : undefined;
|
|
106
|
+
if (!raw || typeof raw !== 'object')
|
|
107
|
+
return undefined;
|
|
108
|
+
const kind = msgObj?.rmsg ? 'reply' : 'quote';
|
|
109
|
+
return {
|
|
110
|
+
kind,
|
|
111
|
+
id: str(raw.id),
|
|
112
|
+
msgType: str(raw.t),
|
|
113
|
+
fromName: str(raw.fname),
|
|
114
|
+
fromUid: str(raw.fuser?.uid),
|
|
115
|
+
fromCid: str(raw.fuser?.cid),
|
|
116
|
+
msgtime: raw.time,
|
|
117
|
+
text: str(raw.rcon),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -241,7 +241,7 @@ const sysmsgTypeSync = {
|
|
|
241
241
|
inputSchema: schemaOf({
|
|
242
242
|
gids: { type: 'array', items: { type: 'integer' }, description: '系统分组 id(与 tids 至少其一)' },
|
|
243
243
|
tids: { type: 'array', items: { type: 'integer' }, description: '消息类型 id' },
|
|
244
|
-
}, { description: 'gids
|
|
244
|
+
}, { requiredAny: ['gids', 'tids'], description: '按 gids/tids 拉取系统消息分组/类型配置(至少其一)' }),
|
|
245
245
|
handler: async (args, { host }) => {
|
|
246
246
|
const gids = Array.isArray(args.gids) ? args.gids.map((x) => Number(x)).filter((n) => Number.isInteger(n)) : [];
|
|
247
247
|
const tids = Array.isArray(args.tids) ? args.tids.map((x) => Number(x)).filter((n) => Number.isInteger(n)) : [];
|
|
@@ -244,6 +244,16 @@ export function assertImOk(value, actionLabel) {
|
|
|
244
244
|
export function unwrapInner(value) {
|
|
245
245
|
return value?.data?.data ?? value?.data;
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* 服务端在建群/入群时自动补入的「系统成员」cid(群内系统消息的投递载体)。
|
|
249
|
+
* 属产品可选能力(租户侧可关闭),不是真实员工:姓名会回落成 uid,hrm 解析不到。
|
|
250
|
+
* 统计真实成员数、判断「是否只剩自己」时必须排除它。
|
|
251
|
+
*/
|
|
252
|
+
export const SYSTEM_MEMBER_CID = '10002';
|
|
253
|
+
/** 是否服务端系统成员(非真实员工)。 */
|
|
254
|
+
export function isSystemMember(member) {
|
|
255
|
+
return str(member?.cid) === SYSTEM_MEMBER_CID;
|
|
256
|
+
}
|
|
247
257
|
/** 群成员条目判定(成员行至少含 uid 或 user 身份)。 */
|
|
248
258
|
function isMemberRow(value) {
|
|
249
259
|
return Boolean(value) && typeof value === 'object' && (value.uid !== undefined || value.user !== undefined);
|
|
@@ -8,11 +8,24 @@ export function imSuccess(action, data, meta) {
|
|
|
8
8
|
}
|
|
9
9
|
/** 构造「对象类型 + 严格禁止额外字段」的 JSON Schema。 */
|
|
10
10
|
export function schemaOf(properties, options = {}) {
|
|
11
|
+
const clauses = [];
|
|
12
|
+
if (options.requiredAny?.length)
|
|
13
|
+
clauses.push({ anyOf: options.requiredAny.map((key) => ({ required: [key] })) });
|
|
14
|
+
if (options.anyOf?.length)
|
|
15
|
+
clauses.push({ anyOf: options.anyOf });
|
|
16
|
+
if (options.allOf?.length)
|
|
17
|
+
clauses.push(...options.allOf);
|
|
18
|
+
const condition = clauses.length === 1
|
|
19
|
+
? clauses[0]
|
|
20
|
+
: clauses.length > 1
|
|
21
|
+
? { allOf: clauses }
|
|
22
|
+
: {};
|
|
11
23
|
return {
|
|
12
24
|
type: 'object',
|
|
13
25
|
additionalProperties: false,
|
|
14
26
|
properties,
|
|
15
27
|
...(options.required?.length ? { required: options.required } : {}),
|
|
28
|
+
...condition,
|
|
16
29
|
...(options.description ? { description: options.description } : {}),
|
|
17
30
|
};
|
|
18
31
|
}
|
package/dist/shortcuts/index.js
CHANGED
|
@@ -35,6 +35,11 @@ export const builtinShortcuts = [
|
|
|
35
35
|
description: '泛微E10聚才林(人事/出勤/薪酬)CLI',
|
|
36
36
|
load: async () => (await import('./ehr/index.js')).ehrShortcut,
|
|
37
37
|
}),
|
|
38
|
+
lazyShortcut({
|
|
39
|
+
name: 'cowork',
|
|
40
|
+
description: '泛微E10协作区(协作事项/讨论/审批/共享权限)CLI',
|
|
41
|
+
load: async () => (await import('./cowork/index.js')).coworkShortcut,
|
|
42
|
+
}),
|
|
38
43
|
lazyShortcut({
|
|
39
44
|
name: 'archive',
|
|
40
45
|
description: 'E10 文书定档案管理 CLI',
|
|
@@ -96,7 +96,10 @@ function mailMessage(value) {
|
|
|
96
96
|
export function createMailHost(ctx, dependencies = {}) {
|
|
97
97
|
let e10Session = null;
|
|
98
98
|
let sessionContext = null;
|
|
99
|
-
|
|
99
|
+
// 与 workflow 模块同口径(2026-10-29 实测):auth 文件里已有登录时落盘的 userId,
|
|
100
|
+
// 业务命令不需要 teamsCheck 的登录人信息富化 ⇒ 跳过它,省掉每次调用一次串行 HTTPS 往返(实测约 150~250ms)。
|
|
101
|
+
const requireSession = dependencies.requireSession
|
|
102
|
+
|| ((innerCtx) => requireE10Session(innerCtx, { skipProfileEnrichment: true }));
|
|
100
103
|
async function ensureSession() {
|
|
101
104
|
if (!e10Session) {
|
|
102
105
|
e10Session = await requireSession(ctx);
|