@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,233 @@
|
|
|
1
|
+
import logger from '../../utils/logger/loggerModule.js';
|
|
2
|
+
import { setQueueDepth } from '../../observability/metrics.js';
|
|
3
|
+
import { runStickerClassificationCycle } from './stickerClassificationBackgroundRuntime.js';
|
|
4
|
+
import { runStickerAutoPackByTagsCycle } from './stickerAutoPackByTagsRuntime.js';
|
|
5
|
+
import {
|
|
6
|
+
claimWorkerTask,
|
|
7
|
+
completeWorkerTask,
|
|
8
|
+
countWorkerTasksByStatus,
|
|
9
|
+
enqueueWorkerTask,
|
|
10
|
+
failWorkerTask,
|
|
11
|
+
hasPendingWorkerTask,
|
|
12
|
+
} from './stickerWorkerTaskQueueRepository.js';
|
|
13
|
+
|
|
14
|
+
const parseEnvBool = (value, fallback) => {
|
|
15
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
16
|
+
const normalized = String(value).trim().toLowerCase();
|
|
17
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
18
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
19
|
+
return fallback;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PIPELINE_ENABLED = parseEnvBool(process.env.STICKER_WORKER_PIPELINE_ENABLED, false);
|
|
23
|
+
const STARTUP_DELAY_MS = Math.max(1_000, Number(process.env.STICKER_WORKER_PIPELINE_STARTUP_DELAY_MS) || 12_000);
|
|
24
|
+
const SCHEDULER_INTERVAL_MS = Math.max(2_000, Number(process.env.STICKER_WORKER_PIPELINE_SCHEDULER_INTERVAL_MS) || 15_000);
|
|
25
|
+
const POLLER_INTERVAL_MS = Math.max(1_000, Number(process.env.STICKER_WORKER_PIPELINE_POLLER_INTERVAL_MS) || 4_000);
|
|
26
|
+
const WORKER_RETRY_DELAY_SECONDS = Math.max(5, Math.min(3600, Number(process.env.STICKER_WORKER_PIPELINE_RETRY_DELAY_SECONDS) || 45));
|
|
27
|
+
|
|
28
|
+
const TASK_CADENCE_MS = {
|
|
29
|
+
classification_cycle: Math.max(10_000, Number(process.env.STICKER_WORKER_CLASSIFICATION_CADENCE_MS) || 120_000),
|
|
30
|
+
curation_cycle: Math.max(15_000, Number(process.env.STICKER_WORKER_CURATION_CADENCE_MS) || 180_000),
|
|
31
|
+
rebuild_cycle: Math.max(15_000, Number(process.env.STICKER_WORKER_REBUILD_CADENCE_MS) || 240_000),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const TASK_PRIORITY = {
|
|
35
|
+
classification_cycle: 70,
|
|
36
|
+
curation_cycle: 55,
|
|
37
|
+
rebuild_cycle: 50,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let startupHandle = null;
|
|
41
|
+
let schedulerHandle = null;
|
|
42
|
+
let pollerHandle = null;
|
|
43
|
+
let runningPoll = false;
|
|
44
|
+
let taskQueueAvailable = true;
|
|
45
|
+
const nextScheduleByTask = new Map();
|
|
46
|
+
|
|
47
|
+
const taskHandlers = {
|
|
48
|
+
classification_cycle: async () => runStickerClassificationCycle({ processPending: true, processReprocess: true }),
|
|
49
|
+
curation_cycle: async () => runStickerAutoPackByTagsCycle({ enableAdditions: true, enableRebuild: false }),
|
|
50
|
+
rebuild_cycle: async () => runStickerAutoPackByTagsCycle({ enableAdditions: false, enableRebuild: true }),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const refreshQueueDepthMetrics = async () => {
|
|
54
|
+
if (!taskQueueAvailable) return;
|
|
55
|
+
const [pending, processing, failed] = await Promise.all([
|
|
56
|
+
countWorkerTasksByStatus('pending'),
|
|
57
|
+
countWorkerTasksByStatus('processing'),
|
|
58
|
+
countWorkerTasksByStatus('failed'),
|
|
59
|
+
]);
|
|
60
|
+
setQueueDepth('sticker_worker_tasks_pending', pending);
|
|
61
|
+
setQueueDepth('sticker_worker_tasks_processing', processing);
|
|
62
|
+
setQueueDepth('sticker_worker_tasks_failed', failed);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const scheduleTaskIfNeeded = async (taskType) => {
|
|
66
|
+
if (!taskQueueAvailable) return;
|
|
67
|
+
const cadence = TASK_CADENCE_MS[taskType];
|
|
68
|
+
if (!cadence) return;
|
|
69
|
+
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const nextDueAt = nextScheduleByTask.get(taskType) || 0;
|
|
72
|
+
if (now < nextDueAt) return;
|
|
73
|
+
|
|
74
|
+
const hasPending = await hasPendingWorkerTask(taskType);
|
|
75
|
+
if (hasPending) {
|
|
76
|
+
nextScheduleByTask.set(taskType, now + Math.floor(cadence / 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await enqueueWorkerTask({
|
|
81
|
+
taskType,
|
|
82
|
+
payload: { scheduled_by: 'sticker_worker_pipeline' },
|
|
83
|
+
priority: TASK_PRIORITY[taskType] || 50,
|
|
84
|
+
});
|
|
85
|
+
nextScheduleByTask.set(taskType, now + cadence);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const schedulerTick = async () => {
|
|
89
|
+
if (!PIPELINE_ENABLED) return;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await Promise.all([
|
|
93
|
+
scheduleTaskIfNeeded('classification_cycle'),
|
|
94
|
+
scheduleTaskIfNeeded('curation_cycle'),
|
|
95
|
+
scheduleTaskIfNeeded('rebuild_cycle'),
|
|
96
|
+
]);
|
|
97
|
+
await refreshQueueDepthMetrics();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code === 'ER_NO_SUCH_TABLE') {
|
|
100
|
+
taskQueueAvailable = false;
|
|
101
|
+
logger.warn('Fila do pipeline de workers indisponivel (migração pendente).', {
|
|
102
|
+
action: 'sticker_worker_pipeline_queue_unavailable',
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const processSingleTaskType = async (taskType) => {
|
|
111
|
+
if (!taskQueueAvailable) return false;
|
|
112
|
+
const task = await claimWorkerTask({ taskType });
|
|
113
|
+
if (!task) return false;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const handler = taskHandlers[taskType];
|
|
117
|
+
if (typeof handler !== 'function') {
|
|
118
|
+
throw new Error(`handler_not_found:${taskType}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await handler(task.payload || {});
|
|
122
|
+
await completeWorkerTask(task.id);
|
|
123
|
+
|
|
124
|
+
logger.debug('Task de worker concluída.', {
|
|
125
|
+
action: 'sticker_worker_task_completed',
|
|
126
|
+
task_type: taskType,
|
|
127
|
+
task_id: task.id,
|
|
128
|
+
attempts: task.attempts,
|
|
129
|
+
});
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
await failWorkerTask(task.id, {
|
|
133
|
+
error: error?.message || 'worker_task_failed',
|
|
134
|
+
retryDelaySeconds: WORKER_RETRY_DELAY_SECONDS,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
logger.warn('Task de worker falhou.', {
|
|
138
|
+
action: 'sticker_worker_task_failed',
|
|
139
|
+
task_type: taskType,
|
|
140
|
+
task_id: task.id,
|
|
141
|
+
attempts: task.attempts,
|
|
142
|
+
error: error?.message,
|
|
143
|
+
});
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const pollerTick = async () => {
|
|
149
|
+
if (runningPoll || !PIPELINE_ENABLED) return;
|
|
150
|
+
runningPoll = true;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
let progressed = false;
|
|
154
|
+
|
|
155
|
+
progressed = (await processSingleTaskType('classification_cycle')) || progressed;
|
|
156
|
+
progressed = (await processSingleTaskType('curation_cycle')) || progressed;
|
|
157
|
+
progressed = (await processSingleTaskType('rebuild_cycle')) || progressed;
|
|
158
|
+
|
|
159
|
+
if (progressed) {
|
|
160
|
+
await refreshQueueDepthMetrics();
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error?.code === 'ER_NO_SUCH_TABLE') {
|
|
164
|
+
taskQueueAvailable = false;
|
|
165
|
+
logger.warn('Fila do pipeline de workers indisponivel durante o poll.', {
|
|
166
|
+
action: 'sticker_worker_pipeline_queue_unavailable_poll',
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
logger.error('Falha no poller do pipeline de workers.', {
|
|
170
|
+
action: 'sticker_worker_pipeline_poll_failed',
|
|
171
|
+
error: error?.message,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
runningPoll = false;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const startStickerWorkerPipeline = () => {
|
|
180
|
+
if (!PIPELINE_ENABLED) {
|
|
181
|
+
logger.info('Pipeline de workers de sticker desabilitado.', {
|
|
182
|
+
action: 'sticker_worker_pipeline_disabled',
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (startupHandle || schedulerHandle || pollerHandle) return;
|
|
188
|
+
|
|
189
|
+
logger.info('Iniciando pipeline de workers de sticker.', {
|
|
190
|
+
action: 'sticker_worker_pipeline_start',
|
|
191
|
+
startup_delay_ms: STARTUP_DELAY_MS,
|
|
192
|
+
scheduler_interval_ms: SCHEDULER_INTERVAL_MS,
|
|
193
|
+
poller_interval_ms: POLLER_INTERVAL_MS,
|
|
194
|
+
cadence_ms: TASK_CADENCE_MS,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
startupHandle = setTimeout(() => {
|
|
198
|
+
startupHandle = null;
|
|
199
|
+
|
|
200
|
+
void schedulerTick();
|
|
201
|
+
void pollerTick();
|
|
202
|
+
|
|
203
|
+
schedulerHandle = setInterval(() => {
|
|
204
|
+
void schedulerTick();
|
|
205
|
+
}, SCHEDULER_INTERVAL_MS);
|
|
206
|
+
|
|
207
|
+
pollerHandle = setInterval(() => {
|
|
208
|
+
void pollerTick();
|
|
209
|
+
}, POLLER_INTERVAL_MS);
|
|
210
|
+
|
|
211
|
+
if (typeof schedulerHandle.unref === 'function') schedulerHandle.unref();
|
|
212
|
+
if (typeof pollerHandle.unref === 'function') pollerHandle.unref();
|
|
213
|
+
}, STARTUP_DELAY_MS);
|
|
214
|
+
|
|
215
|
+
if (typeof startupHandle.unref === 'function') startupHandle.unref();
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const stopStickerWorkerPipeline = () => {
|
|
219
|
+
if (startupHandle) {
|
|
220
|
+
clearTimeout(startupHandle);
|
|
221
|
+
startupHandle = null;
|
|
222
|
+
}
|
|
223
|
+
if (schedulerHandle) {
|
|
224
|
+
clearInterval(schedulerHandle);
|
|
225
|
+
schedulerHandle = null;
|
|
226
|
+
}
|
|
227
|
+
if (pollerHandle) {
|
|
228
|
+
clearInterval(pollerHandle);
|
|
229
|
+
pollerHandle = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const isStickerWorkerPipelineEnabled = () => PIPELINE_ENABLED;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { executeQuery, TABLES } from '../../../database/index.js';
|
|
4
|
+
|
|
5
|
+
const normalizeTaskType = (value) => {
|
|
6
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
7
|
+
if (['classification_cycle', 'curation_cycle', 'rebuild_cycle'].includes(normalized)) return normalized;
|
|
8
|
+
return null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const normalizeStatus = (value) => {
|
|
12
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
13
|
+
if (['pending', 'processing', 'completed', 'failed'].includes(normalized)) return normalized;
|
|
14
|
+
return null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const clampInt = (value, fallback, min, max) => {
|
|
18
|
+
const numeric = Number(value);
|
|
19
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
20
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parseJson = (value, fallback = null) => {
|
|
24
|
+
if (value === null || value === undefined) return fallback;
|
|
25
|
+
if (typeof value === 'object') return value;
|
|
26
|
+
if (Buffer.isBuffer(value)) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(value.toString('utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return fallback;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const normalizeRow = (row) => {
|
|
46
|
+
if (!row) return null;
|
|
47
|
+
return {
|
|
48
|
+
id: Number(row.id),
|
|
49
|
+
task_type: row.task_type,
|
|
50
|
+
payload: parseJson(row.payload, {}),
|
|
51
|
+
priority: Number(row.priority || 0),
|
|
52
|
+
scheduled_at: row.scheduled_at || null,
|
|
53
|
+
status: row.status,
|
|
54
|
+
attempts: Number(row.attempts || 0),
|
|
55
|
+
max_attempts: Number(row.max_attempts || 0),
|
|
56
|
+
worker_token: row.worker_token || null,
|
|
57
|
+
last_error: row.last_error || null,
|
|
58
|
+
locked_at: row.locked_at || null,
|
|
59
|
+
processed_at: row.processed_at || null,
|
|
60
|
+
created_at: row.created_at || null,
|
|
61
|
+
updated_at: row.updated_at || null,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export async function enqueueWorkerTask(
|
|
66
|
+
{ taskType, payload = {}, priority = 50, scheduledAt = null, maxAttempts = 5 },
|
|
67
|
+
connection = null,
|
|
68
|
+
) {
|
|
69
|
+
const normalizedTaskType = normalizeTaskType(taskType);
|
|
70
|
+
if (!normalizedTaskType) return false;
|
|
71
|
+
|
|
72
|
+
const safePriority = clampInt(priority, 50, 1, 100);
|
|
73
|
+
const safeMaxAttempts = clampInt(maxAttempts, 5, 1, 20);
|
|
74
|
+
const safeScheduledAt = scheduledAt ? new Date(scheduledAt) : null;
|
|
75
|
+
const scheduledValue = safeScheduledAt && Number.isFinite(safeScheduledAt.valueOf()) ? safeScheduledAt : null;
|
|
76
|
+
|
|
77
|
+
await executeQuery(
|
|
78
|
+
`INSERT INTO ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
79
|
+
(task_type, payload, priority, scheduled_at, status, attempts, max_attempts)
|
|
80
|
+
VALUES (?, ?, ?, COALESCE(?, UTC_TIMESTAMP()), 'pending', 0, ?)`,
|
|
81
|
+
[normalizedTaskType, JSON.stringify(payload || {}), safePriority, scheduledValue, safeMaxAttempts],
|
|
82
|
+
connection,
|
|
83
|
+
);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function hasPendingWorkerTask(taskType, connection = null) {
|
|
88
|
+
const normalizedTaskType = normalizeTaskType(taskType);
|
|
89
|
+
if (!normalizedTaskType) return false;
|
|
90
|
+
|
|
91
|
+
const rows = await executeQuery(
|
|
92
|
+
`SELECT id
|
|
93
|
+
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
94
|
+
WHERE task_type = ?
|
|
95
|
+
AND status IN ('pending', 'processing')
|
|
96
|
+
LIMIT 1`,
|
|
97
|
+
[normalizedTaskType],
|
|
98
|
+
connection,
|
|
99
|
+
);
|
|
100
|
+
return rows.length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function claimWorkerTask({ taskType, allowRetryFailed = true } = {}, connection = null) {
|
|
104
|
+
const normalizedTaskType = normalizeTaskType(taskType);
|
|
105
|
+
if (!normalizedTaskType) return null;
|
|
106
|
+
|
|
107
|
+
const workerToken = randomUUID();
|
|
108
|
+
const statusClause = allowRetryFailed
|
|
109
|
+
? "(status = 'pending' OR (status = 'failed' AND attempts < max_attempts))"
|
|
110
|
+
: "status = 'pending'";
|
|
111
|
+
|
|
112
|
+
await executeQuery(
|
|
113
|
+
`UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
114
|
+
SET status = 'processing',
|
|
115
|
+
worker_token = ?,
|
|
116
|
+
locked_at = UTC_TIMESTAMP(),
|
|
117
|
+
attempts = attempts + 1,
|
|
118
|
+
updated_at = UTC_TIMESTAMP()
|
|
119
|
+
WHERE id = (
|
|
120
|
+
SELECT id FROM (
|
|
121
|
+
SELECT id
|
|
122
|
+
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
123
|
+
WHERE task_type = ?
|
|
124
|
+
AND ${statusClause}
|
|
125
|
+
AND scheduled_at <= UTC_TIMESTAMP()
|
|
126
|
+
ORDER BY priority DESC, scheduled_at ASC, id ASC
|
|
127
|
+
LIMIT 1
|
|
128
|
+
) pick
|
|
129
|
+
)`,
|
|
130
|
+
[workerToken, normalizedTaskType],
|
|
131
|
+
connection,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const rows = await executeQuery(
|
|
135
|
+
`SELECT *
|
|
136
|
+
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
137
|
+
WHERE worker_token = ?
|
|
138
|
+
AND status = 'processing'
|
|
139
|
+
ORDER BY id DESC
|
|
140
|
+
LIMIT 1`,
|
|
141
|
+
[workerToken],
|
|
142
|
+
connection,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return normalizeRow(rows?.[0] || null);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function completeWorkerTask(taskId, connection = null) {
|
|
149
|
+
if (!taskId) return false;
|
|
150
|
+
await executeQuery(
|
|
151
|
+
`UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
152
|
+
SET status = 'completed',
|
|
153
|
+
processed_at = UTC_TIMESTAMP(),
|
|
154
|
+
worker_token = NULL,
|
|
155
|
+
locked_at = NULL,
|
|
156
|
+
last_error = NULL,
|
|
157
|
+
updated_at = UTC_TIMESTAMP()
|
|
158
|
+
WHERE id = ?`,
|
|
159
|
+
[taskId],
|
|
160
|
+
connection,
|
|
161
|
+
);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function failWorkerTask(
|
|
166
|
+
taskId,
|
|
167
|
+
{
|
|
168
|
+
error = null,
|
|
169
|
+
retryDelaySeconds = 0,
|
|
170
|
+
} = {},
|
|
171
|
+
connection = null,
|
|
172
|
+
) {
|
|
173
|
+
if (!taskId) return false;
|
|
174
|
+
const safeDelay = clampInt(retryDelaySeconds, 0, 0, 86400 * 7);
|
|
175
|
+
const message = String(error || '').trim().slice(0, 255) || null;
|
|
176
|
+
|
|
177
|
+
await executeQuery(
|
|
178
|
+
`UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
179
|
+
SET status = IF(attempts >= max_attempts, 'failed', 'pending'),
|
|
180
|
+
worker_token = NULL,
|
|
181
|
+
locked_at = NULL,
|
|
182
|
+
last_error = ?,
|
|
183
|
+
scheduled_at = IF(attempts >= max_attempts, scheduled_at, UTC_TIMESTAMP() + INTERVAL ${safeDelay} SECOND),
|
|
184
|
+
updated_at = UTC_TIMESTAMP(),
|
|
185
|
+
processed_at = IF(attempts >= max_attempts, UTC_TIMESTAMP(), processed_at)
|
|
186
|
+
WHERE id = ?`,
|
|
187
|
+
[message, taskId],
|
|
188
|
+
connection,
|
|
189
|
+
);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function countWorkerTasksByStatus(status = 'pending', connection = null) {
|
|
194
|
+
const normalizedStatus = normalizeStatus(status);
|
|
195
|
+
if (!normalizedStatus) return 0;
|
|
196
|
+
|
|
197
|
+
const rows = await executeQuery(
|
|
198
|
+
`SELECT COUNT(*) AS total
|
|
199
|
+
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
200
|
+
WHERE status = ?`,
|
|
201
|
+
[normalizedStatus],
|
|
202
|
+
connection,
|
|
203
|
+
);
|
|
204
|
+
return Number(rows?.[0]?.total || 0);
|
|
205
|
+
}
|