botforje 0.0.1
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/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/action/action.js +44 -0
- package/dist/action/catalog.js +40 -0
- package/dist/action/cooldown.js +60 -0
- package/dist/action/executor.js +23 -0
- package/dist/action/template.js +13 -0
- package/dist/action/types.js +2 -0
- package/dist/action/webhook.js +36 -0
- package/dist/actions/action.js +26 -0
- package/dist/actions/cooldown.js +66 -0
- package/dist/actions/request.js +40 -0
- package/dist/api/routes/auth.js +56 -0
- package/dist/api/routes/bots.js +51 -0
- package/dist/api/routes/config.js +24 -0
- package/dist/api/routes/health.js +15 -0
- package/dist/api/routes/index.js +14 -0
- package/dist/api/routes/messages.js +69 -0
- package/dist/api/routes/sessions.js +97 -0
- package/dist/api/routes/status.js +38 -0
- package/dist/api/server.js +153 -0
- package/dist/auth/service.js +102 -0
- package/dist/boot/fleet.js +207 -0
- package/dist/bot/bot.js +30 -0
- package/dist/bot/fleet.js +211 -0
- package/dist/bot/fuzzy.js +20 -0
- package/dist/bot/mapper.js +47 -0
- package/dist/bot/types.js +2 -0
- package/dist/bot/validation.js +38 -0
- package/dist/bot.js +31 -0
- package/dist/cli/create-bot.js +84 -0
- package/dist/cli.js +138 -0
- package/dist/commands/auth.js +200 -0
- package/dist/commands/create-bot.js +64 -0
- package/dist/commands/guide.js +563 -0
- package/dist/commands/lock.js +73 -0
- package/dist/commands/status.js +145 -0
- package/dist/commands/unlock.js +88 -0
- package/dist/commands/validate.js +19 -0
- package/dist/config/mapper.js +152 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/validation.js +705 -0
- package/dist/config/watcher.js +145 -0
- package/dist/config/yaml.js +197 -0
- package/dist/fleet.js +247 -0
- package/dist/flow/executor.js +286 -0
- package/dist/flow/flow.js +2 -0
- package/dist/flow/mapper.js +72 -0
- package/dist/flow/state.js +115 -0
- package/dist/flow/types.js +2 -0
- package/dist/graph/executor.js +294 -0
- package/dist/graph/graph.js +2 -0
- package/dist/graph/state.js +118 -0
- package/dist/helpers/data.js +42 -0
- package/dist/helpers/fuzzy.js +30 -0
- package/dist/helpers/logger.js +89 -0
- package/dist/helpers/validation.js +38 -0
- package/dist/index.js +92 -0
- package/dist/messages/contracts.js +2 -0
- package/dist/messages/inbox.js +58 -0
- package/dist/messages/outbox.js +136 -0
- package/dist/services/auto-response.js +62 -0
- package/dist/services/cooldown.js +60 -0
- package/dist/services/fleet.js +207 -0
- package/dist/services/flow-executor.js +195 -0
- package/dist/services/flow-state.js +118 -0
- package/dist/services/inbox.js +50 -0
- package/dist/services/message-handler.js +78 -0
- package/dist/services/message-queue.js +138 -0
- package/dist/services/outbox.js +138 -0
- package/dist/services/session.js +118 -0
- package/dist/services/webhook.js +87 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/webhook.js +36 -0
- package/dist/validation/validate.js +592 -0
- package/dist/whatsapp/client.js +293 -0
- package/dist/whatsapp/session.js +158 -0
- package/dist/whatsapp/types.js +109 -0
- package/dist/whatsapp/whatsapp.js +109 -0
- package/package.json +97 -0
- package/scripts/postinstall.js +45 -0
- package/scripts/preuninstall.js +48 -0
- package/scripts/setup-systemd.js +91 -0
- package/service/botforje.service.template +25 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowExecutor = void 0;
|
|
4
|
+
const action_1 = require("../action/action");
|
|
5
|
+
const action_2 = require("../action/action");
|
|
6
|
+
const webhook_1 = require("../action/webhook");
|
|
7
|
+
const fuzzy_1 = require("../helpers/fuzzy");
|
|
8
|
+
const logger_1 = require("../helpers/logger");
|
|
9
|
+
class FlowExecutor {
|
|
10
|
+
constructor(actionCatalog, flowCatalog, flowStateService, outboxService, defaultTimeout = 300, cooldownService) {
|
|
11
|
+
this.actionCatalog = actionCatalog;
|
|
12
|
+
this.flowCatalog = flowCatalog;
|
|
13
|
+
this.flowStateService = flowStateService;
|
|
14
|
+
this.outboxService = outboxService;
|
|
15
|
+
this.defaultTimeout = defaultTimeout;
|
|
16
|
+
this.cooldownService = cooldownService;
|
|
17
|
+
}
|
|
18
|
+
get logger() {
|
|
19
|
+
return (0, logger_1.getLogger)();
|
|
20
|
+
}
|
|
21
|
+
updateCatalogs(actionCatalog, flowCatalog) {
|
|
22
|
+
this.actionCatalog = actionCatalog;
|
|
23
|
+
this.flowCatalog = flowCatalog;
|
|
24
|
+
}
|
|
25
|
+
async handleMessage(bot, message) {
|
|
26
|
+
this.flowStateService.cleanupExpired();
|
|
27
|
+
const state = this.flowStateService.findActive(message.from, bot.id);
|
|
28
|
+
if (state) {
|
|
29
|
+
return this.handleActiveState(bot, message, state);
|
|
30
|
+
}
|
|
31
|
+
return this.handleNewMessage(bot, message);
|
|
32
|
+
}
|
|
33
|
+
async handleActiveState(bot, message, state) {
|
|
34
|
+
const flow = this.flowCatalog.get(state.flowId);
|
|
35
|
+
if (!flow) {
|
|
36
|
+
this.flowStateService.destroy(state.id);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const currentStep = flow.steps[state.stepId];
|
|
40
|
+
if (!currentStep) {
|
|
41
|
+
this.flowStateService.destroy(state.id);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
this.logger.debug(`[active] flow="${flow.id}" step="${state.stepId}" action="${currentStep.action}"`);
|
|
45
|
+
this.logger.debug(` message: "${message.content}" from "${message.from}"`);
|
|
46
|
+
const match = this.findBestBranch(flow, state, message.content);
|
|
47
|
+
if (!match) {
|
|
48
|
+
if (flow.fallbackStep && flow.steps[flow.fallbackStep]) {
|
|
49
|
+
await this.transitionToStep(bot, message, state, flow, flow.fallbackStep);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
this.logger.debug(` no branch matched, ignoring`);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
this.logger.debug(` >> goto="${match.branch.goto}" | node=${match.nodeIndex} score=${match.score.toFixed(3)}`);
|
|
56
|
+
await this.transitionToStep(bot, message, state, flow, match.branch.goto);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async handleNewMessage(bot, message) {
|
|
60
|
+
const flowRefs = [...bot.flows].sort((a, b) => b.priority - a.priority);
|
|
61
|
+
for (const flowRef of flowRefs) {
|
|
62
|
+
const flow = this.flowCatalog.get(flowRef.id);
|
|
63
|
+
if (!flow) {
|
|
64
|
+
this.logger.warn(`Flow "${flowRef.id}" referenced by bot "${bot.id}" not found`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const entryStep = flow.steps[flow.entryStep];
|
|
68
|
+
if (!entryStep) {
|
|
69
|
+
this.logger.warn(`Entry step "${flow.entryStep}" for flow "${flow.id}" not found`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!this.matchesFlowTriggers(flow, message.content)) {
|
|
73
|
+
this.logger.debug(`new: flow "${flow.id}" triggers no match, skip`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
this.logger.debug(`new: flow "${flow.id}" matched, enter "${flow.entryStep}"`);
|
|
77
|
+
const consumed = await this.executeStepAction(bot, message, entryStep, {});
|
|
78
|
+
if (!consumed) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (entryStep.branches.length === 0) {
|
|
82
|
+
this.flowStateService.destroyBySenderBot(message.from, bot.id);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const timeout = flow.timeout ?? this.defaultTimeout;
|
|
86
|
+
this.flowStateService.create(message.from, bot.id, flow.id, flow.entryStep, timeout, Date.now(), { __visitedSteps: [] });
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
async transitionToStep(bot, message, state, flow, stepId) {
|
|
93
|
+
const step = flow.steps[stepId];
|
|
94
|
+
if (!step) {
|
|
95
|
+
this.logger.error(`Step "${stepId}" not found in flow "${flow.id}"`);
|
|
96
|
+
this.flowStateService.destroy(state.id);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const vars = { ...state.variables };
|
|
100
|
+
const visited = vars.__visitedSteps ?? [];
|
|
101
|
+
if (stepId === flow.entryStep) {
|
|
102
|
+
vars.__visitedSteps = [];
|
|
103
|
+
}
|
|
104
|
+
else if (stepId !== state.stepId) {
|
|
105
|
+
const prevStep = flow.steps[state.stepId];
|
|
106
|
+
const hasNavBranches = prevStep?.branches.some(b => b.when && b.when.length > 0);
|
|
107
|
+
if (hasNavBranches) {
|
|
108
|
+
if (visited.includes(state.stepId)) {
|
|
109
|
+
const reordered = visited.filter(s => s !== state.stepId);
|
|
110
|
+
vars.__visitedSteps = [...reordered, state.stepId];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
vars.__visitedSteps = [...visited, state.stepId];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await this.executeStepAction(bot, message, step, vars);
|
|
118
|
+
if (step.branches.length === 0) {
|
|
119
|
+
this.flowStateService.destroy(state.id);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.flowStateService.updateStep(state.id, stepId, vars);
|
|
123
|
+
this.logger.debug(` visited updated: [${(vars.__visitedSteps ?? []).join(', ')}]`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
findBestBranch(flow, state, message) {
|
|
127
|
+
const visited = state.variables?.__visitedSteps ?? [];
|
|
128
|
+
const currentStep = flow.steps[state.stepId];
|
|
129
|
+
if (!currentStep)
|
|
130
|
+
return null;
|
|
131
|
+
this.logger.debug(` context: "${message}"`);
|
|
132
|
+
this.logger.debug(` current: ${state.stepId} | visited: [${visited.join(', ')}]`);
|
|
133
|
+
const nodes = [];
|
|
134
|
+
const allStepNames = [...new Set([...visited, state.stepId])];
|
|
135
|
+
for (const stepName of allStepNames) {
|
|
136
|
+
const step = flow.steps[stepName];
|
|
137
|
+
if (step && stepName !== state.stepId) {
|
|
138
|
+
nodes.push({ branches: step.branches, stepName });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
nodes.push({ branches: currentStep.branches, stepName: state.stepId });
|
|
142
|
+
let best = null;
|
|
143
|
+
for (let ni = 0; ni < nodes.length; ni++) {
|
|
144
|
+
const node = nodes[ni];
|
|
145
|
+
const branchList = node.branches || [];
|
|
146
|
+
const condBranches = branchList.filter(b => b.when && b.when.length > 0);
|
|
147
|
+
if (condBranches.length === 0) {
|
|
148
|
+
this.logger.debug(` Node ${ni} ${node.stepName}: 0 conditional branches`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
this.logger.debug(` Node ${ni} ${node.stepName} — ${condBranches.length} branches:`);
|
|
152
|
+
for (let bi = 0; bi < condBranches.length; bi++) {
|
|
153
|
+
const branch = condBranches[bi];
|
|
154
|
+
const threshold = branch.fuzzyThreshold ?? 0.6;
|
|
155
|
+
const result = (0, fuzzy_1.matchFuzzyVerbose)(branch.when, message, threshold);
|
|
156
|
+
if (!result) {
|
|
157
|
+
this.logger.debug(` [${bi}] when="${branch.when}" → ${branch.goto}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const isBest = !best ||
|
|
161
|
+
result.score < best.score ||
|
|
162
|
+
(result.score === best.score && ni > best.nodeIndex);
|
|
163
|
+
const marker = isBest ? '◀ BEST' : '◀ worse';
|
|
164
|
+
this.logger.debug(` [${bi}] when="${branch.when}" → ${branch.goto} | match="${result.match}" score=${result.score.toFixed(3)} ${marker}`);
|
|
165
|
+
if (isBest) {
|
|
166
|
+
best = { branch, score: result.score, nodeIndex: ni };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (best) {
|
|
171
|
+
this.logger.debug(` >> winner: Node ${best.nodeIndex} goto="${best.branch.goto}" score=${best.score.toFixed(3)}`);
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
const defaultBranch = currentStep.branches.find(b => !b.when || b.when.length === 0);
|
|
175
|
+
if (defaultBranch) {
|
|
176
|
+
this.logger.debug(` >> default → goto="${defaultBranch.goto}"`);
|
|
177
|
+
return { branch: defaultBranch, score: 1, nodeIndex: nodes.length - 1 };
|
|
178
|
+
}
|
|
179
|
+
this.logger.debug(` >> no match`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
matchesFlowTriggers(flow, message) {
|
|
183
|
+
if (!flow.triggers || flow.triggers.length === 0) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
this.logger.debug(` triggers: flow="${flow.id}" ${flow.triggers.length} triggers`);
|
|
187
|
+
for (let i = 0; i < flow.triggers.length; i++) {
|
|
188
|
+
const trigger = flow.triggers[i];
|
|
189
|
+
const threshold = trigger.fuzzyThreshold ?? 0.6;
|
|
190
|
+
const result = (0, fuzzy_1.matchFuzzyVerbose)(trigger.phrases, message, threshold);
|
|
191
|
+
if (result) {
|
|
192
|
+
this.logger.debug(` [${i}] MATCH: "${result.match}" score=${result.score.toFixed(3)}`);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
async executeStepAction(bot, message, step, variables) {
|
|
199
|
+
const context = {
|
|
200
|
+
botId: bot.id,
|
|
201
|
+
botName: bot.id,
|
|
202
|
+
sender: message.from,
|
|
203
|
+
senderName: message.senderName,
|
|
204
|
+
message: message.content,
|
|
205
|
+
variables,
|
|
206
|
+
};
|
|
207
|
+
if (this.isActionOnCooldown(bot, step.action, message.from)) {
|
|
208
|
+
const action = (0, action_1.getAction)(this.actionCatalog, step.action);
|
|
209
|
+
if (action.cooldownReply) {
|
|
210
|
+
const reply = (0, action_2.resolveVars)(action.cooldownReply, context);
|
|
211
|
+
this.outboxService.enqueue(bot.id, message.from, reply);
|
|
212
|
+
this.logger.info(`Cooldown reply sent for action "${step.action}" to ${message.from}`);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
this.logger.warn(`Action "${step.action}" on cooldown, no cooldown_reply defined, skipping`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const result = (0, action_1.executeAction)(this.actionCatalog, step.action, context);
|
|
219
|
+
this.setActionCooldown(step.action, message.from);
|
|
220
|
+
if (result.reply) {
|
|
221
|
+
this.outboxService.enqueue(bot.id, message.from, result.reply);
|
|
222
|
+
}
|
|
223
|
+
if (result.location) {
|
|
224
|
+
this.outboxService.enqueue(bot.id, message.from, '', {
|
|
225
|
+
type: 'location',
|
|
226
|
+
latitude: result.location.latitude,
|
|
227
|
+
longitude: result.location.longitude,
|
|
228
|
+
name: result.location.name,
|
|
229
|
+
address: result.location.address,
|
|
230
|
+
url: result.location.url,
|
|
231
|
+
description: result.location.description,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (result.webhook) {
|
|
235
|
+
try {
|
|
236
|
+
const payload = this.buildWebhookPayload(bot, message, result.webhook.name || 'unnamed');
|
|
237
|
+
await (0, webhook_1.sendWebhookRequest)({
|
|
238
|
+
url: result.webhook.url,
|
|
239
|
+
method: result.webhook.method,
|
|
240
|
+
headers: result.webhook.headers,
|
|
241
|
+
body: payload,
|
|
242
|
+
timeout: result.webhook.timeout,
|
|
243
|
+
retries: result.webhook.retries,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.logger.error(`Failed to trigger webhook action for bot "${bot.id}":`, error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
isActionOnCooldown(bot, actionId, sender) {
|
|
253
|
+
if (!this.cooldownService) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const action = (0, action_1.getAction)(this.actionCatalog, actionId);
|
|
257
|
+
if (!action.cooldown || action.cooldown <= 0) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const cooldownMs = action.cooldown * 1000;
|
|
261
|
+
const cooldownKey = `action:${actionId}`;
|
|
262
|
+
return this.cooldownService.isOnCooldown(sender, cooldownKey, cooldownMs);
|
|
263
|
+
}
|
|
264
|
+
setActionCooldown(actionId, sender) {
|
|
265
|
+
if (!this.cooldownService) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const action = (0, action_1.getAction)(this.actionCatalog, actionId);
|
|
269
|
+
if (!action.cooldown || action.cooldown <= 0) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.cooldownService.setCooldown(sender, `action:${actionId}`);
|
|
273
|
+
}
|
|
274
|
+
buildWebhookPayload(bot, message, webhookName) {
|
|
275
|
+
return {
|
|
276
|
+
sender: message.from,
|
|
277
|
+
message: message.content,
|
|
278
|
+
timestamp: message.timestamp.toISOString(),
|
|
279
|
+
botId: bot.id,
|
|
280
|
+
botName: bot.id,
|
|
281
|
+
webhookName,
|
|
282
|
+
metadata: message.metadata || {},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.FlowExecutor = FlowExecutor;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapFlowCatalog = mapFlowCatalog;
|
|
4
|
+
function mapFlowCatalog(config) {
|
|
5
|
+
const catalog = new Map();
|
|
6
|
+
for (const [id, flowConfig] of Object.entries(config)) {
|
|
7
|
+
catalog.set(id, mapFlow(id, flowConfig));
|
|
8
|
+
}
|
|
9
|
+
return catalog;
|
|
10
|
+
}
|
|
11
|
+
function mapFlow(id, config) {
|
|
12
|
+
if (!config.steps[config.entry_step]) {
|
|
13
|
+
throw new Error(`Flow "${id}" entry step "${config.entry_step}" not found`);
|
|
14
|
+
}
|
|
15
|
+
const steps = {};
|
|
16
|
+
for (const [stepId, stepConfig] of Object.entries(config.steps)) {
|
|
17
|
+
steps[stepId] = mapStep(stepConfig);
|
|
18
|
+
}
|
|
19
|
+
if (config.fallback_step && !steps[config.fallback_step]) {
|
|
20
|
+
throw new Error(`Flow "${id}" fallback step "${config.fallback_step}" not found`);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
entryStep: config.entry_step,
|
|
25
|
+
triggers: config.triggers ? mapTriggers(config.triggers) : undefined,
|
|
26
|
+
timeout: config.timeout,
|
|
27
|
+
fallbackStep: config.fallback_step,
|
|
28
|
+
steps,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function mapStep(config) {
|
|
32
|
+
return {
|
|
33
|
+
action: config.action,
|
|
34
|
+
branches: (config.branches || []).map(mapBranch),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function mapTriggers(triggers) {
|
|
38
|
+
if (typeof triggers === 'string') {
|
|
39
|
+
return [{ phrases: splitPhrases(triggers) }];
|
|
40
|
+
}
|
|
41
|
+
if (triggers.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
if (typeof triggers[0] === 'string') {
|
|
45
|
+
return triggers.map(text => ({ phrases: splitPhrases(text) }));
|
|
46
|
+
}
|
|
47
|
+
return triggers.map(item => ({
|
|
48
|
+
phrases: typeof item.phrases === 'string'
|
|
49
|
+
? splitPhrases(item.phrases)
|
|
50
|
+
: item.phrases.flatMap(splitPhrases),
|
|
51
|
+
fuzzyThreshold: item.fuzzy_threshold,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function mapBranch(config) {
|
|
55
|
+
return {
|
|
56
|
+
when: config.when ? mapWhen(config.when) : undefined,
|
|
57
|
+
fuzzyThreshold: config.fuzzy_threshold,
|
|
58
|
+
goto: config.goto,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function mapWhen(when) {
|
|
62
|
+
if (typeof when === 'string') {
|
|
63
|
+
return splitPhrases(when);
|
|
64
|
+
}
|
|
65
|
+
return when.flatMap(splitPhrases);
|
|
66
|
+
}
|
|
67
|
+
function splitPhrases(value) {
|
|
68
|
+
return value
|
|
69
|
+
.split(',')
|
|
70
|
+
.map(s => s.trim())
|
|
71
|
+
.filter(s => s.length > 0);
|
|
72
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowStateService = void 0;
|
|
4
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
5
|
+
class FlowStateService {
|
|
6
|
+
constructor(dbPath) {
|
|
7
|
+
this.db = new node_sqlite_1.DatabaseSync(dbPath);
|
|
8
|
+
this.setupSchema();
|
|
9
|
+
}
|
|
10
|
+
setupSchema() {
|
|
11
|
+
this.db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS flow_states (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
sender TEXT NOT NULL,
|
|
15
|
+
bot_id TEXT NOT NULL,
|
|
16
|
+
flow_id TEXT NOT NULL,
|
|
17
|
+
step_id TEXT NOT NULL,
|
|
18
|
+
variables TEXT NOT NULL DEFAULT '{}',
|
|
19
|
+
started_at INTEGER NOT NULL,
|
|
20
|
+
last_activity_at INTEGER NOT NULL,
|
|
21
|
+
timeout INTEGER NOT NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_flow_states_sender_bot ON flow_states(sender, bot_id);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_flow_states_last_activity ON flow_states(last_activity_at);
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
findActive(sender, botId, now = Date.now()) {
|
|
29
|
+
const row = this.db
|
|
30
|
+
.prepare('SELECT * FROM flow_states WHERE sender = ? AND bot_id = ?')
|
|
31
|
+
.get(sender, botId);
|
|
32
|
+
if (!row) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const state = this.rowToState(row);
|
|
36
|
+
if (this.isExpired(state, now)) {
|
|
37
|
+
this.destroy(state.id);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
create(sender, botId, flowId, stepId, timeout, now = Date.now(), initialVariables) {
|
|
43
|
+
this.destroyBySenderBot(sender, botId);
|
|
44
|
+
const id = `${botId}-${sender}-${now}`;
|
|
45
|
+
const state = {
|
|
46
|
+
id,
|
|
47
|
+
sender,
|
|
48
|
+
botId,
|
|
49
|
+
flowId,
|
|
50
|
+
stepId,
|
|
51
|
+
variables: { ...initialVariables },
|
|
52
|
+
startedAt: now,
|
|
53
|
+
lastActivityAt: now,
|
|
54
|
+
timeout,
|
|
55
|
+
};
|
|
56
|
+
this.db
|
|
57
|
+
.prepare('INSERT INTO flow_states (id, sender, bot_id, flow_id, step_id, variables, started_at, last_activity_at, timeout) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
58
|
+
.run(state.id, state.sender, state.botId, state.flowId, state.stepId, JSON.stringify(state.variables), state.startedAt, state.lastActivityAt, state.timeout);
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
updateStep(id, stepId, variables, now = Date.now()) {
|
|
62
|
+
const state = this.findById(id);
|
|
63
|
+
if (!state) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const mergedVariables = variables ? { ...state.variables, ...variables } : state.variables;
|
|
67
|
+
this.db
|
|
68
|
+
.prepare('UPDATE flow_states SET step_id = ?, variables = ?, last_activity_at = ? WHERE id = ?')
|
|
69
|
+
.run(stepId, JSON.stringify(mergedVariables), now, id);
|
|
70
|
+
}
|
|
71
|
+
destroy(id) {
|
|
72
|
+
this.db.prepare('DELETE FROM flow_states WHERE id = ?').run(id);
|
|
73
|
+
}
|
|
74
|
+
destroyBySenderBot(sender, botId) {
|
|
75
|
+
this.db.prepare('DELETE FROM flow_states WHERE sender = ? AND bot_id = ?').run(sender, botId);
|
|
76
|
+
}
|
|
77
|
+
cleanupExpired(now = Date.now()) {
|
|
78
|
+
const cutoff = now;
|
|
79
|
+
const result = this.db
|
|
80
|
+
.prepare('DELETE FROM flow_states WHERE (last_activity_at + (timeout * 1000)) < ?')
|
|
81
|
+
.run(cutoff);
|
|
82
|
+
return Number(result.changes);
|
|
83
|
+
}
|
|
84
|
+
count() {
|
|
85
|
+
const row = this.db.prepare('SELECT COUNT(*) as count FROM flow_states').get();
|
|
86
|
+
return row.count;
|
|
87
|
+
}
|
|
88
|
+
close() {
|
|
89
|
+
this.db.close();
|
|
90
|
+
}
|
|
91
|
+
findById(id) {
|
|
92
|
+
const row = this.db.prepare('SELECT * FROM flow_states WHERE id = ?').get(id);
|
|
93
|
+
return row ? this.rowToState(row) : null;
|
|
94
|
+
}
|
|
95
|
+
rowToState(row) {
|
|
96
|
+
return {
|
|
97
|
+
id: row.id,
|
|
98
|
+
sender: row.sender,
|
|
99
|
+
botId: row.bot_id,
|
|
100
|
+
flowId: row.flow_id,
|
|
101
|
+
stepId: row.step_id,
|
|
102
|
+
variables: JSON.parse(row.variables || '{}'),
|
|
103
|
+
startedAt: row.started_at,
|
|
104
|
+
lastActivityAt: row.last_activity_at,
|
|
105
|
+
timeout: row.timeout,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
isExpired(state, now) {
|
|
109
|
+
if (state.timeout <= 0) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return now > state.lastActivityAt + state.timeout * 1000;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.FlowStateService = FlowStateService;
|