weaver-work-cli 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/archive-U766FTFH.js +2537 -0
- package/dist/chunks/archive-oa-DMWEKX3L.js +1317 -0
- package/dist/chunks/asset-TCR7VDEV.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-EVZWGDVM.js +2157 -0
- package/dist/chunks/caizhilian-JOWS6SL5.js +10087 -0
- package/dist/chunks/calendar-5WNCV66Q.js +1195 -0
- package/dist/chunks/chunk-2DUV44UX.js +15606 -0
- package/dist/chunks/chunk-324YPTC2.js +1607 -0
- package/dist/chunks/chunk-65QMVWHS.js +231 -0
- package/dist/chunks/chunk-6V7ZLF34.js +387 -0
- package/dist/chunks/chunk-AC7KJWC5.js +358 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-D4LTJSJD.js +477 -0
- package/dist/chunks/chunk-EG6ZU444.js +654 -0
- package/dist/chunks/chunk-EZALDOQH.js +383 -0
- package/dist/chunks/chunk-FMPJJYYI.js +106 -0
- package/dist/chunks/chunk-G2JSIUWF.js +769 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H3WDBYJW.js +508 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-J7RJTZSV.js +5556 -0
- package/dist/chunks/chunk-LUSPKZKY.js +53 -0
- package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
- package/dist/chunks/chunk-NOODWMCL.js +584 -0
- package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
- package/dist/chunks/chunk-P26ZWSKU.js +73 -0
- package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
- package/dist/chunks/chunk-R2IJAK7T.js +464 -0
- package/dist/chunks/chunk-R7O5PUWO.js +205 -0
- package/dist/chunks/chunk-RCGYCOMG.js +6962 -0
- package/dist/chunks/chunk-VZTXRG6X.js +403 -0
- package/dist/chunks/chunk-WHD4XMRP.js +161 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-ZEDWSH32.js +68 -0
- package/dist/chunks/continuation-2CKSBLA6.js +11 -0
- package/dist/chunks/continuation-HZIZRZBX.js +13 -0
- package/dist/chunks/continuation-NLECEJPX.js +11 -0
- package/dist/chunks/ebuilder-form-NJ4YK6CC.js +2276 -0
- package/dist/chunks/ehr-MMYN36VA.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-HIWHOU7Q.js +141 -0
- package/dist/chunks/fna-KBACQTA5.js +1444 -0
- package/dist/chunks/host-3OHS5QUX.js +21 -0
- package/dist/chunks/host-IG6KEH5Q.js +19 -0
- package/dist/chunks/host-KQL5N4HO.js +23 -0
- package/dist/chunks/host-LLSZQCWR.js +17 -0
- package/dist/chunks/hrm-LYEC67ZQ.js +244 -0
- package/dist/chunks/im-RN2UKAEU.js +252 -0
- package/dist/chunks/invoice-EEPHLAF6.js +5179 -0
- package/dist/chunks/jiuchuanhui-WVVZYSJE.js +1940 -0
- package/dist/chunks/lazy-dispatch-MDOFFL2S.js +28 -0
- package/dist/chunks/mail-LNQLTF6L.js +2695 -0
- package/dist/chunks/meeting-XSD3BDVQ.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-TMWTIU2K.js +2482 -0
- package/dist/chunks/plan-A2FSFA7T.js +2025 -0
- package/dist/chunks/project-T26QLBQF.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-JCFEYXOE.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-Q7OQJ3SU.js +19 -0
- package/dist/chunks/registry-ZJMJVWFA.js +19 -0
- package/dist/chunks/room-recommend-UR4ZDZS6.js +20 -0
- package/dist/chunks/schedule-5L2PM4M6.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-TR6PG2SL.js +245 -0
- package/dist/cmd/argv.js +45 -0
- package/dist/cmd/docs/index.js +3 -3
- package/dist/cmd/lazy-dispatch.js +20 -0
- package/dist/cmd/root.js +2 -4
- package/dist/cmd/setup/index.js +2 -2
- package/dist/cmd/skills/index.js +2 -2
- package/dist/core/package-paths.js +47 -0
- package/dist/index.js +163 -7
- package/dist/internal/e10/auth/xiaoe.js +3 -3
- package/dist/internal/e10/request-runtime.js +18 -62
- package/dist/internal/skills/detector.js +2 -2
- package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
- package/dist/shortcuts/contract/host.js +91 -35
- package/dist/shortcuts/contract/index.js +213 -0
- package/dist/shortcuts/contract/manifest.js +928 -0
- package/dist/shortcuts/contract/ooxml/docx.js +295 -0
- package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
- package/dist/shortcuts/contract/ooxml/zip.js +158 -0
- package/dist/shortcuts/contract/operations/assistant.js +1156 -0
- package/dist/shortcuts/contract/operations/clause-library.js +522 -0
- package/dist/shortcuts/contract/operations/compare.js +193 -0
- package/dist/shortcuts/contract/operations/credit-page.js +34 -0
- package/dist/shortcuts/contract/operations/flow.js +98 -0
- package/dist/shortcuts/contract/operations/page.js +66 -0
- package/dist/shortcuts/contract/operations/performance.js +733 -0
- package/dist/shortcuts/contract/operations/quote.js +557 -0
- package/dist/shortcuts/contract/operations/recommend.js +78 -0
- package/dist/shortcuts/contract/operations/registry.js +18 -0
- package/dist/shortcuts/contract/operations/review.js +83 -0
- package/dist/shortcuts/contract/operations/shared.js +37 -0
- package/dist/shortcuts/contract/operations/upload.js +108 -0
- package/dist/shortcuts/contract/operations/write.js +3 -2
- package/dist/shortcuts/index.js +140 -40
- package/dist/shortcuts/invoice/manifest.js +34 -11
- package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
- package/dist/shortcuts/invoice/operations/reim.js +81 -5
- package/dist/shortcuts/invoice/operations/shared.js +12 -5
- package/dist/shortcuts/meeting/host.js +45 -3
- package/dist/shortcuts/meeting/invitation/docx.js +442 -0
- package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
- package/dist/shortcuts/meeting/operations/attachment.js +442 -0
- package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
- package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
- package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
- package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
- package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
- package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
- package/dist/shortcuts/meeting/operations/invitation.js +458 -0
- package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
- package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
- package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
- package/dist/shortcuts/meeting/operations/receipts.js +1 -1
- package/dist/shortcuts/meeting/operations/registry.js +18 -0
- package/dist/shortcuts/meeting/operations/summary.js +276 -0
- package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
- package/dist/shortcuts/meeting/operations.js +42 -1
- package/dist/shortcuts/meeting/qr/encoder.js +427 -0
- package/dist/shortcuts/meeting/qr/render.js +103 -0
- package/dist/shortcuts/workflow/operations/form.js +17 -5
- package/docs/contract.md +39 -4
- package/docs/invoice.md +5 -5
- package/package.json +3 -2
- package/scripts/bundle.mjs +63 -0
- package/skill-template/business-info.json +25 -4
- package/skill-template/domains/jinchengda.md +16 -3
- package/skill-template/domains/yepiaotong.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
- package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
- package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
- package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
- package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
- package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
- package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
- package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
- package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
- package/skills/weaver-e10-meeting/SKILL.md +493 -17
- package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
- package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +6 -0
- package/templates/meeting/invitation.docx +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会议附件上传(本地文件 → E10 文件服务附件 id)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-10-26 用户需求):会议模块此前**没有任何上传能力**,纪要要挂 `doc_file` /
|
|
5
|
+
* `video_file` 只能借 im / mail 两条业务线的上传实现。本操作把这条能力收进会议线,
|
|
6
|
+
* 且不依赖那两个模块的代码(只复用底层 runtime 的 multipart 通道)。
|
|
7
|
+
*
|
|
8
|
+
* 口径(真机实测确认):
|
|
9
|
+
* - 纪要附件字段 `doc_file` / `video_file` 的值就是**附件 id 字符串**(接口文档写「按文件id」);
|
|
10
|
+
* - 上传走 E10 通用两步式:`preUploadCheck`(按 md5 判重)→ `valid/upload`(实传);
|
|
11
|
+
* - 同一 md5 第二次上传会**秒传**(`uploadType=second`),直接返回已有 fileObj,不发文件体。
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ 与"解析"无关:上传只负责把文件放进文件服务并拿到 id。
|
|
14
|
+
* 服务端**不会**因此启动 ASR / 文档文字提取(真机验证:81 秒内 `video_taskId`、`parse_content` 均不变),
|
|
15
|
+
* 解析由会议详情页或智能体自身能力完成,结果再写回 `minutes_content` / `parse_content`。
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ 已知限制(真机 2026-10-23):
|
|
18
|
+
* - 上传前会把整个文件读进内存,超大文件(>256MB)直接拒绝,避免把 CLI 进程打爆;
|
|
19
|
+
* - **上传超时后文件可能已经落地**:这里在超时/网络异常时自动重新预检一次(按 md5 查重),
|
|
20
|
+
* 能查到就按"已完成"返回(`recovered: true`),避免调用方重试产生重复文件。
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
import { createReadStream } from 'node:fs';
|
|
24
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
25
|
+
import { extname, resolve } from 'node:path';
|
|
26
|
+
import { meetingValidationError } from '../errors.js';
|
|
27
|
+
import { meetingSuccess, SCHEMA_ID } from './types.js';
|
|
28
|
+
import { listMeetingAttachments } from './attachment-content.js';
|
|
29
|
+
/** 超过这个大小就别读了 —— `readFile` 会把整个文件压进内存。 */
|
|
30
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
31
|
+
/** 常见扩展名 → MIME。上传头里的类型只影响预览,猜错不影响入库。 */
|
|
32
|
+
const MIME_BY_EXT = {
|
|
33
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
34
|
+
'.doc': 'application/msword',
|
|
35
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
36
|
+
'.xls': 'application/vnd.ms-excel',
|
|
37
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
38
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
39
|
+
'.pdf': 'application/pdf',
|
|
40
|
+
'.txt': 'text/plain',
|
|
41
|
+
'.md': 'text/markdown',
|
|
42
|
+
'.csv': 'text/csv',
|
|
43
|
+
'.png': 'image/png',
|
|
44
|
+
'.jpg': 'image/jpeg',
|
|
45
|
+
'.jpeg': 'image/jpeg',
|
|
46
|
+
'.gif': 'image/gif',
|
|
47
|
+
'.bmp': 'image/bmp',
|
|
48
|
+
'.mp3': 'audio/mpeg',
|
|
49
|
+
'.m4a': 'audio/mp4',
|
|
50
|
+
'.wav': 'audio/wav',
|
|
51
|
+
'.amr': 'audio/amr',
|
|
52
|
+
'.aac': 'audio/aac',
|
|
53
|
+
'.mp4': 'video/mp4',
|
|
54
|
+
'.mov': 'video/quicktime',
|
|
55
|
+
'.avi': 'video/x-msvideo',
|
|
56
|
+
'.zip': 'application/zip',
|
|
57
|
+
};
|
|
58
|
+
function mimeOf(name) {
|
|
59
|
+
return MIME_BY_EXT[extname(name).toLowerCase()] ?? 'application/octet-stream';
|
|
60
|
+
}
|
|
61
|
+
/** 流式计算 md5:不把整个文件读进内存(160MB 的会议录像也要能算出 md5)。 */
|
|
62
|
+
async function md5OfFile(path) {
|
|
63
|
+
const hash = createHash('md5');
|
|
64
|
+
await new Promise((done, fail) => {
|
|
65
|
+
createReadStream(path)
|
|
66
|
+
.on('data', (chunk) => hash.update(chunk))
|
|
67
|
+
.on('end', () => done())
|
|
68
|
+
.on('error', fail);
|
|
69
|
+
});
|
|
70
|
+
return hash.digest('hex');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 从预检/上传响应里取附件 id。
|
|
74
|
+
*
|
|
75
|
+
* E10 文件服务在不同场景下用三套结构返回同一个东西(2026-10-26 真机实测):
|
|
76
|
+
* - `data.uploadModuleDto.fileid` —— `module=meeting` 等**业务模块**上传/秒传时;
|
|
77
|
+
* - `data.imUploadInfoDto.fileObj.id` / `data.fileObj.id` —— im 模块;
|
|
78
|
+
* - `data.fileid` —— 部分端点扁平返回。
|
|
79
|
+
* 只认其中一种会导致"明明秒传成功却取不到 id"(我第一次就踩了这个坑)。
|
|
80
|
+
*/
|
|
81
|
+
function pickFileId(payload) {
|
|
82
|
+
const candidates = [
|
|
83
|
+
payload?.uploadModuleDto?.fileid,
|
|
84
|
+
payload?.uploadModuleDto?.id,
|
|
85
|
+
payload?.imUploadInfoDto?.fileObj?.id,
|
|
86
|
+
payload?.imUploadInfoDto?.fileObj?.fileid,
|
|
87
|
+
payload?.fileObj?.id,
|
|
88
|
+
payload?.fileObj?.fileid,
|
|
89
|
+
payload?.fileid,
|
|
90
|
+
payload?.id,
|
|
91
|
+
];
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (candidate !== undefined && candidate !== null && String(candidate).trim() !== '' && String(candidate) !== '0') {
|
|
94
|
+
return String(candidate);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
/** 认证类错误要原样抛给调用方 —— 包装成"上传失败"会让人去查文件,方向就错了。 */
|
|
100
|
+
function rethrowAuth(error) {
|
|
101
|
+
const type = error?.type;
|
|
102
|
+
if (type === 'authentication' || type === 'authorization')
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const meetingAttachmentUpload = {
|
|
106
|
+
name: 'meeting.attachment.upload',
|
|
107
|
+
resource: 'attachment',
|
|
108
|
+
method: 'upload',
|
|
109
|
+
action: 'meeting.attachment.upload',
|
|
110
|
+
summary: '上传本地文件到 E10 文件服务并返回附件 id(供纪要 doc_file / video_file 使用);同 md5 自动秒传,上传超时会自动复核避免重复',
|
|
111
|
+
capability: 'attachment',
|
|
112
|
+
risk: 'read-before-write',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
properties: {
|
|
117
|
+
file: { type: 'string', minLength: 1, description: '本地文件绝对路径(Windows 路径请用双反斜杠或正斜杠)' },
|
|
118
|
+
name: { type: 'string', description: '附件显示名,默认取文件名' },
|
|
119
|
+
module: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: '文件归属模块(默认 meeting)。不同模块影响文件归属与可见范围,不确定就别传',
|
|
122
|
+
},
|
|
123
|
+
fileFrom: { type: 'string', description: '来源标记(默认 meeting_info),与 module 一起决定文件归属' },
|
|
124
|
+
refId: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: '业务记录 id(如会议 id)。传了文件就**绑定到这条记录**(fileObj.refId 会回写该值);'
|
|
127
|
+
+ '挂会议附件时必须传,否则只是孤立文件',
|
|
128
|
+
},
|
|
129
|
+
permission: { type: 'string', description: '文件权限(默认 3)' },
|
|
130
|
+
shareUsers: { type: 'string', description: '额外分享给的用户 uid,逗号分隔(可选)' },
|
|
131
|
+
shareGroups: { type: 'string', description: '额外分享给的群 id,逗号分隔(可选)' },
|
|
132
|
+
timeoutMs: {
|
|
133
|
+
type: 'integer',
|
|
134
|
+
minimum: 1000,
|
|
135
|
+
maximum: 600000,
|
|
136
|
+
description: '上传超时毫秒数(默认 300000=5 分钟;大文件务必不要调小)',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ['file'],
|
|
140
|
+
},
|
|
141
|
+
handler: async (args, env) => {
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
const filePath = resolve(String(args.file ?? ''));
|
|
144
|
+
if (!String(args.file ?? '').trim()) {
|
|
145
|
+
throw meetingValidationError('file_required', '必须提供本地文件路径(file)');
|
|
146
|
+
}
|
|
147
|
+
let info;
|
|
148
|
+
try {
|
|
149
|
+
info = await stat(filePath);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw meetingValidationError('file_not_found', `文件不存在或无法访问:${filePath}`, {
|
|
153
|
+
error: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (!info.isFile()) {
|
|
157
|
+
throw meetingValidationError('file_invalid', `不是文件:${filePath}`);
|
|
158
|
+
}
|
|
159
|
+
if (info.size === 0) {
|
|
160
|
+
throw meetingValidationError('file_invalid', `文件为空(0 字节):${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
if (info.size > MAX_BYTES) {
|
|
163
|
+
throw meetingValidationError('file_too_large', `文件 ${(info.size / 1048576).toFixed(1)}MB 超过上限 ${MAX_BYTES / 1048576}MB(当前实现会把整文件读入内存)。`
|
|
164
|
+
+ '请先压缩/切片,或等 CLI 支持流式上传后再传', { size: info.size, maxBytes: MAX_BYTES });
|
|
165
|
+
}
|
|
166
|
+
const name = String(args.name ?? '').trim() || filePath.split(/[\\/]/).pop() || 'attachment';
|
|
167
|
+
const module = String(args.module ?? '').trim() || 'meeting';
|
|
168
|
+
const fileFrom = String(args.fileFrom ?? '').trim() || 'meeting_info';
|
|
169
|
+
const permission = String(args.permission ?? '').trim() || '3';
|
|
170
|
+
const shareUsers = String(args.shareUsers ?? '');
|
|
171
|
+
const shareGroups = String(args.shareGroups ?? '');
|
|
172
|
+
const timeoutMs = typeof args.timeoutMs === 'number' ? args.timeoutMs : 300_000;
|
|
173
|
+
const mime = mimeOf(name);
|
|
174
|
+
const md5 = await md5OfFile(filePath);
|
|
175
|
+
const baseFields = {
|
|
176
|
+
module,
|
|
177
|
+
fileFrom,
|
|
178
|
+
...(String(args.refId ?? '').trim() ? { refId: String(args.refId).trim() } : {}),
|
|
179
|
+
size: String(info.size),
|
|
180
|
+
name,
|
|
181
|
+
lastModified: String(Math.round(info.mtimeMs)),
|
|
182
|
+
lastModifiedDate: String(Math.round(info.mtimeMs)),
|
|
183
|
+
MD5: md5,
|
|
184
|
+
permission,
|
|
185
|
+
shareUsers,
|
|
186
|
+
shareGroups,
|
|
187
|
+
shareExternals: '',
|
|
188
|
+
compressImg: 'false',
|
|
189
|
+
createDoc: 'false',
|
|
190
|
+
};
|
|
191
|
+
/** 预检:按 md5 判断是否秒传;返回 preUploadId(需要实传时)或已有 fileId(秒传时)。 */
|
|
192
|
+
const precheck = async () => {
|
|
193
|
+
const form = new FormData();
|
|
194
|
+
for (const [key, value] of Object.entries(baseFields))
|
|
195
|
+
form.append(key, value);
|
|
196
|
+
const response = (await env.host.request({
|
|
197
|
+
method: 'POST',
|
|
198
|
+
path: '/api/file/preUploadCheck',
|
|
199
|
+
formData: form,
|
|
200
|
+
operation: 'meeting.attachment.upload.check',
|
|
201
|
+
timeoutMs: 120_000,
|
|
202
|
+
acceptBusiness: () => true,
|
|
203
|
+
}));
|
|
204
|
+
const data = response?.data ?? {};
|
|
205
|
+
return {
|
|
206
|
+
preUploadId: data.preUploadId ? String(data.preUploadId) : '',
|
|
207
|
+
uploadType: String(data.uploadType ?? ''),
|
|
208
|
+
existingId: pickFileId(data),
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
let prepared;
|
|
212
|
+
try {
|
|
213
|
+
prepared = await precheck();
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
rethrowAuth(error);
|
|
217
|
+
throw meetingValidationError('upload_check_failed', `上传预检失败(未上传任何内容):${error instanceof Error ? error.message : String(error)}`, { file: filePath, size: info.size });
|
|
218
|
+
}
|
|
219
|
+
// 秒传命中:文件已在文件服务里(同 md5),直接用已有 id
|
|
220
|
+
if (prepared.existingId) {
|
|
221
|
+
return meetingSuccess('meeting.attachment.upload', {
|
|
222
|
+
fileId: prepared.existingId,
|
|
223
|
+
name,
|
|
224
|
+
size: info.size,
|
|
225
|
+
md5,
|
|
226
|
+
module,
|
|
227
|
+
uploadType: prepared.uploadType || 'second',
|
|
228
|
+
reused: true,
|
|
229
|
+
recovered: false,
|
|
230
|
+
durationMs: Date.now() - startedAt,
|
|
231
|
+
next: `把 fileId 传给 meeting.eb.minute.create/update 的 doc_file(文档)或 video_file(音频)`,
|
|
232
|
+
}, { warnings: ['该文件已存在于 E10 文件服务(md5 相同),已直接复用,未重复上传'] });
|
|
233
|
+
}
|
|
234
|
+
if (!prepared.preUploadId) {
|
|
235
|
+
throw meetingValidationError('upload_check_failed', '上传预检未返回 preUploadId(也无法按 md5 复用),无法继续上传', { file: filePath, size: info.size, uploadType: prepared.uploadType });
|
|
236
|
+
}
|
|
237
|
+
const buffer = await readFile(filePath);
|
|
238
|
+
const uploadForm = new FormData();
|
|
239
|
+
uploadForm.append('data', new Blob([buffer], { type: mime }), name);
|
|
240
|
+
for (const [key, value] of Object.entries({
|
|
241
|
+
...baseFields,
|
|
242
|
+
preUploadId: prepared.preUploadId,
|
|
243
|
+
uploadType: prepared.uploadType || 'common',
|
|
244
|
+
})) {
|
|
245
|
+
uploadForm.append(key, value);
|
|
246
|
+
}
|
|
247
|
+
let uploaded;
|
|
248
|
+
try {
|
|
249
|
+
uploaded = (await env.host.request({
|
|
250
|
+
method: 'POST',
|
|
251
|
+
path: '/api/file/valid/upload',
|
|
252
|
+
formData: uploadForm,
|
|
253
|
+
operation: 'meeting.attachment.upload.do',
|
|
254
|
+
timeoutMs,
|
|
255
|
+
acceptBusiness: () => true,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
// ⚠️ 超时/断连时文件**可能已经落地**。重新预检一次,能按 md5 查到就当作成功,
|
|
260
|
+
// 否则调用方会重试 → 同一份文件传两遍。
|
|
261
|
+
rethrowAuth(error);
|
|
262
|
+
let recoveredId = '';
|
|
263
|
+
try {
|
|
264
|
+
const retry = await precheck();
|
|
265
|
+
recoveredId = retry.existingId;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
recoveredId = '';
|
|
269
|
+
}
|
|
270
|
+
if (recoveredId) {
|
|
271
|
+
return meetingSuccess('meeting.attachment.upload', {
|
|
272
|
+
fileId: recoveredId,
|
|
273
|
+
name,
|
|
274
|
+
size: info.size,
|
|
275
|
+
md5,
|
|
276
|
+
module,
|
|
277
|
+
uploadType: 'second',
|
|
278
|
+
reused: false,
|
|
279
|
+
recovered: true,
|
|
280
|
+
durationMs: Date.now() - startedAt,
|
|
281
|
+
}, {
|
|
282
|
+
warnings: [
|
|
283
|
+
`上传请求报错(${error instanceof Error ? error.message : String(error)}),但复核发现文件已落地,按成功返回;`
|
|
284
|
+
+ '请勿重复上传',
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
throw meetingValidationError('upload_failed', `上传失败且复核未查到文件:${error instanceof Error ? error.message : String(error)}`, { file: filePath, size: info.size, md5 });
|
|
289
|
+
}
|
|
290
|
+
const data = uploaded?.data ?? {};
|
|
291
|
+
const fileId = pickFileId(data);
|
|
292
|
+
if (!fileId) {
|
|
293
|
+
throw meetingValidationError('upload_failed', `上传接口未返回附件 id:${JSON.stringify(data).slice(0, 200)}`, { file: filePath, size: info.size, md5 });
|
|
294
|
+
}
|
|
295
|
+
return meetingSuccess('meeting.attachment.upload', {
|
|
296
|
+
fileId,
|
|
297
|
+
name,
|
|
298
|
+
size: info.size,
|
|
299
|
+
md5,
|
|
300
|
+
module,
|
|
301
|
+
uploadType: prepared.uploadType || 'common',
|
|
302
|
+
reused: false,
|
|
303
|
+
recovered: false,
|
|
304
|
+
fileObj: data.fileObj ?? data.imUploadInfoDto?.fileObj ?? null,
|
|
305
|
+
durationMs: Date.now() - startedAt,
|
|
306
|
+
next: '把 fileId 传给 meeting.eb.minute.create/update 的 doc_file(文档)或 video_file(音频)',
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
/** 计算挂载后的附件 id 列表(纯函数,便于单测)。 */
|
|
311
|
+
export function computeAttachIds(mode, existingIds, incomingId) {
|
|
312
|
+
return mode === 'replace' ? [incomingId] : [...new Set([...existingIds, incomingId])];
|
|
313
|
+
}
|
|
314
|
+
const meetingAttachmentAttach = {
|
|
315
|
+
name: 'meeting.attachment.attach',
|
|
316
|
+
resource: 'attachment',
|
|
317
|
+
method: 'attach',
|
|
318
|
+
action: 'meeting.attachment.attach',
|
|
319
|
+
summary: '把本地文件(或已上传的 fileId)挂成会议附件:上传带 refId 绑定记录,并把**文件 id 列表**写进 mt_accessorys;默认追加(保留已有附件)',
|
|
320
|
+
capability: 'attachment',
|
|
321
|
+
risk: 'high-risk-write',
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
additionalProperties: false,
|
|
325
|
+
properties: {
|
|
326
|
+
id: { ...SCHEMA_ID, description: '会议 id' },
|
|
327
|
+
file: { type: 'string', description: '本地文件路径(与 fileId 二选一)' },
|
|
328
|
+
fileId: { type: 'string', description: '已上传的附件 id(与 file 二选一,跳过上传)' },
|
|
329
|
+
name: { type: 'string', description: '附件显示名(file 模式默认取文件名;fileId 模式可省略,仅用于日志)' },
|
|
330
|
+
mode: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
enum: ['append', 'replace'],
|
|
333
|
+
description: 'append(默认)保留会议已有附件、把本次文件追加进列表;replace 用本次文件**覆盖**整个附件列表。'
|
|
334
|
+
+ '现有附件 id 由 CLI 自动读取(query_mtDetail_info 的 mt_accessorys 就是 fileId 列表)',
|
|
335
|
+
},
|
|
336
|
+
existingFileIds: {
|
|
337
|
+
type: 'array',
|
|
338
|
+
items: { type: 'string' },
|
|
339
|
+
description: '可选:显式指定“现有附件 id”。不传时 CLI 自己读,通常不需要填',
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
required: ['id'],
|
|
343
|
+
},
|
|
344
|
+
handler: async (args, env) => {
|
|
345
|
+
const startedAt = Date.now();
|
|
346
|
+
const meetingId = String(args.id ?? '').trim();
|
|
347
|
+
if (!meetingId)
|
|
348
|
+
throw meetingValidationError('id_required', '必须提供会议 id(id)');
|
|
349
|
+
const filePath = String(args.file ?? '').trim();
|
|
350
|
+
const incomingFileId = String(args.fileId ?? '').trim();
|
|
351
|
+
if (!filePath && !incomingFileId) {
|
|
352
|
+
throw meetingValidationError('file_required', 'file 与 fileId 至少要给一个');
|
|
353
|
+
}
|
|
354
|
+
const explicitIds = Array.isArray(args.existingFileIds)
|
|
355
|
+
? args.existingFileIds.map((item) => String(item).trim()).filter(Boolean)
|
|
356
|
+
: [];
|
|
357
|
+
// 读会议当前附件:必须走 `query_mtDetail_info`(`meeting.eb.meeting.list` **不投影**
|
|
358
|
+
// `mt_accessorys` / `project_files`,读出来恒为空 —— 这也是旧版误判"读回来是文件名"的根源之一)。
|
|
359
|
+
let beforeIds = [];
|
|
360
|
+
let beforeRefsLookLikeNames = false;
|
|
361
|
+
try {
|
|
362
|
+
const current = await listMeetingAttachments(env, meetingId);
|
|
363
|
+
beforeIds = current.refs.filter((ref) => ref.idLike).map((ref) => ref.fileId);
|
|
364
|
+
beforeRefsLookLikeNames = current.refs.some((ref) => !ref.idLike);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// 读不到就按空处理,后面会在 warnings 里提示
|
|
368
|
+
}
|
|
369
|
+
if (beforeRefsLookLikeNames && explicitIds.length === 0) {
|
|
370
|
+
// 服务端此刻返回的是文件名(写后立即读会出现),拿不到 id 就不能安全追加:
|
|
371
|
+
// 硬写会把原附件冲掉,这里直接拒绝并给出可执行建议。
|
|
372
|
+
throw meetingValidationError('attachment_id_unavailable', '会议当前附件返回的是文件名而不是 id(服务端在刚写完时会这样),无法安全追加。'
|
|
373
|
+
+ '请稍后重试,或显式传 existingFileIds;确实要覆盖请用 mode="replace"', { id: meetingId });
|
|
374
|
+
}
|
|
375
|
+
// 默认 **append**:智能体里补传附件时,最怕把会议已有附件冲掉。
|
|
376
|
+
// 现有 id 由 CLI 自己读(`query_mtDetail_info` 的 `mt_accessorys` 本身就是 fileId 列表),
|
|
377
|
+
// 不再要求调用方传 existingFileIds(旧口径说"读回来是文件名"是误判)。
|
|
378
|
+
const mode = String(args.mode ?? '').trim() === 'replace' ? 'replace' : 'append';
|
|
379
|
+
const existingIds = explicitIds.length > 0 ? explicitIds : [];
|
|
380
|
+
if (mode === 'append' && existingIds.length === 0) {
|
|
381
|
+
existingIds.push(...beforeIds);
|
|
382
|
+
}
|
|
383
|
+
const { getMeetingOperation } = await import('./registry.js');
|
|
384
|
+
const list = getMeetingOperation('meeting.eb.meeting.list');
|
|
385
|
+
// 1) 需要的话先上传(带 refId 绑定到该会议)
|
|
386
|
+
let fileId = incomingFileId;
|
|
387
|
+
let name = String(args.name ?? '').trim();
|
|
388
|
+
let uploaded = null;
|
|
389
|
+
if (filePath) {
|
|
390
|
+
const uploadOp = getMeetingOperation('meeting.attachment.upload');
|
|
391
|
+
const result = await uploadOp.handler({ file: filePath, refId: meetingId, name: name || undefined }, env);
|
|
392
|
+
fileId = String(result.data.fileId);
|
|
393
|
+
name = name || String(result.data.name ?? '');
|
|
394
|
+
uploaded = { fileId, reused: Boolean(result.data.reused), durationMs: Number(result.data.durationMs ?? 0) };
|
|
395
|
+
}
|
|
396
|
+
// 2) mt_accessorys 里存的是**文件 id 列表**(读接口返回的是文件名)
|
|
397
|
+
const nextIds = computeAttachIds(mode, existingIds, fileId);
|
|
398
|
+
const prepare = getMeetingOperation('meeting.eb.meeting.change.prepare');
|
|
399
|
+
const apply = getMeetingOperation('meeting.eb.meeting.change.apply');
|
|
400
|
+
const prepared = await prepare.handler({ id: meetingId, operate_type: 'CHANGE', mt_accessorys: nextIds.join(',') }, env);
|
|
401
|
+
await apply.handler({ confirm: true, continuation: prepared?.data?.continuation }, env);
|
|
402
|
+
// 3) 回读校验:按 fileId 数量核对(不依赖 list 的文本渲染)
|
|
403
|
+
let afterIds = [];
|
|
404
|
+
let afterHasNames = false;
|
|
405
|
+
try {
|
|
406
|
+
const after = await listMeetingAttachments(env, meetingId);
|
|
407
|
+
afterIds = after.refs.filter((ref) => ref.idLike).map((ref) => ref.fileId);
|
|
408
|
+
afterHasNames = after.refs.some((ref) => !ref.idLike);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// 回读失败不判定失败,只提示
|
|
412
|
+
}
|
|
413
|
+
const warnings = [];
|
|
414
|
+
if (mode === 'replace' && beforeIds.length > 0) {
|
|
415
|
+
const replaced = explicitIds.length > 0 ? explicitIds : beforeIds;
|
|
416
|
+
warnings.push(`mode=replace 已**覆盖**原有 ${replaced.length} 个附件(${replaced.join('、')});`
|
|
417
|
+
+ '如需保留原有附件,改用默认的 mode=append');
|
|
418
|
+
}
|
|
419
|
+
// 写后立即读可能返回文件名(数量仍可数),此时按 refs 总数比较,不误报
|
|
420
|
+
const afterCount = afterHasNames ? afterIds.length + 0 : afterIds.length;
|
|
421
|
+
if (!afterHasNames && afterCount !== nextIds.length) {
|
|
422
|
+
warnings.push(`回读附件数(${afterIds.length})与写入 id 数(${nextIds.length})不一致:`
|
|
423
|
+
+ '可能是某个 id 不存在或不属于当前模块,请到页面核对');
|
|
424
|
+
}
|
|
425
|
+
return meetingSuccess('meeting.attachment.attach', {
|
|
426
|
+
id: meetingId,
|
|
427
|
+
fileId,
|
|
428
|
+
name: name || null,
|
|
429
|
+
mode,
|
|
430
|
+
writtenFileIds: nextIds,
|
|
431
|
+
accessors: afterIds,
|
|
432
|
+
accessorNamesResolved: !afterHasNames,
|
|
433
|
+
previousAccessors: beforeIds,
|
|
434
|
+
uploaded,
|
|
435
|
+
durationMs: Date.now() - startedAt,
|
|
436
|
+
}, warnings.length > 0 ? { warnings } : undefined);
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
export const meetingAttachmentOperations = [
|
|
440
|
+
meetingAttachmentUpload,
|
|
441
|
+
meetingAttachmentAttach,
|
|
442
|
+
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { meetingStateDir } from '../env.js';
|
|
4
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
function cacheDir(env) {
|
|
6
|
+
return join(meetingStateDir(env.ctx), 'cache');
|
|
7
|
+
}
|
|
8
|
+
function cacheFile(env, key) {
|
|
9
|
+
return join(cacheDir(env), `${key}.json`);
|
|
10
|
+
}
|
|
11
|
+
async function identity(env) {
|
|
12
|
+
try {
|
|
13
|
+
const session = await env.host.getSession();
|
|
14
|
+
return {
|
|
15
|
+
baseUrl: String(session?.baseUrl ?? ''),
|
|
16
|
+
tenantKey: String(session?.tenantKey ?? ''),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { baseUrl: '', tenantKey: '' };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function readCache(env, key, options = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await readFile(cacheFile(env, key), 'utf8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
const current = await identity(env);
|
|
28
|
+
if (parsed.baseUrl !== current.baseUrl || parsed.tenantKey !== current.tenantKey)
|
|
29
|
+
return null;
|
|
30
|
+
const ageMs = Date.now() - Number(parsed.savedAt || 0);
|
|
31
|
+
const ttl = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
32
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > ttl)
|
|
33
|
+
return null;
|
|
34
|
+
return { data: parsed.data, savedAt: parsed.savedAt, ageMs };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function writeCache(env, key, data) {
|
|
41
|
+
try {
|
|
42
|
+
const current = await identity(env);
|
|
43
|
+
const payload = { savedAt: Date.now(), baseUrl: current.baseUrl, tenantKey: current.tenantKey, data };
|
|
44
|
+
const file = cacheFile(env, key);
|
|
45
|
+
await mkdir(dirname(file), { recursive: true });
|
|
46
|
+
await writeFile(file, JSON.stringify(payload), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// 缓存写失败不影响业务
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** 缓存 key 后缀:按租户隔离,避免多环境共用同一份会议室表。 */
|
|
53
|
+
export async function cacheScopeSuffix(env) {
|
|
54
|
+
const { baseUrl, tenantKey } = await identity(env);
|
|
55
|
+
const host = baseUrl.replace(/^https?:\/\//, '').replace(/[^0-9a-zA-Z]+/g, '-');
|
|
56
|
+
return `${host || 'local'}-${tenantKey || 'unknown'}`;
|
|
57
|
+
}
|