weaver-work-cli 0.1.29 → 0.1.32
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-UMGX72K6.js +2537 -0
- package/dist/chunks/archive-oa-N2IYBLF3.js +1317 -0
- package/dist/chunks/asset-GLEGKFG5.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-E44OTVDA.js +2157 -0
- package/dist/chunks/caizhilian-DQUFZXZL.js +10087 -0
- package/dist/chunks/calendar-JKPIRKFD.js +1195 -0
- package/dist/chunks/chunk-2GA5ZIQL.js +1615 -0
- package/dist/chunks/chunk-4YC43QQ4.js +403 -0
- package/dist/chunks/chunk-6EFWDL7Y.js +161 -0
- package/dist/chunks/chunk-6RKD4DRZ.js +5556 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-C6R3B2ML.js +358 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-DW6AUUKS.js +508 -0
- package/dist/chunks/chunk-E4Z23GHM.js +6962 -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-G3QJULO2.js +387 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-H7J4DC5R.js +464 -0
- package/dist/chunks/chunk-IURUFGZK.js +477 -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-WKI2FBIQ.js +15606 -0
- package/dist/chunks/chunk-WSKBSQ2U.js +231 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-YRVDW4WN.js +205 -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-V5FG2EHC.js +2276 -0
- package/dist/chunks/ehr-HMFY6VIL.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-ZYSVB7SG.js +141 -0
- package/dist/chunks/fna-B7RKPAZD.js +1444 -0
- package/dist/chunks/host-45WED2KK.js +21 -0
- package/dist/chunks/host-I4Z6BFYX.js +23 -0
- package/dist/chunks/host-KNZ52LJZ.js +17 -0
- package/dist/chunks/host-OCH2BCCV.js +19 -0
- package/dist/chunks/hrm-2KAZGUHI.js +244 -0
- package/dist/chunks/im-ACJJWH4U.js +252 -0
- package/dist/chunks/invoice-ZK3WCX2U.js +5179 -0
- package/dist/chunks/jiuchuanhui-5PQD3IL2.js +1940 -0
- package/dist/chunks/lazy-dispatch-JQXBW3GM.js +28 -0
- package/dist/chunks/mail-S2HDDXO2.js +2695 -0
- package/dist/chunks/meeting-QVN3I37L.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-QNG4VC2Q.js +2482 -0
- package/dist/chunks/plan-A25EMW6U.js +2025 -0
- package/dist/chunks/project-G6XV66DZ.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-7C5UMIPL.js +19 -0
- package/dist/chunks/registry-DC6KR3NH.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-TI3CIUXF.js +19 -0
- package/dist/chunks/room-recommend-WRTHO6Y2.js +20 -0
- package/dist/chunks/schedule-KFIITGSK.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-DJ7SYWAF.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 +61 -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,378 @@
|
|
|
1
|
+
import { meetingSuccess, schemaOf, SCHEMA_DATETIME_MINUTE } from '../types.js';
|
|
2
|
+
import { EB_BASE, ebBusinessSuccess, extractEbRows, queryBody } from './factory.js';
|
|
3
|
+
import { readCache, writeCache, cacheScopeSuffix } from './cache.js';
|
|
4
|
+
const ROOMS_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const FREQUENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
const FREQUENT_WINDOW_DAYS = 180;
|
|
7
|
+
const PAGE_SIZE = 100;
|
|
8
|
+
const MAX_PAGES = 20;
|
|
9
|
+
const STATUS_TEXT = { '0': '正常', '1': '封存' };
|
|
10
|
+
const ATTR_TEXT = { '0': '实体', '1': '虚拟', '2': '组合' };
|
|
11
|
+
function toText(value) {
|
|
12
|
+
if (value === undefined || value === null)
|
|
13
|
+
return '';
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value
|
|
16
|
+
.map((item) => (item && typeof item === 'object' ? String(item.name ?? item.id ?? '') : String(item ?? '')))
|
|
17
|
+
.filter((item) => item !== '')
|
|
18
|
+
.join(',');
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'object') {
|
|
21
|
+
const item = value;
|
|
22
|
+
return String(item.name ?? item.id ?? '');
|
|
23
|
+
}
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
function toNumber(value) {
|
|
27
|
+
const text = toText(value).trim();
|
|
28
|
+
if (text === '')
|
|
29
|
+
return 0;
|
|
30
|
+
const parsed = Number(text);
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
32
|
+
}
|
|
33
|
+
async function fetchRoomsPage(env, pageNo) {
|
|
34
|
+
const session = await env.host.getSession();
|
|
35
|
+
const response = await env.host.request({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
path: `${EB_BASE}/getFormDataList/query_meeting_room_list`,
|
|
38
|
+
operation: 'meeting.eb.room.recommend',
|
|
39
|
+
body: queryBody(session.userId, { pageNo, pageSize: PAGE_SIZE }),
|
|
40
|
+
acceptBusiness: ebBusinessSuccess,
|
|
41
|
+
});
|
|
42
|
+
const { rows } = extractEbRows(response);
|
|
43
|
+
return { rows, requests: 1 };
|
|
44
|
+
}
|
|
45
|
+
function normalizeRoom(row) {
|
|
46
|
+
const main = row?.mainTable ?? row ?? {};
|
|
47
|
+
const status = toText(main.status).trim();
|
|
48
|
+
const roomAttr = toText(main.room_attr).trim();
|
|
49
|
+
return {
|
|
50
|
+
id: toText(main.id).trim(),
|
|
51
|
+
name: toText(main.room_name).trim(),
|
|
52
|
+
status,
|
|
53
|
+
statusText: STATUS_TEXT[status] ?? (status === '' ? '未设置' : status),
|
|
54
|
+
roomAttr,
|
|
55
|
+
roomAttrText: ATTR_TEXT[roomAttr] ?? roomAttr,
|
|
56
|
+
minNum: toNumber(main.allow_minnum),
|
|
57
|
+
maxNum: toNumber(main.allow_maxnum),
|
|
58
|
+
place: toText(main.place).trim(),
|
|
59
|
+
equipments: toText(main.equipments).trim(),
|
|
60
|
+
mrType: toText(main.mr_type).trim(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** 全量会议室:优先缓存(24h),`refresh:true` 强制重拉。 */
|
|
64
|
+
export async function loadRooms(env, options = {}) {
|
|
65
|
+
const key = `rooms-${await cacheScopeSuffix(env)}`;
|
|
66
|
+
const warnings = [];
|
|
67
|
+
if (!options.refresh) {
|
|
68
|
+
const cached = await readCache(env, key, { ttlMs: ROOMS_TTL_MS });
|
|
69
|
+
if (cached?.data?.length) {
|
|
70
|
+
return {
|
|
71
|
+
rooms: cached.data,
|
|
72
|
+
fromCache: true,
|
|
73
|
+
ageSeconds: Math.floor(cached.ageMs / 1000),
|
|
74
|
+
requests: 0,
|
|
75
|
+
warnings,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const rooms = [];
|
|
80
|
+
let requests = 0;
|
|
81
|
+
for (let pageNo = 1; pageNo <= MAX_PAGES; pageNo += 1) {
|
|
82
|
+
const page = await fetchRoomsPage(env, pageNo);
|
|
83
|
+
requests += page.requests;
|
|
84
|
+
rooms.push(...page.rows.map(normalizeRoom).filter((room) => room.id !== ''));
|
|
85
|
+
if (page.rows.length < PAGE_SIZE)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (rooms.length === 0) {
|
|
89
|
+
warnings.push('会议室列表为空(或当前账号无会议室读取权限)——无法推荐,请改为手工指定会议室');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await writeCache(env, key, rooms);
|
|
93
|
+
}
|
|
94
|
+
return { rooms, fromCache: false, ageSeconds: 0, requests, warnings };
|
|
95
|
+
}
|
|
96
|
+
/** 个人常用会议室:统计「我近 180 天创建过的会议」里各会议室出现次数。 */
|
|
97
|
+
export async function loadFrequentRooms(env, options = {}) {
|
|
98
|
+
const session = await env.host.getSession();
|
|
99
|
+
const userId = String(session?.userId ?? '').trim();
|
|
100
|
+
const key = `frequent-rooms-${await cacheScopeSuffix(env)}-${userId}`;
|
|
101
|
+
const warnings = [];
|
|
102
|
+
if (!options.refresh) {
|
|
103
|
+
const cached = await readCache(env, key, { ttlMs: FREQUENT_TTL_MS });
|
|
104
|
+
if (cached?.data) {
|
|
105
|
+
return { order: cached.data, fromCache: true, requests: 0, warnings };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!userId) {
|
|
109
|
+
return { order: [], fromCache: false, requests: 0, warnings: ['取不到当前用户 id,跳过「常用会议室」统计'] };
|
|
110
|
+
}
|
|
111
|
+
const since = new Date(Date.now() - FREQUENT_WINDOW_DAYS * 24 * 60 * 60 * 1000);
|
|
112
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
113
|
+
const sinceText = `${since.getFullYear()}-${pad(since.getMonth() + 1)}-${pad(since.getDate())} 00:00`;
|
|
114
|
+
const counts = new Map();
|
|
115
|
+
let requests = 0;
|
|
116
|
+
try {
|
|
117
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
118
|
+
const listOp = getMeetingOperation('meeting.eb.meeting.list');
|
|
119
|
+
for (let pageNo = 1; pageNo <= 5; pageNo += 1) {
|
|
120
|
+
const page = await listOp.handler({ creator: userId, begindatetime: sinceText, pageNo, pageSize: PAGE_SIZE }, env);
|
|
121
|
+
requests += 1;
|
|
122
|
+
const rows = (page?.data?.rows ?? []);
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
const main = row?.mainTable ?? row ?? {};
|
|
125
|
+
const name = toText(main.address).trim();
|
|
126
|
+
if (!name)
|
|
127
|
+
continue;
|
|
128
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
if (rows.length < PAGE_SIZE)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
warnings.push(`常用会议室统计失败(${String(error?.message ?? error).slice(0, 80)}):本次按全量会议室排序推荐`);
|
|
136
|
+
}
|
|
137
|
+
const order = [...counts.entries()]
|
|
138
|
+
.map(([name, count]) => ({ name, count }))
|
|
139
|
+
.sort((left, right) => right.count - left.count)
|
|
140
|
+
.slice(0, 10);
|
|
141
|
+
if (order.length > 0)
|
|
142
|
+
await writeCache(env, key, order);
|
|
143
|
+
return { order, fromCache: false, requests, warnings };
|
|
144
|
+
}
|
|
145
|
+
/** 目标时段内被占用的会议室 id 集合(1 个请求查全部占用)。 */
|
|
146
|
+
async function fetchBusyRoomIds(env, begin, end) {
|
|
147
|
+
const session = await env.host.getSession();
|
|
148
|
+
const ids = new Set();
|
|
149
|
+
let requests = 0;
|
|
150
|
+
const usageQuery = { enddatetime: end, begindatetime: begin };
|
|
151
|
+
for (let pageNo = 1; pageNo <= MAX_PAGES; pageNo += 1) {
|
|
152
|
+
const response = await env.host.request({
|
|
153
|
+
method: 'POST',
|
|
154
|
+
path: `${EB_BASE}/getFormDataList/query_roomusage_list`,
|
|
155
|
+
operation: 'meeting.eb.room.recommend',
|
|
156
|
+
query: usageQuery,
|
|
157
|
+
body: queryBody(session.userId, { pageNo, pageSize: PAGE_SIZE }),
|
|
158
|
+
acceptBusiness: ebBusinessSuccess,
|
|
159
|
+
});
|
|
160
|
+
requests += 1;
|
|
161
|
+
const { rows } = extractEbRows(response);
|
|
162
|
+
for (const row of rows) {
|
|
163
|
+
const main = row?.mainTable ?? row ?? {};
|
|
164
|
+
const roomId = toText(main.address ?? main.room_id).trim();
|
|
165
|
+
if (roomId)
|
|
166
|
+
ids.add(roomId);
|
|
167
|
+
}
|
|
168
|
+
if (rows.length < PAGE_SIZE)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return { ids, requests };
|
|
172
|
+
}
|
|
173
|
+
/** 推荐会议室:返回按得分排序的候选(best 为第一个)。 */
|
|
174
|
+
export async function recommendRooms(env, input) {
|
|
175
|
+
const warnings = [];
|
|
176
|
+
const topN = Math.max(1, Math.min(10, input.topN ?? 3));
|
|
177
|
+
const roomData = await loadRooms(env, { refresh: input.refreshRooms === true });
|
|
178
|
+
warnings.push(...roomData.warnings);
|
|
179
|
+
const frequent = await loadFrequentRooms(env, { refresh: input.refreshFrequent === true });
|
|
180
|
+
warnings.push(...frequent.warnings);
|
|
181
|
+
const busy = await fetchBusyRoomIds(env, input.beginDatetime, input.endDatetime);
|
|
182
|
+
const frequentRank = new Map();
|
|
183
|
+
frequent.order.forEach((item, index) => {
|
|
184
|
+
frequentRank.set(item.name, { rank: index, count: item.count });
|
|
185
|
+
});
|
|
186
|
+
const memberCount = input.memberCount && input.memberCount > 0 ? input.memberCount : null;
|
|
187
|
+
const requirement = input.requirement ?? {};
|
|
188
|
+
const usable = roomData.rooms.filter((room) => room.status === '0');
|
|
189
|
+
let busyCount = 0;
|
|
190
|
+
const candidates = [];
|
|
191
|
+
for (const room of usable) {
|
|
192
|
+
const isBusy = busy.ids.has(room.id);
|
|
193
|
+
if (isBusy) {
|
|
194
|
+
busyCount += 1;
|
|
195
|
+
if (!input.includeBusy)
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (memberCount !== null) {
|
|
199
|
+
// allow_maxnum = 0 视为「不限」,超出上限或低于下限都算不满足(下限只在配置了才判)
|
|
200
|
+
if (room.maxNum > 0 && room.maxNum < memberCount)
|
|
201
|
+
continue;
|
|
202
|
+
if (room.minNum > 0 && room.minNum > memberCount)
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (requirement.roomAttr && room.roomAttr !== requirement.roomAttr)
|
|
206
|
+
continue;
|
|
207
|
+
if (requirement.mrType && room.mrType !== requirement.mrType)
|
|
208
|
+
continue;
|
|
209
|
+
if (requirement.placeKeyword && !room.place.includes(requirement.placeKeyword))
|
|
210
|
+
continue;
|
|
211
|
+
if (requirement.nameKeyword && !room.name.includes(requirement.nameKeyword))
|
|
212
|
+
continue;
|
|
213
|
+
if (requirement.equipmentsKeyword && !room.equipments.includes(requirement.equipmentsKeyword))
|
|
214
|
+
continue;
|
|
215
|
+
const reasons = [];
|
|
216
|
+
let score = 0;
|
|
217
|
+
const freq = frequentRank.get(room.name);
|
|
218
|
+
if (freq) {
|
|
219
|
+
reasons.push(`你近 ${FREQUENT_WINDOW_DAYS} 天用过 ${freq.count} 次`);
|
|
220
|
+
score += 40 - Math.min(30, freq.rank * 5);
|
|
221
|
+
}
|
|
222
|
+
if (memberCount !== null && room.maxNum > 0) {
|
|
223
|
+
const waste = room.maxNum - memberCount;
|
|
224
|
+
reasons.push(`容量 ${room.maxNum} 人,容纳 ${memberCount} 人`);
|
|
225
|
+
score += Math.max(0, 20 - Math.min(20, waste));
|
|
226
|
+
}
|
|
227
|
+
else if (room.maxNum > 0) {
|
|
228
|
+
reasons.push(`容量 ${room.maxNum} 人`);
|
|
229
|
+
score += 5;
|
|
230
|
+
}
|
|
231
|
+
if (requirement.placeKeyword) {
|
|
232
|
+
reasons.push(`地点匹配「${requirement.placeKeyword}」`);
|
|
233
|
+
score += 10;
|
|
234
|
+
}
|
|
235
|
+
if (requirement.equipmentsKeyword) {
|
|
236
|
+
reasons.push(`设备含「${requirement.equipmentsKeyword}」`);
|
|
237
|
+
score += 10;
|
|
238
|
+
}
|
|
239
|
+
if (isBusy) {
|
|
240
|
+
reasons.push('该时段已被占用(按 includeBusy 一并返回)');
|
|
241
|
+
score -= 100;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
reasons.push('该时段空闲');
|
|
245
|
+
}
|
|
246
|
+
candidates.push({
|
|
247
|
+
id: room.id,
|
|
248
|
+
name: room.name,
|
|
249
|
+
capacity: { min: room.minNum, max: room.maxNum },
|
|
250
|
+
place: room.place,
|
|
251
|
+
equipments: room.equipments,
|
|
252
|
+
roomAttrText: room.roomAttrText,
|
|
253
|
+
frequentCount: freq?.count ?? 0,
|
|
254
|
+
score,
|
|
255
|
+
reasons,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
candidates.sort((left, right) => {
|
|
259
|
+
if (right.score !== left.score)
|
|
260
|
+
return right.score - left.score;
|
|
261
|
+
// 同分时优先容量小的(不浪费大会议室),再按名称稳定排序
|
|
262
|
+
const leftCap = left.capacity.max > 0 ? left.capacity.max : Number.MAX_SAFE_INTEGER;
|
|
263
|
+
const rightCap = right.capacity.max > 0 ? right.capacity.max : Number.MAX_SAFE_INTEGER;
|
|
264
|
+
if (leftCap !== rightCap)
|
|
265
|
+
return leftCap - rightCap;
|
|
266
|
+
return left.name.localeCompare(right.name, 'zh-Hans-CN');
|
|
267
|
+
});
|
|
268
|
+
if (candidates.length === 0) {
|
|
269
|
+
warnings.push(`没有满足条件的空闲会议室(共 ${roomData.rooms.length} 间,可用 ${usable.length} 间,`
|
|
270
|
+
+ `该时段被占用 ${busyCount} 间${memberCount ? `,人数 ${memberCount}` : ''})`
|
|
271
|
+
+ '——请放宽人数/地点/设备要求,或改为线上会议。');
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
best: candidates[0] ?? null,
|
|
275
|
+
candidates: candidates.slice(0, topN),
|
|
276
|
+
meta: {
|
|
277
|
+
roomsTotal: roomData.rooms.length,
|
|
278
|
+
roomsUsable: usable.length,
|
|
279
|
+
roomsMatched: candidates.length,
|
|
280
|
+
roomsBusy: busyCount,
|
|
281
|
+
memberCount,
|
|
282
|
+
roomsFromCache: roomData.fromCache,
|
|
283
|
+
roomsCacheAgeSeconds: roomData.ageSeconds,
|
|
284
|
+
frequentFromCache: frequent.fromCache,
|
|
285
|
+
requests: roomData.requests + frequent.requests + busy.requests,
|
|
286
|
+
},
|
|
287
|
+
warnings,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/** 只推不建:按人数/要求/个人习惯推荐空闲会议室。 */
|
|
291
|
+
export const ebRoomRecommendOperation = {
|
|
292
|
+
name: 'meeting.eb.room.recommend',
|
|
293
|
+
resource: 'eb',
|
|
294
|
+
method: 'meeting.eb.room.recommend',
|
|
295
|
+
action: 'meeting.eb.room.recommend',
|
|
296
|
+
summary: '会议室推荐:按人数 + 地点/设备/类型要求 + 个人常用(近 180 天)+ 时段占用,给出排序候选;会议室与常用记录走本地缓存(分别 24h / 7d),占用实时查 1 次',
|
|
297
|
+
capability: 'eb.room',
|
|
298
|
+
risk: 'read',
|
|
299
|
+
inputSchema: schemaOf({
|
|
300
|
+
beginDatetime: { ...SCHEMA_DATETIME_MINUTE, description: '开始时间(必填,yyyy-MM-dd HH:mm)' },
|
|
301
|
+
endDatetime: { ...SCHEMA_DATETIME_MINUTE, description: '结束时间(必填,yyyy-MM-dd HH:mm)' },
|
|
302
|
+
memberCount: { type: 'number', description: '参会人数(可选;用于容量过滤,0 或不传 = 不按人数过滤)' },
|
|
303
|
+
hrmMembers: {
|
|
304
|
+
type: 'string',
|
|
305
|
+
description: '参会人 id 逗号分隔(可选):不传 memberCount 时按它的人数推算',
|
|
306
|
+
},
|
|
307
|
+
placeKeyword: { type: 'string', description: '地点/楼层关键词(模糊匹配会议室 place)' },
|
|
308
|
+
nameKeyword: { type: 'string', description: '会议室名称关键词' },
|
|
309
|
+
mrType: { type: 'string', description: '会议室类型 id(mr_type)' },
|
|
310
|
+
roomAttr: { type: 'string', enum: ['0', '1', '2'], description: '会议室属性:0 实体 / 1 虚拟 / 2 组合' },
|
|
311
|
+
equipmentsKeyword: { type: 'string', description: '设备关键词(模糊匹配 equipments,如 投影/视频)' },
|
|
312
|
+
topN: { type: 'number', description: '返回候选数量(默认 3,最多 10)' },
|
|
313
|
+
refresh: { type: 'boolean', description: 'true=刷新会议室全量与常用记录缓存' },
|
|
314
|
+
includeBusy: { type: 'boolean', description: 'true=把该时段已被占用的会议室也返回(排在最后,带说明)' },
|
|
315
|
+
}, { required: ['beginDatetime', 'endDatetime'] }),
|
|
316
|
+
handler: async (args, env) => {
|
|
317
|
+
const hrmCount = String(args.hrmMembers ?? '')
|
|
318
|
+
.split(/[,;,;\s]+/)
|
|
319
|
+
.map((item) => item.trim())
|
|
320
|
+
.filter((item) => item !== '').length;
|
|
321
|
+
const memberCount = args.memberCount !== undefined ? Number(args.memberCount) : hrmCount;
|
|
322
|
+
const result = await recommendRooms(env, {
|
|
323
|
+
beginDatetime: String(args.beginDatetime),
|
|
324
|
+
endDatetime: String(args.endDatetime),
|
|
325
|
+
...(Number.isFinite(memberCount) && memberCount > 0 ? { memberCount } : {}),
|
|
326
|
+
requirement: {
|
|
327
|
+
...(args.placeKeyword ? { placeKeyword: String(args.placeKeyword) } : {}),
|
|
328
|
+
...(args.nameKeyword ? { nameKeyword: String(args.nameKeyword) } : {}),
|
|
329
|
+
...(args.mrType ? { mrType: String(args.mrType) } : {}),
|
|
330
|
+
...(args.roomAttr ? { roomAttr: String(args.roomAttr) } : {}),
|
|
331
|
+
...(args.equipmentsKeyword ? { equipmentsKeyword: String(args.equipmentsKeyword) } : {}),
|
|
332
|
+
},
|
|
333
|
+
...(args.topN !== undefined ? { topN: Number(args.topN) } : {}),
|
|
334
|
+
...(args.refresh === true ? { refreshRooms: true, refreshFrequent: true } : {}),
|
|
335
|
+
...(args.includeBusy === true ? { includeBusy: true } : {}),
|
|
336
|
+
});
|
|
337
|
+
return meetingSuccess('meeting.eb.room.recommend', {
|
|
338
|
+
best: result.best,
|
|
339
|
+
candidates: result.candidates,
|
|
340
|
+
meta: result.meta,
|
|
341
|
+
}, result.warnings.length > 0 ? { warnings: result.warnings } : {});
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* 建会成功后把该会议室计入「个人常用」缓存(增量更新,不重拉历史)。
|
|
346
|
+
*
|
|
347
|
+
* 为什么需要(2026-10-28 用户追问):常用会议室缓存 TTL 7 天,若只靠定时重算,
|
|
348
|
+
* 「今天刚预订了几场会议」不会立刻反映到下一天的推荐里,用户会觉得"推荐不准"。
|
|
349
|
+
* 这里在 `create.apply` 成功后直接 +1,保证同一会话/同一天内连续预约就能看到偏好变化。
|
|
350
|
+
*
|
|
351
|
+
* 约定:只在**缓存已存在**时增量(缓存不存在说明还没建过,下次推荐会自然全量统计,不必抢跑);
|
|
352
|
+
* 房间 id → 名称优先用会议室缓存映射,映射不到就跳过(不为了计数多发一次请求)。
|
|
353
|
+
*/
|
|
354
|
+
export async function rememberFrequentRoom(env, roomId) {
|
|
355
|
+
const id = String(roomId ?? '').trim();
|
|
356
|
+
if (!id)
|
|
357
|
+
return false;
|
|
358
|
+
const scope = await cacheScopeSuffix(env);
|
|
359
|
+
const roomsCached = await readCache(env, `rooms-${scope}`, { ttlMs: ROOMS_TTL_MS });
|
|
360
|
+
const name = roomsCached?.data?.find((room) => room.id === id)?.name ?? '';
|
|
361
|
+
if (!name)
|
|
362
|
+
return false;
|
|
363
|
+
const session = await env.host.getSession().catch(() => null);
|
|
364
|
+
const userId = String(session?.userId ?? '').trim();
|
|
365
|
+
if (!userId)
|
|
366
|
+
return false;
|
|
367
|
+
const key = `frequent-rooms-${scope}-${userId}`;
|
|
368
|
+
const cached = await readCache(env, key, { ttlMs: FREQUENT_TTL_MS });
|
|
369
|
+
const order = cached?.data ?? [];
|
|
370
|
+
const existing = order.find((item) => item.name === name);
|
|
371
|
+
if (existing)
|
|
372
|
+
existing.count += 1;
|
|
373
|
+
else
|
|
374
|
+
order.push({ name, count: 1 });
|
|
375
|
+
order.sort((left, right) => right.count - left.count);
|
|
376
|
+
await writeCache(env, key, order.slice(0, 10));
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会议室列表(`meeting.eb.room.list`)的结果后处理。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要(2026-10-27 用户明确纠正):
|
|
5
|
+
* 会议室 `status` 是 **Select,`0` = 正常、`1` = 封存**。但它的字面极性反直觉 ——
|
|
6
|
+
* 很容易把 `0` 读成"关闭/停用",于是把一个全部正常的租户报成"6 间会议室都停用"。
|
|
7
|
+
* 直接把语义固化进返回值(`statusText` + `summary.byStatus` + `legend`),
|
|
8
|
+
* 调用方不需要再背字典,也不可能读反。
|
|
9
|
+
*/
|
|
10
|
+
const ROOM_STATUS = {
|
|
11
|
+
'0': '正常',
|
|
12
|
+
'1': '封存',
|
|
13
|
+
};
|
|
14
|
+
const ROOM_ATTR = {
|
|
15
|
+
'0': '实体',
|
|
16
|
+
'1': '虚拟',
|
|
17
|
+
'2': '组合',
|
|
18
|
+
};
|
|
19
|
+
function toText(value) {
|
|
20
|
+
if (value === undefined || value === null)
|
|
21
|
+
return '';
|
|
22
|
+
if (typeof value === 'object') {
|
|
23
|
+
const obj = value;
|
|
24
|
+
if (typeof obj.name === 'string')
|
|
25
|
+
return obj.name;
|
|
26
|
+
if (typeof obj.value === 'string')
|
|
27
|
+
return obj.value;
|
|
28
|
+
if (typeof obj.id === 'string')
|
|
29
|
+
return obj.id;
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
return String(value).trim();
|
|
33
|
+
}
|
|
34
|
+
/** 状态归一化:兼容编码(`0`/`1`)与中文渲染文本(`正常`/`封存`)。 */
|
|
35
|
+
function normalizeRoomStatus(raw) {
|
|
36
|
+
const text = toText(raw);
|
|
37
|
+
if (text === '' || text === '0' || text === '正常') {
|
|
38
|
+
return { code: text === '' ? '' : '0', text: text === '' ? '未设置' : '正常' };
|
|
39
|
+
}
|
|
40
|
+
if (text === '1' || text === '封存')
|
|
41
|
+
return { code: '1', text: '封存' };
|
|
42
|
+
return { code: text, text };
|
|
43
|
+
}
|
|
44
|
+
function normalizeRoomAttr(raw) {
|
|
45
|
+
const text = toText(raw);
|
|
46
|
+
if (text === '')
|
|
47
|
+
return '未设置';
|
|
48
|
+
return ROOM_ATTR[text] ?? text;
|
|
49
|
+
}
|
|
50
|
+
export function summarizeRoomRows(rows) {
|
|
51
|
+
const byStatus = { 正常: 0, 封存: 0, 未设置: 0 };
|
|
52
|
+
const items = rows.map((row) => {
|
|
53
|
+
const main = (row?.mainTable ?? row ?? {});
|
|
54
|
+
const status = normalizeRoomStatus(main.status);
|
|
55
|
+
byStatus[status.text] = (byStatus[status.text] ?? 0) + 1;
|
|
56
|
+
return {
|
|
57
|
+
id: toText(main.id),
|
|
58
|
+
roomName: toText(main.room_name),
|
|
59
|
+
status: status.code,
|
|
60
|
+
statusText: status.text,
|
|
61
|
+
roomAttr: toText(main.room_attr),
|
|
62
|
+
roomAttrText: normalizeRoomAttr(main.room_attr),
|
|
63
|
+
allowMinNum: toText(main.allow_minnum),
|
|
64
|
+
allowMaxNum: toText(main.allow_maxnum),
|
|
65
|
+
place: toText(main.place),
|
|
66
|
+
equipments: toText(main.equipments),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
/** 语义字典:`0`=正常(可用)、`1`=封存(不可用)。写进返回体,避免被读反。 */
|
|
71
|
+
legend: {
|
|
72
|
+
status: ROOM_STATUS,
|
|
73
|
+
roomAttr: ROOM_ATTR,
|
|
74
|
+
note: 'status:0 = 正常(可用),1 = 封存(不可用);不要读成"启用/停用"',
|
|
75
|
+
},
|
|
76
|
+
summary: {
|
|
77
|
+
total: items.length,
|
|
78
|
+
byStatus,
|
|
79
|
+
/** 当前页里"封存/未设置"的房间名,便于直接提示用户哪些不可用。 */
|
|
80
|
+
unusable: items.filter((item) => item.statusText !== '正常').map((item) => item.roomName),
|
|
81
|
+
},
|
|
82
|
+
items,
|
|
83
|
+
};
|
|
84
|
+
}
|