neoagent 3.2.1-beta.1 → 3.2.1-beta.11
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +13 -8
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/logger.js +19 -0
- package/server/utils/retry.js +107 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { describeEnvStatus, resolveSpotifyOAuthConfig } = require('../env');
|
|
4
|
-
const { appendQuery, createOAuthProvider } = require('../oauth_provider');
|
|
4
|
+
const { appendQuery, createOAuthProvider, fetchJson } = require('../oauth_provider');
|
|
5
|
+
const { fetchResponseText } = require('../http');
|
|
5
6
|
|
|
6
7
|
const SPOTIFY_APPS = [
|
|
7
8
|
{
|
|
@@ -121,33 +122,32 @@ function spotifyUrl(path, query) {
|
|
|
121
122
|
return url.toString();
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
async function refreshSpotifyAccessToken(config, credentials) {
|
|
125
|
+
async function refreshSpotifyAccessToken(config, credentials, signal = null) {
|
|
125
126
|
const refreshToken = String(credentials?.refresh_token || '').trim();
|
|
126
127
|
if (!refreshToken) {
|
|
127
128
|
return credentials;
|
|
128
129
|
}
|
|
129
130
|
const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64');
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
const data = await fetchJson(
|
|
132
|
+
'https://accounts.spotify.com/api/token',
|
|
133
|
+
{
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { Authorization: `Basic ${basic}` },
|
|
136
|
+
form: {
|
|
137
|
+
grant_type: 'refresh_token',
|
|
138
|
+
refresh_token: refreshToken,
|
|
139
|
+
},
|
|
140
|
+
signal,
|
|
136
141
|
},
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
const data = await response.json().catch(() => ({}));
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
const message = data?.error_description || data?.error || `${response.status} ${response.statusText}`;
|
|
145
|
-
throw new Error(`Spotify token refresh failed: ${message}`);
|
|
142
|
+
{ serviceName: 'Spotify token refresh' },
|
|
143
|
+
);
|
|
144
|
+
if (!String(data?.access_token || '').trim()) {
|
|
145
|
+
throw new Error('Spotify token refresh did not return an access token.');
|
|
146
146
|
}
|
|
147
147
|
const expiresIn = Number(data?.expires_in) || 3600;
|
|
148
148
|
return {
|
|
149
149
|
...credentials,
|
|
150
|
-
access_token: data
|
|
150
|
+
access_token: data.access_token,
|
|
151
151
|
refresh_token: data?.refresh_token || refreshToken,
|
|
152
152
|
token_type: data?.token_type || credentials.token_type || 'Bearer',
|
|
153
153
|
scope: data?.scope || credentials.scope || '',
|
|
@@ -156,7 +156,7 @@ async function refreshSpotifyAccessToken(config, credentials) {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async function ensureSpotifyAccessToken(config, credentials) {
|
|
159
|
+
async function ensureSpotifyAccessToken(config, credentials, signal = null) {
|
|
160
160
|
const accessToken = String(credentials?.access_token || '').trim();
|
|
161
161
|
if (!accessToken) {
|
|
162
162
|
throw new Error('Spotify access token is missing. Reconnect this integration account.');
|
|
@@ -165,32 +165,36 @@ async function ensureSpotifyAccessToken(config, credentials) {
|
|
|
165
165
|
const expiresAt = Date.parse(String(credentials?.expires_at || ''));
|
|
166
166
|
const isNearExpiry = Number.isFinite(expiresAt) && expiresAt <= Date.now() + 60 * 1000;
|
|
167
167
|
if (isNearExpiry && credentials?.refresh_token) {
|
|
168
|
-
return refreshSpotifyAccessToken(config, credentials);
|
|
168
|
+
return refreshSpotifyAccessToken(config, credentials, signal);
|
|
169
169
|
}
|
|
170
170
|
return credentials;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
async function spotifyRequest(config,
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
async function spotifyRequest(config, context, { method = 'GET', path, query, body }) {
|
|
174
|
+
const { credentials, signal } = context;
|
|
175
|
+
let nextCredentials = await ensureSpotifyAccessToken(config, credentials, signal);
|
|
176
176
|
|
|
177
177
|
const performRequest = async (tokenCreds) => {
|
|
178
178
|
const tokenType = String(tokenCreds?.token_type || 'Bearer').trim() || 'Bearer';
|
|
179
|
-
const response = await
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
const { response, text } = await fetchResponseText(
|
|
180
|
+
spotifyUrl(path, query),
|
|
181
|
+
{
|
|
182
|
+
method: String(method || 'GET').toUpperCase(),
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `${tokenType} ${tokenCreds.access_token}`,
|
|
185
|
+
Accept: 'application/json',
|
|
186
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
187
|
+
},
|
|
188
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
189
|
+
signal,
|
|
185
190
|
},
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
{ serviceName: 'Spotify' },
|
|
192
|
+
);
|
|
188
193
|
|
|
189
194
|
if (response.status === 204) {
|
|
190
195
|
return { ok: true, data: null, response };
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
const text = await response.text();
|
|
194
198
|
let parsed = null;
|
|
195
199
|
try {
|
|
196
200
|
parsed = text ? JSON.parse(text) : null;
|
|
@@ -202,7 +206,7 @@ async function spotifyRequest(config, credentials, { method = 'GET', path, query
|
|
|
202
206
|
|
|
203
207
|
let result = await performRequest(nextCredentials);
|
|
204
208
|
if (!result.ok && result.response.status === 401 && nextCredentials.refresh_token) {
|
|
205
|
-
nextCredentials = await refreshSpotifyAccessToken(config, nextCredentials);
|
|
209
|
+
nextCredentials = await refreshSpotifyAccessToken(config, nextCredentials, signal);
|
|
206
210
|
result = await performRequest(nextCredentials);
|
|
207
211
|
}
|
|
208
212
|
|
|
@@ -219,18 +223,18 @@ async function spotifyRequest(config, credentials, { method = 'GET', path, query
|
|
|
219
223
|
};
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
async function executeSpotifyTool(toolName, args,
|
|
226
|
+
async function executeSpotifyTool(toolName, args, context) {
|
|
223
227
|
const config = resolveSpotifyOAuthConfig();
|
|
224
228
|
switch (toolName) {
|
|
225
229
|
case 'spotify_get_current_playback': {
|
|
226
|
-
const { data, credentials: updated } = await spotifyRequest(config,
|
|
230
|
+
const { data, credentials: updated } = await spotifyRequest(config, context, {
|
|
227
231
|
path: '/v1/me/player',
|
|
228
232
|
});
|
|
229
233
|
return { result: data || { is_playing: false }, credentials: updated };
|
|
230
234
|
}
|
|
231
235
|
case 'spotify_get_recently_played': {
|
|
232
236
|
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 50));
|
|
233
|
-
const { data, credentials: updated } = await spotifyRequest(config,
|
|
237
|
+
const { data, credentials: updated } = await spotifyRequest(config, context, {
|
|
234
238
|
path: '/v1/me/player/recently-played',
|
|
235
239
|
query: { limit },
|
|
236
240
|
});
|
|
@@ -246,7 +250,7 @@ async function executeSpotifyTool(toolName, args, { credentials }) {
|
|
|
246
250
|
const type = types.length > 0 ? types.join(',') : 'track';
|
|
247
251
|
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 50));
|
|
248
252
|
const market = String(args.market || '').trim().toUpperCase();
|
|
249
|
-
const { data, credentials: updated } = await spotifyRequest(config,
|
|
253
|
+
const { data, credentials: updated } = await spotifyRequest(config, context, {
|
|
250
254
|
path: '/v1/search',
|
|
251
255
|
query: {
|
|
252
256
|
q: queryText,
|
|
@@ -348,7 +352,7 @@ async function executeSpotifyTool(toolName, args, { credentials }) {
|
|
|
348
352
|
throw new Error('Unsupported action. Use play, pause, next, previous, seek, set_volume, shuffle, or repeat.');
|
|
349
353
|
}
|
|
350
354
|
|
|
351
|
-
const { data, credentials: updated } = await spotifyRequest(config,
|
|
355
|
+
const { data, credentials: updated } = await spotifyRequest(config, context, request);
|
|
352
356
|
return {
|
|
353
357
|
result: {
|
|
354
358
|
action,
|
|
@@ -359,7 +363,7 @@ async function executeSpotifyTool(toolName, args, { credentials }) {
|
|
|
359
363
|
};
|
|
360
364
|
}
|
|
361
365
|
case 'spotify_api_request': {
|
|
362
|
-
const { data, credentials: updated } = await spotifyRequest(config,
|
|
366
|
+
const { data, credentials: updated } = await spotifyRequest(config, context, {
|
|
363
367
|
method: args.method,
|
|
364
368
|
path: requireText(args.path, 'path'),
|
|
365
369
|
query: args.query,
|
|
@@ -372,20 +376,16 @@ async function executeSpotifyTool(toolName, args, { credentials }) {
|
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
378
|
|
|
375
|
-
async function fetchSpotifyProfile(accessToken) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
async function fetchSpotifyProfile(accessToken, signal = null) {
|
|
380
|
+
return fetchJson(
|
|
381
|
+
'https://api.spotify.com/v1/me',
|
|
382
|
+
{
|
|
383
|
+
method: 'GET',
|
|
384
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
385
|
+
signal,
|
|
381
386
|
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (!response.ok) {
|
|
385
|
-
const message = payload?.error?.message || payload?.error || `${response.status} ${response.statusText}`;
|
|
386
|
-
throw new Error(`Spotify profile request failed: ${message}`);
|
|
387
|
-
}
|
|
388
|
-
return payload;
|
|
387
|
+
{ serviceName: 'Spotify profile' },
|
|
388
|
+
);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
function createSpotifyProvider() {
|
|
@@ -418,27 +418,23 @@ function createSpotifyProvider() {
|
|
|
418
418
|
appId: app.id,
|
|
419
419
|
};
|
|
420
420
|
},
|
|
421
|
-
async finishOAuth({ code, app }) {
|
|
421
|
+
async finishOAuth({ code, app, signal }) {
|
|
422
422
|
const config = resolveSpotifyOAuthConfig();
|
|
423
423
|
const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64');
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
424
|
+
const token = await fetchJson(
|
|
425
|
+
'https://accounts.spotify.com/api/token',
|
|
426
|
+
{
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: { Authorization: `Basic ${basic}` },
|
|
429
|
+
form: {
|
|
430
|
+
grant_type: 'authorization_code',
|
|
431
|
+
code,
|
|
432
|
+
redirect_uri: config.redirectUri,
|
|
433
|
+
},
|
|
434
|
+
signal,
|
|
430
435
|
},
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
code,
|
|
434
|
-
redirect_uri: config.redirectUri,
|
|
435
|
-
}).toString(),
|
|
436
|
-
});
|
|
437
|
-
const token = await tokenResponse.json().catch(() => ({}));
|
|
438
|
-
if (!tokenResponse.ok) {
|
|
439
|
-
const message = token?.error_description || token?.error || `${tokenResponse.status} ${tokenResponse.statusText}`;
|
|
440
|
-
throw new Error(`Spotify OAuth token exchange failed: ${message}`);
|
|
441
|
-
}
|
|
436
|
+
{ serviceName: 'Spotify OAuth token exchange' },
|
|
437
|
+
);
|
|
442
438
|
|
|
443
439
|
const accessToken = String(token?.access_token || '').trim();
|
|
444
440
|
if (!accessToken) {
|
|
@@ -449,7 +445,7 @@ function createSpotifyProvider() {
|
|
|
449
445
|
throw new Error('Spotify OAuth did not return a refresh token.');
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
const profile = await fetchSpotifyProfile(accessToken);
|
|
448
|
+
const profile = await fetchSpotifyProfile(accessToken, signal);
|
|
453
449
|
const accountEmail =
|
|
454
450
|
String(profile?.email || '').trim() ||
|
|
455
451
|
(String(profile?.id || '').trim() ? `spotify:${String(profile.id).trim()}` : 'spotify_user');
|
|
@@ -276,7 +276,10 @@ function trelloUrl(path, query = {}, config = {}) {
|
|
|
276
276
|
async function trelloRequest(config, options = {}) {
|
|
277
277
|
return fetchJson(
|
|
278
278
|
trelloUrl(options.path, options.query, config),
|
|
279
|
-
{
|
|
279
|
+
{
|
|
280
|
+
method: String(options.method || 'GET').toUpperCase(),
|
|
281
|
+
signal: options.signal || config.signal,
|
|
282
|
+
},
|
|
280
283
|
{ serviceName: 'Trello' },
|
|
281
284
|
);
|
|
282
285
|
}
|
|
@@ -492,8 +495,15 @@ function resolveTrelloCredentials(connection, credentials = {}) {
|
|
|
492
495
|
return { apiKey, token };
|
|
493
496
|
}
|
|
494
497
|
|
|
495
|
-
async function executeTrelloTool(
|
|
496
|
-
|
|
498
|
+
async function executeTrelloTool(
|
|
499
|
+
toolName,
|
|
500
|
+
args,
|
|
501
|
+
{ connection, credentials, signal },
|
|
502
|
+
) {
|
|
503
|
+
const config = {
|
|
504
|
+
...resolveTrelloCredentials(connection, credentials),
|
|
505
|
+
signal,
|
|
506
|
+
};
|
|
497
507
|
|
|
498
508
|
switch (toolName) {
|
|
499
509
|
case 'trello_get_me': {
|
|
@@ -731,14 +741,18 @@ function createTrelloProvider() {
|
|
|
731
741
|
}
|
|
732
742
|
return 'Trello: native Trello access is connected in this run with one Trello account for board, list, card, comment, and search tools.';
|
|
733
743
|
},
|
|
734
|
-
async executeTool(toolName, args, connection) {
|
|
744
|
+
async executeTool(toolName, args, connection, executionOptions = {}) {
|
|
735
745
|
let credentials = {};
|
|
736
746
|
try {
|
|
737
747
|
credentials = JSON.parse(decryptValue(connection.credentials_json || '{}') || '{}');
|
|
738
748
|
} catch {
|
|
739
749
|
credentials = {};
|
|
740
750
|
}
|
|
741
|
-
return executeTrelloTool(toolName, args, {
|
|
751
|
+
return executeTrelloTool(toolName, args, {
|
|
752
|
+
connection,
|
|
753
|
+
credentials,
|
|
754
|
+
signal: executionOptions.signal || null,
|
|
755
|
+
});
|
|
742
756
|
},
|
|
743
757
|
getUserConfig({ userId, agentId }) {
|
|
744
758
|
const normalizedUserId = Number(userId);
|
|
@@ -757,7 +771,7 @@ function createTrelloProvider() {
|
|
|
757
771
|
hasConnectedAccount: accountCount > 0,
|
|
758
772
|
};
|
|
759
773
|
},
|
|
760
|
-
async saveUserConfig({ userId, agentId, config }) {
|
|
774
|
+
async saveUserConfig({ userId, agentId, config, signal }) {
|
|
761
775
|
const normalizedUserId = Number(userId);
|
|
762
776
|
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
763
777
|
throw new Error('A valid user is required to save Trello configuration.');
|
|
@@ -796,7 +810,7 @@ function createTrelloProvider() {
|
|
|
796
810
|
|
|
797
811
|
let profile;
|
|
798
812
|
try {
|
|
799
|
-
profile = await fetchTrelloMemberProfile({ apiKey, token });
|
|
813
|
+
profile = await fetchTrelloMemberProfile({ apiKey, token, signal });
|
|
800
814
|
} catch (error) {
|
|
801
815
|
const message = String(error?.message || '').toLowerCase();
|
|
802
816
|
if (
|
|
@@ -152,14 +152,14 @@ function cleanLocationResult(item = {}) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
async function geocodeLocation(locationQuery, limit = 1) {
|
|
155
|
+
async function geocodeLocation(locationQuery, limit = 1, signal = null) {
|
|
156
156
|
const query = String(locationQuery || '').trim();
|
|
157
157
|
if (!query) {
|
|
158
158
|
throw new Error('location is required when latitude/longitude are not provided.');
|
|
159
159
|
}
|
|
160
160
|
const result = await fetchJson(
|
|
161
161
|
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=${Math.max(1, Math.min(Number(limit) || 1, 10))}&language=en&format=json`,
|
|
162
|
-
{ method: 'GET' },
|
|
162
|
+
{ method: 'GET', signal },
|
|
163
163
|
{ serviceName: 'Open-Meteo geocoding' },
|
|
164
164
|
);
|
|
165
165
|
const matches = Array.isArray(result?.results) ? result.results : [];
|
|
@@ -169,7 +169,7 @@ async function geocodeLocation(locationQuery, limit = 1) {
|
|
|
169
169
|
return matches.map(cleanLocationResult);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
async function resolveLocation(args = {}, connection = null) {
|
|
172
|
+
async function resolveLocation(args = {}, connection = null, signal = null) {
|
|
173
173
|
const latitude = toNumber(args.latitude, null);
|
|
174
174
|
const longitude = toNumber(args.longitude, null);
|
|
175
175
|
if (latitude !== null && longitude !== null) {
|
|
@@ -183,7 +183,7 @@ async function resolveLocation(args = {}, connection = null) {
|
|
|
183
183
|
|
|
184
184
|
const location = String(args.location || '').trim();
|
|
185
185
|
if (location) {
|
|
186
|
-
const [best] = await geocodeLocation(location, 1);
|
|
186
|
+
const [best] = await geocodeLocation(location, 1, signal);
|
|
187
187
|
return {
|
|
188
188
|
latitude: best.latitude,
|
|
189
189
|
longitude: best.longitude,
|
|
@@ -221,7 +221,7 @@ function enrichCurrent(current = {}) {
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
async function fetchForecastForLocation(location, forecastHours = 24) {
|
|
224
|
+
async function fetchForecastForLocation(location, forecastHours = 24, signal = null) {
|
|
225
225
|
const horizon = Math.max(1, Math.min(Number(forecastHours) || 24, 72));
|
|
226
226
|
const url = new URL('https://api.open-meteo.com/v1/forecast');
|
|
227
227
|
url.searchParams.set('latitude', String(location.latitude));
|
|
@@ -259,7 +259,11 @@ async function fetchForecastForLocation(location, forecastHours = 24) {
|
|
|
259
259
|
url.searchParams.set('forecast_hours', String(horizon));
|
|
260
260
|
url.searchParams.set('timezone', location.timezone || 'auto');
|
|
261
261
|
|
|
262
|
-
const result = await fetchJson(
|
|
262
|
+
const result = await fetchJson(
|
|
263
|
+
url.toString(),
|
|
264
|
+
{ method: 'GET', signal },
|
|
265
|
+
{ serviceName: 'Open-Meteo forecast' },
|
|
266
|
+
);
|
|
263
267
|
const hourly = result?.hourly || {};
|
|
264
268
|
const times = Array.isArray(hourly.time) ? hourly.time : [];
|
|
265
269
|
const rows = times.map((time, index) => {
|
|
@@ -304,6 +308,7 @@ class WeatherProvider {
|
|
|
304
308
|
this.sessions = new Map();
|
|
305
309
|
this.sessionTTL = 30 * 60 * 1000;
|
|
306
310
|
this.#pruneTimer = setInterval(() => this.pruneExpiredSessions(), this.sessionTTL);
|
|
311
|
+
this.#pruneTimer.unref?.();
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
#pruneTimer = null;
|
|
@@ -504,7 +509,8 @@ class WeatherProvider {
|
|
|
504
509
|
}
|
|
505
510
|
}
|
|
506
511
|
|
|
507
|
-
async executeTool(toolName, args, connection) {
|
|
512
|
+
async executeTool(toolName, args, connection, executionOptions = {}) {
|
|
513
|
+
const signal = executionOptions.signal || null;
|
|
508
514
|
switch (toolName) {
|
|
509
515
|
case 'weather_search_locations': {
|
|
510
516
|
const query = String(args.query || '').trim();
|
|
@@ -512,7 +518,7 @@ class WeatherProvider {
|
|
|
512
518
|
throw new Error('query is required.');
|
|
513
519
|
}
|
|
514
520
|
const limit = Math.max(1, Math.min(Number(args.limit) || 5, 10));
|
|
515
|
-
const results = await geocodeLocation(query, limit);
|
|
521
|
+
const results = await geocodeLocation(query, limit, signal);
|
|
516
522
|
return {
|
|
517
523
|
result: {
|
|
518
524
|
query,
|
|
@@ -522,8 +528,8 @@ class WeatherProvider {
|
|
|
522
528
|
};
|
|
523
529
|
}
|
|
524
530
|
case 'weather_get_current': {
|
|
525
|
-
const location = await resolveLocation(args, connection);
|
|
526
|
-
const forecast = await fetchForecastForLocation(location, 1);
|
|
531
|
+
const location = await resolveLocation(args, connection, signal);
|
|
532
|
+
const forecast = await fetchForecastForLocation(location, 1, signal);
|
|
527
533
|
return {
|
|
528
534
|
result: {
|
|
529
535
|
location: forecast.location,
|
|
@@ -532,9 +538,9 @@ class WeatherProvider {
|
|
|
532
538
|
};
|
|
533
539
|
}
|
|
534
540
|
case 'weather_get_forecast': {
|
|
535
|
-
const location = await resolveLocation(args, connection);
|
|
541
|
+
const location = await resolveLocation(args, connection, signal);
|
|
536
542
|
const forecastHours = Math.max(1, Math.min(Number(args.forecast_hours) || 24, 72));
|
|
537
|
-
const forecast = await fetchForecastForLocation(location, forecastHours);
|
|
543
|
+
const forecast = await fetchForecastForLocation(location, forecastHours, signal);
|
|
538
544
|
return {
|
|
539
545
|
result: {
|
|
540
546
|
location: forecast.location,
|
|
@@ -9,6 +9,10 @@ const { AGENT_DATA_DIR } = require('../../../../runtime/paths');
|
|
|
9
9
|
const { encryptValue, decryptValue } = require('../secrets');
|
|
10
10
|
const { withConnectionAccessMode } = require('../access');
|
|
11
11
|
const { normalizeWhatsAppId, toWhatsAppJid } = require('../../../utils/whatsapp');
|
|
12
|
+
const {
|
|
13
|
+
waitForAbortableResult,
|
|
14
|
+
waitForBoundedResult,
|
|
15
|
+
} = require('../http');
|
|
12
16
|
|
|
13
17
|
const WHATSAPP_APP = {
|
|
14
18
|
id: 'personal',
|
|
@@ -222,6 +226,7 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
222
226
|
this.reconnectTimers = new Map();
|
|
223
227
|
this.sessionReconnectTimers = new Map();
|
|
224
228
|
this.io = options.io || null;
|
|
229
|
+
this.shuttingDown = false;
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
_clearReconnectTimer(connectionId) {
|
|
@@ -241,7 +246,7 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
_scheduleReconnect(client, attempt = 1) {
|
|
244
|
-
if (!client?.connectionId || client.manualDisconnect) {
|
|
249
|
+
if (this.shuttingDown || !client?.connectionId || client.manualDisconnect) {
|
|
245
250
|
return;
|
|
246
251
|
}
|
|
247
252
|
this._clearReconnectTimer(client.connectionId);
|
|
@@ -267,11 +272,17 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
}, delayMs);
|
|
275
|
+
timer.unref?.();
|
|
270
276
|
this.reconnectTimers.set(client.connectionId, timer);
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
_scheduleSessionReconnect(session, attempt = 1) {
|
|
274
|
-
if (
|
|
280
|
+
if (
|
|
281
|
+
this.shuttingDown ||
|
|
282
|
+
!session?.id ||
|
|
283
|
+
session.status === 'connected' ||
|
|
284
|
+
session.status === 'logged_out'
|
|
285
|
+
) {
|
|
275
286
|
return;
|
|
276
287
|
}
|
|
277
288
|
this._clearSessionReconnectTimer(session.id);
|
|
@@ -290,6 +301,7 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
290
301
|
current.error = err?.message || 'connection_failed';
|
|
291
302
|
});
|
|
292
303
|
}, delayMs);
|
|
304
|
+
timer.unref?.();
|
|
293
305
|
this.sessionReconnectTimers.set(session.id, timer);
|
|
294
306
|
}
|
|
295
307
|
|
|
@@ -451,6 +463,9 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
async beginConnection({ userId, agentId, appKey }) {
|
|
466
|
+
if (this.shuttingDown) {
|
|
467
|
+
throw new Error('WhatsApp integration is shutting down.');
|
|
468
|
+
}
|
|
454
469
|
if (!this.getApp(appKey)) {
|
|
455
470
|
throw new Error(`Unknown ${this.label} app: ${appKey || 'missing app key'}`);
|
|
456
471
|
}
|
|
@@ -533,8 +548,9 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
533
548
|
}
|
|
534
549
|
}
|
|
535
550
|
|
|
536
|
-
async executeTool(toolName, args, connection) {
|
|
537
|
-
const
|
|
551
|
+
async executeTool(toolName, args, connection, executionOptions = {}) {
|
|
552
|
+
const signal = executionOptions.signal || null;
|
|
553
|
+
const client = await this._ensureClient(connection, { signal });
|
|
538
554
|
|
|
539
555
|
switch (toolName) {
|
|
540
556
|
case 'whatsapp_personal_get_profile':
|
|
@@ -584,9 +600,16 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
584
600
|
}
|
|
585
601
|
case 'whatsapp_personal_send_message': {
|
|
586
602
|
const jid = this._normalizeChatId(requireText(args.to, 'to'));
|
|
587
|
-
await
|
|
588
|
-
|
|
589
|
-
|
|
603
|
+
await waitForBoundedResult(
|
|
604
|
+
client.socket.sendMessage(jid, {
|
|
605
|
+
text: requireText(args.text, 'text'),
|
|
606
|
+
}),
|
|
607
|
+
{
|
|
608
|
+
signal,
|
|
609
|
+
timeoutMs: 60000,
|
|
610
|
+
serviceName: 'WhatsApp send',
|
|
611
|
+
},
|
|
612
|
+
);
|
|
590
613
|
return {
|
|
591
614
|
result: {
|
|
592
615
|
sent: true,
|
|
@@ -900,13 +923,13 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
900
923
|
return row?.id;
|
|
901
924
|
}
|
|
902
925
|
|
|
903
|
-
async _ensureClient(connection) {
|
|
926
|
+
async _ensureClient(connection, options = {}) {
|
|
904
927
|
const existing = this.clients.get(connection.id);
|
|
905
928
|
if (existing?.status === 'connected' && existing.socket) {
|
|
906
929
|
return existing;
|
|
907
930
|
}
|
|
908
931
|
if (existing?.connectPromise) {
|
|
909
|
-
await existing.connectPromise;
|
|
932
|
+
await waitForAbortableResult(existing.connectPromise, options.signal);
|
|
910
933
|
const ready = this.clients.get(connection.id);
|
|
911
934
|
if (ready?.status === 'connected' && ready.socket) {
|
|
912
935
|
return ready;
|
|
@@ -935,16 +958,16 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
935
958
|
logger: createSilentLogger(),
|
|
936
959
|
connectPromise: null,
|
|
937
960
|
};
|
|
938
|
-
|
|
961
|
+
const connectPromise = this._connectClient(client);
|
|
962
|
+
client.connectPromise = connectPromise;
|
|
939
963
|
this.clients.set(connection.id, client);
|
|
940
|
-
|
|
941
|
-
await client.connectPromise;
|
|
942
|
-
} finally {
|
|
964
|
+
void connectPromise.finally(() => {
|
|
943
965
|
const current = this.clients.get(connection.id);
|
|
944
|
-
if (current) {
|
|
966
|
+
if (current?.connectPromise === connectPromise) {
|
|
945
967
|
current.connectPromise = null;
|
|
946
968
|
}
|
|
947
|
-
}
|
|
969
|
+
}).catch(() => {});
|
|
970
|
+
await waitForAbortableResult(connectPromise, options.signal);
|
|
948
971
|
const ready = this.clients.get(connection.id);
|
|
949
972
|
if (!ready?.socket || ready.status !== 'connected') {
|
|
950
973
|
throw new Error('WhatsApp personal account is not connected.');
|
|
@@ -953,6 +976,9 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
953
976
|
}
|
|
954
977
|
|
|
955
978
|
async _connectClient(client) {
|
|
979
|
+
if (this.shuttingDown) {
|
|
980
|
+
throw new Error('WhatsApp integration is shutting down.');
|
|
981
|
+
}
|
|
956
982
|
ensureDir(client.authDir);
|
|
957
983
|
client.manualDisconnect = false;
|
|
958
984
|
const {
|
|
@@ -981,6 +1007,9 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
981
1007
|
if (Array.isArray(version) && version.length > 0) {
|
|
982
1008
|
socketConfig.version = version;
|
|
983
1009
|
}
|
|
1010
|
+
try {
|
|
1011
|
+
client.socket?.end(new Error('client_reconnect'));
|
|
1012
|
+
} catch {}
|
|
984
1013
|
const sock = makeWASocket(socketConfig);
|
|
985
1014
|
|
|
986
1015
|
client.socket = sock;
|
|
@@ -989,6 +1018,10 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
989
1018
|
|
|
990
1019
|
await new Promise((resolve, reject) => {
|
|
991
1020
|
const timeout = setTimeout(() => {
|
|
1021
|
+
client.status = 'disconnected';
|
|
1022
|
+
try {
|
|
1023
|
+
sock.end(new Error('connection_timeout'));
|
|
1024
|
+
} catch {}
|
|
992
1025
|
reject(new Error('WhatsApp connection timed out.'));
|
|
993
1026
|
}, 60000);
|
|
994
1027
|
|
|
@@ -1017,7 +1050,10 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
1017
1050
|
|
|
1018
1051
|
async _resolveBaileysVersion(fetchLatestBaileysVersion) {
|
|
1019
1052
|
try {
|
|
1020
|
-
const result = await fetchLatestBaileysVersion()
|
|
1053
|
+
const result = await waitForBoundedResult(fetchLatestBaileysVersion(), {
|
|
1054
|
+
timeoutMs: 15000,
|
|
1055
|
+
serviceName: 'WhatsApp version discovery',
|
|
1056
|
+
});
|
|
1021
1057
|
if (Array.isArray(result?.version) && result.version.length > 0) {
|
|
1022
1058
|
return result.version;
|
|
1023
1059
|
}
|
|
@@ -1026,6 +1062,30 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
1026
1062
|
}
|
|
1027
1063
|
return null;
|
|
1028
1064
|
}
|
|
1065
|
+
|
|
1066
|
+
async shutdown() {
|
|
1067
|
+
this.shuttingDown = true;
|
|
1068
|
+
for (const timer of this.reconnectTimers.values()) clearTimeout(timer);
|
|
1069
|
+
for (const timer of this.sessionReconnectTimers.values()) clearTimeout(timer);
|
|
1070
|
+
this.reconnectTimers.clear();
|
|
1071
|
+
this.sessionReconnectTimers.clear();
|
|
1072
|
+
|
|
1073
|
+
const sockets = new Set();
|
|
1074
|
+
for (const client of this.clients.values()) {
|
|
1075
|
+
client.manualDisconnect = true;
|
|
1076
|
+
if (client.socket) sockets.add(client.socket);
|
|
1077
|
+
}
|
|
1078
|
+
for (const session of this.sessions.values()) {
|
|
1079
|
+
if (session.socket) sockets.add(session.socket);
|
|
1080
|
+
}
|
|
1081
|
+
for (const socket of sockets) {
|
|
1082
|
+
try {
|
|
1083
|
+
socket.end(new Error('server_shutdown'));
|
|
1084
|
+
} catch {}
|
|
1085
|
+
}
|
|
1086
|
+
this.clients.clear();
|
|
1087
|
+
this.sessions.clear();
|
|
1088
|
+
}
|
|
1029
1089
|
}
|
|
1030
1090
|
|
|
1031
1091
|
function createWhatsAppPersonalProvider(options = {}) {
|