neoagent 3.2.1-beta.0 → 3.2.1-beta.10
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/docs/agent-run-lifecycle.md +10 -0
- 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 +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -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 +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- 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 +11 -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 +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- package/server/routes/android.js +30 -34
- 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 +16 -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 +496 -166
- 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 +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -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 +4 -4
- 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 +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- 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/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 +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -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 +12 -7
- 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/service.js +160 -50
- 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/retry.js +107 -0
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const { decryptValue } = require('./secrets');
|
|
4
4
|
const { getConnectionAccessMode } = require('./access');
|
|
5
|
+
const { fetchResponseText } = require('./http');
|
|
6
|
+
const { isAbortError } = require('../../utils/abort');
|
|
7
|
+
const {
|
|
8
|
+
abortableDelay,
|
|
9
|
+
computeBackoffMs,
|
|
10
|
+
getHttpStatus,
|
|
11
|
+
isTransientIoError,
|
|
12
|
+
retryAfterMilliseconds,
|
|
13
|
+
} = require('../../utils/retry');
|
|
5
14
|
|
|
6
|
-
const DEFAULT_OAUTH_HTTP_TIMEOUT_MS = 15000;
|
|
7
15
|
const OAUTH_STATE_PATTERN = /^[a-f0-9]{32,128}$/i;
|
|
16
|
+
const RETRYABLE_INTEGRATION_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
17
|
+
const MAX_INTEGRATION_RETRY_DELAY_MS = 10000;
|
|
8
18
|
|
|
9
19
|
function escapeScope(scopes) {
|
|
10
20
|
return Array.from(new Set((scopes || []).filter(Boolean))).join(' ');
|
|
@@ -21,6 +31,17 @@ function appendQuery(url, params) {
|
|
|
21
31
|
return resolved.toString();
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
function sanitizeRemoteError(value) {
|
|
35
|
+
return String(value || '')
|
|
36
|
+
.slice(0, 1000)
|
|
37
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
|
|
38
|
+
.replace(
|
|
39
|
+
/\b(access_token|refresh_token|client_secret|authorization|token)\b\s*[:=]\s*["']?[^\s,"'}\]]+/gi,
|
|
40
|
+
'$1=[redacted]',
|
|
41
|
+
)
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
async function fetchJson(url, options = {}, context = {}) {
|
|
25
46
|
const headers = {
|
|
26
47
|
Accept: 'application/json',
|
|
@@ -42,54 +63,68 @@ async function fetchJson(url, options = {}, context = {}) {
|
|
|
42
63
|
).toString();
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
67
|
+
const maxAttempts =
|
|
68
|
+
options.retry === false || !RETRYABLE_INTEGRATION_METHODS.has(method)
|
|
69
|
+
? 1
|
|
70
|
+
: 2;
|
|
71
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const { response, text } = await fetchResponseText(
|
|
74
|
+
url,
|
|
75
|
+
{
|
|
76
|
+
method,
|
|
77
|
+
headers,
|
|
78
|
+
body,
|
|
79
|
+
signal: options.signal,
|
|
80
|
+
timeoutMs: options.timeoutMs,
|
|
81
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
82
|
+
},
|
|
83
|
+
context,
|
|
84
|
+
);
|
|
50
85
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
signal: controller.signal,
|
|
58
|
-
});
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (error?.name === 'AbortError') {
|
|
61
|
-
const label = String(context.serviceName || 'OAuth provider').trim() || 'OAuth provider';
|
|
62
|
-
throw new Error(`${label} request timed out after ${timeoutMs}ms.`);
|
|
63
|
-
}
|
|
64
|
-
throw error;
|
|
65
|
-
} finally {
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
}
|
|
86
|
+
let data = null;
|
|
87
|
+
try {
|
|
88
|
+
data = text ? JSON.parse(text) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
data = null;
|
|
91
|
+
}
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
data = null;
|
|
75
|
-
}
|
|
93
|
+
const notOkSlackStyle =
|
|
94
|
+
data &&
|
|
95
|
+
typeof data === 'object' &&
|
|
96
|
+
Object.prototype.hasOwnProperty.call(data, 'ok') &&
|
|
97
|
+
data.ok === false;
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
if (!response.ok || notOkSlackStyle) {
|
|
100
|
+
const label = String(context.serviceName || 'OAuth provider').trim() || 'OAuth provider';
|
|
101
|
+
const rawMessage =
|
|
102
|
+
(data && (data.error_description || data.error || data.message)) ||
|
|
103
|
+
text ||
|
|
104
|
+
`${response.status} ${response.statusText}`;
|
|
105
|
+
const error = new Error(
|
|
106
|
+
`${label} request failed: ${sanitizeRemoteError(rawMessage)}`,
|
|
107
|
+
);
|
|
108
|
+
error.status = response.status;
|
|
109
|
+
error.data = data;
|
|
110
|
+
error.headers = response.headers;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
82
113
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
return data;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isAbortError(error, options.signal) || attempt >= maxAttempts) throw error;
|
|
117
|
+
const retryAfterMs = retryAfterMilliseconds(error.headers);
|
|
118
|
+
const rateLimited =
|
|
119
|
+
(getHttpStatus(error) === 403 && retryAfterMs !== null)
|
|
120
|
+
|| String(error.data?.error || '').toLowerCase() === 'ratelimited';
|
|
121
|
+
if (!rateLimited && !isTransientIoError(error)) throw error;
|
|
122
|
+
const delayMs = retryAfterMs ?? computeBackoffMs(attempt, 250, 2000);
|
|
123
|
+
if (delayMs > MAX_INTEGRATION_RETRY_DELAY_MS) throw error;
|
|
124
|
+
await abortableDelay(delayMs, options.signal);
|
|
125
|
+
}
|
|
90
126
|
}
|
|
91
|
-
|
|
92
|
-
return data;
|
|
127
|
+
throw new Error('Integration request exhausted its retry attempts.');
|
|
93
128
|
}
|
|
94
129
|
|
|
95
130
|
function sortConnections(rows) {
|
|
@@ -318,7 +353,7 @@ function createOAuthProvider(options = {}) {
|
|
|
318
353
|
connectionMethod: this.connectionMethod,
|
|
319
354
|
};
|
|
320
355
|
},
|
|
321
|
-
async beginOAuth({ state, codeVerifier, appKey, userId, agentId }) {
|
|
356
|
+
async beginOAuth({ state, codeVerifier, appKey, userId, agentId, signal }) {
|
|
322
357
|
const app = getApp(appKey);
|
|
323
358
|
if (!app) {
|
|
324
359
|
throw new Error(`Unknown ${this.label} app: ${appKey}`);
|
|
@@ -335,9 +370,18 @@ function createOAuthProvider(options = {}) {
|
|
|
335
370
|
agentId,
|
|
336
371
|
app,
|
|
337
372
|
env,
|
|
373
|
+
signal,
|
|
338
374
|
});
|
|
339
375
|
},
|
|
340
|
-
async finishOAuth({
|
|
376
|
+
async finishOAuth({
|
|
377
|
+
code,
|
|
378
|
+
codeVerifier,
|
|
379
|
+
appKey,
|
|
380
|
+
userId,
|
|
381
|
+
agentId,
|
|
382
|
+
state,
|
|
383
|
+
signal,
|
|
384
|
+
}) {
|
|
341
385
|
const app = getApp(appKey);
|
|
342
386
|
if (!app) {
|
|
343
387
|
throw new Error(`Unknown ${this.label} app: ${appKey}`);
|
|
@@ -351,15 +395,16 @@ function createOAuthProvider(options = {}) {
|
|
|
351
395
|
state: normalizedState,
|
|
352
396
|
app,
|
|
353
397
|
env: this.getEnvStatus({ userId, agentId }),
|
|
398
|
+
signal,
|
|
354
399
|
});
|
|
355
400
|
},
|
|
356
|
-
async disconnect(connectionRow) {
|
|
401
|
+
async disconnect(connectionRow, executionOptions = {}) {
|
|
357
402
|
if (typeof options.disconnect === 'function') {
|
|
358
|
-
return options.disconnect(connectionRow);
|
|
403
|
+
return options.disconnect(connectionRow, executionOptions);
|
|
359
404
|
}
|
|
360
405
|
return null;
|
|
361
406
|
},
|
|
362
|
-
async executeTool(toolName, args, connectionRow) {
|
|
407
|
+
async executeTool(toolName, args, connectionRow, executionOptions = {}) {
|
|
363
408
|
if (typeof options.executeTool !== 'function') {
|
|
364
409
|
return null;
|
|
365
410
|
}
|
|
@@ -371,7 +416,8 @@ function createOAuthProvider(options = {}) {
|
|
|
371
416
|
} catch {
|
|
372
417
|
credentials = {};
|
|
373
418
|
}
|
|
374
|
-
|
|
419
|
+
let credentialsUpdated = false;
|
|
420
|
+
const integrationContext = {
|
|
375
421
|
appId: toolAppMap.get(String(toolName || '').trim()) || connectionRow.app_key,
|
|
376
422
|
connection: connectionRow,
|
|
377
423
|
credentials,
|
|
@@ -379,7 +425,25 @@ function createOAuthProvider(options = {}) {
|
|
|
379
425
|
userId: connectionRow?.user_id,
|
|
380
426
|
agentId: connectionRow?.agent_id,
|
|
381
427
|
}),
|
|
382
|
-
|
|
428
|
+
signal: executionOptions.signal || null,
|
|
429
|
+
updateCredentials(nextCredentials) {
|
|
430
|
+
if (!nextCredentials || typeof nextCredentials !== 'object') return;
|
|
431
|
+
integrationContext.credentials = nextCredentials;
|
|
432
|
+
credentialsUpdated = true;
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
const execution = await options.executeTool(
|
|
436
|
+
toolName,
|
|
437
|
+
args,
|
|
438
|
+
integrationContext,
|
|
439
|
+
);
|
|
440
|
+
if (!execution || execution.credentials || !credentialsUpdated) {
|
|
441
|
+
return execution;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
...execution,
|
|
445
|
+
credentials: integrationContext.credentials,
|
|
446
|
+
};
|
|
383
447
|
},
|
|
384
448
|
summarizeConnection(connectionRows) {
|
|
385
449
|
const snapshot = this.buildSnapshot(connectionRows);
|
|
@@ -4,6 +4,7 @@ const { createFigmaProvider } = require('./figma/provider');
|
|
|
4
4
|
const { createGoogleWorkspaceProvider } = require('./google/provider');
|
|
5
5
|
const { createGithubProvider } = require('./github/provider');
|
|
6
6
|
const { createHomeAssistantProvider } = require('./home_assistant/provider');
|
|
7
|
+
const { createBitwardenProvider } = require('./bitwarden/provider');
|
|
7
8
|
const { createTrelloProvider } = require('./trello/provider');
|
|
8
9
|
const { createMicrosoftProvider } = require('./microsoft/provider');
|
|
9
10
|
const { createNotionProvider } = require('./notion/provider');
|
|
@@ -28,6 +29,7 @@ function createIntegrationRegistry(options = {}) {
|
|
|
28
29
|
createNeoArchiveProvider(),
|
|
29
30
|
createNeoRecallProvider(),
|
|
30
31
|
createHomeAssistantProvider(),
|
|
32
|
+
createBitwardenProvider(options),
|
|
31
33
|
createWhatsAppPersonalProvider(options),
|
|
32
34
|
];
|
|
33
35
|
const byKey = new Map(providers.map((provider) => [provider.key, provider]));
|
|
@@ -130,7 +130,11 @@ function requireText(value, label) {
|
|
|
130
130
|
return text;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
async function slackApi(
|
|
133
|
+
async function slackApi(
|
|
134
|
+
credentials,
|
|
135
|
+
methodName,
|
|
136
|
+
{ httpMethod = 'POST', query, body, signal } = {},
|
|
137
|
+
) {
|
|
134
138
|
const normalizedMethod = requireText(methodName, 'method_name');
|
|
135
139
|
if (!/^[a-zA-Z0-9_.]+$/.test(normalizedMethod)) {
|
|
136
140
|
throw new Error('method_name must be a Slack Web API method name.');
|
|
@@ -149,16 +153,92 @@ async function slackApi(credentials, methodName, { httpMethod = 'POST', query, b
|
|
|
149
153
|
method: normalizedHttpMethod,
|
|
150
154
|
headers: { Authorization: `Bearer ${credentials.access_token}` },
|
|
151
155
|
...(body === undefined ? {} : { json: body }),
|
|
156
|
+
signal,
|
|
152
157
|
},
|
|
153
158
|
{ serviceName: 'Slack' },
|
|
154
159
|
);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
|
|
162
|
+
function expiresAtFromSeconds(expiresIn) {
|
|
163
|
+
const seconds = Number(expiresIn);
|
|
164
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
165
|
+
return new Date(Date.now() + seconds * 1000).toISOString();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function tokenExpiresSoon(credentials) {
|
|
169
|
+
const expiresAt = Date.parse(String(credentials?.expires_at || ''));
|
|
170
|
+
return Number.isFinite(expiresAt) && expiresAt <= Date.now() + 60 * 1000;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function refreshSlackCredentials(credentials, signal) {
|
|
174
|
+
const refreshToken = String(credentials?.refresh_token || '').trim();
|
|
175
|
+
if (!refreshToken) {
|
|
176
|
+
throw new Error('Slack refresh token is missing. Reconnect this integration account.');
|
|
177
|
+
}
|
|
178
|
+
const config = resolveSlackOAuthConfig();
|
|
179
|
+
const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64');
|
|
180
|
+
const token = await fetchJson(
|
|
181
|
+
'https://slack.com/api/oauth.v2.access',
|
|
182
|
+
{
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: { Authorization: `Basic ${basic}` },
|
|
185
|
+
form: {
|
|
186
|
+
grant_type: 'refresh_token',
|
|
187
|
+
refresh_token: refreshToken,
|
|
188
|
+
},
|
|
189
|
+
signal,
|
|
190
|
+
},
|
|
191
|
+
{ serviceName: 'Slack token refresh' },
|
|
192
|
+
);
|
|
193
|
+
const userToken = token?.authed_user?.access_token || token.access_token;
|
|
194
|
+
if (!userToken) {
|
|
195
|
+
throw new Error('Slack token refresh did not return an access token.');
|
|
196
|
+
}
|
|
197
|
+
const nextRefreshToken =
|
|
198
|
+
token?.authed_user?.refresh_token || token.refresh_token || refreshToken;
|
|
199
|
+
const expiresIn = token?.authed_user?.expires_in || token.expires_in;
|
|
200
|
+
return {
|
|
201
|
+
...credentials,
|
|
202
|
+
access_token: userToken,
|
|
203
|
+
refresh_token: nextRefreshToken,
|
|
204
|
+
expires_in: expiresIn,
|
|
205
|
+
expires_at: expiresAtFromSeconds(expiresIn),
|
|
206
|
+
bot_access_token: token.access_token || credentials.bot_access_token,
|
|
207
|
+
token_type: token?.authed_user?.token_type || token.token_type || credentials.token_type,
|
|
208
|
+
team: token.team || credentials.team || null,
|
|
209
|
+
enterprise: token.enterprise || credentials.enterprise || null,
|
|
210
|
+
authed_user: token.authed_user
|
|
211
|
+
? { ...(credentials.authed_user || {}), ...token.authed_user }
|
|
212
|
+
: credentials.authed_user || null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function slackRequest(context, methodName, options = {}) {
|
|
217
|
+
const { signal } = context;
|
|
218
|
+
let credentials = context.credentials;
|
|
219
|
+
if (tokenExpiresSoon(credentials)) {
|
|
220
|
+
credentials = await refreshSlackCredentials(credentials, signal);
|
|
221
|
+
context.updateCredentials(credentials);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
return await slackApi(credentials, methodName, { ...options, signal });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const code = String(error?.data?.error || '').toLowerCase();
|
|
228
|
+
if (!credentials.refresh_token || !['invalid_auth', 'token_expired'].includes(code)) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
credentials = await refreshSlackCredentials(credentials, signal);
|
|
232
|
+
context.updateCredentials(credentials);
|
|
233
|
+
return slackApi(credentials, methodName, { ...options, signal });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function executeSlackTool(toolName, args, context) {
|
|
158
238
|
switch (toolName) {
|
|
159
239
|
case 'slack_list_conversations':
|
|
160
240
|
return {
|
|
161
|
-
result: await
|
|
241
|
+
result: await slackRequest(context, 'conversations.list', {
|
|
162
242
|
httpMethod: 'GET',
|
|
163
243
|
query: {
|
|
164
244
|
types: String(args.types || 'public_channel,private_channel,im,mpim'),
|
|
@@ -168,7 +248,7 @@ async function executeSlackTool(toolName, args, { credentials }) {
|
|
|
168
248
|
};
|
|
169
249
|
case 'slack_get_conversation_history':
|
|
170
250
|
return {
|
|
171
|
-
result: await
|
|
251
|
+
result: await slackRequest(context, 'conversations.history', {
|
|
172
252
|
httpMethod: 'GET',
|
|
173
253
|
query: {
|
|
174
254
|
channel: requireText(args.channel, 'channel'),
|
|
@@ -178,7 +258,7 @@ async function executeSlackTool(toolName, args, { credentials }) {
|
|
|
178
258
|
};
|
|
179
259
|
case 'slack_post_message':
|
|
180
260
|
return {
|
|
181
|
-
result: await
|
|
261
|
+
result: await slackRequest(context, 'chat.postMessage', {
|
|
182
262
|
body: {
|
|
183
263
|
channel: requireText(args.channel, 'channel'),
|
|
184
264
|
text: requireText(args.text, 'text'),
|
|
@@ -188,7 +268,7 @@ async function executeSlackTool(toolName, args, { credentials }) {
|
|
|
188
268
|
};
|
|
189
269
|
case 'slack_search_messages':
|
|
190
270
|
return {
|
|
191
|
-
result: await
|
|
271
|
+
result: await slackRequest(context, 'search.messages', {
|
|
192
272
|
httpMethod: 'GET',
|
|
193
273
|
query: {
|
|
194
274
|
query: requireText(args.query, 'query'),
|
|
@@ -198,7 +278,7 @@ async function executeSlackTool(toolName, args, { credentials }) {
|
|
|
198
278
|
};
|
|
199
279
|
case 'slack_get_user_info':
|
|
200
280
|
return {
|
|
201
|
-
result: await
|
|
281
|
+
result: await slackRequest(context, 'users.info', {
|
|
202
282
|
httpMethod: 'GET',
|
|
203
283
|
query: { user: requireText(args.user, 'user') },
|
|
204
284
|
}),
|
|
@@ -208,7 +288,7 @@ async function executeSlackTool(toolName, args, { credentials }) {
|
|
|
208
288
|
throw new Error('slack_api_request is disabled by default. Set NEOAGENT_ENABLE_SLACK_DYNAMIC_API_REQUEST=true to enable it.');
|
|
209
289
|
}
|
|
210
290
|
return {
|
|
211
|
-
result: await
|
|
291
|
+
result: await slackRequest(context, requireText(args.method_name, 'method_name'), {
|
|
212
292
|
httpMethod: args.http_method || 'POST',
|
|
213
293
|
query: args.query,
|
|
214
294
|
body: args.body,
|
|
@@ -247,7 +327,7 @@ function createSlackProvider() {
|
|
|
247
327
|
appId: app.id,
|
|
248
328
|
};
|
|
249
329
|
},
|
|
250
|
-
async finishOAuth({ code, app }) {
|
|
330
|
+
async finishOAuth({ code, app, signal }) {
|
|
251
331
|
const config = resolveSlackOAuthConfig();
|
|
252
332
|
const token = await fetchJson(
|
|
253
333
|
'https://slack.com/api/oauth.v2.access',
|
|
@@ -260,18 +340,24 @@ function createSlackProvider() {
|
|
|
260
340
|
grant_type: 'authorization_code',
|
|
261
341
|
redirect_uri: config.redirectUri,
|
|
262
342
|
},
|
|
343
|
+
signal,
|
|
263
344
|
},
|
|
264
345
|
{ serviceName: 'Slack' },
|
|
265
346
|
);
|
|
266
347
|
|
|
267
348
|
const userToken = token?.authed_user?.access_token || token.access_token;
|
|
349
|
+
const refreshToken =
|
|
350
|
+
token?.authed_user?.refresh_token || token.refresh_token || null;
|
|
351
|
+
const expiresIn = token?.authed_user?.expires_in || token.expires_in || null;
|
|
268
352
|
const authTest = await slackApi({ access_token: userToken }, 'auth.test', {
|
|
269
353
|
httpMethod: 'GET',
|
|
354
|
+
signal,
|
|
270
355
|
});
|
|
271
356
|
const profile = authTest?.user_id
|
|
272
357
|
? await slackApi({ access_token: userToken }, 'users.info', {
|
|
273
358
|
httpMethod: 'GET',
|
|
274
359
|
query: { user: authTest.user_id },
|
|
360
|
+
signal,
|
|
275
361
|
}).catch(() => null)
|
|
276
362
|
: null;
|
|
277
363
|
const user = profile?.user || {};
|
|
@@ -287,6 +373,9 @@ function createSlackProvider() {
|
|
|
287
373
|
accountEmail,
|
|
288
374
|
credentials: {
|
|
289
375
|
access_token: userToken,
|
|
376
|
+
refresh_token: refreshToken,
|
|
377
|
+
expires_in: expiresIn,
|
|
378
|
+
expires_at: expiresAtFromSeconds(expiresIn),
|
|
290
379
|
bot_access_token: token.access_token,
|
|
291
380
|
token_type: token.token_type,
|
|
292
381
|
team: token.team || null,
|