@tbox.cn/app-module-promotion 0.2.0 → 0.9.4
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/AGENTS.md +16 -0
- package/README.md +90 -11
- package/dist/chunk-QOTX4XDS.js +1745 -0
- package/dist/chunk-S7CVFCML.js +1330 -0
- package/dist/client/index.d.ts +21 -3
- package/dist/client/index.js +4 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +16 -4
- package/dist/promotion-6L7HLRC2.css +2135 -0
- package/dist/server/index.d.ts +2481 -41
- package/dist/server/index.js +13 -3
- package/package.json +28 -15
- package/skills/promotion/SKILL.md +137 -0
- package/skills/promotion/references/claim-loop.md +18 -0
- package/src/client/cards/index.ts +10 -0
- package/src/client/cards/promotion-activity-category-list/index.tsx +34 -0
- package/src/client/cards/promotion-activity-detail/index.tsx +84 -0
- package/src/client/cards/promotion-activity-list/index.tsx +56 -0
- package/src/client/cards/promotion-best-deal/index.tsx +18 -0
- package/src/client/cards/promotion-entitlement-list/index.tsx +161 -0
- package/src/client/cards/promotion-offer-detail/index.tsx +116 -0
- package/src/client/cards/promotion-offer-list/index.tsx +283 -0
- package/src/client/cards/view.ts +72 -0
- package/src/client/components/CouponQrCode.tsx +48 -0
- package/src/client/components/CouponSheet.tsx +124 -0
- package/src/client/components/ImageWithFallback.tsx +39 -0
- package/src/client/index.ts +23 -4
- package/src/client/styles/promotion.css +2135 -0
- package/src/client/types/css.d.ts +2 -0
- package/src/client/utils/sanitize-activity-html.ts +48 -0
- package/src/client/utils/scroll-card-bottom.ts +40 -0
- package/src/server/actions.ts +217 -0
- package/src/server/cards/index.ts +19 -0
- package/src/server/cards/promotion-activity-category-list/meta.ts +13 -0
- package/src/server/cards/promotion-activity-detail/meta.ts +29 -0
- package/src/server/cards/promotion-activity-list/meta.ts +13 -0
- package/src/server/cards/promotion-best-deal/meta.ts +20 -0
- package/src/server/cards/promotion-entitlement-list/meta.ts +17 -0
- package/src/server/cards/promotion-entitlement-list/samples.ts +51 -0
- package/src/server/cards/promotion-offer-detail/meta.ts +25 -0
- package/src/server/cards/promotion-offer-list/meta.ts +20 -0
- package/src/server/cards/promotion-offer-list/samples.ts +154 -0
- package/src/server/handler.ts +378 -16
- package/src/server/index.ts +117 -22
- package/src/server/navigation.ts +23 -0
- package/src/server/ports.ts +56 -0
- package/src/server/service.ts +243 -24
- package/src/server/tools.ts +789 -0
- package/tbox.module.json +126 -0
- package/tests/cards-render.test.tsx +656 -0
- package/tests/ports-resolve.test.ts +96 -0
- package/tests/pretool-coupons.test.ts +63 -0
- package/tests/promotion.test.ts +976 -0
- package/tests/samples.test.ts +42 -0
- package/tests/view.test.ts +61 -0
- package/tsup.config.ts +9 -1
- package/dist/chunk-AD6OLHSM.js +0 -86
- package/dist/chunk-LQGF6337.js +0 -32
- package/src/client/cards/coupon/index.tsx +0 -12
- package/src/server/cards/coupon/meta.ts +0 -23
- package/tbox.component.json +0 -20
- package/tests/service.test.ts +0 -31
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/** 营销域完整工具集:业务语义对齐 legacy marketing,传输遵循 tbox-app。
|
|
2
|
+
* 上下文统一化:运行上下文 = 工具 execute 第二参(requireToolRequestContext 显式读取)。 */
|
|
3
|
+
import { createTool, coerceStringifiedFields, withCardEmissionHint } from '@tbox.cn/app-sdk/server';
|
|
4
|
+
import type { ServerContext, ToolExecContextLike } from '@tbox.cn/app-sdk/server';
|
|
5
|
+
import { requireToolRequestContext } from '@tbox.cn/app-sdk/server';
|
|
6
|
+
import { resolveMemberGate, toMallQueryContext } from '@tbox.cn/app-contracts-mall';
|
|
7
|
+
import type {
|
|
8
|
+
MemberStatusFactory,
|
|
9
|
+
OfferType,
|
|
10
|
+
PromotionActivity,
|
|
11
|
+
PromotionActivityDetail,
|
|
12
|
+
PromotionAcquisitionPort,
|
|
13
|
+
PromotionActivityPort,
|
|
14
|
+
PromotionEntitlement,
|
|
15
|
+
PromotionEntitlementPort,
|
|
16
|
+
PromotionOffer,
|
|
17
|
+
PromotionOfferQueryPort,
|
|
18
|
+
PromotionQueryContext,
|
|
19
|
+
} from '@tbox.cn/app-contracts-mall';
|
|
20
|
+
import type { RequestContext } from '@tbox.cn/app-contracts';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import type { PromotionService } from './service';
|
|
23
|
+
import {
|
|
24
|
+
resolvePromotionOffersPort,
|
|
25
|
+
resolvePromotionEntitlementsPort,
|
|
26
|
+
resolvePromotionActivitiesPort,
|
|
27
|
+
} from './ports';
|
|
28
|
+
import { promotionNavigationQuery } from './navigation';
|
|
29
|
+
|
|
30
|
+
function coerce<T extends z.ZodTypeAny>(schema: T): T {
|
|
31
|
+
return z.preprocess(coerceStringifiedFields, schema) as unknown as T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toQueryContext(reqCtx: RequestContext): { q: PromotionQueryContext; reqCtx: RequestContext } {
|
|
35
|
+
return { q: toMallQueryContext(reqCtx), reqCtx };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withCards<T extends Record<string, unknown>>(facts: T, cardType: string, note: string) {
|
|
39
|
+
return { ...facts, cards: [{ cardType, data: facts, note }] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dateText(value?: string): string | undefined {
|
|
43
|
+
if (!value) return undefined;
|
|
44
|
+
const date = new Date(value);
|
|
45
|
+
return Number.isNaN(date.valueOf()) ? undefined : date.toLocaleDateString('zh-CN');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dateRangeText(start?: string, end?: string): string | undefined {
|
|
49
|
+
const startText = dateTimeText(start);
|
|
50
|
+
const endText = dateTimeText(end);
|
|
51
|
+
if (startText && endText) return `${startText} 至 ${endText}`;
|
|
52
|
+
return startText || endText;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dateTimeText(value?: string): string | undefined {
|
|
56
|
+
if (!value) return undefined;
|
|
57
|
+
const text = value.trim().replace('T', ' ');
|
|
58
|
+
return text.length > 19 ? text.slice(0, 19) : text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function yuanText(cents: number): string {
|
|
62
|
+
return (cents / 100).toFixed(2).replace(/\.?(0+)$/, '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function facetText(offer: Pick<PromotionOffer, 'facets'>): string | undefined {
|
|
66
|
+
const facet = offer.facets.find((item) => item.kind === 'COUPON');
|
|
67
|
+
if (facet?.discountType === 'AMOUNT') {
|
|
68
|
+
return facet.thresholdCents !== undefined
|
|
69
|
+
? `满 ${yuanText(facet.thresholdCents)} 减 ${yuanText(facet.value)} 元`
|
|
70
|
+
: `¥${yuanText(facet.value)}`;
|
|
71
|
+
}
|
|
72
|
+
if (facet?.discountType === 'PERCENTAGE') return `${(facet.value / 10).toFixed(1)} 折`;
|
|
73
|
+
const promotion = offer.facets.find((item) => item.kind === 'PROMOTION');
|
|
74
|
+
return promotion?.kind === 'PROMOTION' ? promotion.ruleDescription : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function thresholdText(offer: Pick<PromotionOffer, 'facets'>): string | undefined {
|
|
78
|
+
const facet = offer.facets.find((item) => item.kind === 'COUPON');
|
|
79
|
+
return facet?.kind === 'COUPON' && facet.thresholdCents !== undefined
|
|
80
|
+
? `满 ${yuanText(facet.thresholdCents)} 元可用`
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function offerListPresentation(items: readonly Pick<PromotionOffer, 'offerType'>[]) {
|
|
85
|
+
return items.length > 0 && items.every((item) => item.offerType === 'COUPON')
|
|
86
|
+
? 'coupon' as const
|
|
87
|
+
: 'offer' as const;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function offerPresentation(offerType: PromotionOffer['offerType']) {
|
|
91
|
+
if (offerType === 'PROMOTION') return 'promotion' as const;
|
|
92
|
+
if (offerType === 'BUNDLE') return 'package' as const;
|
|
93
|
+
return 'offer' as const;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function offerSummary(offer: PromotionOffer) {
|
|
97
|
+
const validity = offer.validityText ?? dateRangeText(offer.validity.start, offer.validity.end);
|
|
98
|
+
return {
|
|
99
|
+
offerRef: offer.offerRef,
|
|
100
|
+
title: offer.title,
|
|
101
|
+
...(offer.subtitle ? { subtitle: offer.subtitle } : {}),
|
|
102
|
+
offerType: offer.offerType,
|
|
103
|
+
...(facetText(offer) ? { benefitText: facetText(offer) } : {}),
|
|
104
|
+
...(thresholdText(offer) ? { thresholdText: thresholdText(offer) } : {}),
|
|
105
|
+
...(offer.priceText ? { priceText: offer.priceText } : {}),
|
|
106
|
+
...(offer.originalPriceText ? { originalPriceText: offer.originalPriceText } : {}),
|
|
107
|
+
...(offer.discountLabel ? { discountLabel: offer.discountLabel } : {}),
|
|
108
|
+
...(offer.claimActionLabel ? { claimActionLabel: offer.claimActionLabel } : {}),
|
|
109
|
+
...(offer.purchaseUrl ? { purchaseUrl: offer.purchaseUrl } : {}),
|
|
110
|
+
...(offer.freeCouponClaimMode ? { freeCouponClaimMode: offer.freeCouponClaimMode } : {}),
|
|
111
|
+
...(offer.stockText ? { stockText: offer.stockText } : {}),
|
|
112
|
+
...(offer.usageNoticeText ? { usageNoticeText: offer.usageNoticeText } : {}),
|
|
113
|
+
...(offer.usageSceneText ? { usageSceneText: offer.usageSceneText } : {}),
|
|
114
|
+
...(offer.usageTimeText ? { usageTimeText: offer.usageTimeText } : {}),
|
|
115
|
+
...(offer.claimTimeText ? { claimTimeText: offer.claimTimeText } : {}),
|
|
116
|
+
...(offer.usageRuleText ? { usageRuleText: offer.usageRuleText } : {}),
|
|
117
|
+
...(offer.guaranteeText ? { guaranteeText: offer.guaranteeText } : {}),
|
|
118
|
+
claimable: offer.availability.status === 'AVAILABLE',
|
|
119
|
+
availabilityStatus: offer.availability.status,
|
|
120
|
+
...(validity ? { validityText: validity } : {}),
|
|
121
|
+
...(offer.imageUrl ? { imageUrl: offer.imageUrl } : {}),
|
|
122
|
+
tags: [...offer.tags],
|
|
123
|
+
itemActions: [
|
|
124
|
+
{
|
|
125
|
+
kind: 'send' as const,
|
|
126
|
+
label: '查看详情',
|
|
127
|
+
value: promotionNavigationQuery.offerDetail(offer.title),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function offerDetail(
|
|
134
|
+
offer: PromotionOffer,
|
|
135
|
+
supportsAcquisition: boolean,
|
|
136
|
+
presentation:
|
|
137
|
+
| 'offer'
|
|
138
|
+
| 'promotion'
|
|
139
|
+
| 'promotion-guide'
|
|
140
|
+
| 'coupon'
|
|
141
|
+
| 'package' = 'offer',
|
|
142
|
+
stores?: readonly {
|
|
143
|
+
storeId: string;
|
|
144
|
+
storeName: string;
|
|
145
|
+
businessName?: string;
|
|
146
|
+
address?: string;
|
|
147
|
+
floor?: string;
|
|
148
|
+
}[],
|
|
149
|
+
) {
|
|
150
|
+
const validity = offer.validityText ?? dateRangeText(offer.validity.start, offer.validity.end);
|
|
151
|
+
const claimable = supportsAcquisition && offer.availability.status === 'AVAILABLE';
|
|
152
|
+
return {
|
|
153
|
+
presentation,
|
|
154
|
+
offerRef: offer.offerRef,
|
|
155
|
+
title: offer.title,
|
|
156
|
+
offerType: offer.offerType,
|
|
157
|
+
...(offer.subtitle ? { subtitle: offer.subtitle } : {}),
|
|
158
|
+
...(offer.description ? { description: offer.description } : {}),
|
|
159
|
+
...(offer.imageUrl ? { imageUrl: offer.imageUrl } : {}),
|
|
160
|
+
...(facetText(offer) ? { benefitText: facetText(offer) } : {}),
|
|
161
|
+
...(thresholdText(offer) ? { thresholdText: thresholdText(offer) } : {}),
|
|
162
|
+
...(offer.priceText ? { priceText: offer.priceText } : {}),
|
|
163
|
+
...(offer.originalPriceText ? { originalPriceText: offer.originalPriceText } : {}),
|
|
164
|
+
...(offer.discountLabel ? { discountLabel: offer.discountLabel } : {}),
|
|
165
|
+
...(offer.claimActionLabel ? { claimActionLabel: offer.claimActionLabel } : {}),
|
|
166
|
+
...(offer.purchaseUrl ? { purchaseUrl: offer.purchaseUrl } : {}),
|
|
167
|
+
...(offer.stockText ? { stockText: offer.stockText } : {}),
|
|
168
|
+
...(offer.usageNoticeText ? { usageNoticeText: offer.usageNoticeText } : {}),
|
|
169
|
+
...(offer.usageSceneText ? { usageSceneText: offer.usageSceneText } : {}),
|
|
170
|
+
...(offer.usageTimeText ? { usageTimeText: offer.usageTimeText } : {}),
|
|
171
|
+
...(offer.claimTimeText ? { claimTimeText: offer.claimTimeText } : {}),
|
|
172
|
+
...(offer.usageRuleText ? { usageRuleText: offer.usageRuleText } : {}),
|
|
173
|
+
...(offer.guaranteeText ? { guaranteeText: offer.guaranteeText } : {}),
|
|
174
|
+
...(validity ? { validityText: validity } : {}),
|
|
175
|
+
rules:
|
|
176
|
+
offer.participation?.steps.map((step) =>
|
|
177
|
+
step.description ? `${step.title}:${step.description}` : step.title,
|
|
178
|
+
) ?? [],
|
|
179
|
+
...(offer.participation
|
|
180
|
+
? {
|
|
181
|
+
participationMode: offer.participation.mode,
|
|
182
|
+
steps: offer.participation.steps.map((step) => ({
|
|
183
|
+
title: step.title,
|
|
184
|
+
...(step.description ? { description: step.description } : {}),
|
|
185
|
+
})),
|
|
186
|
+
requirements: [...(offer.participation.requirements ?? [])],
|
|
187
|
+
...(offer.participation.notice ? { notice: offer.participation.notice } : {}),
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
...(stores && stores.length > 0 ? { stores: [...stores] } : {}),
|
|
191
|
+
...(offer.facets.find((facet) => facet.kind === 'BUNDLE')?.kind === 'BUNDLE'
|
|
192
|
+
? {
|
|
193
|
+
bundleItems: offer.facets
|
|
194
|
+
.filter((facet) => facet.kind === 'BUNDLE')
|
|
195
|
+
.flatMap((facet) => facet.items.map((item) => ({ ...item }))),
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
198
|
+
claimable,
|
|
199
|
+
...(!claimable
|
|
200
|
+
? {
|
|
201
|
+
unavailableReason: !supportsAcquisition
|
|
202
|
+
? '当前商场未装配优惠领取通道'
|
|
203
|
+
: offer.availability.reasonCodes.join(';') || '暂不可领取',
|
|
204
|
+
}
|
|
205
|
+
: {}),
|
|
206
|
+
caveats: ['优惠资格、库存和最终优惠以领取或使用时为准'],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function entitlementSummary(entitlement: PromotionEntitlement) {
|
|
211
|
+
const validUntil = dateText(entitlement.validUntil);
|
|
212
|
+
const validity = dateRangeText(entitlement.validFrom, entitlement.validUntil);
|
|
213
|
+
return {
|
|
214
|
+
entitlementRef: entitlement.entitlementRef,
|
|
215
|
+
title: entitlement.title,
|
|
216
|
+
...(entitlement.imageUrl ? { imageUrl: entitlement.imageUrl } : {}),
|
|
217
|
+
entitlementType: entitlement.entitlementType,
|
|
218
|
+
status: entitlement.status,
|
|
219
|
+
...(entitlement.codeMasked ? { codeMasked: entitlement.codeMasked } : {}),
|
|
220
|
+
...(entitlement.code ? { code: entitlement.code } : {}),
|
|
221
|
+
...(entitlement.qrCodeEnabled !== undefined ? { qrCodeEnabled: entitlement.qrCodeEnabled } : {}),
|
|
222
|
+
...(entitlement.benefitText ? { benefitText: entitlement.benefitText } : {}),
|
|
223
|
+
...(entitlement.thresholdText ? { thresholdText: entitlement.thresholdText } : {}),
|
|
224
|
+
...(entitlement.usageNotice ? { usageNotice: entitlement.usageNotice } : {}),
|
|
225
|
+
...(entitlement.guaranteeText ? { guaranteeText: entitlement.guaranteeText } : {}),
|
|
226
|
+
...(entitlement.usageSceneText ? { usageSceneText: entitlement.usageSceneText } : {}),
|
|
227
|
+
...(entitlement.usageTimeText ? { usageTimeText: entitlement.usageTimeText } : {}),
|
|
228
|
+
...(entitlement.claimTimeText ? { claimTimeText: entitlement.claimTimeText } : {}),
|
|
229
|
+
...(entitlement.usageRuleText ? { usageRuleText: entitlement.usageRuleText } : {}),
|
|
230
|
+
...(entitlement.stores?.length ? { stores: entitlement.stores.map((store) => ({ ...store })) } : {}),
|
|
231
|
+
// CouponSheet already supplies the "有效期" label; keep this value label-free.
|
|
232
|
+
...(entitlement.validityText || validity ? { validityText: entitlement.validityText ?? validity } : {}),
|
|
233
|
+
...(validUntil ? { validUntilText: `有效期至 ${validUntil}` } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function activityAvailability(detail: Pick<PromotionActivityDetail, 'startAt' | 'endAt' | 'remainingQuota'>) {
|
|
238
|
+
if (detail.remainingQuota === 0) return 'FULL' as const;
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
if (detail.endAt && new Date(detail.endAt).valueOf() < now) return 'ENDED' as const;
|
|
241
|
+
if (detail.startAt && new Date(detail.startAt).valueOf() > now) return 'UPCOMING' as const;
|
|
242
|
+
return 'AVAILABLE' as const;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function activitySummary(
|
|
246
|
+
item: PromotionActivity,
|
|
247
|
+
categoryNames: ReadonlyMap<string, string> = new Map(),
|
|
248
|
+
) {
|
|
249
|
+
return {
|
|
250
|
+
activityRef: item.activityRef,
|
|
251
|
+
title: item.title,
|
|
252
|
+
...(item.subtitle ? { subtitle: item.subtitle } : {}),
|
|
253
|
+
...(item.coverUrl ? { coverUrl: item.coverUrl } : {}),
|
|
254
|
+
registrationRequired: item.registrationRequired,
|
|
255
|
+
...(item.jumpOnly ? { jumpOnly: true } : {}),
|
|
256
|
+
...(item.jumpLabel ? { jumpLabel: item.jumpLabel } : {}),
|
|
257
|
+
...(item.categoryId && categoryNames.get(item.categoryId)
|
|
258
|
+
? { categoryName: categoryNames.get(item.categoryId) }
|
|
259
|
+
: {}),
|
|
260
|
+
availabilityStatus: activityAvailability(item),
|
|
261
|
+
...(item.remainingQuota !== undefined ? { remainingQuota: item.remainingQuota } : {}),
|
|
262
|
+
...(dateText(item.startAt) ? { periodText: `${dateText(item.startAt)} 起` } : {}),
|
|
263
|
+
itemActions: item.jumpOnly && item.registrationUrl
|
|
264
|
+
? [{ kind: 'link' as const, label: item.jumpLabel ?? '打开小程序', url: item.registrationUrl }]
|
|
265
|
+
: [{ kind: 'send' as const, label: '查看活动详情', value: promotionNavigationQuery.activityDetail(item.title) }],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function activityDetail(
|
|
270
|
+
detail: PromotionActivityDetail,
|
|
271
|
+
presentation: 'detail' | 'schedule' | 'guide' = 'detail',
|
|
272
|
+
) {
|
|
273
|
+
const start = dateText(detail.startAt);
|
|
274
|
+
const end = dateText(detail.endAt);
|
|
275
|
+
// 报名深链为 provider 出参快照(数据驱动,F5 起不再模块侧拼装)
|
|
276
|
+
const registrationUrl = detail.registrationUrl;
|
|
277
|
+
return {
|
|
278
|
+
presentation,
|
|
279
|
+
activityRef: detail.activityRef,
|
|
280
|
+
title: detail.title,
|
|
281
|
+
...(detail.subtitle ? { subtitle: detail.subtitle } : {}),
|
|
282
|
+
...(detail.description ? { description: detail.description } : {}),
|
|
283
|
+
...(detail.coverUrl ? { coverUrl: detail.coverUrl } : {}),
|
|
284
|
+
...(start ? { periodText: end && end !== start ? `${start} - ${end}` : `${start} 起` } : {}),
|
|
285
|
+
registrationRequired: detail.registrationRequired,
|
|
286
|
+
...(detail.jumpOnly ? { jumpOnly: true } : {}),
|
|
287
|
+
...(detail.jumpLabel ? { jumpLabel: detail.jumpLabel } : {}),
|
|
288
|
+
...(registrationUrl ? { registrationUrl } : {}),
|
|
289
|
+
...(detail.remainingQuota !== undefined ? { remainingQuota: detail.remainingQuota } : {}),
|
|
290
|
+
sessions: detail.sessions.map((session) => ({
|
|
291
|
+
sessionRef: session.sessionRef,
|
|
292
|
+
title: session.title,
|
|
293
|
+
startText: new Date(session.startAt).toLocaleString('zh-CN'),
|
|
294
|
+
...(session.endAt ? { endText: new Date(session.endAt).toLocaleString('zh-CN') } : {}),
|
|
295
|
+
...(session.location ? { location: session.location } : {}),
|
|
296
|
+
...(session.remainingQuota !== undefined ? { remainingQuota: session.remainingQuota } : {}),
|
|
297
|
+
status: session.status,
|
|
298
|
+
})),
|
|
299
|
+
guideSteps:
|
|
300
|
+
detail.guide?.steps.map((step) =>
|
|
301
|
+
step.description ? `${step.title}:${step.description}` : step.title,
|
|
302
|
+
) ?? [],
|
|
303
|
+
conditions: [...(detail.guide?.conditions ?? [])],
|
|
304
|
+
guideItems:
|
|
305
|
+
detail.guide?.steps.map((step) => ({
|
|
306
|
+
title: step.title,
|
|
307
|
+
...(step.description ? { description: step.description } : {}),
|
|
308
|
+
})) ?? [],
|
|
309
|
+
...(detail.guide?.notice ? { notice: detail.guide.notice } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const searchInput = coerce(
|
|
314
|
+
z.object({
|
|
315
|
+
keyword: z.string().optional(),
|
|
316
|
+
scene: z.string().optional(),
|
|
317
|
+
cursor: z.string().optional(),
|
|
318
|
+
limit: z.string().optional(),
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
const detailInput = coerce(z.object({ offerRef: z.string().optional(), keyword: z.string().optional() }));
|
|
322
|
+
const activityDetailInput = coerce(
|
|
323
|
+
z.object({ activityRef: z.string().optional(), keyword: z.string().optional() }),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
export function createPromotionTools(opts: { getCtx: () => ServerContext }) {
|
|
327
|
+
/** 活动工具与确定性活动意图共用联单分流;未启用策略/未装模块返回 undefined。 */
|
|
328
|
+
const delegateLinkedActivity = async (reqCtx: RequestContext) => {
|
|
329
|
+
const ctx = opts.getCtx();
|
|
330
|
+
const handler = ctx.handlers.resolve('alipay-activity');
|
|
331
|
+
if (!handler) return undefined;
|
|
332
|
+
const result = await handler.handle('activity-router', { content: '活动中心' }, ctx, reqCtx);
|
|
333
|
+
return result.continueWith ? undefined : result;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** 工具 execute 第二参 → 请求级 promotion 槽服务(require 变体 fail-visible)。
|
|
337
|
+
* 017 槽位原子化:按工具语义选槽——offer 族 → promotion.offers、权益族 →
|
|
338
|
+
* promotion.entitlements、活动族 → promotion.activities;listCoupons 按 mode 分流。 */
|
|
339
|
+
const requireService = async <T>(
|
|
340
|
+
execCtx: ToolExecContextLike,
|
|
341
|
+
resolve: (reqCtx: RequestContext, ctx: ServerContext) => Promise<T>,
|
|
342
|
+
): Promise<{ service: T; q: PromotionQueryContext; reqCtx: RequestContext }> => {
|
|
343
|
+
const ctx = opts.getCtx();
|
|
344
|
+
const reqCtx = requireToolRequestContext(execCtx);
|
|
345
|
+
const context = toQueryContext(reqCtx);
|
|
346
|
+
return { service: await resolve(context.reqCtx, ctx), q: context.q, reqCtx: context.reqCtx };
|
|
347
|
+
};
|
|
348
|
+
const supportsAcquire = (service: PromotionService | PromotionOfferQueryPort | PromotionAcquisitionPort) =>
|
|
349
|
+
(service as PromotionService).supportsAcquisition !== false; // 能力位运行期探测(fat 实例属性)
|
|
350
|
+
const resolveOffer = async (
|
|
351
|
+
service: PromotionOfferQueryPort,
|
|
352
|
+
q: PromotionQueryContext,
|
|
353
|
+
args: Record<string, unknown>,
|
|
354
|
+
offerTypes?: readonly OfferType[],
|
|
355
|
+
) => {
|
|
356
|
+
if (typeof args.offerRef === 'string' && args.offerRef) {
|
|
357
|
+
return service.getOfferDetail(q, { offerRef: args.offerRef });
|
|
358
|
+
}
|
|
359
|
+
const page = await service.searchOffers(q, {
|
|
360
|
+
keyword: args.keyword as string | undefined,
|
|
361
|
+
offerTypes,
|
|
362
|
+
limit: 1,
|
|
363
|
+
});
|
|
364
|
+
return page.items[0] ?? null;
|
|
365
|
+
};
|
|
366
|
+
const resolveActivity = async (
|
|
367
|
+
service: PromotionActivityPort,
|
|
368
|
+
q: PromotionQueryContext,
|
|
369
|
+
args: Record<string, unknown>,
|
|
370
|
+
) => {
|
|
371
|
+
if (typeof args.activityRef === 'string' && args.activityRef) {
|
|
372
|
+
return service.getActivityDetail(q, args.activityRef);
|
|
373
|
+
}
|
|
374
|
+
const page = await service.listActivities(q, { keyword: args.keyword as string | undefined, limit: 1 });
|
|
375
|
+
const first = page.items[0];
|
|
376
|
+
return first ? service.getActivityDetail(q, first.activityRef) : null;
|
|
377
|
+
};
|
|
378
|
+
const search = (
|
|
379
|
+
service: PromotionOfferQueryPort,
|
|
380
|
+
q: PromotionQueryContext,
|
|
381
|
+
args: Record<string, unknown>,
|
|
382
|
+
offerTypes?: readonly OfferType[],
|
|
383
|
+
) =>
|
|
384
|
+
service.searchOffers(q, {
|
|
385
|
+
keyword: args.keyword as string | undefined,
|
|
386
|
+
scene: args.scene as string | undefined,
|
|
387
|
+
cursor: args.cursor as string | undefined,
|
|
388
|
+
limit: args.limit ? Number(args.limit) : 50,
|
|
389
|
+
offerTypes,
|
|
390
|
+
});
|
|
391
|
+
const entitlementFacts = async (
|
|
392
|
+
service: PromotionEntitlementPort,
|
|
393
|
+
q: PromotionQueryContext,
|
|
394
|
+
args: Record<string, unknown>,
|
|
395
|
+
reqCtx?: RequestContext,
|
|
396
|
+
) => {
|
|
397
|
+
// 会员门禁(我的券 = 属人查询):硬门禁态出 member 域 CTA 卡(mall 模板投影半屏闭环);
|
|
398
|
+
// member 未装 / unknown / unavailable → 软放行(InMemory 演示态照常)
|
|
399
|
+
const ctx = opts.getCtx();
|
|
400
|
+
const factory = ctx.services?.resolve?.('member.status') as MemberStatusFactory | undefined;
|
|
401
|
+
const gate = reqCtx ? await resolveMemberGate(factory, reqCtx) : undefined;
|
|
402
|
+
if (gate) {
|
|
403
|
+
return {
|
|
404
|
+
found: false as const,
|
|
405
|
+
items: [] as never[],
|
|
406
|
+
noResultsReason: gate.notice,
|
|
407
|
+
...(gate.card ? { cards: [gate.card] } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const page = await service.listEntitlements(q, {
|
|
411
|
+
cursor: args.cursor as string | undefined,
|
|
412
|
+
limit: args.limit ? Number(args.limit) : 50,
|
|
413
|
+
});
|
|
414
|
+
return {
|
|
415
|
+
mallId: q.scope.mallId,
|
|
416
|
+
miniAppId: q.scope.miniAppId,
|
|
417
|
+
items: page.items.map(entitlementSummary),
|
|
418
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
419
|
+
...(page.items.length === 0 ? { noResultsReason: '还没有优惠券,可让我帮你找找可领优惠' } : {}),
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const listActivityCategories = createTool({
|
|
424
|
+
id: 'listActivityCategories',
|
|
425
|
+
description: '查询当前商场全部活动分类,用于亲子、市集、美食等活动筛选。',
|
|
426
|
+
inputSchema: coerce(z.object({})),
|
|
427
|
+
execute: async (_args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
428
|
+
const reqCtx = requireToolRequestContext(execCtx, 'listActivityCategories');
|
|
429
|
+
const delegated = await delegateLinkedActivity(reqCtx);
|
|
430
|
+
if (delegated) return delegated;
|
|
431
|
+
const { service } = await requireService(execCtx, resolvePromotionActivitiesPort);
|
|
432
|
+
const items = (await service.listActivityCategories?.()) ?? [];
|
|
433
|
+
const facts = {
|
|
434
|
+
items: items.map((item) => ({
|
|
435
|
+
...item,
|
|
436
|
+
itemActions: [
|
|
437
|
+
{
|
|
438
|
+
kind: 'send' as const,
|
|
439
|
+
label: `查看${item.name}活动`,
|
|
440
|
+
value: promotionNavigationQuery.activityCategory(item.name),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
})),
|
|
444
|
+
...(items.length === 0 ? { noResultsReason: '当前商场暂未返回活动分类' } : {}),
|
|
445
|
+
};
|
|
446
|
+
return items.length ? withCards(facts, 'promotion-activity-category-list', `活动分类 ${items.length} 个${items[0] ? ` · 含${items[0]!.name}` : ''}`) : facts;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const listActivities = createTool({
|
|
451
|
+
id: 'listActivities',
|
|
452
|
+
description: '按分类、关键词、当前/往期、报名要求和日期范围查询商场活动;查询本周活动时传本周日期范围。',
|
|
453
|
+
inputSchema: coerce(
|
|
454
|
+
z.object({
|
|
455
|
+
categoryId: z.string().optional(),
|
|
456
|
+
keyword: z.string().optional(),
|
|
457
|
+
dataScope: z.enum(['CURRENT', 'HISTORY']).optional(),
|
|
458
|
+
registrationRequired: z.string().optional(),
|
|
459
|
+
startsAfter: z.string().optional(),
|
|
460
|
+
endsBefore: z.string().optional(),
|
|
461
|
+
cursor: z.string().optional(),
|
|
462
|
+
limit: z.string().optional(),
|
|
463
|
+
}),
|
|
464
|
+
),
|
|
465
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
466
|
+
const reqCtx = requireToolRequestContext(execCtx, 'listActivities');
|
|
467
|
+
const delegated = await delegateLinkedActivity(reqCtx);
|
|
468
|
+
if (delegated) return delegated;
|
|
469
|
+
const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
|
|
470
|
+
let categoryId = args.categoryId as string | undefined;
|
|
471
|
+
// resolveCategoryId = 模块 fat 扩展能力口(非契约 Port 成员),运行期 typeof 探测
|
|
472
|
+
const resolveCategoryId = (service as PromotionService).resolveCategoryId;
|
|
473
|
+
if (categoryId && resolveCategoryId) categoryId = await resolveCategoryId(categoryId);
|
|
474
|
+
const page = await service.listActivities(q, {
|
|
475
|
+
categoryId,
|
|
476
|
+
keyword: args.keyword as string | undefined,
|
|
477
|
+
dataScope: args.dataScope as 'CURRENT' | 'HISTORY' | undefined,
|
|
478
|
+
registrationRequired:
|
|
479
|
+
args.registrationRequired === undefined ? undefined : args.registrationRequired === 'true',
|
|
480
|
+
startsAfter: args.startsAfter as string | undefined,
|
|
481
|
+
endsBefore: args.endsBefore as string | undefined,
|
|
482
|
+
cursor: args.cursor as string | undefined,
|
|
483
|
+
limit: args.limit ? Number(args.limit) : 10,
|
|
484
|
+
});
|
|
485
|
+
const categories = (await service.listActivityCategories?.()) ?? [];
|
|
486
|
+
const categoryNames = new Map(categories.map((item) => [item.categoryId, item.name]));
|
|
487
|
+
const facts = {
|
|
488
|
+
items: page.items.map((item) => activitySummary(item, categoryNames)),
|
|
489
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
490
|
+
...(page.items.length === 0 ? { noResultsReason: '当前筛选范围内暂无活动' } : {}),
|
|
491
|
+
};
|
|
492
|
+
return page.items.length ? withCards(facts, 'promotion-activity-list', `商场活动 ${page.items.length} 场${page.items[0] ? ` · ${page.items[0].title}` : ''}`) : facts;
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const getActivityDetail = createTool({
|
|
497
|
+
id: 'getActivityDetail',
|
|
498
|
+
description: '查询指定活动详情、时间、名额和参与条件。',
|
|
499
|
+
inputSchema: activityDetailInput,
|
|
500
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
501
|
+
const reqCtx = requireToolRequestContext(execCtx, 'getActivityDetail');
|
|
502
|
+
const delegated = await delegateLinkedActivity(reqCtx);
|
|
503
|
+
if (delegated) return delegated;
|
|
504
|
+
const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
|
|
505
|
+
const detail = await resolveActivity(service, q, args);
|
|
506
|
+
if (!detail) return { found: false, message: '未找到该活动' };
|
|
507
|
+
const facts = { found: true as const, ...activityDetail(detail, 'detail') };
|
|
508
|
+
return withCards(facts, 'promotion-activity-detail', `活动详情:${detail.title}`);
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const listActivitySessions = createTool({
|
|
513
|
+
id: 'listActivitySessions',
|
|
514
|
+
description: '查询指定活动的可选场次,只返回只读状态,不执行报名。',
|
|
515
|
+
inputSchema: activityDetailInput,
|
|
516
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
517
|
+
const reqCtx = requireToolRequestContext(execCtx, 'listActivitySessions');
|
|
518
|
+
const delegated = await delegateLinkedActivity(reqCtx);
|
|
519
|
+
if (delegated) return delegated;
|
|
520
|
+
const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
|
|
521
|
+
const detail = await resolveActivity(service, q, args);
|
|
522
|
+
if (!detail) return { found: false, message: '未找到该活动' };
|
|
523
|
+
const sessions = service.listActivitySessions
|
|
524
|
+
? await service.listActivitySessions(q, detail.activityRef)
|
|
525
|
+
: detail.sessions;
|
|
526
|
+
const facts = activityDetail({ ...detail, sessions }, 'schedule');
|
|
527
|
+
return withCards(facts, 'promotion-activity-detail', `活动场次:${detail.title}`);
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const getActivityGuide = createTool({
|
|
532
|
+
id: 'getActivityGuide',
|
|
533
|
+
description: '查询指定活动的报名条件和参与步骤,不执行报名。',
|
|
534
|
+
inputSchema: activityDetailInput,
|
|
535
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
536
|
+
const reqCtx = requireToolRequestContext(execCtx, 'getActivityGuide');
|
|
537
|
+
const delegated = await delegateLinkedActivity(reqCtx);
|
|
538
|
+
if (delegated) return delegated;
|
|
539
|
+
const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
|
|
540
|
+
const detail = await resolveActivity(service, q, args);
|
|
541
|
+
if (!detail) return { found: false, message: '未找到该活动' };
|
|
542
|
+
const facts = activityDetail(detail, 'guide');
|
|
543
|
+
return withCards(facts, 'promotion-activity-detail', `活动参与方式:${detail.title}`);
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const searchOffers = createTool({
|
|
548
|
+
id: 'searchOffers',
|
|
549
|
+
description: '搜索当前商场优惠、折扣、团购并支持分页。',
|
|
550
|
+
inputSchema: searchInput,
|
|
551
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
552
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
553
|
+
const page = await search(service, q, args);
|
|
554
|
+
const facts = {
|
|
555
|
+
presentation: offerListPresentation(page.items),
|
|
556
|
+
mode: 'search' as const,
|
|
557
|
+
items: page.items.map(offerSummary),
|
|
558
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
559
|
+
...(page.items.length === 0 ? { noResultsReason: '未找到相关优惠,可换个关键词试试' } : {}),
|
|
560
|
+
};
|
|
561
|
+
return page.items.length ? withCards(facts, 'promotion-offer-list', `优惠搜索 ${page.items.length} 条${page.items[0] ? ` · ${page.items[0].title}` : ''}`) : facts;
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const getOfferDetail = createTool({
|
|
566
|
+
id: 'getOfferDetail',
|
|
567
|
+
description:
|
|
568
|
+
'查询通用优惠详情卡(规则、门槛、有效期及领取状态),支持 offerRef、entitlementRef 或关键词反查。',
|
|
569
|
+
inputSchema: coerce(
|
|
570
|
+
z.object({ offerRef: z.string().optional(), entitlementRef: z.string().optional(), keyword: z.string().optional() }),
|
|
571
|
+
),
|
|
572
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
573
|
+
const { service, q, reqCtx } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
574
|
+
// entitlementRef 反查 = 我的券详情(属人):仅该分支门禁;offerRef/keyword 公共反查不拦
|
|
575
|
+
if (typeof args.entitlementRef === 'string' && args.entitlementRef) {
|
|
576
|
+
const ctx = opts.getCtx();
|
|
577
|
+
const factory = ctx.services?.resolve?.('member.status') as MemberStatusFactory | undefined;
|
|
578
|
+
const gate = await resolveMemberGate(factory, reqCtx);
|
|
579
|
+
if (gate) {
|
|
580
|
+
return { found: false as const, noResultsReason: gate.notice, ...(gate.card ? { cards: [gate.card] } : {}) };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const offer = await service.getOfferDetail(q, {
|
|
584
|
+
offerRef: args.offerRef as string | undefined,
|
|
585
|
+
entitlementRef: args.entitlementRef as string | undefined,
|
|
586
|
+
keyword: args.keyword as string | undefined,
|
|
587
|
+
});
|
|
588
|
+
if (!offer) return { found: false, message: '未找到该优惠' };
|
|
589
|
+
const facts = {
|
|
590
|
+
found: true as const,
|
|
591
|
+
...offerDetail(offer, supportsAcquire(service), offer.offerType === 'COUPON' ? 'coupon' : offerPresentation(offer.offerType)),
|
|
592
|
+
};
|
|
593
|
+
return withCards(facts, 'promotion-offer-detail', `优惠详情:${offer.title}`);
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const listEntitlements = createTool({
|
|
598
|
+
id: 'listEntitlements',
|
|
599
|
+
description: '仅查询当前登录会员已经领取/拥有的优惠权益和券包;用户问“有什么可领取/可领优惠券”时禁止调用,改用 listCoupons(mode=CLAIMABLE)。已登录未入会时返回入会引导卡(member-enrollment-cta),无需再调 member 域 queryEnrollmentCta。',
|
|
600
|
+
inputSchema: searchInput,
|
|
601
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
602
|
+
const { service, q, reqCtx } = await requireService(execCtx, resolvePromotionEntitlementsPort);
|
|
603
|
+
const facts = await entitlementFacts(service, q, args, reqCtx);
|
|
604
|
+
return facts.items.length
|
|
605
|
+
? withCards(
|
|
606
|
+
facts,
|
|
607
|
+
'promotion-entitlement-list',
|
|
608
|
+
`我的优惠券 ${facts.items.length} 张${facts.items[0] ? ` · ${facts.items[0].title}` : ''}`,
|
|
609
|
+
)
|
|
610
|
+
: facts;
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const listPromotions = createTool({
|
|
615
|
+
id: 'listPromotions',
|
|
616
|
+
description: '查询本周营销专题和促销活动。',
|
|
617
|
+
inputSchema: searchInput,
|
|
618
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
619
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
620
|
+
const page = await search(service, q, args, ['PROMOTION']);
|
|
621
|
+
const facts = {
|
|
622
|
+
presentation: 'promotion' as const,
|
|
623
|
+
mode: 'search' as const,
|
|
624
|
+
summary: '本周值得关注的活动',
|
|
625
|
+
items: page.items.map(offerSummary),
|
|
626
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
627
|
+
...(page.items.length === 0 ? { noResultsReason: '本周暂未查询到营销专题' } : {}),
|
|
628
|
+
};
|
|
629
|
+
return page.items.length ? withCards(facts, 'promotion-offer-list', `营销专题 ${page.items.length} 个${page.items[0] ? ` · ${page.items[0].title}` : ''}`) : facts;
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const getPromotionDetail = createTool({
|
|
634
|
+
id: 'getPromotionDetail',
|
|
635
|
+
description: '按营销专题标识或关键词精确查询活动详情。',
|
|
636
|
+
inputSchema: detailInput,
|
|
637
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
638
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
639
|
+
const offer = await resolveOffer(service, q, args, ['PROMOTION']);
|
|
640
|
+
if (!offer) return { found: false, message: '未找到该营销专题' };
|
|
641
|
+
const facts = { found: true as const, ...offerDetail(offer, false, 'promotion') };
|
|
642
|
+
return withCards(facts, 'promotion-offer-detail', `营销活动详情:${offer.title}`);
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const getParticipationGuide = createTool({
|
|
647
|
+
id: 'getParticipationGuide',
|
|
648
|
+
description: '查询营销专题的厂商参与步骤,不执行报名或领取。',
|
|
649
|
+
inputSchema: detailInput,
|
|
650
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
651
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
652
|
+
const offer = await resolveOffer(service, q, args, ['PROMOTION']);
|
|
653
|
+
if (!offer) return { found: false, message: '未找到该营销专题' };
|
|
654
|
+
const facts = { found: true as const, ...offerDetail(offer, false, 'promotion-guide') };
|
|
655
|
+
return withCards(facts, 'promotion-offer-detail', `参与方式:${offer.title}`);
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const listCoupons = createTool({
|
|
660
|
+
id: 'listCoupons',
|
|
661
|
+
description: '查询优惠券列表。默认只查询当前可领取优惠券(CLAIMABLE);只有用户明确说“我的券/已领取/券包”时才传 mode=OWNED。mode=OWNED 已登录未入会时返回入会引导卡(member-enrollment-cta),无需再调 member 域 queryEnrollmentCta。',
|
|
662
|
+
inputSchema: coerce(
|
|
663
|
+
z.object({
|
|
664
|
+
mode: z.enum(['CLAIMABLE', 'OWNED']).default('CLAIMABLE'),
|
|
665
|
+
keyword: z.string().optional(),
|
|
666
|
+
cursor: z.string().optional(),
|
|
667
|
+
limit: z.string().optional(),
|
|
668
|
+
}),
|
|
669
|
+
),
|
|
670
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
671
|
+
// 模式分流双槽(017):OWNED = 我的券 → entitlements 槽;CLAIMABLE = 可领券 → offers 槽
|
|
672
|
+
if (args.mode === 'OWNED') {
|
|
673
|
+
const { service, q, reqCtx } = await requireService(execCtx, resolvePromotionEntitlementsPort);
|
|
674
|
+
const facts = await entitlementFacts(service, q, args, reqCtx);
|
|
675
|
+
return facts.items.length
|
|
676
|
+
? withCards(facts, 'promotion-entitlement-list', `我的优惠券 ${facts.items.length} 张${facts.items[0] ? ` · ${facts.items[0].title ?? ''}` : ''}`)
|
|
677
|
+
: facts;
|
|
678
|
+
}
|
|
679
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
680
|
+
const page = await search(service, q, args, ['COUPON']);
|
|
681
|
+
const items = page.items.filter((item) => item.availability.status === 'AVAILABLE');
|
|
682
|
+
const facts = {
|
|
683
|
+
presentation: 'coupon' as const,
|
|
684
|
+
mode: 'claimable' as const,
|
|
685
|
+
summary: '可领取优惠券',
|
|
686
|
+
items: items.map(offerSummary),
|
|
687
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
688
|
+
...(items.length === 0 ? { noResultsReason: '当前商场暂未返回可领优惠券' } : {}),
|
|
689
|
+
};
|
|
690
|
+
return items.length ? withCards(facts, 'promotion-offer-list', `可领优惠券 ${items.length} 张${items[0] ? ` · ${items[0].title}` : ''}`) : facts;
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const getPackageDetail = createTool({
|
|
695
|
+
id: 'getPackageDetail',
|
|
696
|
+
description: '查询组合礼包及其子权益。',
|
|
697
|
+
inputSchema: detailInput,
|
|
698
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
699
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
700
|
+
const offer = await resolveOffer(service, q, args, ['BUNDLE']);
|
|
701
|
+
if (!offer) return { found: false, message: '未找到该优惠礼包' };
|
|
702
|
+
const facts = {
|
|
703
|
+
found: true as const,
|
|
704
|
+
...offerDetail(offer, supportsAcquire(service), 'package'),
|
|
705
|
+
};
|
|
706
|
+
return withCards(facts, 'promotion-offer-detail', `优惠礼包:${offer.title}`);
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const recommendOffers = createTool({
|
|
711
|
+
id: 'recommendOffers',
|
|
712
|
+
description: '按资格、可用性与预计节省额推荐当前商场优惠。',
|
|
713
|
+
inputSchema: searchInput,
|
|
714
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
715
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
716
|
+
const items = await service.recommendOffers(q, {
|
|
717
|
+
keyword: args.keyword as string | undefined,
|
|
718
|
+
scene: args.scene as string | undefined,
|
|
719
|
+
limit: args.limit ? Number(args.limit) : 4,
|
|
720
|
+
});
|
|
721
|
+
const unifiedCouponList = (service as PromotionService).usesUnifiedCouponList === true; // 能力位运行期探测
|
|
722
|
+
const facts = {
|
|
723
|
+
presentation: unifiedCouponList ? 'coupon' as const : 'recommendation' as const,
|
|
724
|
+
mode: unifiedCouponList ? 'claimable' as const : 'recommend' as const,
|
|
725
|
+
summary:
|
|
726
|
+
items[0]?.availability.status === 'AVAILABLE'
|
|
727
|
+
? '当前可用,并优先考虑预计节省额'
|
|
728
|
+
: '当前暂时无法确认可用优惠,请查看候选条件',
|
|
729
|
+
items: items.slice(0, 4).map(offerSummary),
|
|
730
|
+
...(items.length === 0 ? { noResultsReason: '当前暂无可推荐优惠' } : {}),
|
|
731
|
+
};
|
|
732
|
+
return items.length ? withCards(facts, 'promotion-offer-list', `推荐优惠 ${items.length} 条${items[0] ? ` · ${items[0].title}` : ''}`) : facts;
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const calculateBestDeal = createTool({
|
|
737
|
+
id: 'calculateBestDeal',
|
|
738
|
+
description: '调用已装配厂商能力进行优惠测算;未装配测算能力时仅返回文字说明,不出测算卡。',
|
|
739
|
+
inputSchema: coerce(
|
|
740
|
+
z.object({ currentAmountCents: z.string(), candidateOfferRefs: z.array(z.string()).optional() }),
|
|
741
|
+
),
|
|
742
|
+
execute: async (args: Record<string, unknown>, execCtx: ToolExecContextLike) => {
|
|
743
|
+
const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
|
|
744
|
+
const amount = Number(args.currentAmountCents);
|
|
745
|
+
if (!Number.isSafeInteger(amount) || amount < 0) return { supported: true, message: '请提供有效的消费金额(分)' };
|
|
746
|
+
const bestDealService = service as PromotionService; // 能力位/能力口运行期探测(fat 实例属性)
|
|
747
|
+
if (!bestDealService.calculateBestDeal || bestDealService.supportsBestDeal === false) {
|
|
748
|
+
return { supported: false, originalAmountCents: amount, message: '当前商场暂不支持优惠测算,请在结算页确认最终优惠' };
|
|
749
|
+
}
|
|
750
|
+
const result = await bestDealService.calculateBestDeal(q, {
|
|
751
|
+
currentAmountCents: amount,
|
|
752
|
+
candidateOfferRefs: args.candidateOfferRefs as string[] | undefined,
|
|
753
|
+
});
|
|
754
|
+
const facts = {
|
|
755
|
+
authoritative: true,
|
|
756
|
+
originalAmountCents: amount,
|
|
757
|
+
savingsCents: result.estimatedSavingCents,
|
|
758
|
+
expectedPayCents: Math.max(0, amount - result.estimatedSavingCents),
|
|
759
|
+
appliedOfferRefs: result.offerRef ? [result.offerRef] : [],
|
|
760
|
+
message: result.reason,
|
|
761
|
+
};
|
|
762
|
+
return withCards(
|
|
763
|
+
facts,
|
|
764
|
+
'promotion-best-deal',
|
|
765
|
+
`预计省 ${(result.estimatedSavingCents / 100).toFixed(0)} 元 · 应付 ${(Math.max(0, amount - result.estimatedSavingCents) / 100).toFixed(0)} 元`,
|
|
766
|
+
);
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// W2(2026-09-01):出卡工具创建点单点 wrap(description 追加预告知句)——
|
|
771
|
+
// 注册即 wrapped 实例;领券/领取类动作卡带 actions 句。
|
|
772
|
+
return {
|
|
773
|
+
listActivityCategories: withCardEmissionHint(listActivityCategories),
|
|
774
|
+
listActivities: withCardEmissionHint(listActivities),
|
|
775
|
+
getActivityDetail: withCardEmissionHint(getActivityDetail),
|
|
776
|
+
listActivitySessions: withCardEmissionHint(listActivitySessions),
|
|
777
|
+
getActivityGuide: withCardEmissionHint(getActivityGuide),
|
|
778
|
+
searchOffers: withCardEmissionHint(searchOffers),
|
|
779
|
+
getOfferDetail: withCardEmissionHint(getOfferDetail, { actions: true }),
|
|
780
|
+
listEntitlements: withCardEmissionHint(listEntitlements),
|
|
781
|
+
listPromotions: withCardEmissionHint(listPromotions),
|
|
782
|
+
getPromotionDetail: withCardEmissionHint(getPromotionDetail),
|
|
783
|
+
getParticipationGuide: withCardEmissionHint(getParticipationGuide),
|
|
784
|
+
listCoupons: withCardEmissionHint(listCoupons, { actions: true }),
|
|
785
|
+
getPackageDetail: withCardEmissionHint(getPackageDetail, { actions: true }),
|
|
786
|
+
recommendOffers: withCardEmissionHint(recommendOffers),
|
|
787
|
+
calculateBestDeal: withCardEmissionHint(calculateBestDeal),
|
|
788
|
+
};
|
|
789
|
+
}
|