@dobby.ai/dobby 0.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 +9 -0
- package/AGENTS.md +267 -0
- package/README.md +382 -0
- package/ROADMAP.md +34 -0
- package/config/cron.example.json +9 -0
- package/config/gateway.example.json +128 -0
- package/config/models.custom.example.json +27 -0
- package/dist/src/agent/event-forwarder.js +341 -0
- package/dist/src/agent/tests/event-forwarder.test.js +113 -0
- package/dist/src/cli/commands/config.js +243 -0
- package/dist/src/cli/commands/configure.js +61 -0
- package/dist/src/cli/commands/cron.js +288 -0
- package/dist/src/cli/commands/doctor.js +189 -0
- package/dist/src/cli/commands/extension.js +151 -0
- package/dist/src/cli/commands/init.js +286 -0
- package/dist/src/cli/commands/start.js +177 -0
- package/dist/src/cli/commands/topology.js +254 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/program.js +386 -0
- package/dist/src/cli/shared/config-io.js +223 -0
- package/dist/src/cli/shared/config-mutators.js +345 -0
- package/dist/src/cli/shared/config-path.js +207 -0
- package/dist/src/cli/shared/config-schema.js +159 -0
- package/dist/src/cli/shared/config-types.js +1 -0
- package/dist/src/cli/shared/configure-sections.js +429 -0
- package/dist/src/cli/shared/discord-config.js +12 -0
- package/dist/src/cli/shared/init-catalog.js +115 -0
- package/dist/src/cli/shared/init-models-file.js +65 -0
- package/dist/src/cli/shared/presets.js +86 -0
- package/dist/src/cli/shared/runtime.js +29 -0
- package/dist/src/cli/shared/schema-prompts.js +325 -0
- package/dist/src/cli/tests/config-command.test.js +42 -0
- package/dist/src/cli/tests/config-io.test.js +64 -0
- package/dist/src/cli/tests/config-mutators.test.js +47 -0
- package/dist/src/cli/tests/config-path.test.js +21 -0
- package/dist/src/cli/tests/discord-config.test.js +23 -0
- package/dist/src/cli/tests/doctor.test.js +107 -0
- package/dist/src/cli/tests/init-catalog.test.js +87 -0
- package/dist/src/cli/tests/presets.test.js +41 -0
- package/dist/src/cli/tests/program-options.test.js +92 -0
- package/dist/src/cli/tests/routing-config.test.js +199 -0
- package/dist/src/cli/tests/routing-legacy.test.js +191 -0
- package/dist/src/core/control-command.js +12 -0
- package/dist/src/core/dedup-store.js +92 -0
- package/dist/src/core/gateway.js +432 -0
- package/dist/src/core/routing.js +306 -0
- package/dist/src/core/runtime-registry.js +119 -0
- package/dist/src/core/tests/control-command.test.js +17 -0
- package/dist/src/core/tests/gateway-update-strategy.test.js +167 -0
- package/dist/src/core/tests/runtime-registry.test.js +116 -0
- package/dist/src/core/tests/typing-controller.test.js +103 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/typing-controller.js +88 -0
- package/dist/src/cron/config.js +114 -0
- package/dist/src/cron/schedule.js +49 -0
- package/dist/src/cron/service.js +196 -0
- package/dist/src/cron/store.js +142 -0
- package/dist/src/cron/types.js +1 -0
- package/dist/src/extension/loader.js +97 -0
- package/dist/src/extension/manager.js +269 -0
- package/dist/src/extension/manifest.js +21 -0
- package/dist/src/extension/registry.js +137 -0
- package/dist/src/main.js +6 -0
- package/dist/src/sandbox/executor.js +1 -0
- package/dist/src/sandbox/host-executor.js +111 -0
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +175 -0
- package/docs/CRON_SCHEDULER_DESIGN.md +374 -0
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +77 -0
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +119 -0
- package/docs/MVP.md +135 -0
- package/docs/RUNBOOK.md +242 -0
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +440 -0
- package/package.json +43 -0
- package/plugins/connector-discord/dobby.manifest.json +18 -0
- package/plugins/connector-discord/index.js +1 -0
- package/plugins/connector-discord/package-lock.json +360 -0
- package/plugins/connector-discord/package.json +38 -0
- package/plugins/connector-discord/src/connector.ts +350 -0
- package/plugins/connector-discord/src/contribution.ts +21 -0
- package/plugins/connector-discord/src/mapper.ts +102 -0
- package/plugins/connector-discord/tsconfig.json +19 -0
- package/plugins/connector-feishu/dobby.manifest.json +18 -0
- package/plugins/connector-feishu/index.js +1 -0
- package/plugins/connector-feishu/package-lock.json +618 -0
- package/plugins/connector-feishu/package.json +38 -0
- package/plugins/connector-feishu/src/connector.ts +343 -0
- package/plugins/connector-feishu/src/contribution.ts +26 -0
- package/plugins/connector-feishu/src/mapper.ts +401 -0
- package/plugins/connector-feishu/tsconfig.json +19 -0
- package/plugins/plugin-sdk/index.d.ts +261 -0
- package/plugins/plugin-sdk/index.js +1 -0
- package/plugins/plugin-sdk/package-lock.json +12 -0
- package/plugins/plugin-sdk/package.json +22 -0
- package/plugins/provider-claude/dobby.manifest.json +17 -0
- package/plugins/provider-claude/index.js +1 -0
- package/plugins/provider-claude/package-lock.json +3398 -0
- package/plugins/provider-claude/package.json +39 -0
- package/plugins/provider-claude/src/contribution.ts +1018 -0
- package/plugins/provider-claude/tsconfig.json +19 -0
- package/plugins/provider-claude-cli/dobby.manifest.json +17 -0
- package/plugins/provider-claude-cli/index.js +1 -0
- package/plugins/provider-claude-cli/package-lock.json +2898 -0
- package/plugins/provider-claude-cli/package.json +38 -0
- package/plugins/provider-claude-cli/src/contribution.ts +1673 -0
- package/plugins/provider-claude-cli/tsconfig.json +19 -0
- package/plugins/provider-pi/dobby.manifest.json +17 -0
- package/plugins/provider-pi/index.js +1 -0
- package/plugins/provider-pi/package-lock.json +3877 -0
- package/plugins/provider-pi/package.json +40 -0
- package/plugins/provider-pi/src/contribution.ts +476 -0
- package/plugins/provider-pi/tsconfig.json +19 -0
- package/plugins/sandbox-core/boxlite.js +1 -0
- package/plugins/sandbox-core/dobby.manifest.json +17 -0
- package/plugins/sandbox-core/docker.js +1 -0
- package/plugins/sandbox-core/package-lock.json +136 -0
- package/plugins/sandbox-core/package.json +39 -0
- package/plugins/sandbox-core/src/boxlite-context.ts +2 -0
- package/plugins/sandbox-core/src/boxlite-contribution.ts +53 -0
- package/plugins/sandbox-core/src/boxlite-executor.ts +911 -0
- package/plugins/sandbox-core/src/docker-contribution.ts +43 -0
- package/plugins/sandbox-core/src/docker-executor.ts +217 -0
- package/plugins/sandbox-core/tsconfig.json +19 -0
- package/scripts/local-extensions.mjs +168 -0
- package/src/agent/event-forwarder.ts +414 -0
- package/src/cli/commands/config.ts +328 -0
- package/src/cli/commands/configure.ts +92 -0
- package/src/cli/commands/cron.ts +410 -0
- package/src/cli/commands/doctor.ts +230 -0
- package/src/cli/commands/extension.ts +205 -0
- package/src/cli/commands/init.ts +396 -0
- package/src/cli/commands/start.ts +223 -0
- package/src/cli/commands/topology.ts +383 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/program.ts +465 -0
- package/src/cli/shared/config-io.ts +277 -0
- package/src/cli/shared/config-mutators.ts +440 -0
- package/src/cli/shared/config-schema.ts +228 -0
- package/src/cli/shared/config-types.ts +121 -0
- package/src/cli/shared/configure-sections.ts +551 -0
- package/src/cli/shared/discord-config.ts +14 -0
- package/src/cli/shared/init-catalog.ts +189 -0
- package/src/cli/shared/init-models-file.ts +77 -0
- package/src/cli/shared/runtime.ts +33 -0
- package/src/cli/shared/schema-prompts.ts +414 -0
- package/src/cli/tests/config-command.test.ts +56 -0
- package/src/cli/tests/config-io.test.ts +92 -0
- package/src/cli/tests/config-mutators.test.ts +59 -0
- package/src/cli/tests/doctor.test.ts +120 -0
- package/src/cli/tests/init-catalog.test.ts +96 -0
- package/src/cli/tests/program-options.test.ts +113 -0
- package/src/cli/tests/routing-config.test.ts +209 -0
- package/src/core/control-command.ts +12 -0
- package/src/core/dedup-store.ts +103 -0
- package/src/core/gateway.ts +607 -0
- package/src/core/routing.ts +379 -0
- package/src/core/runtime-registry.ts +141 -0
- package/src/core/tests/control-command.test.ts +20 -0
- package/src/core/tests/runtime-registry.test.ts +140 -0
- package/src/core/tests/typing-controller.test.ts +129 -0
- package/src/core/types.ts +318 -0
- package/src/core/typing-controller.ts +119 -0
- package/src/cron/config.ts +154 -0
- package/src/cron/schedule.ts +61 -0
- package/src/cron/service.ts +249 -0
- package/src/cron/store.ts +155 -0
- package/src/cron/types.ts +60 -0
- package/src/extension/loader.ts +145 -0
- package/src/extension/manager.ts +355 -0
- package/src/extension/manifest.ts +26 -0
- package/src/extension/registry.ts +229 -0
- package/src/main.ts +8 -0
- package/src/sandbox/executor.ts +44 -0
- package/src/sandbox/host-executor.ts +118 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { EventForwarder } from "../agent/event-forwarder.js";
|
|
3
|
+
import { parseControlCommand } from "./control-command.js";
|
|
4
|
+
import { createTypingKeepAliveController } from "./typing-controller.js";
|
|
5
|
+
function isImageAttachment(attachment) {
|
|
6
|
+
return Boolean(attachment.mimeType?.startsWith("image/") && attachment.localPath);
|
|
7
|
+
}
|
|
8
|
+
function dedupKey(message) {
|
|
9
|
+
return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.messageId}`;
|
|
10
|
+
}
|
|
11
|
+
function conversationKey(message) {
|
|
12
|
+
return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.threadId ?? "root"}`;
|
|
13
|
+
}
|
|
14
|
+
export class Gateway {
|
|
15
|
+
options;
|
|
16
|
+
connectorsById = new Map();
|
|
17
|
+
started = false;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
for (const connector of options.connectors) {
|
|
21
|
+
this.connectorsById.set(connector.id, connector);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async start() {
|
|
25
|
+
if (this.started)
|
|
26
|
+
return;
|
|
27
|
+
await this.options.dedupStore.load();
|
|
28
|
+
this.options.dedupStore.startAutoFlush();
|
|
29
|
+
for (const connector of this.options.connectors) {
|
|
30
|
+
await connector.start({
|
|
31
|
+
emitInbound: async (message) => this.handleInbound(message),
|
|
32
|
+
emitControl: async (event) => this.handleControl(event),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
this.started = true;
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
if (!this.started)
|
|
39
|
+
return;
|
|
40
|
+
for (const connector of this.options.connectors) {
|
|
41
|
+
await connector.stop();
|
|
42
|
+
}
|
|
43
|
+
this.options.dedupStore.stopAutoFlush();
|
|
44
|
+
await this.options.dedupStore.flush();
|
|
45
|
+
await this.options.runtimeRegistry.closeAll();
|
|
46
|
+
this.started = false;
|
|
47
|
+
}
|
|
48
|
+
async handleScheduled(request) {
|
|
49
|
+
const connector = this.connectorsById.get(request.connectorId);
|
|
50
|
+
if (!connector) {
|
|
51
|
+
throw new Error(`No connector found for scheduled run '${request.runId}' (${request.connectorId})`);
|
|
52
|
+
}
|
|
53
|
+
const syntheticInbound = {
|
|
54
|
+
connectorId: request.connectorId,
|
|
55
|
+
platform: connector.platform,
|
|
56
|
+
accountId: request.connectorId,
|
|
57
|
+
source: {
|
|
58
|
+
type: "chat",
|
|
59
|
+
id: request.channelId,
|
|
60
|
+
},
|
|
61
|
+
chatId: request.channelId,
|
|
62
|
+
...(request.threadId ? { threadId: request.threadId } : {}),
|
|
63
|
+
messageId: `cron:${request.runId}`,
|
|
64
|
+
userId: "cron",
|
|
65
|
+
userName: "cron",
|
|
66
|
+
text: request.prompt,
|
|
67
|
+
attachments: [],
|
|
68
|
+
timestampMs: Date.now(),
|
|
69
|
+
raw: {
|
|
70
|
+
type: "cron",
|
|
71
|
+
jobId: request.jobId,
|
|
72
|
+
runId: request.runId,
|
|
73
|
+
},
|
|
74
|
+
isDirectMessage: false,
|
|
75
|
+
mentionedBot: true,
|
|
76
|
+
};
|
|
77
|
+
await this.handleMessage(syntheticInbound, {
|
|
78
|
+
origin: "scheduled",
|
|
79
|
+
useDedup: true,
|
|
80
|
+
stateless: true,
|
|
81
|
+
includeReplyTo: false,
|
|
82
|
+
conversationKeyOverride: `cron:${request.runId}`,
|
|
83
|
+
routeIdOverride: request.routeId,
|
|
84
|
+
sessionPolicy: "ephemeral",
|
|
85
|
+
...(request.timeoutMs !== undefined ? { timeoutMs: request.timeoutMs } : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
outboundBaseFromInbound(message) {
|
|
89
|
+
return {
|
|
90
|
+
platform: message.platform,
|
|
91
|
+
accountId: message.accountId,
|
|
92
|
+
chatId: message.chatId,
|
|
93
|
+
...(message.threadId ? { threadId: message.threadId } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
outboundBaseFromControl(event) {
|
|
97
|
+
return {
|
|
98
|
+
platform: event.platform,
|
|
99
|
+
accountId: event.accountId,
|
|
100
|
+
chatId: event.chatId,
|
|
101
|
+
...(event.threadId ? { threadId: event.threadId } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async sendCommandReply(connector, message, text) {
|
|
105
|
+
await connector.send({
|
|
106
|
+
...this.outboundBaseFromInbound(message),
|
|
107
|
+
mode: "create",
|
|
108
|
+
text,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async handleInbound(message) {
|
|
112
|
+
await this.handleMessage(message, {
|
|
113
|
+
origin: "connector",
|
|
114
|
+
useDedup: true,
|
|
115
|
+
stateless: false,
|
|
116
|
+
includeReplyTo: true,
|
|
117
|
+
sessionPolicy: "shared-session",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
resolveMessageRoute(message, handling) {
|
|
121
|
+
const overrideRouteId = handling.routeIdOverride?.trim();
|
|
122
|
+
if (overrideRouteId) {
|
|
123
|
+
const route = this.options.routeResolver.resolve(overrideRouteId);
|
|
124
|
+
if (!route) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
routeId: overrideRouteId,
|
|
129
|
+
route,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const binding = this.options.bindingResolver.resolve(message.connectorId, message.source);
|
|
133
|
+
if (!binding) {
|
|
134
|
+
if (handling.origin === "connector") {
|
|
135
|
+
this.options.logger.debug({
|
|
136
|
+
connectorId: message.connectorId,
|
|
137
|
+
sourceType: message.source.type,
|
|
138
|
+
sourceId: message.source.id,
|
|
139
|
+
}, "Ignoring inbound message from unbound source");
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const route = this.options.routeResolver.resolve(binding.config.route);
|
|
144
|
+
if (!route) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
routeId: binding.config.route,
|
|
149
|
+
route,
|
|
150
|
+
binding,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async handleMessage(message, handling) {
|
|
154
|
+
const connector = this.connectorsById.get(message.connectorId);
|
|
155
|
+
if (!connector) {
|
|
156
|
+
this.options.logger.warn({ connectorId: message.connectorId }, "No connector found for inbound message");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (handling.useDedup) {
|
|
160
|
+
const key = dedupKey(message);
|
|
161
|
+
if (this.options.dedupStore.has(key)) {
|
|
162
|
+
this.options.logger.debug({ dedupKey: key }, "Skipping duplicate message");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.options.dedupStore.add(key);
|
|
166
|
+
}
|
|
167
|
+
const resolvedRoute = this.resolveMessageRoute(message, handling);
|
|
168
|
+
if (!resolvedRoute) {
|
|
169
|
+
if (handling.routeIdOverride) {
|
|
170
|
+
await connector.send({
|
|
171
|
+
...this.outboundBaseFromInbound(message),
|
|
172
|
+
mode: "create",
|
|
173
|
+
...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
|
|
174
|
+
text: `No route configured for route '${handling.routeIdOverride}'.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (handling.origin === "connector") {
|
|
180
|
+
const command = parseControlCommand(message.text);
|
|
181
|
+
if (command) {
|
|
182
|
+
try {
|
|
183
|
+
await this.handleCommand(connector, message, command, resolvedRoute.route);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
this.options.logger.error({ err: error, messageId: message.messageId }, "Failed to handle control command");
|
|
187
|
+
await this.sendCommandReply(connector, message, `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const { route } = resolvedRoute;
|
|
193
|
+
if (route.profile.mentions === "required" && !message.isDirectMessage && !message.mentionedBot) {
|
|
194
|
+
this.options.logger.debug({
|
|
195
|
+
sourceType: message.source.type,
|
|
196
|
+
sourceId: message.source.id,
|
|
197
|
+
routeId: route.routeId,
|
|
198
|
+
}, "Ignoring non-mention message");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const providerId = route.profile.provider;
|
|
202
|
+
const sandboxId = route.profile.sandbox;
|
|
203
|
+
const provider = this.options.providers.get(providerId);
|
|
204
|
+
const executor = this.options.executors.get(sandboxId);
|
|
205
|
+
if (!provider || !executor) {
|
|
206
|
+
await connector.send({
|
|
207
|
+
...this.outboundBaseFromInbound(message),
|
|
208
|
+
mode: "create",
|
|
209
|
+
...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
|
|
210
|
+
text: `Route runtime not available (provider='${providerId}', sandbox='${sandboxId}')`,
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const convKey = handling.conversationKeyOverride ?? conversationKey(message);
|
|
215
|
+
if (handling.stateless) {
|
|
216
|
+
const runtime = await provider.createRuntime({
|
|
217
|
+
conversationKey: convKey,
|
|
218
|
+
route,
|
|
219
|
+
inbound: message,
|
|
220
|
+
executor,
|
|
221
|
+
...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
await this.processMessage(connector, runtime, route, message, {
|
|
225
|
+
includeReplyTo: handling.includeReplyTo,
|
|
226
|
+
...(handling.timeoutMs !== undefined ? { timeoutMs: handling.timeoutMs } : {}),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
runtime.dispose();
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await this.options.runtimeRegistry.run(convKey, async () => {
|
|
235
|
+
const runtime = await provider.createRuntime({
|
|
236
|
+
conversationKey: convKey,
|
|
237
|
+
route,
|
|
238
|
+
inbound: message,
|
|
239
|
+
executor,
|
|
240
|
+
...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
key: convKey,
|
|
244
|
+
routeId: route.routeId,
|
|
245
|
+
route: route.profile,
|
|
246
|
+
providerId,
|
|
247
|
+
sandboxId,
|
|
248
|
+
runtime,
|
|
249
|
+
close: async () => {
|
|
250
|
+
runtime.dispose();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}, async (runtime) => {
|
|
254
|
+
await this.processMessage(connector, runtime.runtime, route, message, {
|
|
255
|
+
includeReplyTo: handling.includeReplyTo,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async processMessage(connector, runtime, route, message, options) {
|
|
260
|
+
this.options.logger.info({
|
|
261
|
+
connectorId: message.connectorId,
|
|
262
|
+
routeId: route.routeId,
|
|
263
|
+
sourceType: message.source.type,
|
|
264
|
+
sourceId: message.source.id,
|
|
265
|
+
chatId: message.chatId,
|
|
266
|
+
threadId: message.threadId,
|
|
267
|
+
messageId: message.messageId,
|
|
268
|
+
}, "Processing inbound message");
|
|
269
|
+
const typingController = createTypingKeepAliveController(connector, message, this.options.logger);
|
|
270
|
+
let unsubscribe = null;
|
|
271
|
+
const forwarder = new EventForwarder(connector, message, null, this.options.logger, {
|
|
272
|
+
onOutboundActivity: typingController.markVisibleOutput,
|
|
273
|
+
});
|
|
274
|
+
try {
|
|
275
|
+
await typingController.prime();
|
|
276
|
+
unsubscribe = runtime.subscribe(forwarder.handleEvent);
|
|
277
|
+
const payload = await this.buildPromptPayload(message);
|
|
278
|
+
this.options.logger.info({
|
|
279
|
+
routeId: route.routeId,
|
|
280
|
+
messageId: message.messageId,
|
|
281
|
+
rootMessageId: forwarder.primaryMessageId() ?? null,
|
|
282
|
+
hasImages: payload.images.length > 0,
|
|
283
|
+
textLength: payload.text.length,
|
|
284
|
+
}, "Starting provider prompt");
|
|
285
|
+
await this.promptWithOptionalTimeout(runtime, payload, options.timeoutMs);
|
|
286
|
+
this.options.logger.info({
|
|
287
|
+
routeId: route.routeId,
|
|
288
|
+
messageId: message.messageId,
|
|
289
|
+
rootMessageId: forwarder.primaryMessageId() ?? null,
|
|
290
|
+
}, "Provider prompt finished");
|
|
291
|
+
await forwarder.finalize();
|
|
292
|
+
this.options.logger.info({ routeId: route.routeId, messageId: message.messageId }, "Inbound message processed");
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
this.options.logger.error({ err: error, routeId: route.routeId }, "Failed to process inbound message");
|
|
296
|
+
const rootMessageId = forwarder.primaryMessageId();
|
|
297
|
+
const canEditExisting = connector.capabilities.updateStrategy === "edit" && rootMessageId !== null;
|
|
298
|
+
await connector.send(canEditExisting
|
|
299
|
+
? {
|
|
300
|
+
...this.outboundBaseFromInbound(message),
|
|
301
|
+
mode: "update",
|
|
302
|
+
targetMessageId: rootMessageId,
|
|
303
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
304
|
+
}
|
|
305
|
+
: {
|
|
306
|
+
...this.outboundBaseFromInbound(message),
|
|
307
|
+
mode: "create",
|
|
308
|
+
...(rootMessageId
|
|
309
|
+
? { replyToMessageId: rootMessageId }
|
|
310
|
+
: options.includeReplyTo
|
|
311
|
+
? { replyToMessageId: message.messageId }
|
|
312
|
+
: {}),
|
|
313
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
unsubscribe?.();
|
|
318
|
+
typingController.stop();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async promptWithOptionalTimeout(runtime, payload, timeoutMs) {
|
|
322
|
+
if (timeoutMs === undefined || timeoutMs <= 0) {
|
|
323
|
+
await runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
let timer = null;
|
|
327
|
+
try {
|
|
328
|
+
await Promise.race([
|
|
329
|
+
runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined),
|
|
330
|
+
new Promise((_, reject) => {
|
|
331
|
+
timer = setTimeout(() => {
|
|
332
|
+
void runtime.abort().catch(() => {
|
|
333
|
+
// Best-effort abort on timeout.
|
|
334
|
+
});
|
|
335
|
+
reject(new Error(`Cron run timed out after ${timeoutMs}ms`));
|
|
336
|
+
}, timeoutMs);
|
|
337
|
+
}),
|
|
338
|
+
]);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
if (timer) {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async buildPromptPayload(message) {
|
|
347
|
+
const textParts = [];
|
|
348
|
+
const baseText = message.text.trim();
|
|
349
|
+
textParts.push(baseText.length > 0 ? baseText : "(empty message)");
|
|
350
|
+
const images = [];
|
|
351
|
+
const otherAttachments = [];
|
|
352
|
+
for (const attachment of message.attachments) {
|
|
353
|
+
if (isImageAttachment(attachment) && attachment.localPath && attachment.mimeType) {
|
|
354
|
+
try {
|
|
355
|
+
const buffer = await readFile(attachment.localPath);
|
|
356
|
+
images.push({
|
|
357
|
+
type: "image",
|
|
358
|
+
mimeType: attachment.mimeType,
|
|
359
|
+
data: buffer.toString("base64"),
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
this.options.logger.warn({ err: error, attachment: attachment.localPath }, "Failed to read image attachment");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (attachment.localPath) {
|
|
368
|
+
otherAttachments.push(attachment.localPath);
|
|
369
|
+
}
|
|
370
|
+
else if (attachment.remoteUrl) {
|
|
371
|
+
otherAttachments.push(attachment.remoteUrl);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (otherAttachments.length > 0) {
|
|
375
|
+
textParts.push(`<attachments>\n${otherAttachments.join("\n")}\n</attachments>`);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
text: textParts.join("\n\n"),
|
|
379
|
+
images,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async handleCommand(connector, message, command, route) {
|
|
383
|
+
const convKey = conversationKey(message);
|
|
384
|
+
if (command === "cancel") {
|
|
385
|
+
const cancelled = await this.options.runtimeRegistry.cancel(convKey);
|
|
386
|
+
this.options.logger.info({ conversationKey: convKey, cancelled }, "Conversation cancel requested");
|
|
387
|
+
await this.sendCommandReply(connector, message, cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const providerId = route.profile.provider;
|
|
391
|
+
const provider = this.options.providers.get(providerId);
|
|
392
|
+
if (!provider) {
|
|
393
|
+
throw new Error(`Route provider not available (provider='${providerId}')`);
|
|
394
|
+
}
|
|
395
|
+
if (!provider.archiveSession) {
|
|
396
|
+
throw new Error(`Provider '${providerId}' does not support session archival`);
|
|
397
|
+
}
|
|
398
|
+
const hadRuntime = await this.options.runtimeRegistry.reset(convKey);
|
|
399
|
+
const archiveResult = await provider.archiveSession({
|
|
400
|
+
conversationKey: convKey,
|
|
401
|
+
inbound: message,
|
|
402
|
+
sessionPolicy: "shared-session",
|
|
403
|
+
archivedAtMs: message.timestampMs,
|
|
404
|
+
});
|
|
405
|
+
this.options.logger.info({
|
|
406
|
+
conversationKey: convKey,
|
|
407
|
+
providerId,
|
|
408
|
+
hadRuntime,
|
|
409
|
+
archived: archiveResult.archived,
|
|
410
|
+
archivePath: archiveResult.archivePath ?? null,
|
|
411
|
+
}, "New session requested");
|
|
412
|
+
await this.sendCommandReply(connector, message, "_Started a new session._");
|
|
413
|
+
}
|
|
414
|
+
async handleControl(event) {
|
|
415
|
+
const convKey = `${event.connectorId}:${event.platform}:${event.accountId}:${event.chatId}:${event.threadId ?? "root"}`;
|
|
416
|
+
const connector = this.connectorsById.get(event.connectorId);
|
|
417
|
+
const cancelled = await this.options.runtimeRegistry.cancel(convKey);
|
|
418
|
+
this.options.logger.info({ conversationKey: convKey, cancelled }, "Stop requested");
|
|
419
|
+
if (!connector)
|
|
420
|
+
return;
|
|
421
|
+
try {
|
|
422
|
+
await connector.send({
|
|
423
|
+
...this.outboundBaseFromControl(event),
|
|
424
|
+
mode: "create",
|
|
425
|
+
text: cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
this.options.logger.warn({ err: error, conversationKey: convKey }, "Failed to send stop acknowledgement");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|