@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.
Files changed (62) hide show
  1. package/AGENTS.md +16 -0
  2. package/README.md +90 -11
  3. package/dist/chunk-QOTX4XDS.js +1745 -0
  4. package/dist/chunk-S7CVFCML.js +1330 -0
  5. package/dist/client/index.d.ts +21 -3
  6. package/dist/client/index.js +4 -2
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +16 -4
  9. package/dist/promotion-6L7HLRC2.css +2135 -0
  10. package/dist/server/index.d.ts +2481 -41
  11. package/dist/server/index.js +13 -3
  12. package/package.json +28 -15
  13. package/skills/promotion/SKILL.md +137 -0
  14. package/skills/promotion/references/claim-loop.md +18 -0
  15. package/src/client/cards/index.ts +10 -0
  16. package/src/client/cards/promotion-activity-category-list/index.tsx +34 -0
  17. package/src/client/cards/promotion-activity-detail/index.tsx +84 -0
  18. package/src/client/cards/promotion-activity-list/index.tsx +56 -0
  19. package/src/client/cards/promotion-best-deal/index.tsx +18 -0
  20. package/src/client/cards/promotion-entitlement-list/index.tsx +161 -0
  21. package/src/client/cards/promotion-offer-detail/index.tsx +116 -0
  22. package/src/client/cards/promotion-offer-list/index.tsx +283 -0
  23. package/src/client/cards/view.ts +72 -0
  24. package/src/client/components/CouponQrCode.tsx +48 -0
  25. package/src/client/components/CouponSheet.tsx +124 -0
  26. package/src/client/components/ImageWithFallback.tsx +39 -0
  27. package/src/client/index.ts +23 -4
  28. package/src/client/styles/promotion.css +2135 -0
  29. package/src/client/types/css.d.ts +2 -0
  30. package/src/client/utils/sanitize-activity-html.ts +48 -0
  31. package/src/client/utils/scroll-card-bottom.ts +40 -0
  32. package/src/server/actions.ts +217 -0
  33. package/src/server/cards/index.ts +19 -0
  34. package/src/server/cards/promotion-activity-category-list/meta.ts +13 -0
  35. package/src/server/cards/promotion-activity-detail/meta.ts +29 -0
  36. package/src/server/cards/promotion-activity-list/meta.ts +13 -0
  37. package/src/server/cards/promotion-best-deal/meta.ts +20 -0
  38. package/src/server/cards/promotion-entitlement-list/meta.ts +17 -0
  39. package/src/server/cards/promotion-entitlement-list/samples.ts +51 -0
  40. package/src/server/cards/promotion-offer-detail/meta.ts +25 -0
  41. package/src/server/cards/promotion-offer-list/meta.ts +20 -0
  42. package/src/server/cards/promotion-offer-list/samples.ts +154 -0
  43. package/src/server/handler.ts +378 -16
  44. package/src/server/index.ts +117 -22
  45. package/src/server/navigation.ts +23 -0
  46. package/src/server/ports.ts +56 -0
  47. package/src/server/service.ts +243 -24
  48. package/src/server/tools.ts +789 -0
  49. package/tbox.module.json +126 -0
  50. package/tests/cards-render.test.tsx +656 -0
  51. package/tests/ports-resolve.test.ts +96 -0
  52. package/tests/pretool-coupons.test.ts +63 -0
  53. package/tests/promotion.test.ts +976 -0
  54. package/tests/samples.test.ts +42 -0
  55. package/tests/view.test.ts +61 -0
  56. package/tsup.config.ts +9 -1
  57. package/dist/chunk-AD6OLHSM.js +0 -86
  58. package/dist/chunk-LQGF6337.js +0 -32
  59. package/src/client/cards/coupon/index.tsx +0 -12
  60. package/src/server/cards/coupon/meta.ts +0 -23
  61. package/tbox.component.json +0 -20
  62. package/tests/service.test.ts +0 -31
@@ -0,0 +1,1745 @@
1
+ // src/server/index.ts
2
+ import { definePreTool, getRequestContext, loadModuleSkills } from "@tbox.cn/app-sdk/server";
3
+ import { Router } from "express";
4
+ import { toMallQueryContext as toMallQueryContext4, withMallScopeFromQuery } from "@tbox.cn/app-contracts-mall";
5
+
6
+ // src/server/ports.ts
7
+ import {
8
+ mallScopeOf,
9
+ PROMOTION_ACQUISITION_SERVICE,
10
+ PROMOTION_ACTIVITIES_SERVICE,
11
+ PROMOTION_ENTITLEMENTS_SERVICE,
12
+ PROMOTION_OFFERS_SERVICE
13
+ } from "@tbox.cn/app-contracts-mall";
14
+ import { isExpectedProviderAbsence } from "@tbox.cn/app-contracts";
15
+
16
+ // src/server/service.ts
17
+ var now = () => (/* @__PURE__ */ new Date()).toISOString();
18
+ var inDays = (d) => new Date(Date.now() + d * 864e5).toISOString();
19
+ var DEMO_OFFERS = [
20
+ {
21
+ offerRef: "demo:coupon:1001",
22
+ offerType: "COUPON",
23
+ title: "\u6EE1 100 \u51CF 20 \u5143\u5238",
24
+ subtitle: "\u5168\u573A\u901A\u7528\uFF08\u90E8\u5206\u7279\u4F8B\u5546\u54C1\u9664\u5916\uFF09",
25
+ tags: ["\u901A\u7528"],
26
+ validity: { start: now(), end: inDays(30) },
27
+ facets: [{ kind: "COUPON", discountType: "AMOUNT", value: 2e3, thresholdCents: 1e4 }],
28
+ availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: now(), authoritative: false },
29
+ participation: { mode: "CLAIM_THEN_PURCHASE", steps: [{ title: "\u9886\u53D6", description: "\u70B9\u51FB\u9886\u53D6\u540E\u81EA\u52A8\u5B58\u5165\u5361\u5305" }] }
30
+ },
31
+ {
32
+ offerRef: "demo:coupon:1002",
33
+ offerType: "COUPON",
34
+ title: "\u9910\u996E 85 \u6298\u5238",
35
+ subtitle: "\u9650\u9910\u996E\u697C\u5C42\u95E8\u5E97",
36
+ tags: ["\u9910\u996E"],
37
+ validity: { start: now(), end: inDays(15) },
38
+ facets: [{ kind: "COUPON", discountType: "PERCENTAGE", value: 85, thresholdCents: 5e3 }],
39
+ availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: now(), authoritative: false }
40
+ },
41
+ {
42
+ offerRef: "demo-c:groupon:2001",
43
+ offerType: "GROUPON",
44
+ title: "\u53CC\u4EBA\u9910\u56E2\u8D2D 129 \u5143",
45
+ tags: ["\u56E2\u8D2D", "\u9910\u996E"],
46
+ validity: { end: inDays(20) },
47
+ facets: [{ kind: "GROUPON", requiredParticipants: 2 }],
48
+ availability: { status: "AVAILABLE", reasonCodes: [], evaluatedAt: now(), authoritative: true }
49
+ }
50
+ ];
51
+ var DEMO_ACTIVITY = {
52
+ activityRef: "demo-act:3001",
53
+ title: "\u5468\u672B\u4EB2\u5B50\u5E02\u96C6",
54
+ subtitle: "\u624B\u4F5C\u4F53\u9A8C + \u4EB2\u5B50\u6E38\u620F",
55
+ categoryId: "family",
56
+ registrationRequired: true,
57
+ remainingQuota: 12,
58
+ startAt: inDays(3),
59
+ endAt: inDays(4),
60
+ offerRefs: ["demo:coupon:1002"],
61
+ description: "\u672C\u5468\u672B\u4E2D\u5EAD\u4EB2\u5B50\u5E02\u96C6\uFF0C\u542B 10+ \u624B\u4F5C\u644A\u4F4D",
62
+ sessions: [
63
+ { sessionRef: "s_1", title: "\u7B2C\u4E00\u573A 10:00-12:00", startAt: inDays(3), remainingQuota: 5, status: "OPEN" },
64
+ { sessionRef: "s_2", title: "\u7B2C\u4E8C\u573A 14:00-16:00", startAt: inDays(3), remainingQuota: 7, status: "OPEN" }
65
+ ],
66
+ guide: {
67
+ steps: [
68
+ { title: "\u9886\u53D6\u4F18\u60E0\u5238", description: "\u9886\u53D6\u9910\u996E 85 \u6298\u5238\uFF0C\u5E02\u96C6\u9910\u996E\u644A\u4F4D\u53EF\u7528" },
69
+ { title: "\u9009\u62E9\u573A\u6B21\u62A5\u540D", description: "\u51ED\u62A5\u540D\u7801\u5165\u573A" }
70
+ ],
71
+ conditions: ["\u9650\u4F1A\u5458\u672C\u4EBA\u53C2\u4E0E", "\u6BCF\u573A\u9650\u91CF 20 \u7EC4\u5BB6\u5EAD"]
72
+ }
73
+ };
74
+ var InMemoryPromotionService = class {
75
+ supportsAcquisition = true;
76
+ entitlements = /* @__PURE__ */ new Map();
77
+ operations = /* @__PURE__ */ new Map();
78
+ acquireKeys = /* @__PURE__ */ new Map();
79
+ // idempotencyKey → operationRef
80
+ async searchOffers(_q, input) {
81
+ const keyword = input.keyword?.trim();
82
+ const typed = input.offerTypes?.length ? DEMO_OFFERS.filter((offer) => input.offerTypes.includes(offer.offerType)) : DEMO_OFFERS;
83
+ const items = keyword ? typed.filter((o) => o.title.includes(keyword) || o.tags.some((t) => t.includes(keyword))) : typed;
84
+ const limit = Math.min(50, input.limit ?? 10);
85
+ return { items: items.slice(0, limit), hasMore: items.length > limit };
86
+ }
87
+ async getOfferDetail(_q, input) {
88
+ if (input.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === input.offerRef) ?? null;
89
+ if (input.entitlementRef) {
90
+ for (const list of this.entitlements.values()) {
91
+ const ent = list.find((e) => e.entitlementRef === input.entitlementRef);
92
+ if (ent?.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === ent.offerRef) ?? null;
93
+ }
94
+ return null;
95
+ }
96
+ if (input.keyword) {
97
+ const page = await this.searchOffers(_q, { keyword: input.keyword, limit: 1 });
98
+ return page.items[0] ?? null;
99
+ }
100
+ return null;
101
+ }
102
+ async recommendOffers(q, input) {
103
+ const page = await this.searchOffers(q, { ...input, limit: 3 });
104
+ return page.items;
105
+ }
106
+ async calculateBestDeal(_q, input) {
107
+ const candidates = input.candidateOfferRefs ? DEMO_OFFERS.filter((o) => input.candidateOfferRefs.includes(o.offerRef)) : DEMO_OFFERS;
108
+ let best = {
109
+ offerRef: "",
110
+ title: "\u6682\u65E0\u9002\u7528\u4F18\u60E0",
111
+ estimatedSavingCents: 0,
112
+ reason: "\u5F53\u524D\u91D1\u989D\u672A\u8FBE\u5230\u53EF\u7528\u95E8\u69DB",
113
+ candidatesConsidered: candidates.length
114
+ };
115
+ for (const offer of candidates) {
116
+ const facet = offer.facets.find((f) => f.kind === "COUPON");
117
+ if (!facet) continue;
118
+ const threshold = facet.thresholdCents ?? 0;
119
+ if (input.currentAmountCents < threshold) continue;
120
+ const saving = facet.discountType === "AMOUNT" ? facet.value : Math.round(input.currentAmountCents * (100 - facet.value) / 100);
121
+ if (saving > best.estimatedSavingCents) {
122
+ best = {
123
+ offerRef: offer.offerRef,
124
+ title: offer.title,
125
+ estimatedSavingCents: saving,
126
+ reason: `\u6EE1 ${(threshold / 100).toFixed(0)} \u5143\u53EF\u7528\uFF0C\u9884\u8BA1\u7701 ${(saving / 100).toFixed(2)} \u5143`,
127
+ candidatesConsidered: candidates.length
128
+ };
129
+ }
130
+ }
131
+ return best;
132
+ }
133
+ async listEntitlements(q, _input) {
134
+ const items = this.entitlementsOf(q);
135
+ return { items, hasMore: false };
136
+ }
137
+ async getEntitlementDetail(q, entitlementRef) {
138
+ return this.entitlementsOf(q).find((e) => e.entitlementRef === entitlementRef) ?? null;
139
+ }
140
+ async listActivities(_q, input) {
141
+ const { sessions: _s, guide: _g, ...activity } = DEMO_ACTIVITY;
142
+ void _s;
143
+ void _g;
144
+ const keyword = input.keyword?.trim();
145
+ const matchesKeyword = !keyword || `${activity.title} ${activity.subtitle ?? ""}`.includes(keyword);
146
+ const matchesRegistration = input.registrationRequired === void 0 || activity.registrationRequired === input.registrationRequired;
147
+ const matchesCategory = !input.categoryId || activity.categoryId === input.categoryId;
148
+ return {
149
+ items: matchesKeyword && matchesRegistration && matchesCategory ? [activity] : [],
150
+ hasMore: false
151
+ };
152
+ }
153
+ async getActivityDetail(_q, activityRef) {
154
+ return DEMO_ACTIVITY.activityRef === activityRef ? DEMO_ACTIVITY : null;
155
+ }
156
+ async listActivitySessions(_q, activityRef) {
157
+ return activityRef === DEMO_ACTIVITY.activityRef ? DEMO_ACTIVITY.sessions : [];
158
+ }
159
+ async listActivityCategories() {
160
+ return [{ categoryId: "family", name: "\u4EB2\u5B50" }];
161
+ }
162
+ async acquire(q, input) {
163
+ const existingRef = this.acquireKeys.get(input.idempotencyKey);
164
+ if (existingRef) {
165
+ const existing = this.operations.get(existingRef);
166
+ if (existing) return existing;
167
+ }
168
+ const offer = DEMO_OFFERS.find((o) => o.offerRef === input.offerRef);
169
+ const operationRef = `op_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
170
+ const operation = offer ? { operationRef, status: "SUCCEEDED", offerRef: offer.offerRef, codeMasked: "AB**CD**EF", observedAt: now() } : { operationRef, status: "REJECTED", offerRef: input.offerRef, code: "30017", message: "\u4F18\u60E0\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u9886\u53D6", observedAt: now() };
171
+ if (offer) {
172
+ const list = this.entitlements.get(q.subjectId ?? "anon") ?? [];
173
+ list.unshift({
174
+ entitlementRef: `ent_${operationRef}`,
175
+ offerRef: offer.offerRef,
176
+ title: offer.title,
177
+ entitlementType: offer.offerType,
178
+ status: "EFFECTIVE",
179
+ codeMasked: "AB**CD**EF",
180
+ validUntil: offer.validity.end
181
+ });
182
+ this.entitlements.set(q.subjectId ?? "anon", list);
183
+ }
184
+ this.operations.set(operationRef, operation);
185
+ this.acquireKeys.set(input.idempotencyKey, operationRef);
186
+ return operation;
187
+ }
188
+ entitlementsOf(q) {
189
+ return this.entitlements.get(q.subjectId ?? "anon") ?? [];
190
+ }
191
+ };
192
+
193
+ // src/server/ports.ts
194
+ var inMemoryPromotion = new InMemoryPromotionService();
195
+ async function resolvePromotionSlot(service, reqCtx, ctx, fallback) {
196
+ if (!ctx?.integrations) return fallback;
197
+ try {
198
+ return await ctx.integrations.resolveService({
199
+ service,
200
+ scope: mallScopeOf(reqCtx),
201
+ request: reqCtx
202
+ });
203
+ } catch (e) {
204
+ if (isExpectedProviderAbsence(e)) return fallback;
205
+ throw e;
206
+ }
207
+ }
208
+ var resolvePromotionOffersPort = (reqCtx, ctx) => resolvePromotionSlot(PROMOTION_OFFERS_SERVICE, reqCtx, ctx, inMemoryPromotion);
209
+ var resolvePromotionEntitlementsPort = (reqCtx, ctx) => resolvePromotionSlot(PROMOTION_ENTITLEMENTS_SERVICE, reqCtx, ctx, inMemoryPromotion);
210
+ var resolvePromotionActivitiesPort = (reqCtx, ctx) => resolvePromotionSlot(PROMOTION_ACTIVITIES_SERVICE, reqCtx, ctx, inMemoryPromotion);
211
+ var resolvePromotionAcquisitionPort = (reqCtx, ctx) => resolvePromotionSlot(PROMOTION_ACQUISITION_SERVICE, reqCtx, ctx, inMemoryPromotion);
212
+
213
+ // src/server/cards/promotion-offer-list/meta.ts
214
+ import { promotionOfferListCardDataSchema } from "@tbox.cn/app-contracts-mall";
215
+
216
+ // src/server/cards/promotion-offer-list/samples.ts
217
+ function couponImage(accent) {
218
+ return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180"><rect width="240" height="180" rx="12" fill="%23f2f4f7"/><rect x="0" y="0" width="240" height="52" rx="12" fill="%23${accent}" opacity="0.85"/><rect x="20" y="76" width="140" height="16" rx="4" fill="%23d5dae2"/><rect x="20" y="104" width="96" height="12" rx="3" fill="%23e3e7ed"/><circle cx="196" cy="128" r="26" fill="none" stroke="%23c6ccd6" stroke-width="4" stroke-dasharray="6 6"/></svg>`;
219
+ }
220
+ var IMG = {
221
+ coffee: couponImage("8a5a3b"),
222
+ lifestyle: couponImage("3d5a80"),
223
+ dining: couponImage("9e2b25")
224
+ };
225
+ var claimAction = (label, title) => [{
226
+ kind: "send",
227
+ label,
228
+ value: `\u6211\u8981${label}\u300C${title}\u300D`
229
+ }];
230
+ var claimableOffers = [
231
+ {
232
+ offerRef: "offer:coffee-30",
233
+ title: "\u745E\u5E78\u5496\u5561\uFF08\u676D\u5DDE\u62F1\u5885\u793A\u4F8B\u5E97\uFF09\uFF5C30\u5143\u996E\u54C1\u4EE3\u91D1\u5238",
234
+ subtitle: "1F \xB7 \u5230\u5E97\u70B9\u5355\u53EF\u7528 \xB7 \u7206\u5356674\u4E07+",
235
+ offerType: "COUPON",
236
+ benefitText: "\xA530",
237
+ priceText: "\u514D\u8D39",
238
+ originalPriceText: "\xA530",
239
+ claimable: true,
240
+ availabilityStatus: "AVAILABLE",
241
+ validityText: "\u9886\u53D6\u540E 30 \u5929\u5185\u6709\u6548",
242
+ imageUrl: IMG.coffee,
243
+ tags: ["\u996E\u54C1"],
244
+ itemActions: claimAction("\u9886\u53D6", "30\u5143\u996E\u54C1\u4EE3\u91D1\u5238")
245
+ },
246
+ {
247
+ offerRef: "offer:lifestyle-120",
248
+ title: "KKV 120\u5143\u4F18\u60E0\u5238",
249
+ subtitle: "2F \xB7 \u5168\u573A\u7CBE\u9009\u5546\u54C1\u53EF\u7528",
250
+ offerType: "COUPON",
251
+ benefitText: "\xA5105 \xB7 8.8\u6298",
252
+ priceText: "\xA5105",
253
+ originalPriceText: "\xA5120",
254
+ discountLabel: "8.8\u6298",
255
+ claimable: true,
256
+ availabilityStatus: "AVAILABLE",
257
+ validityText: "\u6709\u6548\u671F\u81F3 2026-09-30",
258
+ imageUrl: IMG.lifestyle,
259
+ tags: ["\u96F6\u552E"],
260
+ claimActionLabel: "\u62A2\u8D2D",
261
+ itemActions: claimAction("\u62A2\u8D2D", "KKV 120\u5143\u4F18\u60E0\u5238")
262
+ },
263
+ {
264
+ offerRef: "offer:dining-30",
265
+ title: "\u5357\u4EAC\u5927\u724C\u6863\uFF5C\u62DB\u724C\u83DC\u54C130\u5143\u65E0\u95E8\u69DB\u5238",
266
+ subtitle: "4F \xB7 \u5468\u672B\u53CA\u8282\u5047\u65E5\u901A\u7528",
267
+ offerType: "COUPON",
268
+ benefitText: "\xA530",
269
+ priceText: "\xA528.8",
270
+ originalPriceText: "\xA538.8",
271
+ discountLabel: "7.5\u6298",
272
+ stockText: "\u5269\u4F59 29 \u4EFD",
273
+ usageSceneText: "\u5230\u5E97\u51FA\u793A\u5238\u7801\u6838\u9500",
274
+ guaranteeText: "\u968F\u65F6\u9000 \xB7 \u8FC7\u671F\u81EA\u52A8\u9000",
275
+ claimable: true,
276
+ availabilityStatus: "AVAILABLE",
277
+ validityText: "\u6709\u6548\u671F\u81F3 2026-10-08",
278
+ imageUrl: IMG.dining,
279
+ tags: ["\u9910\u996E"],
280
+ claimActionLabel: "\u62A2\u8D2D",
281
+ itemActions: claimAction("\u62A2\u8D2D", "\u62DB\u724C\u83DC\u54C130\u5143\u65E0\u95E8\u69DB\u5238")
282
+ }
283
+ ];
284
+ var samples = [
285
+ {
286
+ label: "\u9886\u5238\xB7\u591A\u5238\u5217\u8868",
287
+ note: "\u5546\u573A\u795E\u5238\u4E3B\u6001\uFF1A\u5238\u56FE\u3001\u957F\u6807\u9898\u3001\u5229\u76CA\u70B9\u548C\u67E5\u770B\u5165\u53E3\u540C\u5C4F",
288
+ group: "\u9886\u5238\u5217\u8868",
289
+ data: { mode: "claimable", presentation: "coupon", summary: "\u5F53\u524D\u5546\u573A \xB7 \u4F18\u60E0\u7CBE\u9009", items: claimableOffers }
290
+ },
291
+ {
292
+ label: "\u9886\u5238\xB7\u5C55\u5F00\u4E0E\u6536\u8D77",
293
+ note: "maxItems \u622A\u65AD + \u900F\u660E\u5E95\u90E8\u5165\u53E3\u4E0E\u95E8\u5E97\u5BFC\u89C8\u5BF9\u9F50\uFF0C\u6587\u5B57\u53F3\u4FA7\u7BAD\u5934\u968F\u5C55\u5F00\u72B6\u6001\u5207\u6362",
294
+ group: "\u9886\u5238\u5217\u8868",
295
+ data: { mode: "claimable", presentation: "coupon", maxItems: 2, items: claimableOffers }
296
+ },
297
+ {
298
+ label: "\u9886\u5238\xB7\u7F3A\u56FE\u4E0E\u957F\u6587\u6848",
299
+ note: "\u7F3A\u56FE\u8D70\u4E2D\u6027\u7070\u515C\u5E95\uFF08\u4E0D\u5E26\u8272\u76F8\uFF0C\u514D\u5F97\u6BD4\u771F\u56FE\u8FD8\u62A2\u773C\uFF09+ \u4E24\u884C\u6807\u9898\u622A\u65AD\uFF0C\u4E0D\u5E94\u6539\u53D8\u884C\u9AD8\u4E0E\u6309\u94AE\u5BF9\u9F50",
300
+ group: "\u9886\u5238\u5217\u8868",
301
+ data: {
302
+ mode: "claimable",
303
+ presentation: "coupon",
304
+ summary: "\u90E8\u5206\u4F18\u60E0\u8D44\u683C\u6709\u53D8\u5316",
305
+ items: [{
306
+ ...claimableOffers[0],
307
+ offerRef: "offer:no-image",
308
+ imageUrl: void 0,
309
+ title: "\u4F1A\u5458\u9650\u5B9A\u8D85\u957F\u540D\u79F0\u6D4B\u8BD5\uFF5C\u4EC5\u9650\u672C\u5468\u672B\u6307\u5B9A\u65F6\u6BB5\u5230\u5E97\u6D88\u8D39\u4F7F\u7528\u7684\u6EE1\u51CF\u4F18\u60E0\u5238",
310
+ subtitle: "\u9650\u5468\u672B\u53CA\u8282\u5047\u65E5 \xB7 \u6BCF\u4EBA\u6BCF\u65E5\u4E00\u5F20",
311
+ benefitText: "\u6EE1100\u51CF20",
312
+ priceText: "\u514D\u8D39",
313
+ originalPriceText: "\xA520",
314
+ itemActions: claimAction("\u9886\u53D6", "\u4F1A\u5458\u9650\u5B9A\u6EE1\u51CF\u4F18\u60E0\u5238")
315
+ }]
316
+ }
317
+ },
318
+ {
319
+ label: "\u9886\u5238\xB7\u6EE4\u6389\u4E0D\u53EF\u9886",
320
+ note: "\u5217\u8868\u53EA\u5448\u73B0\u80FD\u9886\u7684\uFF1A\u4E09\u6761\u91CC\u4E00\u6761 claimable=false\uFF0C\u6E32\u67D3\u51FA\u6765\u5E94\u53EA\u6709\u4E24\u884C\u2014\u2014\u5217\u51FA\u9886\u4E0D\u4E86\u7684\u5238\uFF0C\u7528\u6237\u70B9\u4E00\u6B21\u624D\u77E5\u9053\u70B9\u4E0D\u52A8",
321
+ group: "\u9886\u5238\u5217\u8868",
322
+ data: {
323
+ mode: "claimable",
324
+ presentation: "coupon",
325
+ items: [
326
+ claimableOffers[0],
327
+ { ...claimableOffers[1], offerRef: "offer:sold-out", claimable: false, availabilityStatus: "UNAVAILABLE" },
328
+ claimableOffers[2]
329
+ ]
330
+ }
331
+ },
332
+ {
333
+ label: "\u9886\u5238\xB7\u7A7A\u6001",
334
+ note: "\u5F53\u524D\u6CA1\u6709\u53EF\u9886\u53D6\u4F18\u60E0\u65F6\u7684\u660E\u786E\u89E3\u91CA",
335
+ group: "\u9886\u5238\u5217\u8868",
336
+ data: { mode: "claimable", presentation: "coupon", items: [], noResultsReason: "\u5F53\u524D\u6682\u65E0\u53EF\u9886\u53D6\u4F18\u60E0\uFF0C\u7A0D\u540E\u518D\u6765\u770B\u770B" }
337
+ },
338
+ {
339
+ label: "\u5238\xB7\u5B9A\u5411\u5355\u5F20",
340
+ note: "\u7528\u6237\u95EE\u300CXX \u5238\u7684\u8BE6\u60C5\u300D\u65F6\u7684\u843D\u70B9\uFF1A\u51FA\u8FD9\u4E00\u5F20\u5238\u7684\u5238\u884C\uFF08mode=search\u2014\u2014\u7528\u6237\u6307\u540D\u8981\u770B\u7684\u641C\u7D22\u6001\uFF09\uFF0C\u70B9\u5F00\u6D6E\u5C42\u770B\u8BE6\u60C5\u5E76\u9886\u53D6\u3002\u4E0D\u50CF\u9886\u5238\u6001\u90A3\u6837\u6EE4\u6389\u4E0D\u53EF\u9886\u2014\u2014\u6307\u540D\u8981\u770B\u7684\u5238\u5373\u4FBF\u5F53\u4E0B\u9886\u4E0D\u4E86\u4E5F\u7167\u6837\u5448\u73B0",
341
+ group: "\u9886\u5238\u5217\u8868",
342
+ data: {
343
+ mode: "search",
344
+ presentation: "coupon",
345
+ items: [{ ...claimableOffers[2], claimable: false, availabilityStatus: "UNAVAILABLE" }]
346
+ }
347
+ }
348
+ ];
349
+
350
+ // src/server/cards/promotion-offer-list/meta.ts
351
+ var meta = {
352
+ cardType: "promotion-offer-list",
353
+ dataSchema: promotionOfferListCardDataSchema,
354
+ displayName: "\u4F18\u60E0\u5217\u8868",
355
+ description: "\u4F18\u60E0\u3001\u4FC3\u9500\u3001\u5238\u4E0E\u63A8\u8350\u56DB\u79CD legacy \u5C55\u793A\u8BED\u4E49",
356
+ sampleData: {
357
+ presentation: "promotion",
358
+ mode: "search",
359
+ summary: "\u672C\u5468\u503C\u5F97\u5173\u6CE8\u7684\u6D3B\u52A8",
360
+ items: [{ offerRef: "demo:promotion:1001", title: "\u4F1A\u5458\u590F\u65E5\u793C\u9047", subtitle: "\u9650\u65F6\u4F1A\u5458\u4E13\u4EAB", offerType: "PROMOTION", benefitText: "\u6EE1\u989D\u8D60\u793C", claimable: true, availabilityStatus: "AVAILABLE", validityText: "\u6709\u6548\u671F\u81F3 2026-08-31", tags: ["\u4F1A\u5458"] }]
361
+ },
362
+ samples,
363
+ schemaVersion: 1
364
+ };
365
+ var meta_default = meta;
366
+
367
+ // src/server/cards/promotion-offer-detail/meta.ts
368
+ import { promotionOfferDetailCardDataSchema } from "@tbox.cn/app-contracts-mall";
369
+ var meta2 = {
370
+ cardType: "promotion-offer-detail",
371
+ dataSchema: promotionOfferDetailCardDataSchema,
372
+ displayName: "\u4F18\u60E0\u8BE6\u60C5",
373
+ description: "\u4F18\u60E0\u8BE6\u60C5 + \u9886\u53D6\uFF08\u52A8\u4F5C promotion.acquire\uFF09",
374
+ sampleData: {
375
+ presentation: "coupon",
376
+ offerRef: "demo:coupon:1001",
377
+ title: "\u6EE1 100 \u51CF 20 \u5143\u5238",
378
+ offerType: "COUPON",
379
+ benefitText: "\u6EE1 100 \u51CF 20",
380
+ subtitle: "\u4F1A\u5458\u4E13\u4EAB",
381
+ validityText: "\u6709\u6548\u671F\u81F3 2026-08-31",
382
+ rules: ["\u5355\u7B14\u6EE1 100 \u53EF\u7528", "\u9650\u9886 1 \u5F20"],
383
+ steps: [{ title: "\u9886\u53D6\u4F18\u60E0", description: "\u8FDB\u5165\u4F1A\u5458\u5238\u5305\u5B8C\u6210\u9886\u53D6" }],
384
+ claimable: true,
385
+ caveats: ["\u4F18\u60E0\u8D44\u683C\u3001\u5E93\u5B58\u548C\u6700\u7EC8\u4F18\u60E0\u4EE5\u9886\u53D6\u6216\u4F7F\u7528\u65F6\u4E3A\u51C6"]
386
+ },
387
+ schemaVersion: 1
388
+ };
389
+ var meta_default2 = meta2;
390
+
391
+ // src/server/cards/promotion-entitlement-list/meta.ts
392
+ import { promotionEntitlementListCardDataSchema } from "@tbox.cn/app-contracts-mall";
393
+
394
+ // src/server/cards/promotion-entitlement-list/samples.ts
395
+ function couponImage2(accent) {
396
+ return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180"><rect width="240" height="180" rx="12" fill="%23f2f4f7"/><rect x="0" y="0" width="240" height="52" rx="12" fill="%23${accent}" opacity="0.85"/><rect x="20" y="76" width="140" height="16" rx="4" fill="%23d5dae2"/><rect x="20" y="104" width="96" height="12" rx="3" fill="%23e3e7ed"/><circle cx="196" cy="128" r="26" fill="none" stroke="%23c6ccd6" stroke-width="4" stroke-dasharray="6 6"/></svg>`;
397
+ }
398
+ var IMG2 = {
399
+ coffee: couponImage2("8a5a3b"),
400
+ lifestyle: couponImage2("3d5a80"),
401
+ dining: couponImage2("9e2b25")
402
+ };
403
+ var samples2 = [
404
+ {
405
+ label: "\u6211\u7684\u5238\xB7\u72B6\u6001\u77E9\u9635",
406
+ note: "\u5F85\u4F7F\u7528\u3001\u5DF2\u4F7F\u7528\u3001\u5DF2\u8FC7\u671F\u4E09\u6001\u53CA\u52A8\u4F5C\u964D\u7EA7\u540C\u5C4F",
407
+ group: "\u6211\u7684\u5238",
408
+ data: {
409
+ items: [
410
+ { entitlementRef: "ent:effective", title: "\u745E\u5E78\u5496\u5561\uFF5C30\u5143\u996E\u54C1\u4EE3\u91D1\u5238", imageUrl: IMG2.coffee, entitlementType: "COUPON", status: "EFFECTIVE", codeMasked: "**** 6688", benefitText: "\xA530", thresholdText: "\u65E0\u95E8\u69DB", usageNotice: "1F \xB7 \u5230\u5E97\u70B9\u5355\u53EF\u7528", validUntilText: "2026-09-27 \u5230\u671F" },
411
+ { entitlementRef: "ent:used", title: "KKV 120\u5143\u4F18\u60E0\u5238", imageUrl: IMG2.lifestyle, entitlementType: "COUPON", status: "USED", codeMasked: "**** 1058", benefitText: "\xA5105 \xB7 8.8\u6298", usageNotice: "\u5DF2\u4E8E 2026-08-26 \u4F7F\u7528", validUntilText: "\u5DF2\u4F7F\u7528" },
412
+ { entitlementRef: "ent:expired", title: "\u5357\u4EAC\u5927\u724C\u6863\uFF5C\u62DB\u724C\u83DC\u54C130\u5143\u5238", imageUrl: IMG2.dining, entitlementType: "COUPON", status: "EXPIRED", benefitText: "\xA530", usageNotice: "4F \xB7 \u9910\u996E\u533A", validUntilText: "\u5DF2\u4E8E 2026-08-20 \u8FC7\u671F" }
413
+ ]
414
+ }
415
+ },
416
+ {
417
+ label: "\u6211\u7684\u5238\xB7\u7F3A\u56FE\u957F\u6587\u6848",
418
+ note: "\u65E0\u56FE\u7247\u3001\u957F\u6807\u9898\u548C UNKNOWN \u72B6\u6001\u7684\u5BB9\u9519\u5F62\u6001",
419
+ group: "\u6211\u7684\u5238",
420
+ data: { items: [{ entitlementRef: "ent:unknown", title: "\u4F1A\u5458\u9650\u5B9A\u8D85\u957F\u540D\u79F0\u6D4B\u8BD5\uFF5C\u6307\u5B9A\u5546\u6237\u5468\u672B\u5230\u5E97\u6D88\u8D39\u4E13\u4EAB\u6743\u76CA", entitlementType: "COUPON", status: "UNKNOWN", codeMasked: "**** 0000", benefitText: "\u6EE1100\u51CF20", usageNotice: "\u4F7F\u7528\u95E8\u5E97\u548C\u9002\u7528\u5546\u54C1\u8BF7\u4EE5\u5238\u8BE6\u60C5\u8BF4\u660E\u4E3A\u51C6", validUntilText: "\u6709\u6548\u671F\u5F85\u54C1\u724C\u65B9\u786E\u8BA4" }] }
421
+ },
422
+ {
423
+ label: "\u6211\u7684\u5238\xB7\u7A7A\u6001",
424
+ note: "\u8D26\u6237\u6CA1\u6709\u5238\u65F6\u4FDD\u7559\u53BB\u9886\u5238\u7684\u8BED\u4E49\u5F15\u5BFC",
425
+ group: "\u6211\u7684\u5238",
426
+ data: { items: [], noResultsReason: "\u8FD8\u6CA1\u6709\u4F18\u60E0\u5238\uFF0C\u5148\u53BB\u770B\u770B\u672C\u5468\u795E\u5238\u5427" }
427
+ }
428
+ ];
429
+
430
+ // src/server/cards/promotion-entitlement-list/meta.ts
431
+ var meta3 = {
432
+ cardType: "promotion-entitlement-list",
433
+ dataSchema: promotionEntitlementListCardDataSchema,
434
+ displayName: "\u6211\u7684\u5238",
435
+ description: "\u5DF2\u9886\u53D6\u8D44\u4EA7\u5217\u8868\uFF08\u5238\u7801\u8131\u654F\uFF09",
436
+ sampleData: {
437
+ items: [{ entitlementRef: "ent_1", title: "\u6EE1 100 \u51CF 20 \u5143\u5238", entitlementType: "COUPON", status: "EFFECTIVE", codeMasked: "AB**CD" }]
438
+ },
439
+ samples: samples2,
440
+ schemaVersion: 1
441
+ };
442
+ var meta_default3 = meta3;
443
+
444
+ // src/server/cards/promotion-activity-list/meta.ts
445
+ import { promotionActivityListCardDataSchema } from "@tbox.cn/app-contracts-mall";
446
+ var meta4 = {
447
+ cardType: "promotion-activity-list",
448
+ dataSchema: promotionActivityListCardDataSchema,
449
+ displayName: "\u6D3B\u52A8\u5217\u8868",
450
+ description: "\u8425\u9500\u6D3B\u52A8\u5217\u8868\uFF08\u62A5\u540D\u8981\u6C42 + \u540D\u989D\uFF09",
451
+ sampleData: { items: [{ activityRef: "demo-act:3001", title: "\u5468\u672B\u4EB2\u5B50\u5E02\u96C6", subtitle: "\u4F1A\u5458\u9650\u5B9A\u4F53\u9A8C", categoryName: "\u4EB2\u5B50", registrationRequired: true, remainingQuota: 12, periodText: "8\u670822\u65E5 14:00", availabilityStatus: "AVAILABLE" }] },
452
+ schemaVersion: 1
453
+ };
454
+ var meta_default4 = meta4;
455
+
456
+ // src/server/cards/promotion-activity-detail/meta.ts
457
+ import { promotionActivityDetailCardDataSchema } from "@tbox.cn/app-contracts-mall";
458
+ var meta5 = {
459
+ cardType: "promotion-activity-detail",
460
+ dataSchema: promotionActivityDetailCardDataSchema,
461
+ displayName: "\u6D3B\u52A8\u8BE6\u60C5",
462
+ description: "\u6D3B\u52A8\u8BE6\u60C5 + \u573A\u6B21 + \u53C2\u4E0E\u6307\u5357",
463
+ sampleData: {
464
+ presentation: "detail",
465
+ activityRef: "demo-act:3001",
466
+ title: "\u5468\u672B\u4EB2\u5B50\u5E02\u96C6",
467
+ subtitle: "\u4F1A\u5458\u9650\u5B9A\u4F53\u9A8C",
468
+ categoryName: "\u4EB2\u5B50",
469
+ periodText: "8\u670822\u65E5 14:00",
470
+ location: "\u5546\u573A\u5317\u5EA7",
471
+ remainingQuota: 12,
472
+ registrationRequired: true,
473
+ registrationUrl: "alipays://platformapi/startapp?appId=demo&page=v%2Fmarketing%2Factivity%2Fdetail%3Fid%3Ddemo-activity",
474
+ sessions: [{ sessionRef: "s_1", title: "\u7B2C\u4E00\u573A", startText: "\u5468\u516D 10:00", remainingQuota: 12, status: "OPEN" }],
475
+ guideSteps: ["\u9886\u53D6\u4F18\u60E0\u5238", "\u9009\u62E9\u573A\u6B21\u62A5\u540D"],
476
+ guideItems: [{ title: "\u9886\u53D6\u4F18\u60E0\u5238", description: "\u5728\u4F1A\u5458\u4E2D\u5FC3\u5B8C\u6210\u8D44\u683C\u6821\u9A8C" }, { title: "\u9009\u62E9\u573A\u6B21\u62A5\u540D" }],
477
+ conditions: ["\u9650\u4F1A\u5458\u672C\u4EBA\u53C2\u4E0E"]
478
+ },
479
+ schemaVersion: 2,
480
+ migrate: (oldData) => oldData
481
+ };
482
+ var meta_default5 = meta5;
483
+
484
+ // src/server/cards/promotion-activity-category-list/meta.ts
485
+ import { promotionActivityCategoryListCardDataSchema } from "@tbox.cn/app-contracts-mall";
486
+ var meta6 = {
487
+ cardType: "promotion-activity-category-list",
488
+ dataSchema: promotionActivityCategoryListCardDataSchema,
489
+ displayName: "\u6D3B\u52A8\u5206\u7C7B",
490
+ description: "\u5546\u573A\u6D3B\u52A8\u5206\u7C7B\u5165\u53E3",
491
+ sampleData: { items: [{ categoryId: "family", name: "\u4EB2\u5B50" }, { categoryId: "market", name: "\u5E02\u96C6" }] },
492
+ schemaVersion: 1
493
+ };
494
+ var meta_default6 = meta6;
495
+
496
+ // src/server/cards/promotion-best-deal/meta.ts
497
+ import { promotionBestDealCardDataSchema } from "@tbox.cn/app-contracts-mall";
498
+ var meta7 = {
499
+ cardType: "promotion-best-deal",
500
+ dataSchema: promotionBestDealCardDataSchema,
501
+ displayName: "\u4F18\u60E0\u6D4B\u7B97",
502
+ description: "\u4F18\u60E0\u6D4B\u7B97\u7ED3\u679C\u6216\u5907\u9009\u5EFA\u8BAE",
503
+ sampleData: {
504
+ authoritative: true,
505
+ originalAmountCents: 2e4,
506
+ savingsCents: 3e3,
507
+ expectedPayCents: 17e3,
508
+ appliedOfferRefs: ["demo:coupon:demo"],
509
+ message: "\u9884\u8BA1\u8282\u7701 30 \u5143"
510
+ },
511
+ schemaVersion: 1
512
+ };
513
+ var meta_default7 = meta7;
514
+
515
+ // src/server/tools.ts
516
+ import { createTool, coerceStringifiedFields, withCardEmissionHint } from "@tbox.cn/app-sdk/server";
517
+ import { requireToolRequestContext } from "@tbox.cn/app-sdk/server";
518
+ import { resolveMemberGate, toMallQueryContext } from "@tbox.cn/app-contracts-mall";
519
+ import { z } from "zod";
520
+
521
+ // src/server/navigation.ts
522
+ var promotionNavigationQuery = {
523
+ activityDetail: (title) => `\u67E5\u770B\u300C${title}\u300D\u7684\u6D3B\u52A8\u8BE6\u60C5`,
524
+ offerDetail: (title) => `\u67E5\u770B\u300C${title}\u300D\u7684\u4F18\u60E0\u8BE6\u60C5`,
525
+ entitlementDetail: (title) => `\u67E5\u770B\u300C${title}\u300D\u8FD9\u5F20\u4F18\u60E0\u5238\u7684\u4F7F\u7528\u8BE6\u60C5`,
526
+ activityCategory: (name) => `\u67E5\u770B\u300C${name}\u300D\u5206\u7C7B\u7684\u6D3B\u52A8`
527
+ };
528
+ function parsePromotionNavigationQuery(text) {
529
+ const subject = text.match(/「([^」]{1,200})」/)?.[1]?.trim();
530
+ if (!subject) return null;
531
+ if (text === promotionNavigationQuery.activityDetail(subject)) return { kind: "activity-detail", title: subject };
532
+ if (text === promotionNavigationQuery.offerDetail(subject)) return { kind: "offer-detail", title: subject };
533
+ if (text === promotionNavigationQuery.entitlementDetail(subject)) return { kind: "entitlement-detail", title: subject };
534
+ if (text === promotionNavigationQuery.activityCategory(subject)) return { kind: "activity-category", name: subject };
535
+ return null;
536
+ }
537
+
538
+ // src/server/tools.ts
539
+ function coerce(schema) {
540
+ return z.preprocess(coerceStringifiedFields, schema);
541
+ }
542
+ function toQueryContext(reqCtx) {
543
+ return { q: toMallQueryContext(reqCtx), reqCtx };
544
+ }
545
+ function withCards(facts, cardType, note) {
546
+ return { ...facts, cards: [{ cardType, data: facts, note }] };
547
+ }
548
+ function dateText(value) {
549
+ if (!value) return void 0;
550
+ const date = new Date(value);
551
+ return Number.isNaN(date.valueOf()) ? void 0 : date.toLocaleDateString("zh-CN");
552
+ }
553
+ function dateRangeText(start, end) {
554
+ const startText = dateTimeText(start);
555
+ const endText = dateTimeText(end);
556
+ if (startText && endText) return `${startText} \u81F3 ${endText}`;
557
+ return startText || endText;
558
+ }
559
+ function dateTimeText(value) {
560
+ if (!value) return void 0;
561
+ const text = value.trim().replace("T", " ");
562
+ return text.length > 19 ? text.slice(0, 19) : text;
563
+ }
564
+ function yuanText(cents) {
565
+ return (cents / 100).toFixed(2).replace(/\.?(0+)$/, "");
566
+ }
567
+ function facetText(offer) {
568
+ const facet = offer.facets.find((item) => item.kind === "COUPON");
569
+ if (facet?.discountType === "AMOUNT") {
570
+ return facet.thresholdCents !== void 0 ? `\u6EE1 ${yuanText(facet.thresholdCents)} \u51CF ${yuanText(facet.value)} \u5143` : `\xA5${yuanText(facet.value)}`;
571
+ }
572
+ if (facet?.discountType === "PERCENTAGE") return `${(facet.value / 10).toFixed(1)} \u6298`;
573
+ const promotion = offer.facets.find((item) => item.kind === "PROMOTION");
574
+ return promotion?.kind === "PROMOTION" ? promotion.ruleDescription : void 0;
575
+ }
576
+ function thresholdText(offer) {
577
+ const facet = offer.facets.find((item) => item.kind === "COUPON");
578
+ return facet?.kind === "COUPON" && facet.thresholdCents !== void 0 ? `\u6EE1 ${yuanText(facet.thresholdCents)} \u5143\u53EF\u7528` : void 0;
579
+ }
580
+ function offerListPresentation(items) {
581
+ return items.length > 0 && items.every((item) => item.offerType === "COUPON") ? "coupon" : "offer";
582
+ }
583
+ function offerPresentation(offerType) {
584
+ if (offerType === "PROMOTION") return "promotion";
585
+ if (offerType === "BUNDLE") return "package";
586
+ return "offer";
587
+ }
588
+ function offerSummary(offer) {
589
+ const validity = offer.validityText ?? dateRangeText(offer.validity.start, offer.validity.end);
590
+ return {
591
+ offerRef: offer.offerRef,
592
+ title: offer.title,
593
+ ...offer.subtitle ? { subtitle: offer.subtitle } : {},
594
+ offerType: offer.offerType,
595
+ ...facetText(offer) ? { benefitText: facetText(offer) } : {},
596
+ ...thresholdText(offer) ? { thresholdText: thresholdText(offer) } : {},
597
+ ...offer.priceText ? { priceText: offer.priceText } : {},
598
+ ...offer.originalPriceText ? { originalPriceText: offer.originalPriceText } : {},
599
+ ...offer.discountLabel ? { discountLabel: offer.discountLabel } : {},
600
+ ...offer.claimActionLabel ? { claimActionLabel: offer.claimActionLabel } : {},
601
+ ...offer.purchaseUrl ? { purchaseUrl: offer.purchaseUrl } : {},
602
+ ...offer.freeCouponClaimMode ? { freeCouponClaimMode: offer.freeCouponClaimMode } : {},
603
+ ...offer.stockText ? { stockText: offer.stockText } : {},
604
+ ...offer.usageNoticeText ? { usageNoticeText: offer.usageNoticeText } : {},
605
+ ...offer.usageSceneText ? { usageSceneText: offer.usageSceneText } : {},
606
+ ...offer.usageTimeText ? { usageTimeText: offer.usageTimeText } : {},
607
+ ...offer.claimTimeText ? { claimTimeText: offer.claimTimeText } : {},
608
+ ...offer.usageRuleText ? { usageRuleText: offer.usageRuleText } : {},
609
+ ...offer.guaranteeText ? { guaranteeText: offer.guaranteeText } : {},
610
+ claimable: offer.availability.status === "AVAILABLE",
611
+ availabilityStatus: offer.availability.status,
612
+ ...validity ? { validityText: validity } : {},
613
+ ...offer.imageUrl ? { imageUrl: offer.imageUrl } : {},
614
+ tags: [...offer.tags],
615
+ itemActions: [
616
+ {
617
+ kind: "send",
618
+ label: "\u67E5\u770B\u8BE6\u60C5",
619
+ value: promotionNavigationQuery.offerDetail(offer.title)
620
+ }
621
+ ]
622
+ };
623
+ }
624
+ function offerDetail(offer, supportsAcquisition, presentation = "offer", stores) {
625
+ const validity = offer.validityText ?? dateRangeText(offer.validity.start, offer.validity.end);
626
+ const claimable = supportsAcquisition && offer.availability.status === "AVAILABLE";
627
+ return {
628
+ presentation,
629
+ offerRef: offer.offerRef,
630
+ title: offer.title,
631
+ offerType: offer.offerType,
632
+ ...offer.subtitle ? { subtitle: offer.subtitle } : {},
633
+ ...offer.description ? { description: offer.description } : {},
634
+ ...offer.imageUrl ? { imageUrl: offer.imageUrl } : {},
635
+ ...facetText(offer) ? { benefitText: facetText(offer) } : {},
636
+ ...thresholdText(offer) ? { thresholdText: thresholdText(offer) } : {},
637
+ ...offer.priceText ? { priceText: offer.priceText } : {},
638
+ ...offer.originalPriceText ? { originalPriceText: offer.originalPriceText } : {},
639
+ ...offer.discountLabel ? { discountLabel: offer.discountLabel } : {},
640
+ ...offer.claimActionLabel ? { claimActionLabel: offer.claimActionLabel } : {},
641
+ ...offer.purchaseUrl ? { purchaseUrl: offer.purchaseUrl } : {},
642
+ ...offer.stockText ? { stockText: offer.stockText } : {},
643
+ ...offer.usageNoticeText ? { usageNoticeText: offer.usageNoticeText } : {},
644
+ ...offer.usageSceneText ? { usageSceneText: offer.usageSceneText } : {},
645
+ ...offer.usageTimeText ? { usageTimeText: offer.usageTimeText } : {},
646
+ ...offer.claimTimeText ? { claimTimeText: offer.claimTimeText } : {},
647
+ ...offer.usageRuleText ? { usageRuleText: offer.usageRuleText } : {},
648
+ ...offer.guaranteeText ? { guaranteeText: offer.guaranteeText } : {},
649
+ ...validity ? { validityText: validity } : {},
650
+ rules: offer.participation?.steps.map(
651
+ (step) => step.description ? `${step.title}\uFF1A${step.description}` : step.title
652
+ ) ?? [],
653
+ ...offer.participation ? {
654
+ participationMode: offer.participation.mode,
655
+ steps: offer.participation.steps.map((step) => ({
656
+ title: step.title,
657
+ ...step.description ? { description: step.description } : {}
658
+ })),
659
+ requirements: [...offer.participation.requirements ?? []],
660
+ ...offer.participation.notice ? { notice: offer.participation.notice } : {}
661
+ } : {},
662
+ ...stores && stores.length > 0 ? { stores: [...stores] } : {},
663
+ ...offer.facets.find((facet) => facet.kind === "BUNDLE")?.kind === "BUNDLE" ? {
664
+ bundleItems: offer.facets.filter((facet) => facet.kind === "BUNDLE").flatMap((facet) => facet.items.map((item) => ({ ...item })))
665
+ } : {},
666
+ claimable,
667
+ ...!claimable ? {
668
+ unavailableReason: !supportsAcquisition ? "\u5F53\u524D\u5546\u573A\u672A\u88C5\u914D\u4F18\u60E0\u9886\u53D6\u901A\u9053" : offer.availability.reasonCodes.join("\uFF1B") || "\u6682\u4E0D\u53EF\u9886\u53D6"
669
+ } : {},
670
+ caveats: ["\u4F18\u60E0\u8D44\u683C\u3001\u5E93\u5B58\u548C\u6700\u7EC8\u4F18\u60E0\u4EE5\u9886\u53D6\u6216\u4F7F\u7528\u65F6\u4E3A\u51C6"]
671
+ };
672
+ }
673
+ function entitlementSummary(entitlement) {
674
+ const validUntil = dateText(entitlement.validUntil);
675
+ const validity = dateRangeText(entitlement.validFrom, entitlement.validUntil);
676
+ return {
677
+ entitlementRef: entitlement.entitlementRef,
678
+ title: entitlement.title,
679
+ ...entitlement.imageUrl ? { imageUrl: entitlement.imageUrl } : {},
680
+ entitlementType: entitlement.entitlementType,
681
+ status: entitlement.status,
682
+ ...entitlement.codeMasked ? { codeMasked: entitlement.codeMasked } : {},
683
+ ...entitlement.code ? { code: entitlement.code } : {},
684
+ ...entitlement.qrCodeEnabled !== void 0 ? { qrCodeEnabled: entitlement.qrCodeEnabled } : {},
685
+ ...entitlement.benefitText ? { benefitText: entitlement.benefitText } : {},
686
+ ...entitlement.thresholdText ? { thresholdText: entitlement.thresholdText } : {},
687
+ ...entitlement.usageNotice ? { usageNotice: entitlement.usageNotice } : {},
688
+ ...entitlement.guaranteeText ? { guaranteeText: entitlement.guaranteeText } : {},
689
+ ...entitlement.usageSceneText ? { usageSceneText: entitlement.usageSceneText } : {},
690
+ ...entitlement.usageTimeText ? { usageTimeText: entitlement.usageTimeText } : {},
691
+ ...entitlement.claimTimeText ? { claimTimeText: entitlement.claimTimeText } : {},
692
+ ...entitlement.usageRuleText ? { usageRuleText: entitlement.usageRuleText } : {},
693
+ ...entitlement.stores?.length ? { stores: entitlement.stores.map((store) => ({ ...store })) } : {},
694
+ // CouponSheet already supplies the "有效期" label; keep this value label-free.
695
+ ...entitlement.validityText || validity ? { validityText: entitlement.validityText ?? validity } : {},
696
+ ...validUntil ? { validUntilText: `\u6709\u6548\u671F\u81F3 ${validUntil}` } : {}
697
+ };
698
+ }
699
+ function activityAvailability(detail) {
700
+ if (detail.remainingQuota === 0) return "FULL";
701
+ const now2 = Date.now();
702
+ if (detail.endAt && new Date(detail.endAt).valueOf() < now2) return "ENDED";
703
+ if (detail.startAt && new Date(detail.startAt).valueOf() > now2) return "UPCOMING";
704
+ return "AVAILABLE";
705
+ }
706
+ function activitySummary(item, categoryNames = /* @__PURE__ */ new Map()) {
707
+ return {
708
+ activityRef: item.activityRef,
709
+ title: item.title,
710
+ ...item.subtitle ? { subtitle: item.subtitle } : {},
711
+ ...item.coverUrl ? { coverUrl: item.coverUrl } : {},
712
+ registrationRequired: item.registrationRequired,
713
+ ...item.jumpOnly ? { jumpOnly: true } : {},
714
+ ...item.jumpLabel ? { jumpLabel: item.jumpLabel } : {},
715
+ ...item.categoryId && categoryNames.get(item.categoryId) ? { categoryName: categoryNames.get(item.categoryId) } : {},
716
+ availabilityStatus: activityAvailability(item),
717
+ ...item.remainingQuota !== void 0 ? { remainingQuota: item.remainingQuota } : {},
718
+ ...dateText(item.startAt) ? { periodText: `${dateText(item.startAt)} \u8D77` } : {},
719
+ itemActions: item.jumpOnly && item.registrationUrl ? [{ kind: "link", label: item.jumpLabel ?? "\u6253\u5F00\u5C0F\u7A0B\u5E8F", url: item.registrationUrl }] : [{ kind: "send", label: "\u67E5\u770B\u6D3B\u52A8\u8BE6\u60C5", value: promotionNavigationQuery.activityDetail(item.title) }]
720
+ };
721
+ }
722
+ function activityDetail(detail, presentation = "detail") {
723
+ const start = dateText(detail.startAt);
724
+ const end = dateText(detail.endAt);
725
+ const registrationUrl = detail.registrationUrl;
726
+ return {
727
+ presentation,
728
+ activityRef: detail.activityRef,
729
+ title: detail.title,
730
+ ...detail.subtitle ? { subtitle: detail.subtitle } : {},
731
+ ...detail.description ? { description: detail.description } : {},
732
+ ...detail.coverUrl ? { coverUrl: detail.coverUrl } : {},
733
+ ...start ? { periodText: end && end !== start ? `${start} - ${end}` : `${start} \u8D77` } : {},
734
+ registrationRequired: detail.registrationRequired,
735
+ ...detail.jumpOnly ? { jumpOnly: true } : {},
736
+ ...detail.jumpLabel ? { jumpLabel: detail.jumpLabel } : {},
737
+ ...registrationUrl ? { registrationUrl } : {},
738
+ ...detail.remainingQuota !== void 0 ? { remainingQuota: detail.remainingQuota } : {},
739
+ sessions: detail.sessions.map((session) => ({
740
+ sessionRef: session.sessionRef,
741
+ title: session.title,
742
+ startText: new Date(session.startAt).toLocaleString("zh-CN"),
743
+ ...session.endAt ? { endText: new Date(session.endAt).toLocaleString("zh-CN") } : {},
744
+ ...session.location ? { location: session.location } : {},
745
+ ...session.remainingQuota !== void 0 ? { remainingQuota: session.remainingQuota } : {},
746
+ status: session.status
747
+ })),
748
+ guideSteps: detail.guide?.steps.map(
749
+ (step) => step.description ? `${step.title}\uFF1A${step.description}` : step.title
750
+ ) ?? [],
751
+ conditions: [...detail.guide?.conditions ?? []],
752
+ guideItems: detail.guide?.steps.map((step) => ({
753
+ title: step.title,
754
+ ...step.description ? { description: step.description } : {}
755
+ })) ?? [],
756
+ ...detail.guide?.notice ? { notice: detail.guide.notice } : {}
757
+ };
758
+ }
759
+ var searchInput = coerce(
760
+ z.object({
761
+ keyword: z.string().optional(),
762
+ scene: z.string().optional(),
763
+ cursor: z.string().optional(),
764
+ limit: z.string().optional()
765
+ })
766
+ );
767
+ var detailInput = coerce(z.object({ offerRef: z.string().optional(), keyword: z.string().optional() }));
768
+ var activityDetailInput = coerce(
769
+ z.object({ activityRef: z.string().optional(), keyword: z.string().optional() })
770
+ );
771
+ function createPromotionTools(opts) {
772
+ const delegateLinkedActivity2 = async (reqCtx) => {
773
+ const ctx = opts.getCtx();
774
+ const handler = ctx.handlers.resolve("alipay-activity");
775
+ if (!handler) return void 0;
776
+ const result = await handler.handle("activity-router", { content: "\u6D3B\u52A8\u4E2D\u5FC3" }, ctx, reqCtx);
777
+ return result.continueWith ? void 0 : result;
778
+ };
779
+ const requireService = async (execCtx, resolve) => {
780
+ const ctx = opts.getCtx();
781
+ const reqCtx = requireToolRequestContext(execCtx);
782
+ const context = toQueryContext(reqCtx);
783
+ return { service: await resolve(context.reqCtx, ctx), q: context.q, reqCtx: context.reqCtx };
784
+ };
785
+ const supportsAcquire = (service) => service.supportsAcquisition !== false;
786
+ const resolveOffer = async (service, q, args, offerTypes) => {
787
+ if (typeof args.offerRef === "string" && args.offerRef) {
788
+ return service.getOfferDetail(q, { offerRef: args.offerRef });
789
+ }
790
+ const page = await service.searchOffers(q, {
791
+ keyword: args.keyword,
792
+ offerTypes,
793
+ limit: 1
794
+ });
795
+ return page.items[0] ?? null;
796
+ };
797
+ const resolveActivity = async (service, q, args) => {
798
+ if (typeof args.activityRef === "string" && args.activityRef) {
799
+ return service.getActivityDetail(q, args.activityRef);
800
+ }
801
+ const page = await service.listActivities(q, { keyword: args.keyword, limit: 1 });
802
+ const first = page.items[0];
803
+ return first ? service.getActivityDetail(q, first.activityRef) : null;
804
+ };
805
+ const search = (service, q, args, offerTypes) => service.searchOffers(q, {
806
+ keyword: args.keyword,
807
+ scene: args.scene,
808
+ cursor: args.cursor,
809
+ limit: args.limit ? Number(args.limit) : 50,
810
+ offerTypes
811
+ });
812
+ const entitlementFacts = async (service, q, args, reqCtx) => {
813
+ const ctx = opts.getCtx();
814
+ const factory = ctx.services?.resolve?.("member.status");
815
+ const gate = reqCtx ? await resolveMemberGate(factory, reqCtx) : void 0;
816
+ if (gate) {
817
+ return {
818
+ found: false,
819
+ items: [],
820
+ noResultsReason: gate.notice,
821
+ ...gate.card ? { cards: [gate.card] } : {}
822
+ };
823
+ }
824
+ const page = await service.listEntitlements(q, {
825
+ cursor: args.cursor,
826
+ limit: args.limit ? Number(args.limit) : 50
827
+ });
828
+ return {
829
+ mallId: q.scope.mallId,
830
+ miniAppId: q.scope.miniAppId,
831
+ items: page.items.map(entitlementSummary),
832
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
833
+ ...page.items.length === 0 ? { noResultsReason: "\u8FD8\u6CA1\u6709\u4F18\u60E0\u5238\uFF0C\u53EF\u8BA9\u6211\u5E2E\u4F60\u627E\u627E\u53EF\u9886\u4F18\u60E0" } : {}
834
+ };
835
+ };
836
+ const listActivityCategories = createTool({
837
+ id: "listActivityCategories",
838
+ description: "\u67E5\u8BE2\u5F53\u524D\u5546\u573A\u5168\u90E8\u6D3B\u52A8\u5206\u7C7B\uFF0C\u7528\u4E8E\u4EB2\u5B50\u3001\u5E02\u96C6\u3001\u7F8E\u98DF\u7B49\u6D3B\u52A8\u7B5B\u9009\u3002",
839
+ inputSchema: coerce(z.object({})),
840
+ execute: async (_args, execCtx) => {
841
+ const reqCtx = requireToolRequestContext(execCtx, "listActivityCategories");
842
+ const delegated = await delegateLinkedActivity2(reqCtx);
843
+ if (delegated) return delegated;
844
+ const { service } = await requireService(execCtx, resolvePromotionActivitiesPort);
845
+ const items = await service.listActivityCategories?.() ?? [];
846
+ const facts = {
847
+ items: items.map((item) => ({
848
+ ...item,
849
+ itemActions: [
850
+ {
851
+ kind: "send",
852
+ label: `\u67E5\u770B${item.name}\u6D3B\u52A8`,
853
+ value: promotionNavigationQuery.activityCategory(item.name)
854
+ }
855
+ ]
856
+ })),
857
+ ...items.length === 0 ? { noResultsReason: "\u5F53\u524D\u5546\u573A\u6682\u672A\u8FD4\u56DE\u6D3B\u52A8\u5206\u7C7B" } : {}
858
+ };
859
+ return items.length ? withCards(facts, "promotion-activity-category-list", `\u6D3B\u52A8\u5206\u7C7B ${items.length} \u4E2A${items[0] ? ` \xB7 \u542B${items[0].name}` : ""}`) : facts;
860
+ }
861
+ });
862
+ const listActivities = createTool({
863
+ id: "listActivities",
864
+ description: "\u6309\u5206\u7C7B\u3001\u5173\u952E\u8BCD\u3001\u5F53\u524D/\u5F80\u671F\u3001\u62A5\u540D\u8981\u6C42\u548C\u65E5\u671F\u8303\u56F4\u67E5\u8BE2\u5546\u573A\u6D3B\u52A8\uFF1B\u67E5\u8BE2\u672C\u5468\u6D3B\u52A8\u65F6\u4F20\u672C\u5468\u65E5\u671F\u8303\u56F4\u3002",
865
+ inputSchema: coerce(
866
+ z.object({
867
+ categoryId: z.string().optional(),
868
+ keyword: z.string().optional(),
869
+ dataScope: z.enum(["CURRENT", "HISTORY"]).optional(),
870
+ registrationRequired: z.string().optional(),
871
+ startsAfter: z.string().optional(),
872
+ endsBefore: z.string().optional(),
873
+ cursor: z.string().optional(),
874
+ limit: z.string().optional()
875
+ })
876
+ ),
877
+ execute: async (args, execCtx) => {
878
+ const reqCtx = requireToolRequestContext(execCtx, "listActivities");
879
+ const delegated = await delegateLinkedActivity2(reqCtx);
880
+ if (delegated) return delegated;
881
+ const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
882
+ let categoryId = args.categoryId;
883
+ const resolveCategoryId = service.resolveCategoryId;
884
+ if (categoryId && resolveCategoryId) categoryId = await resolveCategoryId(categoryId);
885
+ const page = await service.listActivities(q, {
886
+ categoryId,
887
+ keyword: args.keyword,
888
+ dataScope: args.dataScope,
889
+ registrationRequired: args.registrationRequired === void 0 ? void 0 : args.registrationRequired === "true",
890
+ startsAfter: args.startsAfter,
891
+ endsBefore: args.endsBefore,
892
+ cursor: args.cursor,
893
+ limit: args.limit ? Number(args.limit) : 10
894
+ });
895
+ const categories = await service.listActivityCategories?.() ?? [];
896
+ const categoryNames = new Map(categories.map((item) => [item.categoryId, item.name]));
897
+ const facts = {
898
+ items: page.items.map((item) => activitySummary(item, categoryNames)),
899
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
900
+ ...page.items.length === 0 ? { noResultsReason: "\u5F53\u524D\u7B5B\u9009\u8303\u56F4\u5185\u6682\u65E0\u6D3B\u52A8" } : {}
901
+ };
902
+ return page.items.length ? withCards(facts, "promotion-activity-list", `\u5546\u573A\u6D3B\u52A8 ${page.items.length} \u573A${page.items[0] ? ` \xB7 ${page.items[0].title}` : ""}`) : facts;
903
+ }
904
+ });
905
+ const getActivityDetail = createTool({
906
+ id: "getActivityDetail",
907
+ description: "\u67E5\u8BE2\u6307\u5B9A\u6D3B\u52A8\u8BE6\u60C5\u3001\u65F6\u95F4\u3001\u540D\u989D\u548C\u53C2\u4E0E\u6761\u4EF6\u3002",
908
+ inputSchema: activityDetailInput,
909
+ execute: async (args, execCtx) => {
910
+ const reqCtx = requireToolRequestContext(execCtx, "getActivityDetail");
911
+ const delegated = await delegateLinkedActivity2(reqCtx);
912
+ if (delegated) return delegated;
913
+ const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
914
+ const detail = await resolveActivity(service, q, args);
915
+ if (!detail) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u6D3B\u52A8" };
916
+ const facts = { found: true, ...activityDetail(detail, "detail") };
917
+ return withCards(facts, "promotion-activity-detail", `\u6D3B\u52A8\u8BE6\u60C5\uFF1A${detail.title}`);
918
+ }
919
+ });
920
+ const listActivitySessions = createTool({
921
+ id: "listActivitySessions",
922
+ description: "\u67E5\u8BE2\u6307\u5B9A\u6D3B\u52A8\u7684\u53EF\u9009\u573A\u6B21\uFF0C\u53EA\u8FD4\u56DE\u53EA\u8BFB\u72B6\u6001\uFF0C\u4E0D\u6267\u884C\u62A5\u540D\u3002",
923
+ inputSchema: activityDetailInput,
924
+ execute: async (args, execCtx) => {
925
+ const reqCtx = requireToolRequestContext(execCtx, "listActivitySessions");
926
+ const delegated = await delegateLinkedActivity2(reqCtx);
927
+ if (delegated) return delegated;
928
+ const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
929
+ const detail = await resolveActivity(service, q, args);
930
+ if (!detail) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u6D3B\u52A8" };
931
+ const sessions = service.listActivitySessions ? await service.listActivitySessions(q, detail.activityRef) : detail.sessions;
932
+ const facts = activityDetail({ ...detail, sessions }, "schedule");
933
+ return withCards(facts, "promotion-activity-detail", `\u6D3B\u52A8\u573A\u6B21\uFF1A${detail.title}`);
934
+ }
935
+ });
936
+ const getActivityGuide = createTool({
937
+ id: "getActivityGuide",
938
+ description: "\u67E5\u8BE2\u6307\u5B9A\u6D3B\u52A8\u7684\u62A5\u540D\u6761\u4EF6\u548C\u53C2\u4E0E\u6B65\u9AA4\uFF0C\u4E0D\u6267\u884C\u62A5\u540D\u3002",
939
+ inputSchema: activityDetailInput,
940
+ execute: async (args, execCtx) => {
941
+ const reqCtx = requireToolRequestContext(execCtx, "getActivityGuide");
942
+ const delegated = await delegateLinkedActivity2(reqCtx);
943
+ if (delegated) return delegated;
944
+ const { service, q } = await requireService(execCtx, resolvePromotionActivitiesPort);
945
+ const detail = await resolveActivity(service, q, args);
946
+ if (!detail) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u6D3B\u52A8" };
947
+ const facts = activityDetail(detail, "guide");
948
+ return withCards(facts, "promotion-activity-detail", `\u6D3B\u52A8\u53C2\u4E0E\u65B9\u5F0F\uFF1A${detail.title}`);
949
+ }
950
+ });
951
+ const searchOffers = createTool({
952
+ id: "searchOffers",
953
+ description: "\u641C\u7D22\u5F53\u524D\u5546\u573A\u4F18\u60E0\u3001\u6298\u6263\u3001\u56E2\u8D2D\u5E76\u652F\u6301\u5206\u9875\u3002",
954
+ inputSchema: searchInput,
955
+ execute: async (args, execCtx) => {
956
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
957
+ const page = await search(service, q, args);
958
+ const facts = {
959
+ presentation: offerListPresentation(page.items),
960
+ mode: "search",
961
+ items: page.items.map(offerSummary),
962
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
963
+ ...page.items.length === 0 ? { noResultsReason: "\u672A\u627E\u5230\u76F8\u5173\u4F18\u60E0\uFF0C\u53EF\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5" } : {}
964
+ };
965
+ return page.items.length ? withCards(facts, "promotion-offer-list", `\u4F18\u60E0\u641C\u7D22 ${page.items.length} \u6761${page.items[0] ? ` \xB7 ${page.items[0].title}` : ""}`) : facts;
966
+ }
967
+ });
968
+ const getOfferDetail = createTool({
969
+ id: "getOfferDetail",
970
+ description: "\u67E5\u8BE2\u901A\u7528\u4F18\u60E0\u8BE6\u60C5\u5361\uFF08\u89C4\u5219\u3001\u95E8\u69DB\u3001\u6709\u6548\u671F\u53CA\u9886\u53D6\u72B6\u6001\uFF09\uFF0C\u652F\u6301 offerRef\u3001entitlementRef \u6216\u5173\u952E\u8BCD\u53CD\u67E5\u3002",
971
+ inputSchema: coerce(
972
+ z.object({ offerRef: z.string().optional(), entitlementRef: z.string().optional(), keyword: z.string().optional() })
973
+ ),
974
+ execute: async (args, execCtx) => {
975
+ const { service, q, reqCtx } = await requireService(execCtx, resolvePromotionOffersPort);
976
+ if (typeof args.entitlementRef === "string" && args.entitlementRef) {
977
+ const ctx = opts.getCtx();
978
+ const factory = ctx.services?.resolve?.("member.status");
979
+ const gate = await resolveMemberGate(factory, reqCtx);
980
+ if (gate) {
981
+ return { found: false, noResultsReason: gate.notice, ...gate.card ? { cards: [gate.card] } : {} };
982
+ }
983
+ }
984
+ const offer = await service.getOfferDetail(q, {
985
+ offerRef: args.offerRef,
986
+ entitlementRef: args.entitlementRef,
987
+ keyword: args.keyword
988
+ });
989
+ if (!offer) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u4F18\u60E0" };
990
+ const facts = {
991
+ found: true,
992
+ ...offerDetail(offer, supportsAcquire(service), offer.offerType === "COUPON" ? "coupon" : offerPresentation(offer.offerType))
993
+ };
994
+ return withCards(facts, "promotion-offer-detail", `\u4F18\u60E0\u8BE6\u60C5\uFF1A${offer.title}`);
995
+ }
996
+ });
997
+ const listEntitlements = createTool({
998
+ id: "listEntitlements",
999
+ description: "\u4EC5\u67E5\u8BE2\u5F53\u524D\u767B\u5F55\u4F1A\u5458\u5DF2\u7ECF\u9886\u53D6/\u62E5\u6709\u7684\u4F18\u60E0\u6743\u76CA\u548C\u5238\u5305\uFF1B\u7528\u6237\u95EE\u201C\u6709\u4EC0\u4E48\u53EF\u9886\u53D6/\u53EF\u9886\u4F18\u60E0\u5238\u201D\u65F6\u7981\u6B62\u8C03\u7528\uFF0C\u6539\u7528 listCoupons(mode=CLAIMABLE)\u3002\u5DF2\u767B\u5F55\u672A\u5165\u4F1A\u65F6\u8FD4\u56DE\u5165\u4F1A\u5F15\u5BFC\u5361\uFF08member-enrollment-cta\uFF09\uFF0C\u65E0\u9700\u518D\u8C03 member \u57DF queryEnrollmentCta\u3002",
1000
+ inputSchema: searchInput,
1001
+ execute: async (args, execCtx) => {
1002
+ const { service, q, reqCtx } = await requireService(execCtx, resolvePromotionEntitlementsPort);
1003
+ const facts = await entitlementFacts(service, q, args, reqCtx);
1004
+ return facts.items.length ? withCards(
1005
+ facts,
1006
+ "promotion-entitlement-list",
1007
+ `\u6211\u7684\u4F18\u60E0\u5238 ${facts.items.length} \u5F20${facts.items[0] ? ` \xB7 ${facts.items[0].title}` : ""}`
1008
+ ) : facts;
1009
+ }
1010
+ });
1011
+ const listPromotions = createTool({
1012
+ id: "listPromotions",
1013
+ description: "\u67E5\u8BE2\u672C\u5468\u8425\u9500\u4E13\u9898\u548C\u4FC3\u9500\u6D3B\u52A8\u3002",
1014
+ inputSchema: searchInput,
1015
+ execute: async (args, execCtx) => {
1016
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1017
+ const page = await search(service, q, args, ["PROMOTION"]);
1018
+ const facts = {
1019
+ presentation: "promotion",
1020
+ mode: "search",
1021
+ summary: "\u672C\u5468\u503C\u5F97\u5173\u6CE8\u7684\u6D3B\u52A8",
1022
+ items: page.items.map(offerSummary),
1023
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
1024
+ ...page.items.length === 0 ? { noResultsReason: "\u672C\u5468\u6682\u672A\u67E5\u8BE2\u5230\u8425\u9500\u4E13\u9898" } : {}
1025
+ };
1026
+ return page.items.length ? withCards(facts, "promotion-offer-list", `\u8425\u9500\u4E13\u9898 ${page.items.length} \u4E2A${page.items[0] ? ` \xB7 ${page.items[0].title}` : ""}`) : facts;
1027
+ }
1028
+ });
1029
+ const getPromotionDetail = createTool({
1030
+ id: "getPromotionDetail",
1031
+ description: "\u6309\u8425\u9500\u4E13\u9898\u6807\u8BC6\u6216\u5173\u952E\u8BCD\u7CBE\u786E\u67E5\u8BE2\u6D3B\u52A8\u8BE6\u60C5\u3002",
1032
+ inputSchema: detailInput,
1033
+ execute: async (args, execCtx) => {
1034
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1035
+ const offer = await resolveOffer(service, q, args, ["PROMOTION"]);
1036
+ if (!offer) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u8425\u9500\u4E13\u9898" };
1037
+ const facts = { found: true, ...offerDetail(offer, false, "promotion") };
1038
+ return withCards(facts, "promotion-offer-detail", `\u8425\u9500\u6D3B\u52A8\u8BE6\u60C5\uFF1A${offer.title}`);
1039
+ }
1040
+ });
1041
+ const getParticipationGuide = createTool({
1042
+ id: "getParticipationGuide",
1043
+ description: "\u67E5\u8BE2\u8425\u9500\u4E13\u9898\u7684\u5382\u5546\u53C2\u4E0E\u6B65\u9AA4\uFF0C\u4E0D\u6267\u884C\u62A5\u540D\u6216\u9886\u53D6\u3002",
1044
+ inputSchema: detailInput,
1045
+ execute: async (args, execCtx) => {
1046
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1047
+ const offer = await resolveOffer(service, q, args, ["PROMOTION"]);
1048
+ if (!offer) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u8425\u9500\u4E13\u9898" };
1049
+ const facts = { found: true, ...offerDetail(offer, false, "promotion-guide") };
1050
+ return withCards(facts, "promotion-offer-detail", `\u53C2\u4E0E\u65B9\u5F0F\uFF1A${offer.title}`);
1051
+ }
1052
+ });
1053
+ const listCoupons = createTool({
1054
+ id: "listCoupons",
1055
+ description: "\u67E5\u8BE2\u4F18\u60E0\u5238\u5217\u8868\u3002\u9ED8\u8BA4\u53EA\u67E5\u8BE2\u5F53\u524D\u53EF\u9886\u53D6\u4F18\u60E0\u5238\uFF08CLAIMABLE\uFF09\uFF1B\u53EA\u6709\u7528\u6237\u660E\u786E\u8BF4\u201C\u6211\u7684\u5238/\u5DF2\u9886\u53D6/\u5238\u5305\u201D\u65F6\u624D\u4F20 mode=OWNED\u3002mode=OWNED \u5DF2\u767B\u5F55\u672A\u5165\u4F1A\u65F6\u8FD4\u56DE\u5165\u4F1A\u5F15\u5BFC\u5361\uFF08member-enrollment-cta\uFF09\uFF0C\u65E0\u9700\u518D\u8C03 member \u57DF queryEnrollmentCta\u3002",
1056
+ inputSchema: coerce(
1057
+ z.object({
1058
+ mode: z.enum(["CLAIMABLE", "OWNED"]).default("CLAIMABLE"),
1059
+ keyword: z.string().optional(),
1060
+ cursor: z.string().optional(),
1061
+ limit: z.string().optional()
1062
+ })
1063
+ ),
1064
+ execute: async (args, execCtx) => {
1065
+ if (args.mode === "OWNED") {
1066
+ const { service: service2, q: q2, reqCtx } = await requireService(execCtx, resolvePromotionEntitlementsPort);
1067
+ const facts2 = await entitlementFacts(service2, q2, args, reqCtx);
1068
+ return facts2.items.length ? withCards(facts2, "promotion-entitlement-list", `\u6211\u7684\u4F18\u60E0\u5238 ${facts2.items.length} \u5F20${facts2.items[0] ? ` \xB7 ${facts2.items[0].title ?? ""}` : ""}`) : facts2;
1069
+ }
1070
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1071
+ const page = await search(service, q, args, ["COUPON"]);
1072
+ const items = page.items.filter((item) => item.availability.status === "AVAILABLE");
1073
+ const facts = {
1074
+ presentation: "coupon",
1075
+ mode: "claimable",
1076
+ summary: "\u53EF\u9886\u53D6\u4F18\u60E0\u5238",
1077
+ items: items.map(offerSummary),
1078
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
1079
+ ...items.length === 0 ? { noResultsReason: "\u5F53\u524D\u5546\u573A\u6682\u672A\u8FD4\u56DE\u53EF\u9886\u4F18\u60E0\u5238" } : {}
1080
+ };
1081
+ return items.length ? withCards(facts, "promotion-offer-list", `\u53EF\u9886\u4F18\u60E0\u5238 ${items.length} \u5F20${items[0] ? ` \xB7 ${items[0].title}` : ""}`) : facts;
1082
+ }
1083
+ });
1084
+ const getPackageDetail = createTool({
1085
+ id: "getPackageDetail",
1086
+ description: "\u67E5\u8BE2\u7EC4\u5408\u793C\u5305\u53CA\u5176\u5B50\u6743\u76CA\u3002",
1087
+ inputSchema: detailInput,
1088
+ execute: async (args, execCtx) => {
1089
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1090
+ const offer = await resolveOffer(service, q, args, ["BUNDLE"]);
1091
+ if (!offer) return { found: false, message: "\u672A\u627E\u5230\u8BE5\u4F18\u60E0\u793C\u5305" };
1092
+ const facts = {
1093
+ found: true,
1094
+ ...offerDetail(offer, supportsAcquire(service), "package")
1095
+ };
1096
+ return withCards(facts, "promotion-offer-detail", `\u4F18\u60E0\u793C\u5305\uFF1A${offer.title}`);
1097
+ }
1098
+ });
1099
+ const recommendOffers = createTool({
1100
+ id: "recommendOffers",
1101
+ description: "\u6309\u8D44\u683C\u3001\u53EF\u7528\u6027\u4E0E\u9884\u8BA1\u8282\u7701\u989D\u63A8\u8350\u5F53\u524D\u5546\u573A\u4F18\u60E0\u3002",
1102
+ inputSchema: searchInput,
1103
+ execute: async (args, execCtx) => {
1104
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1105
+ const items = await service.recommendOffers(q, {
1106
+ keyword: args.keyword,
1107
+ scene: args.scene,
1108
+ limit: args.limit ? Number(args.limit) : 4
1109
+ });
1110
+ const unifiedCouponList = service.usesUnifiedCouponList === true;
1111
+ const facts = {
1112
+ presentation: unifiedCouponList ? "coupon" : "recommendation",
1113
+ mode: unifiedCouponList ? "claimable" : "recommend",
1114
+ summary: items[0]?.availability.status === "AVAILABLE" ? "\u5F53\u524D\u53EF\u7528\uFF0C\u5E76\u4F18\u5148\u8003\u8651\u9884\u8BA1\u8282\u7701\u989D" : "\u5F53\u524D\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u53EF\u7528\u4F18\u60E0\uFF0C\u8BF7\u67E5\u770B\u5019\u9009\u6761\u4EF6",
1115
+ items: items.slice(0, 4).map(offerSummary),
1116
+ ...items.length === 0 ? { noResultsReason: "\u5F53\u524D\u6682\u65E0\u53EF\u63A8\u8350\u4F18\u60E0" } : {}
1117
+ };
1118
+ return items.length ? withCards(facts, "promotion-offer-list", `\u63A8\u8350\u4F18\u60E0 ${items.length} \u6761${items[0] ? ` \xB7 ${items[0].title}` : ""}`) : facts;
1119
+ }
1120
+ });
1121
+ const calculateBestDeal = createTool({
1122
+ id: "calculateBestDeal",
1123
+ description: "\u8C03\u7528\u5DF2\u88C5\u914D\u5382\u5546\u80FD\u529B\u8FDB\u884C\u4F18\u60E0\u6D4B\u7B97\uFF1B\u672A\u88C5\u914D\u6D4B\u7B97\u80FD\u529B\u65F6\u4EC5\u8FD4\u56DE\u6587\u5B57\u8BF4\u660E\uFF0C\u4E0D\u51FA\u6D4B\u7B97\u5361\u3002",
1124
+ inputSchema: coerce(
1125
+ z.object({ currentAmountCents: z.string(), candidateOfferRefs: z.array(z.string()).optional() })
1126
+ ),
1127
+ execute: async (args, execCtx) => {
1128
+ const { service, q } = await requireService(execCtx, resolvePromotionOffersPort);
1129
+ const amount = Number(args.currentAmountCents);
1130
+ if (!Number.isSafeInteger(amount) || amount < 0) return { supported: true, message: "\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u6D88\u8D39\u91D1\u989D\uFF08\u5206\uFF09" };
1131
+ const bestDealService = service;
1132
+ if (!bestDealService.calculateBestDeal || bestDealService.supportsBestDeal === false) {
1133
+ return { supported: false, originalAmountCents: amount, message: "\u5F53\u524D\u5546\u573A\u6682\u4E0D\u652F\u6301\u4F18\u60E0\u6D4B\u7B97\uFF0C\u8BF7\u5728\u7ED3\u7B97\u9875\u786E\u8BA4\u6700\u7EC8\u4F18\u60E0" };
1134
+ }
1135
+ const result = await bestDealService.calculateBestDeal(q, {
1136
+ currentAmountCents: amount,
1137
+ candidateOfferRefs: args.candidateOfferRefs
1138
+ });
1139
+ const facts = {
1140
+ authoritative: true,
1141
+ originalAmountCents: amount,
1142
+ savingsCents: result.estimatedSavingCents,
1143
+ expectedPayCents: Math.max(0, amount - result.estimatedSavingCents),
1144
+ appliedOfferRefs: result.offerRef ? [result.offerRef] : [],
1145
+ message: result.reason
1146
+ };
1147
+ return withCards(
1148
+ facts,
1149
+ "promotion-best-deal",
1150
+ `\u9884\u8BA1\u7701 ${(result.estimatedSavingCents / 100).toFixed(0)} \u5143 \xB7 \u5E94\u4ED8 ${(Math.max(0, amount - result.estimatedSavingCents) / 100).toFixed(0)} \u5143`
1151
+ );
1152
+ }
1153
+ });
1154
+ return {
1155
+ listActivityCategories: withCardEmissionHint(listActivityCategories),
1156
+ listActivities: withCardEmissionHint(listActivities),
1157
+ getActivityDetail: withCardEmissionHint(getActivityDetail),
1158
+ listActivitySessions: withCardEmissionHint(listActivitySessions),
1159
+ getActivityGuide: withCardEmissionHint(getActivityGuide),
1160
+ searchOffers: withCardEmissionHint(searchOffers),
1161
+ getOfferDetail: withCardEmissionHint(getOfferDetail, { actions: true }),
1162
+ listEntitlements: withCardEmissionHint(listEntitlements),
1163
+ listPromotions: withCardEmissionHint(listPromotions),
1164
+ getPromotionDetail: withCardEmissionHint(getPromotionDetail),
1165
+ getParticipationGuide: withCardEmissionHint(getParticipationGuide),
1166
+ listCoupons: withCardEmissionHint(listCoupons, { actions: true }),
1167
+ getPackageDetail: withCardEmissionHint(getPackageDetail, { actions: true }),
1168
+ recommendOffers: withCardEmissionHint(recommendOffers),
1169
+ calculateBestDeal: withCardEmissionHint(calculateBestDeal)
1170
+ };
1171
+ }
1172
+
1173
+ // src/server/actions.ts
1174
+ import { randomUUID } from "crypto";
1175
+ import { memberGateCardOf, externalMemberRefOf, toMallQueryContext as toMallQueryContext2 } from "@tbox.cn/app-contracts-mall";
1176
+ import { z as z2 } from "zod";
1177
+ async function listEntitlementsAfterAcquire(service, q) {
1178
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1179
+ const page = await service.listEntitlements(q, { limit: 10 });
1180
+ if (page.items.length > 0 || attempt === 2) return page;
1181
+ await new Promise((resolve) => setTimeout(resolve, 150));
1182
+ }
1183
+ return { items: [] };
1184
+ }
1185
+ function classifyRejection(code) {
1186
+ switch (code) {
1187
+ case "30013":
1188
+ case "30011":
1189
+ return { kind: "AUTH_FAILED", message: "\u4F1A\u5458\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u4F1A\u5458\u540E\u518D\u9886\u53D6" };
1190
+ case "636":
1191
+ case "637":
1192
+ return { kind: "OUT_OF_STOCK", message: "\u5238\u5DF2\u88AB\u9886\u5B8C\uFF0C\u53EF\u5173\u6CE8\u4E0B\u6B21\u653E\u91CF" };
1193
+ case "638":
1194
+ return { kind: "ALREADY_ACQUIRED", message: "\u5DF2\u9886\u53D6\u8FC7\u8BE5\u5238\uFF0C\u53EF\u5728\u300C\u6211\u7684\u5238\u300D\u4E2D\u67E5\u770B" };
1195
+ case "5113":
1196
+ return { kind: "ALREADY_ACQUIRED", message: "\u9886\u53D6\u6B21\u6570\u5DF2\u8FBE\u5230\u4E0A\u9650" };
1197
+ case "30017":
1198
+ return { kind: "NOT_ELIGIBLE", message: "\u4E0D\u6EE1\u8DB3\u9886\u53D6\u6761\u4EF6\uFF08\u5982\u4EC5\u9650\u7279\u5B9A\u7B49\u7EA7\u4F1A\u5458\uFF09" };
1199
+ default:
1200
+ return { kind: "UNKNOWN", message: "\u9886\u53D6\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" };
1201
+ }
1202
+ }
1203
+ var ACQUIRE_MEMBER_NOTICE = "\u9886\u53D6\u9700\u8981\u4F1A\u5458\u8EAB\u4EFD\uFF0C\u8BF7\u5148\u767B\u5F55\u6216\u529E\u7406\u5165\u4F1A\u540E\u518D\u9886\u5238";
1204
+ var ACQUIRE_ENROLL_NOTICE = "\u9886\u53D6\u9700\u8981\u4F1A\u5458\u8EAB\u4EFD\uFF0C\u529E\u7406\u5165\u4F1A\u540E\u5373\u53EF\u9886\u5238";
1205
+ var ACQUIRE_UNKNOWN_NOTICE = "\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u4F1A\u5458\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5";
1206
+ var ACQUIRE_GATE_COPY = {
1207
+ unauthenticated: ACQUIRE_MEMBER_NOTICE,
1208
+ not_enrolled: ACQUIRE_ENROLL_NOTICE,
1209
+ closed: ACQUIRE_ENROLL_NOTICE,
1210
+ unknown: ACQUIRE_UNKNOWN_NOTICE,
1211
+ unavailable: ACQUIRE_UNKNOWN_NOTICE
1212
+ };
1213
+ function createPromotionActions(ctx) {
1214
+ const resolveAcquireGate = async (reqCtx) => {
1215
+ const factory = ctx.services?.resolve?.("member.status");
1216
+ if (!factory) return { blocked: ACQUIRE_MEMBER_NOTICE };
1217
+ const status = await factory(reqCtx);
1218
+ if (status.outcome !== "active") {
1219
+ const card = memberGateCardOf(reqCtx?.identity?.userId ?? "", status);
1220
+ return card ? { blocked: ACQUIRE_GATE_COPY[status.outcome], card } : { blocked: ACQUIRE_GATE_COPY[status.outcome] };
1221
+ }
1222
+ if (!status.account) {
1223
+ ctx.logger?.warn("promotion.acquire\uFF1Amember.status active \u7F3A\u8D26\u6237\u5FEB\u7167\uFF08\u89E3\u6790\u5668\u539F\u5B50\u6027\u7834\u574F\uFF09");
1224
+ return { blocked: ACQUIRE_UNKNOWN_NOTICE };
1225
+ }
1226
+ const externalMemberRef = externalMemberRefOf(status.account) ?? "";
1227
+ if (!externalMemberRef) {
1228
+ ctx.logger?.warn("promotion.acquire\uFF1Aactive \u4F1A\u5458\u8D26\u6237\u65E0\u5382\u5546\u4FA7\u5F15\u7528\uFF08\u771F\u5B9E\u5361\u53F7\u4E0E opaque \u5F15\u7528\u53CC\u7F3A\uFF09");
1229
+ return { blocked: ACQUIRE_UNKNOWN_NOTICE };
1230
+ }
1231
+ return { externalMemberRef };
1232
+ };
1233
+ const executeAcquire = async (input, reqCtx) => {
1234
+ const { offerRef } = input ?? {};
1235
+ const userId = reqCtx?.identity?.userId ?? "";
1236
+ if (!offerRef || !userId || !reqCtx) {
1237
+ return { text: "\u9886\u53D6\u5931\u8D25\uFF1A\u7F3A\u5C11\u4F18\u60E0\u5F15\u7528\u6216\u8EAB\u4EFD" };
1238
+ }
1239
+ const gate = await resolveAcquireGate(reqCtx);
1240
+ if ("blocked" in gate) {
1241
+ return gate.card ? { card: { cardType: gate.card.cardType, data: gate.card.data }, text: gate.blocked } : { text: gate.blocked };
1242
+ }
1243
+ const externalMemberRef = gate.externalMemberRef;
1244
+ const q = toMallQueryContext2(reqCtx);
1245
+ const service = await resolvePromotionAcquisitionPort(reqCtx, ctx);
1246
+ const idempotencyKey = randomUUID();
1247
+ let operation;
1248
+ try {
1249
+ operation = await service.acquire(q, { offerRef, externalMemberRef, idempotencyKey });
1250
+ } catch (e) {
1251
+ const errorType = e instanceof Error ? e.name : "UnknownError";
1252
+ ctx.logger?.warn(`\u9886\u5238\u8BF7\u6C42\u5F02\u5E38 key=${idempotencyKey.slice(0, 8)}\u2026\uFF1Atype=${errorType}`);
1253
+ return { text: "\u9886\u53D6\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" };
1254
+ }
1255
+ if (operation.status === "SUCCEEDED") {
1256
+ try {
1257
+ const entitlements = await resolvePromotionEntitlementsPort(reqCtx, ctx);
1258
+ const page = await listEntitlementsAfterAcquire(entitlements, q);
1259
+ if (page.items.length) {
1260
+ return {
1261
+ card: {
1262
+ cardType: "promotion-entitlement-list",
1263
+ data: { mallId: q.scope.mallId, miniAppId: q.scope.miniAppId, items: page.items.map(entitlementSummary) }
1264
+ },
1265
+ text: "\u9886\u53D6\u6210\u529F\uFF01\u5DF2\u5B58\u5165\u5361\u5305"
1266
+ };
1267
+ }
1268
+ } catch (e) {
1269
+ ctx.logger?.warn(`\u9886\u5238\u6210\u529F\u4F46\u6211\u7684\u5238\u5217\u8868\u67E5\u8BE2\u5931\u8D25\uFF1Aerr=${e.message}`);
1270
+ }
1271
+ return { text: "\u9886\u53D6\u6210\u529F\uFF01\u5DF2\u5B58\u5165\u5361\u5305\uFF0C\u53EF\u5728\u300C\u6211\u7684\u5238\u300D\u4E2D\u67E5\u770B" };
1272
+ }
1273
+ if (operation.status === "PENDING_AUTHORITY") {
1274
+ return { text: "\u9886\u53D6\u5DF2\u53D7\u7406\uFF0C\u6B63\u5728\u786E\u8BA4\u7ED3\u679C\uFF0C\u7A0D\u540E\u53EF\u5728\u300C\u6211\u7684\u5238\u300D\u67E5\u770B" };
1275
+ }
1276
+ const providerMessage = operation.message?.trim();
1277
+ ctx.logger?.warn(`\u9886\u5238\u88AB\u5382\u5546\u62D2\u7EDD code=${operation.code ?? "unknown"} operation=${operation.operationRef.slice(0, 12)} message=${logText(providerMessage)}`);
1278
+ const reason = providerMessage || classifyRejection(operation.code).message;
1279
+ return { text: `\u9886\u53D6\u5931\u8D25\uFF1A${reason}${operation.code ? `\uFF08code ${operation.code}\uFF09` : ""}` };
1280
+ };
1281
+ return {
1282
+ acquire: {
1283
+ actionId: "promotion.acquire",
1284
+ moduleId: "promotion",
1285
+ cardType: "promotion-offer-detail",
1286
+ label: "\u7ACB\u5373\u9886\u53D6",
1287
+ singleUse: true,
1288
+ inputSchema: z2.object({ offerRef: z2.string().min(1) }),
1289
+ execute: executeAcquire
1290
+ },
1291
+ // 列表内浮层的领取入口:一张列表卡对应 N 张券,令牌必须可重复使用——
1292
+ // singleUse 按 grantId 一次性消费,领完第一张后同卡其余券会全部撞 TOKEN_CONSUMED。
1293
+ // 重复领同一张券由 provider 侧幂等兜底(已领会回一句文字提示)。
1294
+ acquireFromList: {
1295
+ actionId: "promotion.acquire-from-list",
1296
+ moduleId: "promotion",
1297
+ cardType: "promotion-offer-list",
1298
+ label: "\u9886\u53D6",
1299
+ singleUse: false,
1300
+ inputSchema: z2.object({ offerRef: z2.string().min(1) }),
1301
+ execute: executeAcquire
1302
+ },
1303
+ // 门店券行领取入口(2026-09-02):couponList 携 offerRef 的券——与优惠列表领取逐字同链路
1304
+ // (executeAcquire 单源)。token 按 cardType 签发而注册表按 actionId 键控——两个卡上下文
1305
+ // (shop-detail 卡直开 / shop-list 卡下钻浮层)= 两个变体 id;门店列表卡本体无领取按钮
1306
+ // (按钮只在两个上下文打开的门店详情浮层券条目里)。
1307
+ acquireFromShopDetail: {
1308
+ actionId: "promotion.acquire-from-shop-detail",
1309
+ moduleId: "promotion",
1310
+ cardType: "shop-detail",
1311
+ label: "\u9886\u53D6",
1312
+ singleUse: false,
1313
+ inputSchema: z2.object({ offerRef: z2.string().min(1) }),
1314
+ execute: executeAcquire
1315
+ },
1316
+ acquireFromShopList: {
1317
+ actionId: "promotion.acquire-from-shop-list",
1318
+ moduleId: "promotion",
1319
+ cardType: "shop-list",
1320
+ label: "\u9886\u53D6",
1321
+ singleUse: false,
1322
+ inputSchema: z2.object({ offerRef: z2.string().min(1) }),
1323
+ execute: executeAcquire
1324
+ }
1325
+ };
1326
+ }
1327
+ function logText(value) {
1328
+ const text = typeof value === "string" ? value : String(value ?? "");
1329
+ const bounded = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
1330
+ return JSON.stringify(bounded);
1331
+ }
1332
+
1333
+ // src/server/handler.ts
1334
+ import { matchIntent } from "@tbox.cn/app-sdk/server";
1335
+ import { resolveMemberGate as resolveMemberGate2, toMallQueryContext as toMallQueryContext3 } from "@tbox.cn/app-contracts-mall";
1336
+ var PROMOTION_INTENTS = {
1337
+ "list-entitlements": {
1338
+ patterns: ["\u6211\u7684\u4F18\u60E0\u5238", "\u6211\u7684\u5238\u5305", "\u6211\u7684\u5238", "\u5DF2\u9886\u53D6\u7684\u5238", "\u5DF2\u9886\u4F18\u60E0\u5238", "\u6301\u6709\u7684\u4F18\u60E0\u5238"],
1339
+ description: "\u67E5\u8BE2\u5F53\u524D\u4F1A\u5458\u5DF2\u62E5\u6709\u7684\u4F18\u60E0\u5238"
1340
+ },
1341
+ "search-offers": {
1342
+ patterns: ["\u9886\u5238", "\u9886\u53D6\u4F18\u60E0\u5238", "\u6709\u4EC0\u4E48\u4F18\u60E0", "\u73B0\u5728\u6709\u4EC0\u4E48\u4F18\u60E0\uFF1F"],
1343
+ description: "\u641C\u7D22\u53EF\u9886\u4F18\u60E0"
1344
+ },
1345
+ "list-activities": {
1346
+ patterns: ["\u8425\u9500\u6D3B\u52A8", "\u6709\u4EC0\u4E48\u6D3B\u52A8", "\u6D3B\u52A8\u4E2D\u5FC3"],
1347
+ description: "\u67E5\u8BE2\u8425\u9500\u6D3B\u52A8"
1348
+ }
1349
+ };
1350
+ var PROMOTION_RESOLVE_ROUTES = [
1351
+ {
1352
+ id: "list-entitlements",
1353
+ keywords: [...PROMOTION_INTENTS["list-entitlements"].patterns],
1354
+ handlerId: "promotion",
1355
+ action: "list-entitlements"
1356
+ },
1357
+ {
1358
+ id: "search-offers",
1359
+ keywords: [...PROMOTION_INTENTS["search-offers"].patterns],
1360
+ handlerId: "promotion",
1361
+ action: "search-offers"
1362
+ },
1363
+ {
1364
+ id: "list-activities",
1365
+ keywords: [...PROMOTION_INTENTS["list-activities"].patterns],
1366
+ handlerId: "promotion",
1367
+ action: "list-activities"
1368
+ }
1369
+ ];
1370
+ var OWNED_INTENT_HINTS = ["\u6211\u7684\u4F18\u60E0\u5238", "\u6211\u7684\u5238\u5305", "\u6211\u7684\u5238", "\u5DF2\u9886\u53D6", "\u5DF2\u9886\u4F18\u60E0\u5238", "\u6301\u6709\u7684\u4F18\u60E0\u5238"];
1371
+ var CLAIMABLE_INTENT_HINTS = ["\u6709\u4EC0\u4E48\u4F18\u60E0", "\u53EF\u9886\u53D6", "\u53EF\u9886", "\u9886\u53D6\u4F18\u60E0\u5238", "\u9886\u5238"];
1372
+ function intentContent(payload) {
1373
+ if (typeof payload === "string") return payload;
1374
+ if (payload && typeof payload === "object" && "content" in payload) {
1375
+ const content = payload.content;
1376
+ return typeof content === "string" ? content : "";
1377
+ }
1378
+ return "";
1379
+ }
1380
+ function resolveIntent(payload) {
1381
+ const content = intentContent(payload);
1382
+ const navigation = parsePromotionNavigationQuery(content);
1383
+ if (navigation) return navigation;
1384
+ if (content.startsWith(NAVIGATION_SENTENCE_PREFIX)) return null;
1385
+ const owned = OWNED_INTENT_HINTS.some((hint) => content.includes(hint));
1386
+ const claimable = CLAIMABLE_INTENT_HINTS.some((hint) => content.includes(hint));
1387
+ if (claimable && !owned) return { kind: "search-coupons" };
1388
+ if (owned) return { kind: "list-entitlements" };
1389
+ const id = matchIntent(PROMOTION_RESOLVE_ROUTES, content)?.id;
1390
+ return {
1391
+ kind: id === "list-entitlements" ? "list-entitlements" : id === "list-activities" ? "list-activities" : "search-offers"
1392
+ };
1393
+ }
1394
+ function resolveAction(action, payload) {
1395
+ if (action === "intent") return resolveIntent(payload);
1396
+ if (action === "list-entitlements" || action === "search-offers" || action === "list-activities") {
1397
+ return { kind: action };
1398
+ }
1399
+ return null;
1400
+ }
1401
+ function offerPresentation2(offerType) {
1402
+ if (offerType === "PROMOTION") return "promotion";
1403
+ if (offerType === "BUNDLE") return "package";
1404
+ return "offer";
1405
+ }
1406
+ function offerListPresentation2(items) {
1407
+ return items.length > 0 && items.every((item) => item.offerType === "COUPON") ? "coupon" : "offer";
1408
+ }
1409
+ async function activityCategoryNames(service) {
1410
+ const categories = await service.listActivityCategories?.() ?? [];
1411
+ return {
1412
+ categories,
1413
+ names: new Map(categories.map((item) => [item.categoryId, item.name]))
1414
+ };
1415
+ }
1416
+ var NAVIGATION_SENTENCE_PREFIX = "\u67E5\u770B\u300C";
1417
+ async function delegateLinkedActivity(ctx, payload, reqCtx) {
1418
+ const handler = ctx.handlers.resolve("alipay-activity");
1419
+ if (!handler) return void 0;
1420
+ const result = await handler.handle("activity-router", payload, ctx, reqCtx);
1421
+ return result.continueWith ? void 0 : result;
1422
+ }
1423
+ function createPromotionIntentHandler(ctx) {
1424
+ return {
1425
+ id: "promotion",
1426
+ // 顶层意图 = 确定性触发器(exact 整句 + prefix 导航模板头):语义分类职责归
1427
+ // skill 路由与 LLM 工具编排(BM25 单标签 + 硬短路会截断复合句,如「推荐门店+领券」)。
1428
+ // QuickCommands 固定句(现在有什么优惠?/活动中心)随 exact 句面直出。
1429
+ intents: [
1430
+ ...Object.values(PROMOTION_INTENTS).map(({ patterns }) => ({ patterns: [...patterns], match: "exact" })),
1431
+ {
1432
+ // 卡片导航模板句(「查看「X」…」四式)前缀路由:handler 内
1433
+ // parsePromotionNavigationQuery 模板全等校验承接;未命中模板 → 洋葱放行(resolveIntent null)。
1434
+ patterns: [NAVIGATION_SENTENCE_PREFIX],
1435
+ match: "prefix"
1436
+ }
1437
+ ],
1438
+ handle: async (action, payload, _ctx, reqCtx) => {
1439
+ if (!reqCtx) return { cards: [], text: "\u8425\u9500\u670D\u52A1\u4E0D\u53EF\u7528\uFF08\u7F3A\u5C11\u8FD0\u884C\u4E0A\u4E0B\u6587\uFF09" };
1440
+ const q = toMallQueryContext3(reqCtx);
1441
+ const resolved = resolveAction(action, payload);
1442
+ if (!resolved) {
1443
+ return { cards: [], continueWith: { content: intentContent(payload) } };
1444
+ }
1445
+ switch (resolved.kind) {
1446
+ case "activity-detail": {
1447
+ const delegated = await delegateLinkedActivity(ctx, payload, reqCtx);
1448
+ if (delegated) return delegated;
1449
+ const service = await resolvePromotionActivitiesPort(reqCtx, ctx);
1450
+ const page = await service.listActivities(q, { keyword: resolved.title, limit: 10 });
1451
+ const summary = page.items.find((item) => item.title === resolved.title) ?? page.items[0];
1452
+ if (!summary) return { cards: [], text: `\u672A\u627E\u5230\u300C${resolved.title}\u300D` };
1453
+ const { names } = await activityCategoryNames(service);
1454
+ const registrationUrl = summary.registrationUrl;
1455
+ let detail;
1456
+ try {
1457
+ detail = await service.getActivityDetail(q, summary.activityRef);
1458
+ } catch (error) {
1459
+ ctx.logger?.warn(
1460
+ `\u6D3B\u52A8\u8BE6\u60C5\u67E5\u8BE2\u5931\u8D25\uFF0C\u964D\u7EA7\u5C55\u793A\u5217\u8868\u6458\u8981\uFF1A${error.message}`
1461
+ );
1462
+ }
1463
+ const projected = detail ? activityDetail(detail, "detail") : activityDetail(
1464
+ {
1465
+ ...summary,
1466
+ sessions: [],
1467
+ guide: {
1468
+ steps: [],
1469
+ conditions: [],
1470
+ notice: registrationUrl ? "\u5B8C\u6574\u6D3B\u52A8\u5B89\u6392\u8BF7\u524D\u5F80\u652F\u4ED8\u5B9D\u6D3B\u52A8\u9875\u67E5\u770B" : "\u6D3B\u52A8\u8BE6\u7EC6\u5B89\u6392\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
1471
+ }
1472
+ },
1473
+ "detail"
1474
+ );
1475
+ return {
1476
+ cards: [
1477
+ {
1478
+ cardType: "promotion-activity-detail",
1479
+ data: {
1480
+ ...projected,
1481
+ ...summary.categoryId && names.get(summary.categoryId) ? { categoryName: names.get(summary.categoryId) } : {}
1482
+ }
1483
+ }
1484
+ ],
1485
+ text: detail ? `\u8FD9\u662F\u300C${detail.title}\u300D\u7684\u6D3B\u52A8\u8BE6\u60C5` : registrationUrl ? `\u8FD9\u662F\u300C${summary.title}\u300D\u7684\u6D3B\u52A8\u6982\u89C8\uFF0C\u53EF\u524D\u5F80\u652F\u4ED8\u5B9D\u67E5\u770B\u5B8C\u6574\u5B89\u6392\u5E76\u62A5\u540D` : `\u5148\u4E3A\u4F60\u5C55\u793A\u300C${summary.title}\u300D\u7684\u6D3B\u52A8\u6982\u89C8\uFF0C\u8BE6\u7EC6\u5B89\u6392\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6`
1486
+ };
1487
+ }
1488
+ case "offer-detail": {
1489
+ const service = await resolvePromotionOffersPort(reqCtx, ctx);
1490
+ const offer = await service.getOfferDetail(q, { keyword: resolved.title });
1491
+ if (!offer) return { cards: [], text: `\u672A\u627E\u5230\u300C${resolved.title}\u300D` };
1492
+ const stores = await service.listOfferStores?.(q, offer.offerRef);
1493
+ return {
1494
+ cards: [
1495
+ {
1496
+ cardType: "promotion-offer-detail",
1497
+ data: offerDetail(offer, service.supportsAcquisition !== false, offerPresentation2(offer.offerType), stores)
1498
+ // 能力位运行期探测
1499
+ }
1500
+ ],
1501
+ text: `\u8FD9\u662F\u300C${offer.title}\u300D\u7684\u4F18\u60E0\u8BE6\u60C5\uFF0C\u53EF\u67E5\u770B\u4F7F\u7528\u987B\u77E5\u5E76${offer.claimActionLabel === "\u62A2\u8D2D" ? "\u524D\u5F80\u5C0F\u7A0B\u5E8F\u62A2\u8D2D" : "\u9886\u53D6"}`
1502
+ };
1503
+ }
1504
+ case "entitlement-detail": {
1505
+ const gate = await resolveMemberGate2(
1506
+ ctx.services?.resolve?.("member.status"),
1507
+ reqCtx
1508
+ );
1509
+ if (gate) return { cards: gate.card ? [gate.card] : [], text: gate.notice };
1510
+ const service = await resolvePromotionEntitlementsPort(reqCtx, ctx);
1511
+ const page = await service.listEntitlements(q, { limit: 50 });
1512
+ const summary = page.items.find((item) => item.title === resolved.title) ?? page.items.find((item) => item.title.includes(resolved.title) || resolved.title.includes(item.title));
1513
+ if (!summary) return { cards: [], text: `\u672A\u5728\u4F60\u7684\u5238\u5305\u4E2D\u627E\u5230\u300C${resolved.title}\u300D` };
1514
+ return {
1515
+ cards: [{ cardType: "promotion-entitlement-list", data: { mallId: q.scope.mallId, miniAppId: q.scope.miniAppId, items: [entitlementSummary(summary)] } }],
1516
+ text: `\u8FD9\u662F\u300C${summary.title}\u300D\uFF0C\u70B9\u5F00\u53EF\u770B\u5238\u7801\u4E0E\u4F7F\u7528\u8BF4\u660E`
1517
+ };
1518
+ }
1519
+ case "activity-category": {
1520
+ const delegated = await delegateLinkedActivity(ctx, payload, reqCtx);
1521
+ if (delegated) return delegated;
1522
+ const service = await resolvePromotionActivitiesPort(reqCtx, ctx);
1523
+ const { categories, names } = await activityCategoryNames(service);
1524
+ const category = categories.find((item) => item.name === resolved.name) ?? categories.find((item) => item.name.includes(resolved.name) || resolved.name.includes(item.name));
1525
+ const categoryId = category?.categoryId ?? await service.resolveCategoryId?.(resolved.name);
1526
+ if (!categoryId) return { cards: [], text: `\u672A\u627E\u5230\u300C${resolved.name}\u300D\u6D3B\u52A8\u5206\u7C7B` };
1527
+ const page = await service.listActivities(q, { categoryId, dataScope: "CURRENT", limit: 10 });
1528
+ if (page.items.length === 0) return { cards: [], text: `\u8FD1\u671F\u6682\u65E0\u300C${resolved.name}\u300D\u5206\u7C7B\u6D3B\u52A8` };
1529
+ return {
1530
+ cards: [
1531
+ {
1532
+ cardType: "promotion-activity-list",
1533
+ data: {
1534
+ items: page.items.map((item) => activitySummary(item, names)),
1535
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
1536
+ }
1537
+ }
1538
+ ],
1539
+ text: `\u4E3A\u4F60\u627E\u5230 ${page.items.length} \u573A\u300C${resolved.name}\u300D\u6D3B\u52A8`
1540
+ };
1541
+ }
1542
+ case "search-offers": {
1543
+ const service = await resolvePromotionOffersPort(reqCtx, ctx);
1544
+ const page = await service.searchOffers(q, {});
1545
+ const items = page.items.filter((item) => item.availability.status === "AVAILABLE");
1546
+ if (items.length === 0) return { cards: [], text: "\u6682\u65E0\u53EF\u9886\u4F18\u60E0" };
1547
+ const presentation = offerListPresentation2(items);
1548
+ return {
1549
+ cards: [
1550
+ {
1551
+ cardType: "promotion-offer-list",
1552
+ data: {
1553
+ presentation,
1554
+ mode: "claimable",
1555
+ items: items.map(offerSummary)
1556
+ }
1557
+ }
1558
+ ],
1559
+ text: `\u4E3A\u4F60\u627E\u5230 ${items.length} \u6761\u53EF\u9886\u4F18\u60E0`
1560
+ };
1561
+ }
1562
+ case "search-coupons": {
1563
+ const service = await resolvePromotionOffersPort(reqCtx, ctx);
1564
+ const page = await service.searchOffers(q, { offerTypes: ["COUPON"], limit: 50 });
1565
+ const items = page.items.filter((item) => item.availability.status === "AVAILABLE");
1566
+ if (items.length === 0) return { cards: [], text: "\u5F53\u524D\u5546\u573A\u6682\u672A\u627E\u5230\u53EF\u9886\u53D6\u4F18\u60E0\u5238" };
1567
+ return {
1568
+ cards: [{ cardType: "promotion-offer-list", data: {
1569
+ presentation: "coupon",
1570
+ mode: "claimable",
1571
+ items: items.map(offerSummary)
1572
+ } }],
1573
+ text: `\u4E3A\u4F60\u627E\u5230 ${items.length} \u5F20\u53EF\u9886\u53D6\u4F18\u60E0\u5238`
1574
+ };
1575
+ }
1576
+ case "list-entitlements": {
1577
+ const gate = await resolveMemberGate2(
1578
+ ctx.services?.resolve?.("member.status"),
1579
+ reqCtx
1580
+ );
1581
+ if (gate) return { cards: gate.card ? [gate.card] : [], text: gate.notice };
1582
+ const service = await resolvePromotionEntitlementsPort(reqCtx, ctx);
1583
+ const page = await service.listEntitlements(q, { limit: 50 });
1584
+ if (page.items.length === 0) {
1585
+ const offersService = await resolvePromotionOffersPort(reqCtx, ctx);
1586
+ const offers = await offersService.searchOffers(q, { offerTypes: ["COUPON"], limit: 50 });
1587
+ const items = offers.items.filter((item) => item.availability.status === "AVAILABLE");
1588
+ if (items.length > 0) {
1589
+ return {
1590
+ cards: [{ cardType: "promotion-offer-list", data: {
1591
+ presentation: "coupon",
1592
+ mode: "claimable",
1593
+ items: items.map(offerSummary)
1594
+ } }],
1595
+ text: `\u4F60\u7684\u5238\u5305\u91CC\u8FD8\u6CA1\u6709\u4F18\u60E0\u5238\uFF0C\u4E3A\u4F60\u627E\u5230 ${items.length} \u6761\u53EF\u9886\u4F18\u60E0`
1596
+ };
1597
+ }
1598
+ return { cards: [], text: "\u4F60\u7684\u5238\u5305\u91CC\u8FD8\u6CA1\u6709\u4F18\u60E0\u5238\uFF0C\u5F53\u524D\u4E5F\u6CA1\u6709\u53EF\u9886\u53D6\u4F18\u60E0" };
1599
+ }
1600
+ return {
1601
+ cards: [
1602
+ {
1603
+ cardType: "promotion-entitlement-list",
1604
+ data: {
1605
+ mallId: q.scope.mallId,
1606
+ miniAppId: q.scope.miniAppId,
1607
+ items: page.items.map(entitlementSummary),
1608
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
1609
+ }
1610
+ }
1611
+ ],
1612
+ text: `\u4F60\u7684\u5238\u5305\u91CC\u6709 ${page.items.length} \u5F20\u4F18\u60E0\u5238`
1613
+ };
1614
+ }
1615
+ case "list-activities": {
1616
+ const delegated = await delegateLinkedActivity(ctx, payload, reqCtx);
1617
+ if (delegated) return delegated;
1618
+ const service = await resolvePromotionActivitiesPort(reqCtx, ctx);
1619
+ const page = await service.listActivities(q, { dataScope: "CURRENT", limit: 10 });
1620
+ const { names: categoryNames } = await activityCategoryNames(service);
1621
+ return {
1622
+ cards: [
1623
+ {
1624
+ cardType: "promotion-activity-list",
1625
+ data: {
1626
+ items: page.items.map((item) => activitySummary(item, categoryNames)),
1627
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
1628
+ ...page.items.length === 0 ? { noResultsReason: "\u5F53\u524D\u5546\u573A\u8FD1\u671F\u6682\u65E0\u8425\u9500\u6D3B\u52A8" } : {}
1629
+ }
1630
+ }
1631
+ ],
1632
+ text: page.items.length > 0 ? `\u8FD1\u671F\u6709 ${page.items.length} \u573A\u6D3B\u52A8` : "\u8FD1\u671F\u6682\u65E0\u8425\u9500\u6D3B\u52A8"
1633
+ };
1634
+ }
1635
+ default:
1636
+ return { cards: [], text: "\u8425\u9500\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF0C\u53EF\u95EE\u6211\u4F18\u60E0\u6216\u6D3B\u52A8" };
1637
+ }
1638
+ }
1639
+ };
1640
+ }
1641
+
1642
+ // src/server/cards/index.ts
1643
+ var promotionCards = {
1644
+ "promotion-offer-list": meta_default,
1645
+ "promotion-offer-detail": meta_default2,
1646
+ "promotion-entitlement-list": meta_default3,
1647
+ "promotion-activity-list": meta_default4,
1648
+ "promotion-activity-detail": meta_default5,
1649
+ "promotion-activity-category-list": meta_default6,
1650
+ "promotion-best-deal": meta_default7
1651
+ };
1652
+
1653
+ // src/server/index.ts
1654
+ var serverCtx;
1655
+ var getCtx = () => {
1656
+ if (!serverCtx) throw new Error("promotion \u6A21\u5757\u672A\u6CE8\u518C\uFF08ServerContext \u672A\u7ED1\u5B9A\uFF09");
1657
+ return serverCtx;
1658
+ };
1659
+ var tools = createPromotionTools({ getCtx });
1660
+ var OWNED_INTENT_RE = /我的券|已领|券包|券码/;
1661
+ var listCouponsPreTool = definePreTool(tools.listCoupons, {
1662
+ routing: { mode: "keywords", terms: ["\u4F18\u60E0\u5238", "\u9886\u5238", "\u6709\u4EC0\u4E48\u5238", "\u53EF\u9886"] },
1663
+ decide: ({ text }) => {
1664
+ if (!text?.trim()) return { decision: "skip", reason: "\u7A7A\u6587\u672C" };
1665
+ if (OWNED_INTENT_RE.test(text)) return { decision: "skip", reason: "OWNED \u610F\u56FE\uFF08\u6A21\u578B\u4F20 mode\uFF09" };
1666
+ return { decision: "run", args: { mode: "CLAIMABLE" } };
1667
+ },
1668
+ silent: true
1669
+ });
1670
+ var preTools = {
1671
+ listCoupons: listCouponsPreTool
1672
+ };
1673
+ var cards = {
1674
+ "promotion-offer-list": meta_default,
1675
+ "promotion-offer-detail": meta_default2,
1676
+ "promotion-entitlement-list": meta_default3,
1677
+ "promotion-activity-list": meta_default4,
1678
+ "promotion-activity-detail": meta_default5,
1679
+ "promotion-activity-category-list": meta_default6,
1680
+ "promotion-best-deal": meta_default7
1681
+ };
1682
+ var skills = Object.fromEntries(
1683
+ (await loadModuleSkills("@tbox.cn/app-module-promotion", import.meta.url)).map((s) => [s.name, s])
1684
+ );
1685
+ var serverModule = {
1686
+ declaration: {
1687
+ moduleId: "promotion"
1688
+ },
1689
+ cards,
1690
+ skills,
1691
+ preTools,
1692
+ register(ctx) {
1693
+ serverCtx = ctx;
1694
+ ctx.services.register("promotion", inMemoryPromotion);
1695
+ for (const tool of Object.values(tools)) {
1696
+ ctx.tools.register(tool);
1697
+ }
1698
+ ctx.cards.registerMap(cards);
1699
+ ctx.handlers.register(createPromotionIntentHandler(ctx));
1700
+ const router = Router();
1701
+ router.get("/entitlement-detail", async (req, res) => {
1702
+ const entitlementRef = typeof req.query["entitlementRef"] === "string" ? req.query["entitlementRef"].trim() : "";
1703
+ if (!entitlementRef) {
1704
+ res.status(400).json({ error: "entitlementRef required" });
1705
+ return;
1706
+ }
1707
+ const reqCtx = getRequestContext(req);
1708
+ if (!reqCtx) {
1709
+ res.status(401).json({ error: "unauthorized" });
1710
+ return;
1711
+ }
1712
+ try {
1713
+ const scopedReqCtx = withMallScopeFromQuery(reqCtx, req.query);
1714
+ const service = await resolvePromotionEntitlementsPort(scopedReqCtx, ctx);
1715
+ const detail = await service.getEntitlementDetail({
1716
+ ...toMallQueryContext4(scopedReqCtx)
1717
+ }, entitlementRef);
1718
+ if (!detail) {
1719
+ res.status(404).json({ error: "not_found" });
1720
+ return;
1721
+ }
1722
+ res.json({ data: entitlementSummary(detail) });
1723
+ } catch (error) {
1724
+ ctx.logger?.warn(`promotion entitlement-detail route failed: ${error.message}`);
1725
+ res.status(500).json({ error: "internal_error" });
1726
+ }
1727
+ });
1728
+ ctx.routes.register("promotion", router);
1729
+ const actions = createPromotionActions(ctx);
1730
+ for (const action of Object.values(actions)) {
1731
+ ctx.cardActions.register(action);
1732
+ }
1733
+ }
1734
+ };
1735
+
1736
+ export {
1737
+ InMemoryPromotionService,
1738
+ inMemoryPromotion,
1739
+ resolvePromotionOffersPort,
1740
+ resolvePromotionEntitlementsPort,
1741
+ resolvePromotionActivitiesPort,
1742
+ resolvePromotionAcquisitionPort,
1743
+ promotionCards,
1744
+ serverModule
1745
+ };