neoagent 3.2.1-beta.1 → 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/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 +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 +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/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 +390 -162
- 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 +510 -332
- 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 +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 +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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
6
|
+
const { spawn, spawnSync } = require('child_process');
|
|
7
7
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
8
8
|
const { stageGuestPayload, normalizeRuntimeProfile } = require('./guest_bootstrap');
|
|
9
9
|
|
|
@@ -21,11 +21,12 @@ const BUILD_TIMEOUT_MS = Number(process.env.NEOAGENT_GUEST_IMAGE_BUILD_TIMEOUT_M
|
|
|
21
21
|
// container start. Copy package.json first so the dependency layer stays cached
|
|
22
22
|
// across source-only changes.
|
|
23
23
|
function dockerfileFor(profile) {
|
|
24
|
-
|
|
24
|
+
const normalizedProfile = normalizeRuntimeProfile(profile);
|
|
25
|
+
if (normalizedProfile === 'android' || normalizedProfile === 'cli') {
|
|
25
26
|
return [
|
|
26
27
|
`FROM ${BASE_IMAGE}`,
|
|
27
28
|
'ENV NODE_ENV=production',
|
|
28
|
-
|
|
29
|
+
`ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
|
|
29
30
|
'WORKDIR /opt/neoagent',
|
|
30
31
|
'COPY package.json ./',
|
|
31
32
|
'RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force',
|
|
@@ -42,7 +43,7 @@ function dockerfileFor(profile) {
|
|
|
42
43
|
`FROM ${BASE_IMAGE}`,
|
|
43
44
|
'ENV NODE_ENV=production',
|
|
44
45
|
'ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
|
|
45
|
-
|
|
46
|
+
`ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
|
|
46
47
|
'WORKDIR /opt/neoagent',
|
|
47
48
|
'COPY package.json ./',
|
|
48
49
|
// Install deps without running package postinstall scripts, then fetch the
|
|
@@ -104,6 +105,34 @@ function dockerAvailable() {
|
|
|
104
105
|
return !result.error && result.status === 0;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function runDockerCommand(args, options = {}) {
|
|
109
|
+
const timeoutMs = Number(options.timeout || BUILD_TIMEOUT_MS);
|
|
110
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const child = spawnImpl('docker', args, {
|
|
113
|
+
stdio: options.stdio || ['ignore', 'inherit', 'inherit'],
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
let settled = false;
|
|
117
|
+
const finish = (error, status = null) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
if (error) reject(error);
|
|
122
|
+
else resolve(status);
|
|
123
|
+
};
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
const error = new Error(`docker ${args[0]} timed out after ${timeoutMs}ms`);
|
|
126
|
+
error.code = 'DOCKER_COMMAND_TIMEOUT';
|
|
127
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
128
|
+
finish(error);
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
timeout.unref?.();
|
|
131
|
+
child.once('error', (error) => finish(error));
|
|
132
|
+
child.once('close', (status) => finish(null, status));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
// Builds and caches the per-profile guest-agent Docker image. The image bakes in
|
|
108
137
|
// the guest agent, its dependencies, and (for browser_cli) the Chromium browser,
|
|
109
138
|
// so containers start the agent directly with no runtime installation step.
|
|
@@ -165,15 +194,16 @@ class GuestImageBuilder {
|
|
|
165
194
|
async #build(tag) {
|
|
166
195
|
console.log(`[GuestImage:${this.profile}] Building guest image ${tag} (one-time; downloads browser + deps)…`);
|
|
167
196
|
const started = Date.now();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
let status;
|
|
198
|
+
try {
|
|
199
|
+
status = await runDockerCommand(['build', '-t', tag, this.contextDir], {
|
|
200
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`Guest image build failed: ${error.message}`, { cause: error });
|
|
174
204
|
}
|
|
175
|
-
if (
|
|
176
|
-
throw new Error(`Guest image build for ${tag} exited with status ${
|
|
205
|
+
if (status !== 0) {
|
|
206
|
+
throw new Error(`Guest image build for ${tag} exited with status ${status}`);
|
|
177
207
|
}
|
|
178
208
|
this.#cachedBuiltAt = Date.now();
|
|
179
209
|
console.log(`[GuestImage:${this.profile}] Built ${tag} in ${Math.round((Date.now() - started) / 1000)}s`);
|
|
@@ -184,5 +214,6 @@ class GuestImageBuilder {
|
|
|
184
214
|
module.exports = {
|
|
185
215
|
GuestImageBuilder,
|
|
186
216
|
dockerAvailable,
|
|
217
|
+
runDockerCommand,
|
|
187
218
|
BASE_IMAGE,
|
|
188
219
|
};
|
|
@@ -10,23 +10,36 @@ const { DesktopProvider } = require('../desktop/provider');
|
|
|
10
10
|
// Resource defaults for Docker VMs (overridable via env).
|
|
11
11
|
const DEFAULT_VM_MEMORY_MB = Number(process.env.NEOAGENT_VM_MEMORY_MB ?? 2048);
|
|
12
12
|
const DEFAULT_VM_CPUS = Number(process.env.NEOAGENT_VM_CPUS ?? 2);
|
|
13
|
+
const DEFAULT_CLI_VM_MEMORY_MB = Number(process.env.NEOAGENT_CLI_VM_MEMORY_MB ?? 1024);
|
|
14
|
+
const DEFAULT_CLI_VM_CPUS = Number(process.env.NEOAGENT_CLI_VM_CPUS ?? 1);
|
|
13
15
|
|
|
14
16
|
class RuntimeManager {
|
|
15
17
|
constructor(options = {}) {
|
|
16
18
|
this.browserExtensionRegistry = options.browserExtensionRegistry || null;
|
|
17
19
|
this.desktopCompanionRegistry = options.desktopCompanionRegistry || null;
|
|
18
20
|
this.shellWorkerPool = options.shellWorkerPool || null;
|
|
21
|
+
this.androidControllers = new Map();
|
|
19
22
|
|
|
20
23
|
const browserVmManager = options.browserVmManager || new DockerVMManager({
|
|
21
|
-
runtimeProfile: '
|
|
24
|
+
runtimeProfile: 'browser',
|
|
22
25
|
memoryMb: DEFAULT_VM_MEMORY_MB,
|
|
23
26
|
cpus: DEFAULT_VM_CPUS,
|
|
24
27
|
});
|
|
25
28
|
this.browserBackend = new LocalVmExecutionBackend({
|
|
26
|
-
runtimeProfile: '
|
|
29
|
+
runtimeProfile: 'browser',
|
|
27
30
|
vmManager: browserVmManager,
|
|
28
31
|
artifactStore: options.artifactStore,
|
|
29
32
|
});
|
|
33
|
+
const cliVmManager = options.cliVmManager || new DockerVMManager({
|
|
34
|
+
runtimeProfile: 'cli',
|
|
35
|
+
memoryMb: DEFAULT_CLI_VM_MEMORY_MB,
|
|
36
|
+
cpus: DEFAULT_CLI_VM_CPUS,
|
|
37
|
+
});
|
|
38
|
+
this.cliBackend = new LocalVmExecutionBackend({
|
|
39
|
+
runtimeProfile: 'cli',
|
|
40
|
+
vmManager: cliVmManager,
|
|
41
|
+
artifactStore: options.artifactStore,
|
|
42
|
+
});
|
|
30
43
|
|
|
31
44
|
this.artifactStore = options.artifactStore || null;
|
|
32
45
|
|
|
@@ -44,6 +57,12 @@ class RuntimeManager {
|
|
|
44
57
|
artifactStore: options.artifactStore,
|
|
45
58
|
userId,
|
|
46
59
|
}));
|
|
60
|
+
|
|
61
|
+
this.createAndroidController = options.createAndroidController
|
|
62
|
+
|| ((userId) => new AndroidController({
|
|
63
|
+
userId,
|
|
64
|
+
artifactStore: this.artifactStore,
|
|
65
|
+
}));
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
|
|
@@ -68,37 +87,73 @@ class RuntimeManager {
|
|
|
68
87
|
);
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
getCapabilitySnapshot(userId) {
|
|
91
|
+
const settings = this.getSettings(userId) || {};
|
|
92
|
+
const key = String(userId || '').trim();
|
|
93
|
+
const extensionConnected = this.hasActiveExtensionBrowser(userId);
|
|
94
|
+
const androidController = key && this.androidControllers instanceof Map
|
|
95
|
+
? this.androidControllers.get(key)
|
|
96
|
+
: null;
|
|
97
|
+
let androidStatus = null;
|
|
98
|
+
if (androidController && typeof androidController.getStatusSync === 'function') {
|
|
99
|
+
try {
|
|
100
|
+
androidStatus = androidController.getStatusSync();
|
|
101
|
+
} catch {
|
|
102
|
+
androidStatus = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
browser: {
|
|
108
|
+
preferredBackend: settings.browser_backend,
|
|
109
|
+
activeBackend: extensionConnected ? 'extension' : 'vm',
|
|
110
|
+
extensionConnected,
|
|
111
|
+
vmInitialized: Boolean(
|
|
112
|
+
this.browserBackend?.vmManager?.hasTrackedVm?.(userId),
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
android: {
|
|
116
|
+
initialized: Boolean(androidController),
|
|
117
|
+
status: androidStatus,
|
|
118
|
+
},
|
|
119
|
+
desktop: {
|
|
120
|
+
connected: this.hasActiveDesktopCompanion(userId),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
resolveBackend(userId, requested) {
|
|
72
126
|
void userId;
|
|
73
|
-
|
|
74
|
-
return this.browserBackend;
|
|
127
|
+
return requested === 'browser' ? this.browserBackend : this.cliBackend;
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
async executeCommand(userId, command, options = {}) {
|
|
78
|
-
const backend = this.resolveBackend(userId, '
|
|
131
|
+
const backend = this.resolveBackend(userId, 'cli');
|
|
79
132
|
return backend.executeCommand(userId, command, options);
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
hasVmForUser(userId, capability = 'browser') {
|
|
83
|
-
|
|
136
|
+
const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
|
|
137
|
+
return Boolean(backend?.vmManager?.hasVm?.(userId));
|
|
84
138
|
}
|
|
85
139
|
|
|
86
140
|
async killCommand(userId, pid, reason = 'aborted') {
|
|
87
|
-
return this.
|
|
141
|
+
return this.cliBackend.killCommand(userId, pid, reason);
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
async getCommandExecutorForUser(userId) {
|
|
91
|
-
return this.
|
|
145
|
+
return this.cliBackend.getCommandExecutorForUser(userId);
|
|
92
146
|
}
|
|
93
147
|
|
|
94
|
-
async getBrowserProviderForUser(userId) {
|
|
148
|
+
async getBrowserProviderForUser(userId, options = {}) {
|
|
95
149
|
const settings = this.getSettings(userId);
|
|
96
150
|
if (settings.browser_backend === 'extension' && this.hasActiveExtensionBrowser(userId)) {
|
|
97
151
|
return this.getExtensionBrowserProvider(userId, {
|
|
98
152
|
tokenId: settings.browser_extension_token_id,
|
|
153
|
+
signal: options.signal,
|
|
99
154
|
});
|
|
100
155
|
}
|
|
101
|
-
return this.browserBackend.getBrowserProviderForUser(userId);
|
|
156
|
+
return this.browserBackend.getBrowserProviderForUser(userId, options);
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
async getCliProviderForUser(userId) {
|
|
@@ -116,18 +171,12 @@ class RuntimeManager {
|
|
|
116
171
|
kill: () => Promise.resolve(false),
|
|
117
172
|
};
|
|
118
173
|
}
|
|
119
|
-
const executor = await this.
|
|
174
|
+
const executor = await this.cliBackend.getCommandExecutorForUser(userId);
|
|
120
175
|
return { ...executor, backend: 'vm' };
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
async executeCliCommand(userId, command, options = {}) {
|
|
124
179
|
const provider = await this.getCliProviderForUser(userId);
|
|
125
|
-
// Route desktop-companion shell commands through the isolated worker pool
|
|
126
|
-
// when available. The VM (Docker) path is already isolated.
|
|
127
|
-
if (provider.backend === 'desktop-companion' && this.shellWorkerPool && options.pty !== true) {
|
|
128
|
-
const result = await this.shellWorkerPool.execute(command, options);
|
|
129
|
-
return { ...result, backend: 'desktop-companion-worker' };
|
|
130
|
-
}
|
|
131
180
|
const result = await (options.pty === true && provider.executeInteractive
|
|
132
181
|
? provider.executeInteractive(command, options.inputs || [], options)
|
|
133
182
|
: provider.execute(command, options));
|
|
@@ -146,23 +195,28 @@ class RuntimeManager {
|
|
|
146
195
|
if (userId == null || String(userId).trim() === '') {
|
|
147
196
|
throw new Error('Android provider requires a user ID.');
|
|
148
197
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
198
|
+
const key = String(userId).trim();
|
|
199
|
+
if (!this.androidControllers.has(key)) {
|
|
200
|
+
this.androidControllers.set(key, this.createAndroidController(key));
|
|
201
|
+
}
|
|
202
|
+
return this.androidControllers.get(key);
|
|
153
203
|
}
|
|
154
204
|
|
|
155
205
|
async isGuestAgentReadyForUser(userId, timeoutMs = 1000, capability = 'browser') {
|
|
156
|
-
|
|
206
|
+
const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
|
|
207
|
+
if (typeof backend?.isGuestAgentReadyForUser !== 'function') {
|
|
157
208
|
return false;
|
|
158
209
|
}
|
|
159
|
-
return
|
|
210
|
+
return backend.isGuestAgentReadyForUser(userId, timeoutMs);
|
|
160
211
|
}
|
|
161
212
|
|
|
162
213
|
async shutdown() {
|
|
163
214
|
const tasks = [
|
|
164
215
|
this.browserBackend?.shutdown?.(),
|
|
216
|
+
this.cliBackend?.shutdown?.(),
|
|
217
|
+
...Array.from(this.androidControllers.values(), (controller) => controller?.close?.()),
|
|
165
218
|
];
|
|
219
|
+
this.androidControllers.clear();
|
|
166
220
|
if (typeof this.shellWorkerPool?.shutdown === 'function') {
|
|
167
221
|
tasks.push(Promise.resolve().then(() => this.shellWorkerPool.shutdown()));
|
|
168
222
|
}
|
|
@@ -6,14 +6,14 @@ function getRuntimeValidation(runtimeManager) {
|
|
|
6
6
|
const policy = getDeploymentPolicy();
|
|
7
7
|
const nodeEnvIsProd = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'prod';
|
|
8
8
|
const browserVmReadiness = runtimeManager?.browserBackend?.vmManager?.getReadiness?.() || null;
|
|
9
|
-
const
|
|
9
|
+
const cliVmReadiness = runtimeManager?.cliBackend?.vmManager?.getReadiness?.() || null;
|
|
10
10
|
const issues = [];
|
|
11
11
|
|
|
12
12
|
if (policy.profile === 'prod' || nodeEnvIsProd) {
|
|
13
|
-
if (!browserVmReadiness) {
|
|
14
|
-
issues.push('prod profile requires
|
|
15
|
-
} else if (!browserVmReadiness.dockerAvailable) {
|
|
16
|
-
issues.push('prod profile requires Docker to be installed and running for the browser
|
|
13
|
+
if (!browserVmReadiness || !cliVmReadiness) {
|
|
14
|
+
issues.push('prod profile requires working isolated container runtimes for browser and CLI.');
|
|
15
|
+
} else if (!browserVmReadiness.dockerAvailable || !cliVmReadiness.dockerAvailable) {
|
|
16
|
+
issues.push('prod profile requires Docker to be installed and running for the browser and CLI runtimes.');
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -21,7 +21,8 @@ function getRuntimeValidation(runtimeManager) {
|
|
|
21
21
|
ready: issues.length === 0,
|
|
22
22
|
issues,
|
|
23
23
|
vm: {
|
|
24
|
-
browser:
|
|
24
|
+
browser: browserVmReadiness,
|
|
25
|
+
cli: cliVmReadiness,
|
|
25
26
|
android: null,
|
|
26
27
|
},
|
|
27
28
|
guestTokenConfigured: true,
|
|
@@ -18,6 +18,7 @@ const TOOL_CATEGORIES = {
|
|
|
18
18
|
'desktop_observe',
|
|
19
19
|
],
|
|
20
20
|
browser_privileged: ['browser_evaluate'],
|
|
21
|
+
credential_use: ['credential_fill_browser', 'credential_http_request'],
|
|
21
22
|
network_write: ['http_request'],
|
|
22
23
|
skill_mutation: [
|
|
23
24
|
'create_skill',
|
|
@@ -78,6 +79,10 @@ const BUILT_IN_TOOLS = new Set([
|
|
|
78
79
|
'browser_extract',
|
|
79
80
|
'browser_screenshot',
|
|
80
81
|
'browser_evaluate',
|
|
82
|
+
'credential_fill_browser',
|
|
83
|
+
'credential_submit_browser',
|
|
84
|
+
'credential_cancel_browser',
|
|
85
|
+
'credential_http_request',
|
|
81
86
|
'desktop_list_devices',
|
|
82
87
|
'desktop_select_device',
|
|
83
88
|
'desktop_observe',
|
|
@@ -166,6 +171,7 @@ const DEFAULT_POLICY = {
|
|
|
166
171
|
android_privileged: 'require_approval',
|
|
167
172
|
desktop_control: 'require_approval',
|
|
168
173
|
browser_privileged: 'require_approval',
|
|
174
|
+
credential_use: 'require_approval',
|
|
169
175
|
network_write: 'require_approval',
|
|
170
176
|
skill_mutation: 'deny',
|
|
171
177
|
external: 'require_approval',
|
|
@@ -28,9 +28,12 @@ class GithubChannel extends SocialReachChannel {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async read({ url }) {
|
|
31
|
+
async read({ url, signal }) {
|
|
32
32
|
const { owner, repo } = parseRepoUrl(url);
|
|
33
|
-
const data = await fetchJson(
|
|
33
|
+
const data = await fetchJson(
|
|
34
|
+
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
|
|
35
|
+
{ signal },
|
|
36
|
+
);
|
|
34
37
|
return {
|
|
35
38
|
platform: this.id,
|
|
36
39
|
owner,
|
|
@@ -47,14 +50,17 @@ class GithubChannel extends SocialReachChannel {
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
async search({ query, limit }) {
|
|
53
|
+
async search({ query, limit, signal }) {
|
|
51
54
|
const q = String(query || '').trim();
|
|
52
55
|
if (!q) {
|
|
53
56
|
const error = new Error('query is required.');
|
|
54
57
|
error.status = 400;
|
|
55
58
|
throw error;
|
|
56
59
|
}
|
|
57
|
-
const data = await fetchJson(
|
|
60
|
+
const data = await fetchJson(
|
|
61
|
+
`https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=${normalizeLimit(limit, 10, 30)}`,
|
|
62
|
+
{ signal },
|
|
63
|
+
);
|
|
58
64
|
return {
|
|
59
65
|
platform: this.id,
|
|
60
66
|
query: q,
|
|
@@ -67,8 +67,8 @@ class RedditChannel extends SocialReachChannel {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
async read({ url, limit }) {
|
|
71
|
-
const data = await fetchJson(postUrlForJson(url));
|
|
70
|
+
async read({ url, limit, signal }) {
|
|
71
|
+
const data = await fetchJson(postUrlForJson(url), { signal });
|
|
72
72
|
const post = shapePost(data?.[0]?.data?.children?.[0]);
|
|
73
73
|
const comments = (data?.[1]?.data?.children || [])
|
|
74
74
|
.filter((item) => item?.kind === 't1')
|
|
@@ -82,7 +82,7 @@ class RedditChannel extends SocialReachChannel {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async search({ query, limit }) {
|
|
85
|
+
async search({ query, limit, signal }) {
|
|
86
86
|
const q = String(query || '').trim();
|
|
87
87
|
if (!q) {
|
|
88
88
|
const error = new Error('query is required.');
|
|
@@ -90,7 +90,7 @@ class RedditChannel extends SocialReachChannel {
|
|
|
90
90
|
throw error;
|
|
91
91
|
}
|
|
92
92
|
const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=${normalizeLimit(limit, 10, 50)}&raw_json=1`;
|
|
93
|
-
const data = await fetchJson(url);
|
|
93
|
+
const data = await fetchJson(url, { signal });
|
|
94
94
|
return {
|
|
95
95
|
platform: this.id,
|
|
96
96
|
query: q,
|
|
@@ -31,9 +31,9 @@ class RssChannel extends SocialReachChannel {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async read({ url, limit }) {
|
|
34
|
+
async read({ url, limit, signal }) {
|
|
35
35
|
const parsed = assertHttpUrl(url);
|
|
36
|
-
const xml = await fetchText(parsed.toString());
|
|
36
|
+
const xml = await fetchText(parsed.toString(), { publicOnly: true, signal });
|
|
37
37
|
const $ = cheerio.load(xml, { xmlMode: true });
|
|
38
38
|
const entries = $('item, entry').toArray().slice(0, normalizeLimit(limit, 20, 100));
|
|
39
39
|
const title = $('channel > title, feed > title').first().text().trim() || null;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { getPlatformDefinition } = require('../platforms');
|
|
4
4
|
const { SocialReachChannel } = require('./base');
|
|
5
5
|
const { normalizeAndDetectPlatform } = require('../../social_video/url');
|
|
6
|
+
const { createAbortError } = require('../../../utils/abort');
|
|
6
7
|
|
|
7
8
|
const PLATFORM_LABELS = Object.freeze({
|
|
8
9
|
youtube: 'YouTube',
|
|
@@ -26,7 +27,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
async check() {
|
|
30
|
+
async check({ signal } = {}) {
|
|
30
31
|
if (!this.socialVideoService || typeof this.socialVideoService.getHealthStatus !== 'function') {
|
|
31
32
|
return {
|
|
32
33
|
...(await super.check()),
|
|
@@ -37,11 +38,14 @@ class SocialVideoReachChannel extends SocialReachChannel {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const health = await this.socialVideoService.getHealthStatus().catch((error) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const health = await this.socialVideoService.getHealthStatus({ signal }).catch((error) => {
|
|
42
|
+
if (signal?.aborted) throw createAbortError(signal);
|
|
43
|
+
return {
|
|
44
|
+
ready: false,
|
|
45
|
+
dependencies: [],
|
|
46
|
+
error: error.message || String(error),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
45
49
|
const missing = (health.dependencies || [])
|
|
46
50
|
.filter((item) => !item.available)
|
|
47
51
|
.map((item) => item.name)
|
|
@@ -59,7 +63,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
|
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
async read({ userId, url, include_frame: includeFrame, force_stt: forceStt, agentId }) {
|
|
66
|
+
async read({ userId, url, include_frame: includeFrame, force_stt: forceStt, agentId, signal }) {
|
|
63
67
|
if (!this.socialVideoService || typeof this.socialVideoService.extractFromUrl !== 'function') {
|
|
64
68
|
const error = new Error('Social video extraction is not connected right now.');
|
|
65
69
|
error.status = 503;
|
|
@@ -70,6 +74,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
|
|
|
70
74
|
includeFrame: includeFrame !== false,
|
|
71
75
|
forceStt: forceStt === true,
|
|
72
76
|
agentId: agentId || null,
|
|
77
|
+
signal,
|
|
73
78
|
});
|
|
74
79
|
return {
|
|
75
80
|
...result,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { SocialReachChannel } = require('./base');
|
|
4
4
|
const { getPlatformDefinition } = require('../platforms');
|
|
5
5
|
const { assertHttpUrl, fetchJson, normalizeLimit } = require('../utils');
|
|
6
|
+
const { createAbortError } = require('../../../utils/abort');
|
|
6
7
|
|
|
7
8
|
function shapeTopic(item = {}) {
|
|
8
9
|
const node = item.node || {};
|
|
@@ -33,17 +34,26 @@ class V2exChannel extends SocialReachChannel {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async read({ url, limit }) {
|
|
37
|
+
async read({ url, limit, signal }) {
|
|
37
38
|
const parsed = assertHttpUrl(url);
|
|
38
39
|
const match = parsed.pathname.match(/^\/t\/(\d+)/);
|
|
39
40
|
if (!match) {
|
|
40
|
-
return this.search({ query: parsed.searchParams.get('q') || 'hot', limit });
|
|
41
|
+
return this.search({ query: parsed.searchParams.get('q') || 'hot', limit, signal });
|
|
41
42
|
}
|
|
42
43
|
const topicId = match[1];
|
|
43
|
-
const topicData = await fetchJson(
|
|
44
|
+
const topicData = await fetchJson(
|
|
45
|
+
`https://www.v2ex.com/api/topics/show.json?id=${encodeURIComponent(topicId)}`,
|
|
46
|
+
{ signal },
|
|
47
|
+
);
|
|
44
48
|
const topic = Array.isArray(topicData) ? topicData[0] || {} : topicData || {};
|
|
45
|
-
const replies = await fetchJson(
|
|
46
|
-
.
|
|
49
|
+
const replies = await fetchJson(
|
|
50
|
+
`https://www.v2ex.com/api/replies/show.json?topic_id=${encodeURIComponent(topicId)}&page=1`,
|
|
51
|
+
{ signal },
|
|
52
|
+
)
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
if (signal?.aborted) throw createAbortError(signal);
|
|
55
|
+
return [];
|
|
56
|
+
});
|
|
47
57
|
return {
|
|
48
58
|
platform: this.id,
|
|
49
59
|
...shapeTopic(topic),
|
|
@@ -58,11 +68,14 @@ class V2exChannel extends SocialReachChannel {
|
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
async search({ query, limit }) {
|
|
71
|
+
async search({ query, limit, signal }) {
|
|
62
72
|
const normalized = String(query || '').trim().toLowerCase();
|
|
63
73
|
const capped = normalizeLimit(limit, 20, 100);
|
|
64
74
|
if (normalized && normalized !== 'hot') {
|
|
65
|
-
const data = await fetchJson(
|
|
75
|
+
const data = await fetchJson(
|
|
76
|
+
`https://www.v2ex.com/api/topics/show.json?node_name=${encodeURIComponent(normalized)}&page=1`,
|
|
77
|
+
{ signal },
|
|
78
|
+
);
|
|
66
79
|
return {
|
|
67
80
|
platform: this.id,
|
|
68
81
|
query: normalized,
|
|
@@ -70,7 +83,7 @@ class V2exChannel extends SocialReachChannel {
|
|
|
70
83
|
source: 'v2ex_public_api',
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
|
-
const data = await fetchJson('https://www.v2ex.com/api/topics/hot.json');
|
|
86
|
+
const data = await fetchJson('https://www.v2ex.com/api/topics/hot.json', { signal });
|
|
74
87
|
return {
|
|
75
88
|
platform: this.id,
|
|
76
89
|
query: 'hot',
|
|
@@ -43,7 +43,7 @@ class XChannel extends SocialReachChannel {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
async read({ userId, url }) {
|
|
46
|
+
async read({ userId, url, signal }) {
|
|
47
47
|
const id = extractStatusId(url);
|
|
48
48
|
if (!id) {
|
|
49
49
|
const error = new Error('A public X post URL is required.');
|
|
@@ -54,7 +54,7 @@ class XChannel extends SocialReachChannel {
|
|
|
54
54
|
const source = cookieHeaderForPlatform(userId, this.id) ? 'x_syndication_cookies' : 'x_syndication_public';
|
|
55
55
|
const data = await fetchJson(
|
|
56
56
|
`https://cdn.syndication.twimg.com/tweet-result?id=${encodeURIComponent(id)}&lang=en`,
|
|
57
|
-
{ headers: this.#headers(userId) },
|
|
57
|
+
{ headers: this.#headers(userId), signal },
|
|
58
58
|
);
|
|
59
59
|
return {
|
|
60
60
|
platform: this.id,
|
|
@@ -37,7 +37,7 @@ class XueqiuChannel extends SocialReachChannel {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async search({ userId, query, limit }) {
|
|
40
|
+
async search({ userId, query, limit, signal }) {
|
|
41
41
|
const q = String(query || '').trim();
|
|
42
42
|
if (!q) {
|
|
43
43
|
const error = new Error('query is required.');
|
|
@@ -45,7 +45,7 @@ class XueqiuChannel extends SocialReachChannel {
|
|
|
45
45
|
throw error;
|
|
46
46
|
}
|
|
47
47
|
const url = `https://xueqiu.com/stock/search.json?code=${encodeURIComponent(q)}&size=${normalizeLimit(limit, 10, 50)}`;
|
|
48
|
-
const data = await fetchJson(url, { headers: this.#headers(userId) });
|
|
48
|
+
const data = await fetchJson(url, { headers: this.#headers(userId), signal });
|
|
49
49
|
return {
|
|
50
50
|
platform: this.id,
|
|
51
51
|
query: q,
|
|
@@ -58,12 +58,12 @@ class XueqiuChannel extends SocialReachChannel {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async read({ userId, symbol, limit }) {
|
|
61
|
+
async read({ userId, symbol, limit, signal }) {
|
|
62
62
|
const raw = String(symbol || '').trim().toUpperCase();
|
|
63
63
|
if (raw) {
|
|
64
64
|
const data = await fetchJson(
|
|
65
65
|
`https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=${encodeURIComponent(raw)}`,
|
|
66
|
-
{ headers: this.#headers(userId) },
|
|
66
|
+
{ headers: this.#headers(userId), signal },
|
|
67
67
|
);
|
|
68
68
|
const quote = data.data?.items?.[0]?.quote || {};
|
|
69
69
|
return {
|
|
@@ -76,7 +76,7 @@ class XueqiuChannel extends SocialReachChannel {
|
|
|
76
76
|
}
|
|
77
77
|
const data = await fetchJson(
|
|
78
78
|
'https://xueqiu.com/v4/statuses/public_timeline_by_category.json?since_id=-1&max_id=-1&count=20&category=-1',
|
|
79
|
-
{ headers: this.#headers(userId) },
|
|
79
|
+
{ headers: this.#headers(userId), signal },
|
|
80
80
|
);
|
|
81
81
|
return {
|
|
82
82
|
platform: this.id,
|
|
@@ -5,6 +5,7 @@ const { createChannels } = require('./channels');
|
|
|
5
5
|
const { deleteCookieBundle, getCookieSummary, writeCookieBundle } = require('./store');
|
|
6
6
|
const { domainsForPlatform, getPlatformDefinition, normalizePlatformId } = require('./platforms');
|
|
7
7
|
const { assertHttpUrl } = require('./utils');
|
|
8
|
+
const { createAbortError } = require('../../utils/abort');
|
|
8
9
|
|
|
9
10
|
const COOKIE_IMPORT_PLATFORMS = new Set(['xueqiu', 'x']);
|
|
10
11
|
const MAX_IMPORTED_COOKIES = 80;
|
|
@@ -59,16 +60,17 @@ class SocialReachService {
|
|
|
59
60
|
return this.channels.find((channel) => channel.canHandleUrl(parsed.toString())) || null;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
async getStatus(userId) {
|
|
63
|
+
async getStatus(userId, options = {}) {
|
|
63
64
|
const statuses = [];
|
|
64
65
|
for (const channel of this.channels) {
|
|
65
66
|
try {
|
|
66
|
-
const status = await channel.check({ userId });
|
|
67
|
+
const status = await channel.check({ userId, signal: options.signal });
|
|
67
68
|
if (!status.cookie && channel.setupKind === 'cookies') {
|
|
68
69
|
status.cookie = getCookieSummary(userId, channel.id);
|
|
69
70
|
}
|
|
70
71
|
statuses.push(status);
|
|
71
72
|
} catch (error) {
|
|
73
|
+
if (options.signal?.aborted) throw createAbortError(options.signal);
|
|
72
74
|
statuses.push({
|
|
73
75
|
platform: channel.id,
|
|
74
76
|
label: channel.label,
|
|
@@ -87,7 +89,7 @@ class SocialReachService {
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
async read(userId, args = {}) {
|
|
92
|
+
async read(userId, args = {}, options = {}) {
|
|
91
93
|
const platform = normalizePlatformId(args.platform || '');
|
|
92
94
|
const channel = platform ? this.getChannel(platform) : this.detectChannelForUrl(args.url);
|
|
93
95
|
if (!channel) {
|
|
@@ -97,10 +99,10 @@ class SocialReachService {
|
|
|
97
99
|
error.status = 400;
|
|
98
100
|
throw error;
|
|
99
101
|
}
|
|
100
|
-
return channel.read({ ...args, userId });
|
|
102
|
+
return channel.read({ ...args, userId, signal: options.signal });
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
async search(userId, args = {}) {
|
|
105
|
+
async search(userId, args = {}, options = {}) {
|
|
104
106
|
const platform = normalizePlatformId(args.platform || '');
|
|
105
107
|
const channel = this.getChannel(platform);
|
|
106
108
|
if (!channel) {
|
|
@@ -108,7 +110,7 @@ class SocialReachService {
|
|
|
108
110
|
error.status = 400;
|
|
109
111
|
throw error;
|
|
110
112
|
}
|
|
111
|
-
return channel.search({ ...args, userId });
|
|
113
|
+
return channel.search({ ...args, userId, signal: options.signal });
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
async importCookiesFromExtension(userId, platform, options = {}) {
|
|
@@ -132,6 +134,7 @@ class SocialReachService {
|
|
|
132
134
|
}, {
|
|
133
135
|
tokenId: options.tokenId || null,
|
|
134
136
|
timeoutMs: 15000,
|
|
137
|
+
signal: options.signal,
|
|
135
138
|
});
|
|
136
139
|
const cookies = sanitizeCookies(response?.cookies || [], domains);
|
|
137
140
|
if (cookies.length === 0) {
|