@dotsetlabs/dotclaw 1.9.0 → 2.1.0
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 +6 -0
- package/README.md +13 -8
- package/config-examples/groups/global/CLAUDE.md +6 -14
- package/config-examples/groups/main/CLAUDE.md +8 -39
- package/config-examples/runtime.json +16 -122
- package/config-examples/tool-policy.json +2 -15
- package/container/agent-runner/package-lock.json +258 -0
- package/container/agent-runner/package.json +2 -1
- package/container/agent-runner/src/agent-config.ts +62 -47
- package/container/agent-runner/src/browser.ts +180 -0
- package/container/agent-runner/src/container-protocol.ts +4 -9
- package/container/agent-runner/src/id.ts +3 -2
- package/container/agent-runner/src/index.ts +331 -846
- package/container/agent-runner/src/ipc.ts +3 -33
- package/container/agent-runner/src/mcp-client.ts +222 -0
- package/container/agent-runner/src/mcp-registry.ts +163 -0
- package/container/agent-runner/src/skill-loader.ts +375 -0
- package/container/agent-runner/src/tools.ts +154 -184
- package/container/agent-runner/src/tts.ts +61 -0
- package/dist/admin-commands.d.ts.map +1 -1
- package/dist/admin-commands.js +12 -0
- package/dist/admin-commands.js.map +1 -1
- package/dist/agent-execution.d.ts +5 -9
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +32 -20
- package/dist/agent-execution.js.map +1 -1
- package/dist/cli.js +61 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -5
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +4 -9
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +3 -8
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +5 -6
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +12 -60
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +1 -59
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +41 -262
- package/dist/db.js.map +1 -1
- package/dist/error-messages.d.ts.map +1 -1
- package/dist/error-messages.js +5 -1
- package/dist/error-messages.js.map +1 -1
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +93 -0
- package/dist/hooks.js.map +1 -0
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +2 -1
- package/dist/id.js.map +1 -1
- package/dist/index.js +673 -2790
- package/dist/index.js.map +1 -1
- package/dist/ipc-dispatcher.d.ts +26 -0
- package/dist/ipc-dispatcher.d.ts.map +1 -0
- package/dist/ipc-dispatcher.js +861 -0
- package/dist/ipc-dispatcher.js.map +1 -0
- package/dist/local-embeddings.d.ts +7 -0
- package/dist/local-embeddings.d.ts.map +1 -0
- package/dist/local-embeddings.js +60 -0
- package/dist/local-embeddings.js.map +1 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +3 -7
- package/dist/maintenance.js.map +1 -1
- package/dist/memory-embeddings.d.ts +1 -1
- package/dist/memory-embeddings.d.ts.map +1 -1
- package/dist/memory-embeddings.js +59 -31
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-store.d.ts +0 -10
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +11 -27
- package/dist/memory-store.js.map +1 -1
- package/dist/message-pipeline.d.ts +47 -0
- package/dist/message-pipeline.d.ts.map +1 -0
- package/dist/message-pipeline.js +652 -0
- package/dist/message-pipeline.js.map +1 -0
- package/dist/metrics.d.ts +7 -10
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +2 -33
- package/dist/metrics.js.map +1 -1
- package/dist/model-registry.d.ts +0 -14
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +0 -36
- package/dist/model-registry.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/providers/discord/discord-format.d.ts +16 -0
- package/dist/providers/discord/discord-format.d.ts.map +1 -0
- package/dist/providers/discord/discord-format.js +153 -0
- package/dist/providers/discord/discord-format.js.map +1 -0
- package/dist/providers/discord/discord-provider.d.ts +50 -0
- package/dist/providers/discord/discord-provider.d.ts.map +1 -0
- package/dist/providers/discord/discord-provider.js +607 -0
- package/dist/providers/discord/discord-provider.js.map +1 -0
- package/dist/providers/discord/index.d.ts +4 -0
- package/dist/providers/discord/index.d.ts.map +1 -0
- package/dist/providers/discord/index.js +3 -0
- package/dist/providers/discord/index.js.map +1 -0
- package/dist/providers/registry.d.ts +14 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +49 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/telegram/index.d.ts +4 -0
- package/dist/providers/telegram/index.d.ts.map +1 -0
- package/dist/providers/telegram/index.js +3 -0
- package/dist/providers/telegram/index.js.map +1 -0
- package/dist/providers/telegram/telegram-format.d.ts +3 -0
- package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-format.js +215 -0
- package/dist/providers/telegram/telegram-format.js.map +1 -0
- package/dist/providers/telegram/telegram-provider.d.ts +51 -0
- package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-provider.js +824 -0
- package/dist/providers/telegram/telegram-provider.js.map +1 -0
- package/dist/providers/types.d.ts +107 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/request-router.d.ts +9 -31
- package/dist/request-router.d.ts.map +1 -1
- package/dist/request-router.js +12 -142
- package/dist/request-router.js.map +1 -1
- package/dist/runtime-config.d.ts +79 -101
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +140 -208
- package/dist/runtime-config.js.map +1 -1
- package/dist/skill-manager.d.ts +39 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +286 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/streaming.d.ts +58 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +196 -0
- package/dist/streaming.js.map +1 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +11 -45
- package/dist/task-scheduler.js.map +1 -1
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +13 -5
- package/dist/tool-policy.js.map +1 -1
- package/dist/transcription.d.ts +8 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +174 -0
- package/dist/transcription.js.map +1 -0
- package/dist/types.d.ts +2 -50
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -4
- package/scripts/bootstrap.js +40 -4
- package/scripts/configure.js +129 -7
- package/scripts/doctor.js +30 -4
- package/scripts/init.js +13 -6
- package/scripts/install.sh +1 -1
- package/config-examples/plugin-http.json +0 -18
- package/container/skills/agent-browser.md +0 -159
- package/dist/background-job-classifier.d.ts +0 -20
- package/dist/background-job-classifier.d.ts.map +0 -1
- package/dist/background-job-classifier.js +0 -145
- package/dist/background-job-classifier.js.map +0 -1
- package/dist/background-jobs.d.ts +0 -56
- package/dist/background-jobs.d.ts.map +0 -1
- package/dist/background-jobs.js +0 -550
- package/dist/background-jobs.js.map +0 -1
- package/dist/planner-probe.d.ts +0 -14
- package/dist/planner-probe.d.ts.map +0 -1
- package/dist/planner-probe.js +0 -97
- package/dist/planner-probe.js.map +0 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createTask, updateTask, deleteTask, getTaskById, } from './db.js';
|
|
4
|
+
import { resolveContainerGroupPathToHost } from './path-mapping.js';
|
|
5
|
+
import { upsertMemoryItems, searchMemories, listMemories, forgetMemories, getMemoryStats, } from './memory-store.js';
|
|
6
|
+
import { invalidatePersonalizationCache } from './personalization.js';
|
|
7
|
+
import { loadModelRegistry, saveModelRegistry } from './model-registry.js';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
import { generateId } from './id.js';
|
|
10
|
+
import { isValidTimezone, normalizeTaskTimezone, parseScheduledTimestamp } from './timezone.js';
|
|
11
|
+
import { loadRuntimeConfig } from './runtime-config.js';
|
|
12
|
+
import { DATA_DIR, MAIN_GROUP_FOLDER, GROUPS_DIR, TIMEZONE, IPC_POLL_INTERVAL, } from './config.js';
|
|
13
|
+
import { runTaskNow } from './task-scheduler.js';
|
|
14
|
+
const runtime = loadRuntimeConfig();
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null;
|
|
17
|
+
}
|
|
18
|
+
function isMemoryScope(value) {
|
|
19
|
+
return value === 'user' || value === 'group' || value === 'global';
|
|
20
|
+
}
|
|
21
|
+
function isMemoryType(value) {
|
|
22
|
+
return value === 'identity'
|
|
23
|
+
|| value === 'preference'
|
|
24
|
+
|| value === 'fact'
|
|
25
|
+
|| value === 'relationship'
|
|
26
|
+
|| value === 'project'
|
|
27
|
+
|| value === 'task'
|
|
28
|
+
|| value === 'note'
|
|
29
|
+
|| value === 'archive';
|
|
30
|
+
}
|
|
31
|
+
function isMemoryKind(value) {
|
|
32
|
+
return value === 'semantic'
|
|
33
|
+
|| value === 'episodic'
|
|
34
|
+
|| value === 'procedural'
|
|
35
|
+
|| value === 'preference';
|
|
36
|
+
}
|
|
37
|
+
function coerceMemoryItems(input) {
|
|
38
|
+
if (!Array.isArray(input))
|
|
39
|
+
return [];
|
|
40
|
+
const items = [];
|
|
41
|
+
for (const raw of input) {
|
|
42
|
+
if (!isRecord(raw))
|
|
43
|
+
continue;
|
|
44
|
+
const scope = raw.scope;
|
|
45
|
+
const type = raw.type;
|
|
46
|
+
const kind = raw.kind;
|
|
47
|
+
const content = raw.content;
|
|
48
|
+
if (!isMemoryScope(scope) || !isMemoryType(type) || typeof content !== 'string' || !content.trim()) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
items.push({
|
|
52
|
+
scope,
|
|
53
|
+
type,
|
|
54
|
+
kind: isMemoryKind(kind) ? kind : undefined,
|
|
55
|
+
conflict_key: typeof raw.conflict_key === 'string' ? raw.conflict_key : undefined,
|
|
56
|
+
content: content.trim(),
|
|
57
|
+
subject_id: typeof raw.subject_id === 'string' ? raw.subject_id : null,
|
|
58
|
+
importance: typeof raw.importance === 'number' ? raw.importance : undefined,
|
|
59
|
+
confidence: typeof raw.confidence === 'number' ? raw.confidence : undefined,
|
|
60
|
+
tags: Array.isArray(raw.tags) ? raw.tags.filter((tag) => typeof tag === 'string') : undefined,
|
|
61
|
+
ttl_days: typeof raw.ttl_days === 'number' ? raw.ttl_days : undefined,
|
|
62
|
+
source: typeof raw.source === 'string' ? raw.source : undefined,
|
|
63
|
+
metadata: isRecord(raw.metadata) ? raw.metadata : undefined
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
function normalizePollOptions(rawOptions) {
|
|
69
|
+
if (!Array.isArray(rawOptions))
|
|
70
|
+
return null;
|
|
71
|
+
const options = rawOptions
|
|
72
|
+
.filter((value) => typeof value === 'string')
|
|
73
|
+
.map(option => option.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
if (options.length < 2 || options.length > 10)
|
|
76
|
+
return null;
|
|
77
|
+
if (options.some(option => option.length > 100))
|
|
78
|
+
return null;
|
|
79
|
+
if (new Set(options.map(option => option.toLowerCase())).size !== options.length)
|
|
80
|
+
return null;
|
|
81
|
+
return options;
|
|
82
|
+
}
|
|
83
|
+
function isAllowedInlineButtonUrl(value) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(value);
|
|
86
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'tg:';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function normalizeInlineKeyboard(rawButtons) {
|
|
93
|
+
if (!Array.isArray(rawButtons) || rawButtons.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
const rows = [];
|
|
96
|
+
for (const rawRow of rawButtons) {
|
|
97
|
+
if (!Array.isArray(rawRow) || rawRow.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const row = [];
|
|
100
|
+
for (const rawButton of rawRow) {
|
|
101
|
+
if (!rawButton || typeof rawButton !== 'object')
|
|
102
|
+
return null;
|
|
103
|
+
const button = rawButton;
|
|
104
|
+
const text = typeof button.text === 'string' ? button.text.trim() : '';
|
|
105
|
+
const url = typeof button.url === 'string' ? button.url.trim() : '';
|
|
106
|
+
const callbackData = typeof button.callback_data === 'string' ? button.callback_data : '';
|
|
107
|
+
const hasUrl = url.length > 0;
|
|
108
|
+
const hasCallback = callbackData.length > 0;
|
|
109
|
+
if (!text || hasUrl === hasCallback)
|
|
110
|
+
return null;
|
|
111
|
+
if (hasUrl && !isAllowedInlineButtonUrl(url))
|
|
112
|
+
return null;
|
|
113
|
+
if (hasCallback && callbackData.length > 64)
|
|
114
|
+
return null;
|
|
115
|
+
if (hasUrl)
|
|
116
|
+
row.push({ text, url });
|
|
117
|
+
else
|
|
118
|
+
row.push({ text, callback_data: callbackData });
|
|
119
|
+
}
|
|
120
|
+
rows.push(row);
|
|
121
|
+
}
|
|
122
|
+
return rows;
|
|
123
|
+
}
|
|
124
|
+
function resolveContainerPathToHost(containerPath, groupFolder) {
|
|
125
|
+
return resolveContainerGroupPathToHost(containerPath, groupFolder, GROUPS_DIR);
|
|
126
|
+
}
|
|
127
|
+
let ipcWatcher = null;
|
|
128
|
+
let ipcPollingTimer = null;
|
|
129
|
+
let ipcStopped = false;
|
|
130
|
+
export function stopIpcWatcher() {
|
|
131
|
+
ipcStopped = true;
|
|
132
|
+
if (ipcWatcher) {
|
|
133
|
+
ipcWatcher.close();
|
|
134
|
+
ipcWatcher = null;
|
|
135
|
+
}
|
|
136
|
+
if (ipcPollingTimer) {
|
|
137
|
+
clearTimeout(ipcPollingTimer);
|
|
138
|
+
ipcPollingTimer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function startIpcWatcher(deps) {
|
|
142
|
+
const ipcBaseDir = path.join(DATA_DIR, 'ipc');
|
|
143
|
+
fs.mkdirSync(ipcBaseDir, { recursive: true });
|
|
144
|
+
ipcStopped = false;
|
|
145
|
+
let processing = false;
|
|
146
|
+
let scheduled = false;
|
|
147
|
+
let rerunRequested = false;
|
|
148
|
+
const processIpcFiles = async () => {
|
|
149
|
+
if (processing) {
|
|
150
|
+
rerunRequested = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
processing = true;
|
|
154
|
+
try {
|
|
155
|
+
do {
|
|
156
|
+
rerunRequested = false;
|
|
157
|
+
let groupFolders;
|
|
158
|
+
try {
|
|
159
|
+
groupFolders = fs.readdirSync(ipcBaseDir).filter(f => {
|
|
160
|
+
const stat = fs.statSync(path.join(ipcBaseDir, f));
|
|
161
|
+
return stat.isDirectory() && f !== 'errors';
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.error({ err }, 'Error reading IPC base directory');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const sourceGroup of groupFolders) {
|
|
169
|
+
const isMain = sourceGroup === MAIN_GROUP_FOLDER;
|
|
170
|
+
const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
|
|
171
|
+
const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
|
|
172
|
+
const requestsDir = path.join(ipcBaseDir, sourceGroup, 'requests');
|
|
173
|
+
const responsesDir = path.join(ipcBaseDir, sourceGroup, 'responses');
|
|
174
|
+
// Process messages
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(messagesDir)) {
|
|
177
|
+
const messageFiles = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
|
|
178
|
+
for (const file of messageFiles) {
|
|
179
|
+
const filePath = path.join(messagesDir, file);
|
|
180
|
+
try {
|
|
181
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
182
|
+
await processIpcMessage(deps, data, sourceGroup, isMain);
|
|
183
|
+
fs.unlinkSync(filePath);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
logger.error({ file, sourceGroup, err }, 'Error processing IPC message');
|
|
187
|
+
const errorDir = path.join(ipcBaseDir, 'errors');
|
|
188
|
+
fs.mkdirSync(errorDir, { recursive: true });
|
|
189
|
+
fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${file}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
logger.error({ err, sourceGroup }, 'Error reading IPC messages directory');
|
|
196
|
+
}
|
|
197
|
+
// Process tasks
|
|
198
|
+
try {
|
|
199
|
+
if (fs.existsSync(tasksDir)) {
|
|
200
|
+
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
|
201
|
+
for (const file of taskFiles) {
|
|
202
|
+
const filePath = path.join(tasksDir, file);
|
|
203
|
+
try {
|
|
204
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
205
|
+
await processTaskIpc(deps, data, sourceGroup, isMain);
|
|
206
|
+
fs.unlinkSync(filePath);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.error({ file, sourceGroup, err }, 'Error processing IPC task');
|
|
210
|
+
const errorDir = path.join(ipcBaseDir, 'errors');
|
|
211
|
+
fs.mkdirSync(errorDir, { recursive: true });
|
|
212
|
+
fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${file}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
logger.error({ err, sourceGroup }, 'Error reading IPC tasks directory');
|
|
219
|
+
}
|
|
220
|
+
// Process request/response IPC
|
|
221
|
+
try {
|
|
222
|
+
if (fs.existsSync(requestsDir)) {
|
|
223
|
+
fs.mkdirSync(responsesDir, { recursive: true });
|
|
224
|
+
const requestFiles = fs.readdirSync(requestsDir).filter(f => f.endsWith('.json'));
|
|
225
|
+
for (const file of requestFiles) {
|
|
226
|
+
const filePath = path.join(requestsDir, file);
|
|
227
|
+
try {
|
|
228
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
229
|
+
const response = await processRequestIpc(deps, data, sourceGroup, isMain);
|
|
230
|
+
if (response?.id) {
|
|
231
|
+
const responsePath = path.join(responsesDir, `${response.id}.json`);
|
|
232
|
+
const tmpPath = responsePath + '.tmp';
|
|
233
|
+
fs.writeFileSync(tmpPath, JSON.stringify(response, null, 2));
|
|
234
|
+
fs.renameSync(tmpPath, responsePath);
|
|
235
|
+
}
|
|
236
|
+
fs.unlinkSync(filePath);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
logger.error({ file, sourceGroup, err }, 'Error processing IPC request');
|
|
240
|
+
const errorDir = path.join(ipcBaseDir, 'errors');
|
|
241
|
+
fs.mkdirSync(errorDir, { recursive: true });
|
|
242
|
+
fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${file}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
logger.error({ err, sourceGroup }, 'Error reading IPC requests directory');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} while (rerunRequested && !ipcStopped);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
processing = false;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const scheduleProcess = () => {
|
|
258
|
+
if (ipcStopped)
|
|
259
|
+
return;
|
|
260
|
+
if (processing) {
|
|
261
|
+
rerunRequested = true;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (scheduled)
|
|
265
|
+
return;
|
|
266
|
+
scheduled = true;
|
|
267
|
+
setTimeout(async () => {
|
|
268
|
+
scheduled = false;
|
|
269
|
+
if (!ipcStopped)
|
|
270
|
+
await processIpcFiles();
|
|
271
|
+
}, 100);
|
|
272
|
+
};
|
|
273
|
+
let watcherActive = false;
|
|
274
|
+
try {
|
|
275
|
+
ipcWatcher = fs.watch(ipcBaseDir, { recursive: true }, () => {
|
|
276
|
+
scheduleProcess();
|
|
277
|
+
});
|
|
278
|
+
ipcWatcher.on('error', (err) => {
|
|
279
|
+
logger.warn({ err }, 'IPC watcher error; falling back to polling');
|
|
280
|
+
ipcWatcher?.close();
|
|
281
|
+
ipcWatcher = null;
|
|
282
|
+
if (!ipcPollingTimer && !ipcStopped) {
|
|
283
|
+
const poll = () => {
|
|
284
|
+
if (ipcStopped)
|
|
285
|
+
return;
|
|
286
|
+
scheduleProcess();
|
|
287
|
+
ipcPollingTimer = setTimeout(poll, IPC_POLL_INTERVAL);
|
|
288
|
+
};
|
|
289
|
+
poll();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
watcherActive = true;
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
logger.warn({ err }, 'IPC watch unsupported; falling back to polling');
|
|
296
|
+
}
|
|
297
|
+
if (!watcherActive) {
|
|
298
|
+
const poll = () => {
|
|
299
|
+
if (ipcStopped)
|
|
300
|
+
return;
|
|
301
|
+
scheduleProcess();
|
|
302
|
+
ipcPollingTimer = setTimeout(poll, IPC_POLL_INTERVAL);
|
|
303
|
+
};
|
|
304
|
+
poll();
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
scheduleProcess();
|
|
308
|
+
}
|
|
309
|
+
if (ipcPollingTimer) {
|
|
310
|
+
logger.info('IPC watcher started (polling)');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
logger.info('IPC watcher started (fs.watch)');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function processIpcMessage(deps, data, sourceGroup, isMain) {
|
|
317
|
+
const registeredGroups = deps.registeredGroups();
|
|
318
|
+
const chatJid = data.chatJid;
|
|
319
|
+
const targetGroup = chatJid ? registeredGroups[chatJid] : undefined;
|
|
320
|
+
const isAuthorized = chatJid && (isMain || (targetGroup && targetGroup.folder === sourceGroup));
|
|
321
|
+
const rawReplyTo = typeof data.reply_to_message_id === 'number'
|
|
322
|
+
? Math.trunc(data.reply_to_message_id)
|
|
323
|
+
: NaN;
|
|
324
|
+
const replyTo = Number.isInteger(rawReplyTo) && rawReplyTo > 0
|
|
325
|
+
? String(rawReplyTo)
|
|
326
|
+
: undefined;
|
|
327
|
+
const messageText = typeof data.text === 'string' ? data.text.trim() : '';
|
|
328
|
+
const provider = chatJid ? deps.registry.getProviderForChat(chatJid) : null;
|
|
329
|
+
if (!provider || !chatJid)
|
|
330
|
+
return;
|
|
331
|
+
if (data.type === 'message' && messageText) {
|
|
332
|
+
if (isAuthorized) {
|
|
333
|
+
await provider.sendMessage(chatJid, messageText, { replyToMessageId: replyTo });
|
|
334
|
+
logger.info({ chatJid, sourceGroup }, 'IPC message sent');
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC message attempt blocked');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if ((data.type === 'send_file' || data.type === 'send_photo') && data.path) {
|
|
341
|
+
if (isAuthorized) {
|
|
342
|
+
const hostPath = resolveContainerPathToHost(data.path, sourceGroup);
|
|
343
|
+
if (hostPath && fs.existsSync(hostPath)) {
|
|
344
|
+
const caption = typeof data.caption === 'string' ? data.caption : undefined;
|
|
345
|
+
if (data.type === 'send_photo') {
|
|
346
|
+
await provider.sendPhoto(chatJid, hostPath, { caption, replyToMessageId: replyTo });
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
await provider.sendDocument(chatJid, hostPath, { caption, replyToMessageId: replyTo });
|
|
350
|
+
}
|
|
351
|
+
logger.info({ chatJid, sourceGroup, type: data.type, path: data.path }, 'IPC file sent');
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
logger.warn({ chatJid, sourceGroup, path: data.path, hostPath }, 'IPC file not found');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC file send attempt blocked');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if ((data.type === 'send_voice' || data.type === 'send_audio') && data.path) {
|
|
362
|
+
if (isAuthorized) {
|
|
363
|
+
const hostPath = resolveContainerPathToHost(data.path, sourceGroup);
|
|
364
|
+
if (hostPath && fs.existsSync(hostPath)) {
|
|
365
|
+
const caption = typeof data.caption === 'string' ? data.caption : undefined;
|
|
366
|
+
const duration = typeof data.duration === 'number' ? data.duration : undefined;
|
|
367
|
+
if (data.type === 'send_voice') {
|
|
368
|
+
await provider.sendVoice(chatJid, hostPath, { caption, duration, replyToMessageId: replyTo });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const performer = typeof data.performer === 'string' ? data.performer : undefined;
|
|
372
|
+
const title = typeof data.title === 'string' ? data.title : undefined;
|
|
373
|
+
await provider.sendAudio(chatJid, hostPath, { caption, duration, performer, title, replyToMessageId: replyTo });
|
|
374
|
+
}
|
|
375
|
+
logger.info({ chatJid, sourceGroup, type: data.type }, 'IPC audio/voice sent');
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
logger.warn({ chatJid, sourceGroup, path: data.path }, 'IPC audio file not found');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC audio send blocked');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (data.type === 'send_location') {
|
|
386
|
+
if (isAuthorized) {
|
|
387
|
+
const lat = typeof data.latitude === 'number' ? data.latitude : NaN;
|
|
388
|
+
const lng = typeof data.longitude === 'number' ? data.longitude : NaN;
|
|
389
|
+
if (Number.isFinite(lat) && Number.isFinite(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
|
390
|
+
await provider.sendLocation(chatJid, lat, lng, { replyToMessageId: replyTo });
|
|
391
|
+
logger.info({ chatJid, sourceGroup }, 'IPC location sent');
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
logger.warn({ chatJid, sourceGroup }, 'Invalid location coordinates');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC location send blocked');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (data.type === 'send_contact') {
|
|
402
|
+
if (isAuthorized) {
|
|
403
|
+
const phone = typeof data.phone_number === 'string' ? data.phone_number.trim() : '';
|
|
404
|
+
const firstName = typeof data.first_name === 'string' ? data.first_name.trim() : '';
|
|
405
|
+
const lastName = typeof data.last_name === 'string' ? data.last_name.trim() : undefined;
|
|
406
|
+
if (phone && firstName) {
|
|
407
|
+
await provider.sendContact(chatJid, phone, firstName, { lastName, replyToMessageId: replyTo });
|
|
408
|
+
logger.info({ chatJid, sourceGroup }, 'IPC contact sent');
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
logger.warn({ chatJid, sourceGroup }, 'Invalid contact (phone/name missing)');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC contact send blocked');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (data.type === 'send_poll') {
|
|
419
|
+
if (isAuthorized) {
|
|
420
|
+
const question = typeof data.question === 'string' ? data.question.trim() : '';
|
|
421
|
+
const options = normalizePollOptions(data.options);
|
|
422
|
+
const pollType = data.poll_type === 'quiz' ? 'quiz' : 'regular';
|
|
423
|
+
const allowsMultipleAnswers = typeof data.allows_multiple_answers === 'boolean'
|
|
424
|
+
? data.allows_multiple_answers
|
|
425
|
+
: undefined;
|
|
426
|
+
const rawCorrectOptionId = typeof data.correct_option_id === 'number' ? Math.trunc(data.correct_option_id) : undefined;
|
|
427
|
+
const hasValidCorrectOption = rawCorrectOptionId !== undefined
|
|
428
|
+
&& options !== null
|
|
429
|
+
&& rawCorrectOptionId >= 0
|
|
430
|
+
&& rawCorrectOptionId < options.length;
|
|
431
|
+
const invalidQuizConfig = pollType === 'quiz' && allowsMultipleAnswers;
|
|
432
|
+
const unexpectedCorrectOption = pollType !== 'quiz' && rawCorrectOptionId !== undefined;
|
|
433
|
+
if (question && question.length <= 300 && options && !invalidQuizConfig && !unexpectedCorrectOption && (rawCorrectOptionId === undefined || hasValidCorrectOption)) {
|
|
434
|
+
await provider.sendPoll(chatJid, question, options, {
|
|
435
|
+
isAnonymous: typeof data.is_anonymous === 'boolean' ? data.is_anonymous : undefined,
|
|
436
|
+
type: pollType,
|
|
437
|
+
allowsMultipleAnswers,
|
|
438
|
+
correctOptionId: pollType === 'quiz' ? rawCorrectOptionId : undefined,
|
|
439
|
+
replyToMessageId: replyTo
|
|
440
|
+
});
|
|
441
|
+
logger.info({ chatJid, sourceGroup }, 'IPC poll sent');
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
logger.warn({ chatJid, sourceGroup }, 'Invalid poll payload');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC poll send blocked');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (data.type === 'send_buttons') {
|
|
452
|
+
if (isAuthorized) {
|
|
453
|
+
const text = typeof data.text === 'string' ? data.text.trim() : '';
|
|
454
|
+
const normalizedButtons = normalizeInlineKeyboard(data.buttons);
|
|
455
|
+
if (text && normalizedButtons) {
|
|
456
|
+
const buttons = normalizedButtons.map(row => row.map(btn => ({
|
|
457
|
+
text: btn.text,
|
|
458
|
+
callbackData: btn.callback_data,
|
|
459
|
+
url: btn.url,
|
|
460
|
+
})));
|
|
461
|
+
await provider.sendButtons(chatJid, text, buttons, { replyToMessageId: replyTo });
|
|
462
|
+
logger.info({ chatJid, sourceGroup }, 'IPC buttons sent');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
logger.warn({ chatJid, sourceGroup }, 'Invalid buttons message');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
logger.warn({ chatJid, sourceGroup }, 'Unauthorized IPC buttons send blocked');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function processTaskIpc(deps, data, sourceGroup, isMain) {
|
|
474
|
+
const { CronExpressionParser } = await import('cron-parser');
|
|
475
|
+
const registeredGroups = deps.registeredGroups();
|
|
476
|
+
switch (data.type) {
|
|
477
|
+
case 'schedule_task': {
|
|
478
|
+
if (data.prompt && data.schedule_type && data.schedule_value && data.groupFolder) {
|
|
479
|
+
const targetGroup = data.groupFolder;
|
|
480
|
+
if (!isMain && targetGroup !== sourceGroup) {
|
|
481
|
+
logger.warn({ sourceGroup, targetGroup }, 'Unauthorized schedule_task attempt blocked');
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const targetChatId = Object.entries(registeredGroups).find(([, group]) => group.folder === targetGroup)?.[0];
|
|
485
|
+
if (!targetChatId) {
|
|
486
|
+
logger.warn({ targetGroup }, 'Cannot schedule task: target group not registered');
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
const scheduleType = data.schedule_type;
|
|
490
|
+
let taskTimezone = TIMEZONE;
|
|
491
|
+
if (typeof data.timezone === 'string' && data.timezone.trim()) {
|
|
492
|
+
const candidateTimezone = data.timezone.trim();
|
|
493
|
+
if (!isValidTimezone(candidateTimezone)) {
|
|
494
|
+
logger.warn({ timezone: data.timezone }, 'Invalid task timezone');
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
taskTimezone = candidateTimezone;
|
|
498
|
+
}
|
|
499
|
+
let nextRun = null;
|
|
500
|
+
if (scheduleType === 'cron') {
|
|
501
|
+
try {
|
|
502
|
+
const interval = CronExpressionParser.parse(data.schedule_value, { tz: taskTimezone });
|
|
503
|
+
nextRun = interval.next().toISOString();
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
logger.warn({ scheduleValue: data.schedule_value, timezone: taskTimezone }, 'Invalid cron expression');
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else if (scheduleType === 'interval') {
|
|
511
|
+
const ms = parseInt(data.schedule_value, 10);
|
|
512
|
+
if (isNaN(ms) || ms <= 0) {
|
|
513
|
+
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid interval');
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
nextRun = new Date(Date.now() + ms).toISOString();
|
|
517
|
+
}
|
|
518
|
+
else if (scheduleType === 'once') {
|
|
519
|
+
const scheduled = parseScheduledTimestamp(data.schedule_value, taskTimezone);
|
|
520
|
+
if (!scheduled) {
|
|
521
|
+
logger.warn({ scheduleValue: data.schedule_value, timezone: taskTimezone }, 'Invalid timestamp');
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
nextRun = scheduled.toISOString();
|
|
525
|
+
}
|
|
526
|
+
const taskId = generateId('task');
|
|
527
|
+
const contextMode = (data.context_mode === 'group' || data.context_mode === 'isolated')
|
|
528
|
+
? data.context_mode
|
|
529
|
+
: 'isolated';
|
|
530
|
+
createTask({
|
|
531
|
+
id: taskId,
|
|
532
|
+
group_folder: targetGroup,
|
|
533
|
+
chat_jid: targetChatId,
|
|
534
|
+
prompt: data.prompt,
|
|
535
|
+
schedule_type: scheduleType,
|
|
536
|
+
schedule_value: data.schedule_value,
|
|
537
|
+
timezone: taskTimezone,
|
|
538
|
+
context_mode: contextMode,
|
|
539
|
+
next_run: nextRun,
|
|
540
|
+
status: 'active',
|
|
541
|
+
created_at: new Date().toISOString()
|
|
542
|
+
});
|
|
543
|
+
logger.info({ taskId, sourceGroup, targetGroup, contextMode, timezone: taskTimezone }, 'Task created via IPC');
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case 'pause_task': {
|
|
548
|
+
if (data.taskId) {
|
|
549
|
+
const task = getTaskById(data.taskId);
|
|
550
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
551
|
+
updateTask(data.taskId, { status: 'paused' });
|
|
552
|
+
logger.info({ taskId: data.taskId, sourceGroup }, 'Task paused via IPC');
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task pause attempt');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case 'resume_task': {
|
|
561
|
+
if (data.taskId) {
|
|
562
|
+
const task = getTaskById(data.taskId);
|
|
563
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
564
|
+
updateTask(data.taskId, { status: 'active' });
|
|
565
|
+
logger.info({ taskId: data.taskId, sourceGroup }, 'Task resumed via IPC');
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task resume attempt');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case 'cancel_task': {
|
|
574
|
+
if (data.taskId) {
|
|
575
|
+
const task = getTaskById(data.taskId);
|
|
576
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
577
|
+
deleteTask(data.taskId);
|
|
578
|
+
logger.info({ taskId: data.taskId, sourceGroup }, 'Task cancelled via IPC');
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task cancel attempt');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case 'update_task': {
|
|
587
|
+
if (data.taskId) {
|
|
588
|
+
const task = getTaskById(data.taskId);
|
|
589
|
+
if (!task || (!isMain && task.group_folder !== sourceGroup)) {
|
|
590
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task update attempt');
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
const updates = {};
|
|
594
|
+
if (typeof data.prompt === 'string')
|
|
595
|
+
updates.prompt = data.prompt;
|
|
596
|
+
if (typeof data.context_mode === 'string')
|
|
597
|
+
updates.context_mode = data.context_mode;
|
|
598
|
+
if (typeof data.status === 'string')
|
|
599
|
+
updates.status = data.status;
|
|
600
|
+
if (typeof data.state_json === 'string')
|
|
601
|
+
updates.state_json = data.state_json;
|
|
602
|
+
if (typeof data.timezone === 'string') {
|
|
603
|
+
const timezoneValue = data.timezone.trim();
|
|
604
|
+
if (timezoneValue) {
|
|
605
|
+
if (!isValidTimezone(timezoneValue)) {
|
|
606
|
+
logger.warn({ timezone: data.timezone }, 'Invalid timezone for update_task');
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
updates.timezone = timezoneValue;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
updates.timezone = normalizeTaskTimezone(task.timezone, TIMEZONE);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (typeof data.schedule_type === 'string' && typeof data.schedule_value === 'string') {
|
|
616
|
+
updates.schedule_type = data.schedule_type;
|
|
617
|
+
updates.schedule_value = data.schedule_value;
|
|
618
|
+
const taskTimezone = updates.timezone || task.timezone || TIMEZONE;
|
|
619
|
+
let nextRun = null;
|
|
620
|
+
if (updates.schedule_type === 'cron') {
|
|
621
|
+
try {
|
|
622
|
+
const interval = CronExpressionParser.parse(updates.schedule_value, { tz: taskTimezone });
|
|
623
|
+
nextRun = interval.next().toISOString();
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
logger.warn({ scheduleValue: updates.schedule_value, timezone: taskTimezone }, 'Invalid cron expression for update_task');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else if (updates.schedule_type === 'interval') {
|
|
630
|
+
const ms = parseInt(updates.schedule_value, 10);
|
|
631
|
+
if (!isNaN(ms) && ms > 0) {
|
|
632
|
+
nextRun = new Date(Date.now() + ms).toISOString();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (updates.schedule_type === 'once') {
|
|
636
|
+
const scheduled = parseScheduledTimestamp(updates.schedule_value, taskTimezone);
|
|
637
|
+
if (scheduled) {
|
|
638
|
+
nextRun = scheduled.toISOString();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (nextRun) {
|
|
642
|
+
updates.next_run = nextRun;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
updateTask(data.taskId, updates);
|
|
646
|
+
logger.info({ taskId: data.taskId, sourceGroup }, 'Task updated via IPC');
|
|
647
|
+
}
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'register_group': {
|
|
651
|
+
if (!isMain) {
|
|
652
|
+
logger.warn({ sourceGroup }, 'Unauthorized register_group attempt blocked');
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
if (data.jid && data.name && data.folder) {
|
|
656
|
+
deps.registerGroup(data.jid, {
|
|
657
|
+
name: data.name,
|
|
658
|
+
folder: data.folder,
|
|
659
|
+
trigger: data.trigger,
|
|
660
|
+
added_at: new Date().toISOString(),
|
|
661
|
+
containerConfig: data.containerConfig,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
logger.warn({ data }, 'Invalid register_group request - missing required fields');
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'remove_group': {
|
|
670
|
+
if (!isMain) {
|
|
671
|
+
logger.warn({ sourceGroup }, 'Unauthorized remove_group attempt blocked');
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
if (!data.identifier || typeof data.identifier !== 'string') {
|
|
675
|
+
logger.warn({ data }, 'Invalid remove_group request - missing identifier');
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
const result = deps.unregisterGroup(data.identifier);
|
|
679
|
+
if (!result.ok) {
|
|
680
|
+
logger.warn({ identifier: data.identifier, error: result.error }, 'Failed to remove group');
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case 'set_model': {
|
|
685
|
+
if (!isMain) {
|
|
686
|
+
logger.warn({ sourceGroup }, 'Unauthorized set_model attempt blocked');
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
if (!data.model || typeof data.model !== 'string') {
|
|
690
|
+
logger.warn({ data }, 'Invalid set_model request - missing model');
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
const defaultModel = runtime.host.defaultModel;
|
|
694
|
+
const config = loadModelRegistry(defaultModel);
|
|
695
|
+
const nextModel = data.model.trim();
|
|
696
|
+
if (config.allowlist && config.allowlist.length > 0 && !config.allowlist.includes(nextModel)) {
|
|
697
|
+
logger.warn({ model: nextModel }, 'Model not in allowlist; refusing set_model');
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
const scope = typeof data.scope === 'string' ? data.scope : 'global';
|
|
701
|
+
const targetId = typeof data.target_id === 'string' ? data.target_id : undefined;
|
|
702
|
+
if (scope === 'user' && !targetId) {
|
|
703
|
+
logger.warn({ data }, 'set_model missing target_id for user scope');
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (scope === 'group' && !targetId) {
|
|
707
|
+
logger.warn({ data }, 'set_model missing target_id for group scope');
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
const nextConfig = { ...config };
|
|
711
|
+
if (scope === 'global') {
|
|
712
|
+
nextConfig.model = nextModel;
|
|
713
|
+
}
|
|
714
|
+
else if (scope === 'group') {
|
|
715
|
+
nextConfig.per_group = nextConfig.per_group || {};
|
|
716
|
+
nextConfig.per_group[targetId] = { model: nextModel };
|
|
717
|
+
}
|
|
718
|
+
else if (scope === 'user') {
|
|
719
|
+
nextConfig.per_user = nextConfig.per_user || {};
|
|
720
|
+
nextConfig.per_user[targetId] = { model: nextModel };
|
|
721
|
+
}
|
|
722
|
+
nextConfig.updated_at = new Date().toISOString();
|
|
723
|
+
saveModelRegistry(nextConfig);
|
|
724
|
+
logger.info({ model: nextModel, scope, targetId }, 'Model updated via IPC');
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
default:
|
|
728
|
+
logger.warn({ type: data.type }, 'Unknown IPC task type');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function processRequestIpc(deps, data, sourceGroup, isMain) {
|
|
732
|
+
const requestId = typeof data.id === 'string' ? data.id : undefined;
|
|
733
|
+
const payload = data.payload || {};
|
|
734
|
+
const registeredGroups = deps.registeredGroups();
|
|
735
|
+
const sessions = deps.sessions();
|
|
736
|
+
const resolveGroupFolder = () => {
|
|
737
|
+
const target = typeof payload.target_group === 'string' ? payload.target_group : null;
|
|
738
|
+
if (target && isMain)
|
|
739
|
+
return target;
|
|
740
|
+
return sourceGroup;
|
|
741
|
+
};
|
|
742
|
+
try {
|
|
743
|
+
switch (data.type) {
|
|
744
|
+
case 'memory_upsert': {
|
|
745
|
+
const items = coerceMemoryItems(payload.items);
|
|
746
|
+
const groupFolder = resolveGroupFolder();
|
|
747
|
+
const source = typeof payload.source === 'string' ? payload.source : 'agent';
|
|
748
|
+
const results = upsertMemoryItems(groupFolder, items, source);
|
|
749
|
+
invalidatePersonalizationCache(groupFolder);
|
|
750
|
+
return { id: requestId, ok: true, result: { count: results.length } };
|
|
751
|
+
}
|
|
752
|
+
case 'memory_forget': {
|
|
753
|
+
const groupFolder = resolveGroupFolder();
|
|
754
|
+
const ids = Array.isArray(payload.ids) ? payload.ids : undefined;
|
|
755
|
+
const content = typeof payload.content === 'string' ? payload.content : undefined;
|
|
756
|
+
const scope = isMemoryScope(payload.scope) ? payload.scope : undefined;
|
|
757
|
+
const userId = typeof payload.userId === 'string' ? payload.userId : undefined;
|
|
758
|
+
const count = forgetMemories({ groupFolder, ids, content, scope, userId });
|
|
759
|
+
invalidatePersonalizationCache(groupFolder);
|
|
760
|
+
return { id: requestId, ok: true, result: { count } };
|
|
761
|
+
}
|
|
762
|
+
case 'memory_list': {
|
|
763
|
+
const groupFolder = resolveGroupFolder();
|
|
764
|
+
const scope = isMemoryScope(payload.scope) ? payload.scope : undefined;
|
|
765
|
+
const type = isMemoryType(payload.type) ? payload.type : undefined;
|
|
766
|
+
const userId = typeof payload.userId === 'string' ? payload.userId : undefined;
|
|
767
|
+
const limit = typeof payload.limit === 'number' ? payload.limit : undefined;
|
|
768
|
+
const items = listMemories({ groupFolder, scope, type, userId, limit });
|
|
769
|
+
return { id: requestId, ok: true, result: { items } };
|
|
770
|
+
}
|
|
771
|
+
case 'memory_search': {
|
|
772
|
+
const groupFolder = resolveGroupFolder();
|
|
773
|
+
const query = typeof payload.query === 'string' ? payload.query : '';
|
|
774
|
+
const userId = typeof payload.userId === 'string' ? payload.userId : undefined;
|
|
775
|
+
const limit = typeof payload.limit === 'number' ? payload.limit : undefined;
|
|
776
|
+
const results = searchMemories({ groupFolder, userId, query, limit });
|
|
777
|
+
return { id: requestId, ok: true, result: { items: results } };
|
|
778
|
+
}
|
|
779
|
+
case 'memory_stats': {
|
|
780
|
+
const groupFolder = resolveGroupFolder();
|
|
781
|
+
const userId = typeof payload.userId === 'string' ? payload.userId : undefined;
|
|
782
|
+
const stats = getMemoryStats({ groupFolder, userId });
|
|
783
|
+
return { id: requestId, ok: true, result: { stats } };
|
|
784
|
+
}
|
|
785
|
+
case 'list_groups': {
|
|
786
|
+
if (!isMain) {
|
|
787
|
+
return { id: requestId, ok: false, error: 'Only the main group can list groups.' };
|
|
788
|
+
}
|
|
789
|
+
const groups = deps.listRegisteredGroups();
|
|
790
|
+
return { id: requestId, ok: true, result: { groups } };
|
|
791
|
+
}
|
|
792
|
+
case 'run_task': {
|
|
793
|
+
const taskId = typeof payload.task_id === 'string' ? payload.task_id : '';
|
|
794
|
+
if (!taskId) {
|
|
795
|
+
return { id: requestId, ok: false, error: 'task_id is required.' };
|
|
796
|
+
}
|
|
797
|
+
const task = getTaskById(taskId);
|
|
798
|
+
if (!task) {
|
|
799
|
+
return { id: requestId, ok: false, error: 'Task not found.' };
|
|
800
|
+
}
|
|
801
|
+
if (!isMain && task.group_folder !== sourceGroup) {
|
|
802
|
+
return { id: requestId, ok: false, error: 'Unauthorized task run attempt.' };
|
|
803
|
+
}
|
|
804
|
+
const provider = deps.registry.getProviderForChat(task.chat_jid);
|
|
805
|
+
const result = await runTaskNow(taskId, {
|
|
806
|
+
sendMessage: async (jid, text) => { await provider.sendMessage(jid, text); },
|
|
807
|
+
registeredGroups: () => registeredGroups,
|
|
808
|
+
getSessions: () => sessions,
|
|
809
|
+
setSession: (groupFolder, sessionId) => { deps.setSession(groupFolder, sessionId); }
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
id: requestId,
|
|
813
|
+
ok: result.ok,
|
|
814
|
+
result: { result: result.result ?? null },
|
|
815
|
+
error: result.ok ? undefined : result.error
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
case 'edit_message': {
|
|
819
|
+
const messageId = typeof payload.message_id === 'number' ? String(payload.message_id) : String(payload.message_id);
|
|
820
|
+
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
821
|
+
const chatJid = typeof payload.chat_jid === 'string' ? payload.chat_jid : '';
|
|
822
|
+
if (!text || !chatJid) {
|
|
823
|
+
return { id: requestId, ok: false, error: 'message_id, text, and chat_jid are required.' };
|
|
824
|
+
}
|
|
825
|
+
const group = Object.entries(registeredGroups).find(([id]) => id === chatJid);
|
|
826
|
+
if (!group) {
|
|
827
|
+
return { id: requestId, ok: false, error: 'Chat not registered.' };
|
|
828
|
+
}
|
|
829
|
+
if (!isMain && group[1].folder !== sourceGroup) {
|
|
830
|
+
return { id: requestId, ok: false, error: 'Unauthorized edit_message attempt.' };
|
|
831
|
+
}
|
|
832
|
+
const provider = deps.registry.getProviderForChat(chatJid);
|
|
833
|
+
await provider.editMessage(chatJid, messageId, text);
|
|
834
|
+
return { id: requestId, ok: true, result: { edited: true } };
|
|
835
|
+
}
|
|
836
|
+
case 'delete_message': {
|
|
837
|
+
const messageId = typeof payload.message_id === 'number' ? String(payload.message_id) : String(payload.message_id);
|
|
838
|
+
const chatJid = typeof payload.chat_jid === 'string' ? payload.chat_jid : '';
|
|
839
|
+
if (!chatJid) {
|
|
840
|
+
return { id: requestId, ok: false, error: 'message_id and chat_jid are required.' };
|
|
841
|
+
}
|
|
842
|
+
const group = Object.entries(registeredGroups).find(([id]) => id === chatJid);
|
|
843
|
+
if (!group) {
|
|
844
|
+
return { id: requestId, ok: false, error: 'Chat not registered.' };
|
|
845
|
+
}
|
|
846
|
+
if (!isMain && group[1].folder !== sourceGroup) {
|
|
847
|
+
return { id: requestId, ok: false, error: 'Unauthorized delete_message attempt.' };
|
|
848
|
+
}
|
|
849
|
+
const provider = deps.registry.getProviderForChat(chatJid);
|
|
850
|
+
await provider.deleteMessage(chatJid, messageId);
|
|
851
|
+
return { id: requestId, ok: true, result: { deleted: true } };
|
|
852
|
+
}
|
|
853
|
+
default:
|
|
854
|
+
return { id: requestId, ok: false, error: `Unknown request type: ${data.type}` };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
return { id: requestId, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
//# sourceMappingURL=ipc-dispatcher.js.map
|