@kaikybrofc/omnizap-system 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +534 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/RELEASE-v2.1.2.md +83 -0
- package/app/config/adminIdentity.js +87 -0
- package/app/config/baileysConfig.js +693 -0
- package/app/config/groupUtils.js +388 -0
- package/app/connection/socketController.js +992 -0
- package/app/controllers/messageController.js +354 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
- package/app/modules/adminModule/groupEventHandlers.js +355 -0
- package/app/modules/aiModule/catCommand.js +1006 -0
- package/app/modules/broadcastModule/noticeCommand.js +416 -0
- package/app/modules/gameModule/diceCommand.js +67 -0
- package/app/modules/menuModule/common.js +311 -0
- package/app/modules/menuModule/menus.js +59 -0
- package/app/modules/playModule/playCommand.js +1615 -0
- package/app/modules/quoteModule/quoteCommand.js +851 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/globalRankingCommand.js +65 -0
- package/app/modules/statsModule/noMessageCommand.js +288 -0
- package/app/modules/statsModule/rankingCommand.js +60 -0
- package/app/modules/statsModule/rankingCommon.js +889 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/convertToWebp.js +390 -0
- package/app/modules/stickerModule/stickerCommand.js +454 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
- package/app/modules/stickerModule/stickerTextCommand.js +657 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
- package/app/modules/stickerPackModule/stickerPackService.js +788 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
- package/app/modules/systemMetricsModule/pingCommand.js +421 -0
- package/app/modules/tiktokModule/tiktokCommand.js +798 -0
- package/app/modules/userModule/userCommand.js +1217 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
- package/app/observability/metrics.js +734 -0
- package/app/services/captchaService.js +492 -0
- package/app/services/dbWriteQueue.js +572 -0
- package/app/services/groupMetadataService.js +279 -0
- package/app/services/lidMapService.js +663 -0
- package/app/services/messagePersistenceService.js +56 -0
- package/app/services/newsBroadcastService.js +351 -0
- package/app/services/pokeApiService.js +398 -0
- package/app/services/queueUtils.js +57 -0
- package/app/services/socketState.js +7 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +36 -0
- package/app/utils/antiLink/antiLinkModule.js +804 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/logger/loggerModule.js +262 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
- package/database/index.js +2052 -0
- package/database/init.js +516 -0
- package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
- package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
- package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
- package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
- package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
- package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
- package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
- package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
- package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
- package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
- package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
- package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
- package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
- package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
- package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
- package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
- package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
- package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
- package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
- package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
- package/docker-compose.yml +103 -0
- package/ecosystem.prod.config.cjs +35 -0
- package/eslint.config.js +61 -0
- package/index.js +437 -0
- package/ml/clip_classifier/Dockerfile +16 -0
- package/ml/clip_classifier/README.md +120 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-exporter.cnf +5 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +32 -0
- package/observability/promtail-config.yml +84 -0
- package/package.json +109 -0
- package/public/api-docs/index.html +144 -0
- package/public/css/github-project-panel.css +297 -0
- package/public/css/stickers-admin.css +1272 -0
- package/public/css/styles.css +671 -0
- package/public/index.html +1311 -0
- package/public/js/apps/apiDocsApp.js +310 -0
- package/public/js/apps/createPackApp.js +2069 -0
- package/public/js/apps/homeApp.js +396 -0
- package/public/js/apps/stickersAdminApp.js +1744 -0
- package/public/js/apps/stickersApp.js +4830 -0
- package/public/js/catalog.js +1019 -0
- package/public/js/github-panel/components/CommitList.js +34 -0
- package/public/js/github-panel/components/ErrorState.js +16 -0
- package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
- package/public/js/github-panel/components/ReleaseList.js +38 -0
- package/public/js/github-panel/components/SkeletonPanel.js +22 -0
- package/public/js/github-panel/components/StatCard.js +15 -0
- package/public/js/github-panel/index.js +15 -0
- package/public/js/github-panel/useGithubRepoData.js +154 -0
- package/public/js/github-panel/vendor/react.js +11 -0
- package/public/js/runtime/react-runtime.js +19 -0
- package/public/licenca/index.html +106 -0
- package/public/stickers/admin/index.html +23 -0
- package/public/stickers/create/index.html +47 -0
- package/public/stickers/index.html +48 -0
- package/public/termos-de-uso/index.html +125 -0
- package/scripts/cache-bust.mjs +107 -0
- package/scripts/deploy.sh +458 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/release.sh +129 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
const parseEnvList = (value) =>
|
|
2
|
+
String(value || '')
|
|
3
|
+
.split(',')
|
|
4
|
+
.map((entry) => entry.trim())
|
|
5
|
+
.filter(Boolean);
|
|
6
|
+
|
|
7
|
+
const VERIFIED_PUBLISHERS = new Set(parseEnvList(process.env.STICKER_CREATOR_VERIFIED_PUBLISHERS).map((entry) => entry.toLowerCase()));
|
|
8
|
+
const NSFW_EXPLICIT_THRESHOLD = Number.isFinite(Number(process.env.STICKER_NSFW_EXPLICIT_THRESHOLD))
|
|
9
|
+
? Number(process.env.STICKER_NSFW_EXPLICIT_THRESHOLD)
|
|
10
|
+
: 0.78;
|
|
11
|
+
const NSFW_SUGGESTIVE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_NSFW_SUGGESTIVE_THRESHOLD))
|
|
12
|
+
? Number(process.env.STICKER_NSFW_SUGGESTIVE_THRESHOLD)
|
|
13
|
+
: 0.4;
|
|
14
|
+
const AGE_DECAY_DAYS = Math.max(1, Number(process.env.STICKER_PACK_AGE_DECAY_DAYS) || 45);
|
|
15
|
+
|
|
16
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
17
|
+
const safeNumber = (value, fallback = 0) => {
|
|
18
|
+
const numeric = Number(value);
|
|
19
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const normalizeTag = (value) =>
|
|
23
|
+
String(value || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.normalize('NFD')
|
|
27
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, 40);
|
|
31
|
+
|
|
32
|
+
const buildVectorNorm = (vector) =>
|
|
33
|
+
Math.sqrt(
|
|
34
|
+
Object.values(vector || {})
|
|
35
|
+
.map((value) => safeNumber(value))
|
|
36
|
+
.reduce((sum, current) => sum + current * current, 0),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const cosineSimilarity = (left, right) => {
|
|
40
|
+
if (!left || !right) return 0;
|
|
41
|
+
const leftKeys = Object.keys(left);
|
|
42
|
+
if (!leftKeys.length || !Object.keys(right).length) return 0;
|
|
43
|
+
|
|
44
|
+
const leftNorm = buildVectorNorm(left);
|
|
45
|
+
const rightNorm = buildVectorNorm(right);
|
|
46
|
+
if (leftNorm <= 0 || rightNorm <= 0) return 0;
|
|
47
|
+
|
|
48
|
+
let dot = 0;
|
|
49
|
+
for (const key of leftKeys) {
|
|
50
|
+
dot += safeNumber(left[key]) * safeNumber(right[key]);
|
|
51
|
+
}
|
|
52
|
+
return clamp(dot / (leftNorm * rightNorm), 0, 1);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resolveNsfwLevel = (packClassification) => {
|
|
56
|
+
const nsfw = packClassification?.nsfw || {};
|
|
57
|
+
const maxScore = safeNumber(nsfw.max_score);
|
|
58
|
+
const avgScore = safeNumber(nsfw.avg_score);
|
|
59
|
+
const flagged = safeNumber(nsfw.flagged_items) > 0;
|
|
60
|
+
const reference = Math.max(maxScore, avgScore);
|
|
61
|
+
if (reference >= NSFW_EXPLICIT_THRESHOLD) return 'explicit';
|
|
62
|
+
if (reference >= NSFW_SUGGESTIVE_THRESHOLD || flagged) return 'suggestive';
|
|
63
|
+
return 'safe';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const computeEngagementScore = (engagement) => {
|
|
67
|
+
const opens = safeNumber(engagement?.open_count);
|
|
68
|
+
const likes = safeNumber(engagement?.like_count);
|
|
69
|
+
const dislikes = safeNumber(engagement?.dislike_count);
|
|
70
|
+
if (!opens && !likes && !dislikes) return 0;
|
|
71
|
+
const positive = likes * 2 + opens * 0.15;
|
|
72
|
+
const negative = dislikes * 1.25;
|
|
73
|
+
const raw = positive - negative;
|
|
74
|
+
return Number(clamp(raw / 100, 0, 1.2).toFixed(6));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const computeTrendScore = (interactionStats, { horizonHours = 24, baselineDays = 7 } = {}) => {
|
|
78
|
+
const openHorizon = safeNumber(interactionStats?.open_horizon);
|
|
79
|
+
const openBaseline = safeNumber(interactionStats?.open_baseline);
|
|
80
|
+
const horizonPer24h = horizonHours > 0 ? openHorizon * (24 / horizonHours) : openHorizon;
|
|
81
|
+
const baselineDaily = baselineDays > 0 ? openBaseline / baselineDays : openBaseline;
|
|
82
|
+
const denominator = Math.max(0.5, baselineDaily);
|
|
83
|
+
const trend = horizonPer24h / denominator;
|
|
84
|
+
return Number(clamp(trend, 0, 20).toFixed(6));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const computeQualityScore = ({ items = [], itemClassifications = [] }) => {
|
|
88
|
+
if (!items.length) return 0;
|
|
89
|
+
let sum = 0;
|
|
90
|
+
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
const width = safeNumber(item?.asset?.width);
|
|
93
|
+
const height = safeNumber(item?.asset?.height);
|
|
94
|
+
const bytes = safeNumber(item?.asset?.size_bytes);
|
|
95
|
+
const area = width > 0 && height > 0 ? width * height : 0;
|
|
96
|
+
const areaScore = area > 0 ? clamp(area / (512 * 512), 0.2, 1) : 0.45;
|
|
97
|
+
const sizeScore = bytes > 0 ? clamp(bytes / (18 * 1024), 0.25, 1) : 0.5;
|
|
98
|
+
sum += areaScore * 0.65 + sizeScore * 0.35;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const baseQuality = sum / items.length;
|
|
102
|
+
const classificationPenalty =
|
|
103
|
+
itemClassifications.length > 0
|
|
104
|
+
? itemClassifications.reduce((acc, classification) => {
|
|
105
|
+
const scores = classification?.all_scores || {};
|
|
106
|
+
return acc + safeNumber(scores['blurry image']) * 0.3 + safeNumber(scores['low quality compressed image']) * 0.35;
|
|
107
|
+
}, 0) / itemClassifications.length
|
|
108
|
+
: 0;
|
|
109
|
+
|
|
110
|
+
return Number(clamp(baseQuality - classificationPenalty, 0, 1).toFixed(6));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const computeDiversityScore = ({ tags = [], itemClassifications = [] }) => {
|
|
114
|
+
if (!itemClassifications.length) return tags.length > 0 ? 0.45 : 0.25;
|
|
115
|
+
|
|
116
|
+
const uniqueTags = new Set((Array.isArray(tags) ? tags : []).map((tag) => normalizeTag(tag)).filter(Boolean));
|
|
117
|
+
let pairCount = 0;
|
|
118
|
+
let similaritySum = 0;
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < itemClassifications.length; i += 1) {
|
|
121
|
+
for (let j = i + 1; j < itemClassifications.length; j += 1) {
|
|
122
|
+
pairCount += 1;
|
|
123
|
+
similaritySum += cosineSimilarity(itemClassifications[i]?.all_scores || {}, itemClassifications[j]?.all_scores || {});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const avgSimilarity = pairCount > 0 ? similaritySum / pairCount : 0.5;
|
|
128
|
+
const diversityFromSimilarity = clamp(1 - avgSimilarity, 0, 1);
|
|
129
|
+
const diversityFromTags = clamp(uniqueTags.size / Math.max(3, Math.min(12, itemClassifications.length)), 0, 1);
|
|
130
|
+
return Number((diversityFromSimilarity * 0.7 + diversityFromTags * 0.3).toFixed(6));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const computeCohesionScore = (itemClassifications = []) => {
|
|
134
|
+
if (!Array.isArray(itemClassifications) || itemClassifications.length <= 1) return 1;
|
|
135
|
+
let pairCount = 0;
|
|
136
|
+
let similaritySum = 0;
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < itemClassifications.length; i += 1) {
|
|
139
|
+
for (let j = i + 1; j < itemClassifications.length; j += 1) {
|
|
140
|
+
pairCount += 1;
|
|
141
|
+
similaritySum += cosineSimilarity(itemClassifications[i]?.all_scores || {}, itemClassifications[j]?.all_scores || {});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!pairCount) return 1;
|
|
146
|
+
return Number(clamp(similaritySum / pairCount, 0, 1).toFixed(6));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const computeDuplicatePenalty = ({ itemClassifications = [], duplicateRate = 0 }) => {
|
|
150
|
+
if (!itemClassifications.length) return 0;
|
|
151
|
+
const vectors = itemClassifications.map((entry) => entry?.all_scores || {});
|
|
152
|
+
let nearDuplicates = 0;
|
|
153
|
+
let pairCount = 0;
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < vectors.length; i += 1) {
|
|
156
|
+
for (let j = i + 1; j < vectors.length; j += 1) {
|
|
157
|
+
pairCount += 1;
|
|
158
|
+
if (cosineSimilarity(vectors[i], vectors[j]) >= 0.985) {
|
|
159
|
+
nearDuplicates += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const semanticDuplicateRate = pairCount > 0 ? nearDuplicates / pairCount : 0;
|
|
165
|
+
return Number(clamp(semanticDuplicateRate * 0.7 + safeNumber(duplicateRate) * 0.3, 0, 1).toFixed(6));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const computePackSignals = ({
|
|
169
|
+
pack,
|
|
170
|
+
engagement,
|
|
171
|
+
packClassification,
|
|
172
|
+
itemClassifications = [],
|
|
173
|
+
interactionStats = null,
|
|
174
|
+
duplicateRate = 0,
|
|
175
|
+
scoringWeights = null,
|
|
176
|
+
ageDecayDays = AGE_DECAY_DAYS,
|
|
177
|
+
}) => {
|
|
178
|
+
const resolvedWeights = {
|
|
179
|
+
classification: clamp(safeNumber(scoringWeights?.classification, 0.4), 0.1, 0.7),
|
|
180
|
+
engagement: clamp(safeNumber(scoringWeights?.engagement, 0.3), 0.1, 0.7),
|
|
181
|
+
quality: clamp(safeNumber(scoringWeights?.quality, 0.2), 0.05, 0.6),
|
|
182
|
+
diversity: clamp(safeNumber(scoringWeights?.diversity, 0.1), 0.05, 0.4),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const classificationConfidence = clamp(safeNumber(packClassification?.confidence), 0, 1);
|
|
186
|
+
const qualityScore = computeQualityScore({ items: pack?.items || [], itemClassifications });
|
|
187
|
+
const engagementScore = computeEngagementScore(engagement);
|
|
188
|
+
const diversityScore = computeDiversityScore({
|
|
189
|
+
tags: packClassification?.tags || [],
|
|
190
|
+
itemClassifications,
|
|
191
|
+
});
|
|
192
|
+
const cohesionScore = computeCohesionScore(itemClassifications);
|
|
193
|
+
const duplicatePenalty = computeDuplicatePenalty({ itemClassifications, duplicateRate });
|
|
194
|
+
const trendScore = computeTrendScore(interactionStats);
|
|
195
|
+
const nsfwLevel = resolveNsfwLevel(packClassification);
|
|
196
|
+
const sensitiveContent = nsfwLevel !== 'safe';
|
|
197
|
+
const packScoreRaw =
|
|
198
|
+
classificationConfidence * resolvedWeights.classification +
|
|
199
|
+
engagementScore * resolvedWeights.engagement +
|
|
200
|
+
qualityScore * resolvedWeights.quality +
|
|
201
|
+
diversityScore * resolvedWeights.diversity -
|
|
202
|
+
duplicatePenalty * 0.25;
|
|
203
|
+
const packScore = Number(clamp(packScoreRaw, 0, 1.5).toFixed(6));
|
|
204
|
+
const referenceDate = pack?.updated_at || pack?.created_at || null;
|
|
205
|
+
const ageMs = referenceDate ? Date.now() - Date.parse(referenceDate) : 0;
|
|
206
|
+
const ageDays = Number.isFinite(ageMs) && ageMs > 0 ? ageMs / (24 * 60 * 60 * 1000) : 0;
|
|
207
|
+
const decayWindow = Math.max(1, Number(ageDecayDays) || AGE_DECAY_DAYS);
|
|
208
|
+
const ageDecayFactor = Number(Math.exp(-ageDays / decayWindow).toFixed(6));
|
|
209
|
+
const rankingScore = Number((packScore * ageDecayFactor + trendScore * 0.08 + cohesionScore * 0.05).toFixed(6));
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
classification_confidence: Number(classificationConfidence.toFixed(6)),
|
|
213
|
+
quality_score: qualityScore,
|
|
214
|
+
engagement_score: engagementScore,
|
|
215
|
+
diversity_score: diversityScore,
|
|
216
|
+
cohesion_score: cohesionScore,
|
|
217
|
+
duplicate_penalty: duplicatePenalty,
|
|
218
|
+
trend_score: trendScore,
|
|
219
|
+
pack_score: packScore,
|
|
220
|
+
ranking_score: rankingScore,
|
|
221
|
+
age_decay_factor: ageDecayFactor,
|
|
222
|
+
age_decay_days: decayWindow,
|
|
223
|
+
trending_now: trendScore >= 1.4,
|
|
224
|
+
nsfw_level: nsfwLevel,
|
|
225
|
+
sensitive_content: sensitiveContent,
|
|
226
|
+
scoring_weights: resolvedWeights,
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const sortByScoreDesc = (list, field) =>
|
|
231
|
+
[...list].sort((left, right) => safeNumber(right?.signals?.[field]) - safeNumber(left?.signals?.[field]));
|
|
232
|
+
|
|
233
|
+
const sortByUpdatedDesc = (list) =>
|
|
234
|
+
[...list].sort((left, right) => {
|
|
235
|
+
const leftTime = Date.parse(left?.pack?.updated_at || 0);
|
|
236
|
+
const rightTime = Date.parse(right?.pack?.updated_at || 0);
|
|
237
|
+
return rightTime - leftTime;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export const buildIntentCollections = (entries, { limit = 18 } = {}) => {
|
|
241
|
+
const safeLimit = Math.max(4, Math.min(50, Number(limit) || 18));
|
|
242
|
+
const safeOnly = entries.filter((entry) => entry?.signals?.nsfw_level === 'safe');
|
|
243
|
+
const all = entries;
|
|
244
|
+
const pick = (list) => list.slice(0, safeLimit);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
em_alta: pick(sortByScoreDesc(safeOnly, 'ranking_score')),
|
|
248
|
+
novos: pick(sortByUpdatedDesc(safeOnly)),
|
|
249
|
+
crescendo_agora: pick(sortByScoreDesc(all.filter((entry) => entry?.signals?.trending_now), 'trend_score')),
|
|
250
|
+
mais_curtidos: pick([...safeOnly].sort((a, b) => safeNumber(b?.engagement?.like_count) - safeNumber(a?.engagement?.like_count))),
|
|
251
|
+
melhor_avaliados: pick(
|
|
252
|
+
[...safeOnly].sort(
|
|
253
|
+
(a, b) =>
|
|
254
|
+
safeNumber(b?.engagement?.like_count) - safeNumber(b?.engagement?.dislike_count) - (safeNumber(a?.engagement?.like_count) - safeNumber(a?.engagement?.dislike_count)),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const buildCreatorRanking = (entries, { limit = 50 } = {}) => {
|
|
261
|
+
const safeLimit = Math.max(5, Math.min(200, Number(limit) || 50));
|
|
262
|
+
const grouped = new Map();
|
|
263
|
+
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const publisher = String(entry?.pack?.publisher || '').trim() || 'Unknown';
|
|
266
|
+
const key = publisher.toLowerCase();
|
|
267
|
+
const current = grouped.get(key) || {
|
|
268
|
+
publisher,
|
|
269
|
+
verified: VERIFIED_PUBLISHERS.has(key),
|
|
270
|
+
packs_count: 0,
|
|
271
|
+
total_likes: 0,
|
|
272
|
+
total_opens: 0,
|
|
273
|
+
avg_pack_score: 0,
|
|
274
|
+
top_pack: null,
|
|
275
|
+
};
|
|
276
|
+
current.packs_count += 1;
|
|
277
|
+
current.total_likes += safeNumber(entry?.engagement?.like_count);
|
|
278
|
+
current.total_opens += safeNumber(entry?.engagement?.open_count);
|
|
279
|
+
current.avg_pack_score += safeNumber(entry?.signals?.pack_score);
|
|
280
|
+
if (!current.top_pack || safeNumber(entry?.signals?.pack_score) > safeNumber(current?.top_pack?.signals?.pack_score)) {
|
|
281
|
+
current.top_pack = entry;
|
|
282
|
+
}
|
|
283
|
+
grouped.set(key, current);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const ranking = Array.from(grouped.values())
|
|
287
|
+
.map((entry) => ({
|
|
288
|
+
...entry,
|
|
289
|
+
avg_pack_score: Number((entry.avg_pack_score / Math.max(1, entry.packs_count)).toFixed(6)),
|
|
290
|
+
}))
|
|
291
|
+
.sort((left, right) => {
|
|
292
|
+
const leftScore = left.avg_pack_score * 0.45 + left.total_likes * 0.0008 + left.total_opens * 0.00015;
|
|
293
|
+
const rightScore = right.avg_pack_score * 0.45 + right.total_likes * 0.0008 + right.total_opens * 0.00015;
|
|
294
|
+
return rightScore - leftScore;
|
|
295
|
+
})
|
|
296
|
+
.slice(0, safeLimit);
|
|
297
|
+
|
|
298
|
+
return ranking;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const buildViewerTagAffinity = ({ viewerEntries = [], packClassificationById = new Map() }) => {
|
|
302
|
+
const affinity = new Map();
|
|
303
|
+
for (const viewerEntry of viewerEntries) {
|
|
304
|
+
const summary = packClassificationById.get(viewerEntry.pack_id);
|
|
305
|
+
const tags = Array.isArray(summary?.tags) ? summary.tags : [];
|
|
306
|
+
const weight = Math.max(1, safeNumber(viewerEntry.interactions));
|
|
307
|
+
for (const rawTag of tags) {
|
|
308
|
+
const tag = normalizeTag(rawTag);
|
|
309
|
+
if (!tag) continue;
|
|
310
|
+
affinity.set(tag, (affinity.get(tag) || 0) + weight);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return affinity;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const buildPersonalizedRecommendations = ({
|
|
317
|
+
entries = [],
|
|
318
|
+
viewerAffinity = new Map(),
|
|
319
|
+
excludePackIds = new Set(),
|
|
320
|
+
limit = 18,
|
|
321
|
+
}) => {
|
|
322
|
+
const safeLimit = Math.max(4, Math.min(50, Number(limit) || 18));
|
|
323
|
+
const ranked = [];
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
const packId = entry?.pack?.id;
|
|
326
|
+
if (!packId || excludePackIds.has(packId)) continue;
|
|
327
|
+
if (entry?.signals?.nsfw_level !== 'safe') continue;
|
|
328
|
+
|
|
329
|
+
const tags = Array.isArray(entry?.packClassification?.tags) ? entry.packClassification.tags : [];
|
|
330
|
+
const affinityBoost = tags.reduce((sum, rawTag) => sum + safeNumber(viewerAffinity.get(normalizeTag(rawTag))), 0);
|
|
331
|
+
const score = safeNumber(entry?.signals?.pack_score) * 0.7 + affinityBoost * 0.04 + safeNumber(entry?.signals?.trend_score) * 0.2;
|
|
332
|
+
ranked.push({ entry, score });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
ranked.sort((left, right) => right.score - left.score);
|
|
336
|
+
return ranked.slice(0, safeLimit).map((item) => item.entry);
|
|
337
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
2
|
+
import { sendAndStore } from '../../services/messagePersistenceService.js';
|
|
3
|
+
import { readStickerAssetBuffer } from './stickerStorageService.js';
|
|
4
|
+
import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Serviço de montagem/envio de mensagens para packs de figurinha.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_PREVIEW_LIST_LINES = Math.max(5, Number(process.env.STICKER_PACK_PREVIEW_LINES) || 20);
|
|
10
|
+
const FALLBACK_SEND_LIMIT = Math.max(1, Number(process.env.STICKER_PACK_FALLBACK_SEND_LIMIT) || 30);
|
|
11
|
+
const ZIP_FEATURE_ENABLED = process.env.STICKER_PACK_ZIP_ENABLED === 'true';
|
|
12
|
+
const PACK_VISUAL_DIVIDER = '━━━━━━━━━━━━━━━━━━━';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normaliza lista de emojis de um item.
|
|
16
|
+
*
|
|
17
|
+
* @param {unknown} value Valor de origem.
|
|
18
|
+
* @returns {string[]} Emojis válidos.
|
|
19
|
+
*/
|
|
20
|
+
const normalizeEmojis = (value) => (Array.isArray(value) ? value.map((item) => String(item)).filter(Boolean).slice(0, 8) : []);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Renderiza linha textual de um item para o preview fallback.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} item Item do pack.
|
|
26
|
+
* @param {number} index Índice exibido.
|
|
27
|
+
* @returns {string} Linha renderizada.
|
|
28
|
+
*/
|
|
29
|
+
const renderItemLabel = (item, index) => {
|
|
30
|
+
const emojiText = item.emojis?.length ? ` ${item.emojis.join(' ')}` : '';
|
|
31
|
+
const accessibility = item.accessibility_label ? ` — ${item.accessibility_label}` : '';
|
|
32
|
+
return `${index}. #${item.position}${emojiText}${accessibility}`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Monta texto de preview para modo de envio compatível.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ pack: object, items: object[], sentCount: number }} params Dados para composição.
|
|
39
|
+
* @returns {string} Mensagem textual pronta para envio.
|
|
40
|
+
*/
|
|
41
|
+
const buildPreviewText = ({ pack, items, sentCount }) => {
|
|
42
|
+
const lines = items.slice(0, MAX_PREVIEW_LIST_LINES).map((item, index) => renderItemLabel(item, index + 1));
|
|
43
|
+
const previewLines = [...lines];
|
|
44
|
+
if (items.length > MAX_PREVIEW_LIST_LINES) {
|
|
45
|
+
previewLines.push(`... e mais ${items.length - MAX_PREVIEW_LIST_LINES} figurinha(s).`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const compatibilityNote =
|
|
49
|
+
sentCount < items.length
|
|
50
|
+
? `⚠️ Por compatibilidade, enviei ${sentCount}/${items.length} figurinha(s) neste fallback.`
|
|
51
|
+
: `✅ Envio completo no fallback: ${sentCount}/${items.length} figurinha(s).`;
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
'📦 *GERENCIADOR DE PACKS DE FIGURINHAS*',
|
|
55
|
+
'',
|
|
56
|
+
'📤 *ENVIO EM MODO DE COMPATIBILIDADE*',
|
|
57
|
+
'Seu cliente não aceitou o pack nativo, então enviei preview + figurinhas individuais.',
|
|
58
|
+
'',
|
|
59
|
+
PACK_VISUAL_DIVIDER,
|
|
60
|
+
'📌 *RESUMO DO PACK*',
|
|
61
|
+
'',
|
|
62
|
+
`📛 Nome: *${pack.name}*`,
|
|
63
|
+
`👤 Publisher: *${pack.publisher}*`,
|
|
64
|
+
`🆔 ID: \`${pack.pack_key}\``,
|
|
65
|
+
`🧩 Figurinhas disponíveis: *${items.length}*`,
|
|
66
|
+
'',
|
|
67
|
+
PACK_VISUAL_DIVIDER,
|
|
68
|
+
'🖼 *PRÉVIA DAS FIGURINHAS*',
|
|
69
|
+
'',
|
|
70
|
+
previewLines.join('\n') || 'Nenhuma figurinha disponível para listar.',
|
|
71
|
+
'',
|
|
72
|
+
PACK_VISUAL_DIVIDER,
|
|
73
|
+
compatibilityNote,
|
|
74
|
+
].join('\n');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gera ZIP opcional (feature flag) com conteúdo do pack.
|
|
79
|
+
*
|
|
80
|
+
* @param {{ pack: object, coverBuffer: Buffer, stickers: Array<{ data: Buffer }> }} params Dados do pacote.
|
|
81
|
+
* @returns {Promise<Buffer|null>} ZIP gerado ou `null`.
|
|
82
|
+
*/
|
|
83
|
+
async function buildZipPayload({ pack, coverBuffer, stickers }) {
|
|
84
|
+
if (!ZIP_FEATURE_ENABLED) return null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const { zipSync, strToU8 } = await import('fflate');
|
|
88
|
+
const files = {};
|
|
89
|
+
|
|
90
|
+
files['tray-icon.webp'] = new Uint8Array(coverBuffer);
|
|
91
|
+
|
|
92
|
+
stickers.forEach((sticker, index) => {
|
|
93
|
+
const indexLabel = String(index + 1).padStart(3, '0');
|
|
94
|
+
files[`sticker-${indexLabel}.webp`] = new Uint8Array(sticker.data);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
files['manifest.json'] = strToU8(
|
|
98
|
+
JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
packId: pack.pack_key,
|
|
101
|
+
name: pack.name,
|
|
102
|
+
publisher: pack.publisher,
|
|
103
|
+
stickerCount: stickers.length,
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const zipped = zipSync(files, { level: 6 });
|
|
111
|
+
return Buffer.from(zipped.buffer, zipped.byteOffset, zipped.byteLength);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logger.warn('Não foi possível gerar ZIP do sticker pack (feature-flag).', {
|
|
114
|
+
action: 'sticker_pack_zip_failed',
|
|
115
|
+
error: error.message,
|
|
116
|
+
pack_id: pack.id,
|
|
117
|
+
});
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Monta payload nativo do pack com buffers já carregados.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} packDetails Pack completo com `items`.
|
|
126
|
+
* @returns {Promise<{ pack: object, items: object[], payload: object, zipBuffer: Buffer|null }>} Dados prontos para envio.
|
|
127
|
+
*/
|
|
128
|
+
export async function buildStickerPackMessage(packDetails) {
|
|
129
|
+
const pack = packDetails;
|
|
130
|
+
const items = Array.isArray(packDetails?.items) ? packDetails.items : [];
|
|
131
|
+
|
|
132
|
+
if (!pack || !items.length) {
|
|
133
|
+
throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Pack sem figurinhas para envio.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const preparedItems = [];
|
|
137
|
+
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
if (!item?.asset) continue;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const buffer = await readStickerAssetBuffer(item.asset);
|
|
143
|
+
preparedItems.push({
|
|
144
|
+
...item,
|
|
145
|
+
data: buffer,
|
|
146
|
+
emojis: normalizeEmojis(item.emojis),
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.warn('Sticker ignorado por falha de leitura no storage.', {
|
|
150
|
+
action: 'sticker_pack_item_skipped',
|
|
151
|
+
pack_id: pack.id,
|
|
152
|
+
sticker_id: item.sticker_id,
|
|
153
|
+
error: error.message,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!preparedItems.length) {
|
|
159
|
+
throw new StickerPackError(
|
|
160
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
161
|
+
'Nenhuma figurinha do pack está disponível para envio.',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const coverItem = preparedItems.find((item) => item.sticker_id === pack.cover_sticker_id) || preparedItems[0];
|
|
166
|
+
const stickers = preparedItems.map((item) => ({
|
|
167
|
+
data: item.data,
|
|
168
|
+
emojis: item.emojis,
|
|
169
|
+
accessibilityLabel: item.accessibility_label || undefined,
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const payload = {
|
|
173
|
+
// Baileys feat-add-stickerpack-support espera cover como media direto (Buffer/stream/url).
|
|
174
|
+
cover: coverItem.data,
|
|
175
|
+
name: pack.name,
|
|
176
|
+
publisher: pack.publisher,
|
|
177
|
+
description: pack.description || undefined,
|
|
178
|
+
packId: pack.pack_key,
|
|
179
|
+
stickers,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const zipBuffer = await buildZipPayload({
|
|
183
|
+
pack,
|
|
184
|
+
coverBuffer: coverItem.data,
|
|
185
|
+
stickers,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
pack,
|
|
190
|
+
items: preparedItems,
|
|
191
|
+
payload,
|
|
192
|
+
zipBuffer,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Tenta enviar pack no modo nativo e cai para fallback quando necessário.
|
|
198
|
+
*
|
|
199
|
+
* @param {{
|
|
200
|
+
* sock: object,
|
|
201
|
+
* jid: string,
|
|
202
|
+
* messageInfo: object,
|
|
203
|
+
* expirationMessage: number|undefined,
|
|
204
|
+
* packBuild: { pack: object, items: object[], payload: object },
|
|
205
|
+
* fallbackLimit?: number,
|
|
206
|
+
* }} params Contexto de envio.
|
|
207
|
+
* @returns {Promise<{ mode: 'native'|'fallback', sentCount: number, total?: number, nativeError?: string|null }>}
|
|
208
|
+
*/
|
|
209
|
+
export async function sendStickerPackWithFallback({
|
|
210
|
+
sock,
|
|
211
|
+
jid,
|
|
212
|
+
messageInfo,
|
|
213
|
+
expirationMessage,
|
|
214
|
+
packBuild,
|
|
215
|
+
fallbackLimit = FALLBACK_SEND_LIMIT,
|
|
216
|
+
}) {
|
|
217
|
+
const options = {
|
|
218
|
+
quoted: messageInfo,
|
|
219
|
+
ephemeralExpiration: expirationMessage,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let nativeError = null;
|
|
223
|
+
try {
|
|
224
|
+
// Modo pack nativo.
|
|
225
|
+
await sendAndStore(
|
|
226
|
+
sock,
|
|
227
|
+
jid,
|
|
228
|
+
{
|
|
229
|
+
stickerPack: packBuild.payload,
|
|
230
|
+
},
|
|
231
|
+
options,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
mode: 'native',
|
|
236
|
+
sentCount: packBuild.items.length,
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
nativeError = error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
logger.warn('Envio nativo de sticker pack falhou, ativando fallback.', {
|
|
243
|
+
action: 'sticker_pack_fallback',
|
|
244
|
+
pack_id: packBuild.pack.id,
|
|
245
|
+
owner_jid: packBuild.pack.owner_jid,
|
|
246
|
+
error: nativeError?.message,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const total = packBuild.items.length;
|
|
250
|
+
const sendCount = Math.min(total, Math.max(1, Number(fallbackLimit) || FALLBACK_SEND_LIMIT));
|
|
251
|
+
|
|
252
|
+
await sendAndStore(
|
|
253
|
+
sock,
|
|
254
|
+
jid,
|
|
255
|
+
{
|
|
256
|
+
text: buildPreviewText({
|
|
257
|
+
pack: packBuild.pack,
|
|
258
|
+
items: packBuild.items,
|
|
259
|
+
sentCount: sendCount,
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
options,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (packBuild.payload?.cover) {
|
|
266
|
+
await sendAndStore(
|
|
267
|
+
sock,
|
|
268
|
+
jid,
|
|
269
|
+
{
|
|
270
|
+
sticker: packBuild.payload.cover,
|
|
271
|
+
},
|
|
272
|
+
options,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (let index = 0; index < sendCount; index += 1) {
|
|
277
|
+
const item = packBuild.items[index];
|
|
278
|
+
if (!item?.data) continue;
|
|
279
|
+
|
|
280
|
+
await sendAndStore(
|
|
281
|
+
sock,
|
|
282
|
+
jid,
|
|
283
|
+
{
|
|
284
|
+
sticker: item.data,
|
|
285
|
+
},
|
|
286
|
+
options,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
mode: 'fallback',
|
|
292
|
+
sentCount: sendCount,
|
|
293
|
+
total,
|
|
294
|
+
nativeError: nativeError?.message || null,
|
|
295
|
+
};
|
|
296
|
+
}
|