neoagent 3.2.1-beta.2 → 3.2.1-beta.3
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 +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- 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/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -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 +1 -1
- package/server/public/main.dart.js +30803 -30805
- 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 +5 -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/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +347 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- 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 +8 -41
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- 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/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -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 +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- 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/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 +158 -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/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 +87 -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 +84 -9
- 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/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- 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/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- 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
package/lib/schema_migrations.js
CHANGED
|
@@ -30,20 +30,41 @@ function migrateMemoryEmbeddingIndex(db) {
|
|
|
30
30
|
.map((column) => column.name),
|
|
31
31
|
);
|
|
32
32
|
if (!columns.has('index_version')) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
user_id,
|
|
40
|
-
agent_id,
|
|
41
|
-
dimension,
|
|
42
|
-
index_version,
|
|
43
|
-
band_index,
|
|
44
|
-
band_value
|
|
33
|
+
try {
|
|
34
|
+
db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const refreshedColumns = new Set(
|
|
37
|
+
db.prepare('PRAGMA table_info(memory_embedding_bands)').all()
|
|
38
|
+
.map((column) => column.name),
|
|
45
39
|
);
|
|
46
|
-
|
|
40
|
+
if (!refreshedColumns.has('index_version')) throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const expectedIndexColumns = [
|
|
44
|
+
'user_id',
|
|
45
|
+
'agent_id',
|
|
46
|
+
'dimension',
|
|
47
|
+
'index_version',
|
|
48
|
+
'band_index',
|
|
49
|
+
'band_value',
|
|
50
|
+
];
|
|
51
|
+
const indexColumns = db.prepare(
|
|
52
|
+
'PRAGMA index_info(idx_memory_embedding_bands_lookup)',
|
|
53
|
+
).all().map((column) => column.name);
|
|
54
|
+
if (indexColumns.join('\0') !== expectedIndexColumns.join('\0')) {
|
|
55
|
+
db.exec(`
|
|
56
|
+
DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_memory_embedding_bands_lookup
|
|
58
|
+
ON memory_embedding_bands(
|
|
59
|
+
user_id,
|
|
60
|
+
agent_id,
|
|
61
|
+
dimension,
|
|
62
|
+
index_version,
|
|
63
|
+
band_index,
|
|
64
|
+
band_value
|
|
65
|
+
);
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
function removeRetiredCaptureData(db) {
|
|
@@ -661,6 +682,37 @@ function migrateAgentRunLifecycle(db) {
|
|
|
661
682
|
`);
|
|
662
683
|
}
|
|
663
684
|
|
|
685
|
+
function migrateMessagingInboundJobs(db) {
|
|
686
|
+
db.exec(`
|
|
687
|
+
CREATE TABLE IF NOT EXISTS messaging_inbound_jobs (
|
|
688
|
+
id TEXT PRIMARY KEY,
|
|
689
|
+
message_id INTEGER NOT NULL UNIQUE,
|
|
690
|
+
user_id INTEGER NOT NULL,
|
|
691
|
+
agent_id TEXT,
|
|
692
|
+
platform TEXT NOT NULL,
|
|
693
|
+
platform_msg_id TEXT,
|
|
694
|
+
platform_chat_id TEXT,
|
|
695
|
+
payload_json TEXT NOT NULL,
|
|
696
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
697
|
+
CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
|
|
698
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
699
|
+
run_id TEXT,
|
|
700
|
+
last_error TEXT,
|
|
701
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
702
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
703
|
+
completed_at TEXT,
|
|
704
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
|
|
705
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
706
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_pending
|
|
710
|
+
ON messaging_inbound_jobs(status, platform, user_id, agent_id, created_at);
|
|
711
|
+
CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_run
|
|
712
|
+
ON messaging_inbound_jobs(run_id);
|
|
713
|
+
`);
|
|
714
|
+
}
|
|
715
|
+
|
|
664
716
|
function runSchemaMigrations(db) {
|
|
665
717
|
removeRetiredCaptureData(db);
|
|
666
718
|
migrateMemoryEmbeddingIndex(db);
|
|
@@ -676,6 +728,7 @@ function runSchemaMigrations(db) {
|
|
|
676
728
|
migrateToolPoliciesAllowAlways(db);
|
|
677
729
|
migrateBilling(db);
|
|
678
730
|
migrateAgentRunLifecycle(db);
|
|
731
|
+
migrateMessagingInboundJobs(db);
|
|
679
732
|
}
|
|
680
733
|
|
|
681
734
|
module.exports = {
|
|
@@ -694,5 +747,6 @@ module.exports = {
|
|
|
694
747
|
migrateToolPoliciesAllowAlways,
|
|
695
748
|
migrateBilling,
|
|
696
749
|
migrateAgentRunLifecycle,
|
|
750
|
+
migrateMessagingInboundJobs,
|
|
697
751
|
runSchemaMigrations,
|
|
698
752
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neoagent",
|
|
3
|
-
"version": "3.2.1-beta.
|
|
3
|
+
"version": "3.2.1-beta.3",
|
|
4
4
|
"description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"main": "server/index.js",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=20.19.0"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"neoagent": "bin/neoagent.js"
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
70
70
|
"@google/generative-ai": "^0.24.0",
|
|
71
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
71
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
72
72
|
"@remotion/cli": "^4.0.459",
|
|
73
|
-
"@slidev/cli": "^52.
|
|
73
|
+
"@slidev/cli": "^52.18.0",
|
|
74
74
|
"@slidev/theme-default": "^0.25.0",
|
|
75
75
|
"baileys": "^6.7.21",
|
|
76
76
|
"bcrypt": "^6.0.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"better-sqlite3-session-store": "^0.1.0",
|
|
79
79
|
"cheerio": "^1.0.0-rc.12",
|
|
80
80
|
"cors": "^2.8.5",
|
|
81
|
-
"discord.js": "^14.
|
|
81
|
+
"discord.js": "^14.27.0",
|
|
82
82
|
"dotenv": "^16.4.7",
|
|
83
83
|
"express": "^4.21.2",
|
|
84
84
|
"express-rate-limit": "^7.5.0",
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"googleapis": "^150.0.1",
|
|
88
88
|
"helmet": "^8.0.0",
|
|
89
89
|
"multer": "^1.4.5-lts.1",
|
|
90
|
-
"node-cron": "^
|
|
90
|
+
"node-cron": "^4.6.0",
|
|
91
91
|
"node-pty": "^1.0.0",
|
|
92
|
-
"nodemailer": "^
|
|
92
|
+
"nodemailer": "^9.0.3",
|
|
93
93
|
"openai": "^4.85.4",
|
|
94
94
|
"otplib": "^13.4.0",
|
|
95
95
|
"playwright-chromium": "^1.59.1",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
100
100
|
"qrcode": "^1.5.4",
|
|
101
101
|
"remotion": "^4.0.459",
|
|
102
|
-
"sharp": "^0.
|
|
102
|
+
"sharp": "^0.35.3",
|
|
103
103
|
"socket.io": "^4.8.1",
|
|
104
104
|
"stripe": "^22.2.1",
|
|
105
105
|
"telegraf": "^4.16.3",
|
|
@@ -109,19 +109,26 @@
|
|
|
109
109
|
"ws": "^8.21.0"
|
|
110
110
|
},
|
|
111
111
|
"overrides": {
|
|
112
|
+
"@hono/node-server": "^2.0.11",
|
|
112
113
|
"axios": "^1.15.2",
|
|
113
114
|
"basic-ftp": "^5.3.0",
|
|
115
|
+
"dompurify": "^3.4.12",
|
|
116
|
+
"fast-uri": "^3.1.4",
|
|
114
117
|
"follow-redirects": "^1.16.0",
|
|
115
|
-
"hono": "^4.12.
|
|
116
|
-
"
|
|
118
|
+
"hono": "^4.12.31",
|
|
119
|
+
"ip-address": "^10.2.0",
|
|
120
|
+
"linkify-it": "^5.0.2",
|
|
121
|
+
"protobufjs": "^7.6.5",
|
|
117
122
|
"serialize-javascript": "^7.0.5",
|
|
118
|
-
"undici": "^6.
|
|
123
|
+
"undici": "^6.27.0",
|
|
124
|
+
"uuid": "$uuid",
|
|
119
125
|
"webpackbar": "^7.0.0",
|
|
120
126
|
"ws": "^8.21.0"
|
|
121
127
|
},
|
|
122
128
|
"devDependencies": {
|
|
123
|
-
"@docusaurus/core": "3.10.
|
|
124
|
-
"@docusaurus/preset-classic": "3.10.
|
|
129
|
+
"@docusaurus/core": "3.10.2",
|
|
130
|
+
"@docusaurus/preset-classic": "3.10.2",
|
|
131
|
+
"@types/node": "^20.19.0",
|
|
125
132
|
"autocannon": "^7.15.0",
|
|
126
133
|
"react": "18.3.1",
|
|
127
134
|
"react-dom": "18.3.1",
|
package/runtime/paths.js
CHANGED
|
@@ -109,26 +109,69 @@ function readEnvFileRaw(envFile = ENV_FILE) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function writeEnvFileAtomic(envFile, content) {
|
|
113
|
+
const resolved = path.resolve(envFile);
|
|
114
|
+
const directory = path.dirname(resolved);
|
|
115
|
+
const temporary = path.join(
|
|
116
|
+
directory,
|
|
117
|
+
`.${path.basename(resolved)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
|
|
118
|
+
);
|
|
119
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
120
|
+
let descriptor = null;
|
|
121
|
+
try {
|
|
122
|
+
descriptor = fs.openSync(temporary, 'wx', 0o600);
|
|
123
|
+
fs.writeFileSync(descriptor, content, 'utf8');
|
|
124
|
+
fs.fsyncSync(descriptor);
|
|
125
|
+
fs.closeSync(descriptor);
|
|
126
|
+
descriptor = null;
|
|
127
|
+
fs.renameSync(temporary, resolved);
|
|
128
|
+
} finally {
|
|
129
|
+
if (descriptor !== null) {
|
|
130
|
+
try { fs.closeSync(descriptor); } catch {}
|
|
131
|
+
}
|
|
132
|
+
try { fs.rmSync(temporary, { force: true }); } catch {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeEnvKey(key) {
|
|
137
|
+
const normalized = String(key).replace(/[\r\n]/g, '');
|
|
138
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) {
|
|
139
|
+
throw new Error(`Invalid environment variable name: ${normalized || '(empty)'}`);
|
|
140
|
+
}
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
function upsertEnvValue(envFile, key, value) {
|
|
145
|
+
const safeKey = normalizeEnvKey(key);
|
|
146
|
+
const safeValue = String(value).replace(/[\r\n]/g, '');
|
|
113
147
|
const raw = readEnvFileRaw(envFile);
|
|
114
148
|
const lines = raw ? raw.split('\n') : [];
|
|
115
149
|
let replaced = false;
|
|
116
150
|
|
|
117
151
|
for (let i = 0; i < lines.length; i++) {
|
|
118
|
-
if (lines[i].startsWith(`${
|
|
119
|
-
lines[i] = `${
|
|
152
|
+
if (lines[i].startsWith(`${safeKey}=`)) {
|
|
153
|
+
lines[i] = `${safeKey}=${safeValue}`;
|
|
120
154
|
replaced = true;
|
|
121
155
|
break;
|
|
122
156
|
}
|
|
123
157
|
}
|
|
124
158
|
|
|
125
159
|
if (!replaced) {
|
|
126
|
-
lines.push(`${
|
|
160
|
+
lines.push(`${safeKey}=${safeValue}`);
|
|
127
161
|
}
|
|
128
162
|
|
|
129
163
|
const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
|
|
130
|
-
|
|
131
|
-
|
|
164
|
+
writeEnvFileAtomic(envFile, output);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function removeEnvValue(envFile, key) {
|
|
168
|
+
const safeKey = normalizeEnvKey(key);
|
|
169
|
+
const raw = readEnvFileRaw(envFile);
|
|
170
|
+
if (!raw) return false;
|
|
171
|
+
const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
|
|
172
|
+
const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
|
|
173
|
+
writeEnvFileAtomic(envFile, output);
|
|
174
|
+
return true;
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
function generateSecret(bytes = 32) {
|
|
@@ -247,6 +290,7 @@ module.exports = {
|
|
|
247
290
|
ensureSecureRuntimeEnv,
|
|
248
291
|
getDefaultVmBaseImageUrl,
|
|
249
292
|
migrateLegacyRuntime,
|
|
293
|
+
removeEnvValue,
|
|
250
294
|
upsertEnvValue,
|
|
251
295
|
readEnvFileRaw,
|
|
252
296
|
};
|
package/server/guest_agent.js
CHANGED
|
@@ -105,6 +105,26 @@ async function handle(res, work) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
async function handleRequest(req, res, work) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const abort = () => {
|
|
111
|
+
if (!res.writableEnded) controller.abort('Guest runtime request disconnected.');
|
|
112
|
+
};
|
|
113
|
+
req.once('aborted', abort);
|
|
114
|
+
res.once('close', abort);
|
|
115
|
+
try {
|
|
116
|
+
const result = await work(controller.signal);
|
|
117
|
+
if (!res.headersSent && !res.writableEnded) res.json(result);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
120
|
+
res.status(controller.signal.aborted ? 499 : 500).json({ error: sanitizeError(err) });
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
req.removeListener('aborted', abort);
|
|
124
|
+
res.removeListener('close', abort);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
app.use(requireToken);
|
|
109
129
|
|
|
110
130
|
app.get('/health', (_req, res) => {
|
|
@@ -118,7 +138,7 @@ app.get('/health', (_req, res) => {
|
|
|
118
138
|
});
|
|
119
139
|
|
|
120
140
|
app.post('/exec', async (req, res) => {
|
|
121
|
-
await
|
|
141
|
+
await handleRequest(req, res, async (signal) => {
|
|
122
142
|
const command = String(req.body?.command || '').trim();
|
|
123
143
|
if (!command) {
|
|
124
144
|
return { error: 'command is required' };
|
|
@@ -127,12 +147,14 @@ app.post('/exec', async (req, res) => {
|
|
|
127
147
|
return cliExecutor.executeInteractive(command, req.body?.inputs || [], {
|
|
128
148
|
cwd: req.body?.cwd,
|
|
129
149
|
timeout: req.body?.timeout,
|
|
150
|
+
signal,
|
|
130
151
|
});
|
|
131
152
|
}
|
|
132
153
|
return cliExecutor.execute(command, {
|
|
133
154
|
cwd: req.body?.cwd,
|
|
134
155
|
timeout: req.body?.timeout,
|
|
135
156
|
stdinInput: req.body?.stdin_input,
|
|
157
|
+
signal,
|
|
136
158
|
});
|
|
137
159
|
});
|
|
138
160
|
});
|
|
@@ -192,43 +214,43 @@ function requireCapability(controller, name) {
|
|
|
192
214
|
return controller;
|
|
193
215
|
}
|
|
194
216
|
|
|
195
|
-
app.post('/browser/launch', async (req, res) =>
|
|
196
|
-
app.post('/browser/navigate', async (req, res) =>
|
|
197
|
-
app.post('/browser/screenshot', async (req, res) =>
|
|
198
|
-
app.post('/browser/screenshot-jpeg', async (req, res) =>
|
|
199
|
-
const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, req.body || {});
|
|
217
|
+
app.post('/browser/launch', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').launch({ ...(req.body || {}), signal })));
|
|
218
|
+
app.post('/browser/navigate', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').navigate(req.body?.url, { ...(req.body || {}), signal })));
|
|
219
|
+
app.post('/browser/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').screenshot({ ...(req.body || {}), signal })));
|
|
220
|
+
app.post('/browser/screenshot-jpeg', async (req, res) => handleRequest(req, res, async (signal) => {
|
|
221
|
+
const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, { ...(req.body || {}), signal });
|
|
200
222
|
return {
|
|
201
223
|
contentType: 'image/jpeg',
|
|
202
224
|
contentBase64: Buffer.from(jpeg).toString('base64'),
|
|
203
225
|
};
|
|
204
226
|
}));
|
|
205
|
-
app.post('/browser/click', async (req, res) =>
|
|
206
|
-
app.post('/browser/click-point', async (req, res) =>
|
|
207
|
-
app.post('/browser/fill', async (req, res) =>
|
|
208
|
-
app.post('/browser/type-text', async (req, res) =>
|
|
209
|
-
app.post('/browser/press-key', async (req, res) =>
|
|
210
|
-
app.post('/browser/scroll', async (req, res) =>
|
|
211
|
-
app.post('/browser/extract', async (req, res) =>
|
|
212
|
-
app.post('/browser/execute', async (req, res) =>
|
|
227
|
+
app.post('/browser/click', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').click(req.body?.selector, req.body?.text, req.body?.screenshot !== false, { signal })));
|
|
228
|
+
app.post('/browser/click-point', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').clickPoint(req.body?.x, req.body?.y, req.body?.screenshot !== false, { signal })));
|
|
229
|
+
app.post('/browser/fill', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').type(req.body?.selector, String(req.body?.value ?? req.body?.text ?? ''), { ...(req.body || {}), signal })));
|
|
230
|
+
app.post('/browser/type-text', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').typeText(String(req.body?.text || ''), { ...(req.body || {}), signal })));
|
|
231
|
+
app.post('/browser/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').pressKey(req.body?.key, req.body?.screenshot !== false, { signal })));
|
|
232
|
+
app.post('/browser/scroll', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').scroll(req.body?.deltaX ?? 0, req.body?.deltaY ?? 0, req.body?.screenshot !== false, { signal })));
|
|
233
|
+
app.post('/browser/extract', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').extractContent({ ...(req.body || {}), signal })));
|
|
234
|
+
app.post('/browser/execute', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').executeJS(req.body?.code, { signal })));
|
|
213
235
|
app.post('/browser/close', async (_req, res) => handle(res, () => requireCapability(browserController, 'browser').closeBrowser().then(() => ({ success: true }))));
|
|
214
236
|
|
|
215
237
|
app.get('/android/status', async (_req, res) => handle(res, () => requireCapability(androidController, 'android').getStatus()));
|
|
216
|
-
app.post('/android/start', async (req, res) =>
|
|
217
|
-
app.post('/android/stop', async (
|
|
218
|
-
app.get('/android/devices', async (
|
|
219
|
-
app.post('/android/screenshot', async (req, res) =>
|
|
220
|
-
app.post('/android/observe', async (req, res) =>
|
|
221
|
-
app.post('/android/ui-dump', async (req, res) =>
|
|
222
|
-
app.get('/android/apps', async (req, res) =>
|
|
223
|
-
app.post('/android/open-app', async (req, res) =>
|
|
224
|
-
app.post('/android/open-intent', async (req, res) =>
|
|
225
|
-
app.post('/android/tap', async (req, res) =>
|
|
226
|
-
app.post('/android/long-press', async (req, res) =>
|
|
227
|
-
app.post('/android/type', async (req, res) =>
|
|
228
|
-
app.post('/android/swipe', async (req, res) =>
|
|
229
|
-
app.post('/android/press-key', async (req, res) =>
|
|
230
|
-
app.post('/android/wait-for', async (req, res) =>
|
|
231
|
-
app.post('/android/shell', async (req, res) =>
|
|
238
|
+
app.post('/android/start', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').requestStartEmulator({ ...(req.body || {}), signal })));
|
|
239
|
+
app.post('/android/stop', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').stopEmulator({ signal })));
|
|
240
|
+
app.get('/android/devices', async (req, res) => handleRequest(req, res, async (signal) => ({ devices: await requireCapability(androidController, 'android').listDevices({ signal }) })));
|
|
241
|
+
app.post('/android/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').screenshot({ ...(req.body || {}), signal })));
|
|
242
|
+
app.post('/android/observe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').observe({ ...(req.body || {}), signal })));
|
|
243
|
+
app.post('/android/ui-dump', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').dumpUi({ ...(req.body || {}), signal })));
|
|
244
|
+
app.get('/android/apps', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').listApps({ includeSystem: req.query.includeSystem === 'true', signal })));
|
|
245
|
+
app.post('/android/open-app', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openApp({ ...(req.body || {}), signal })));
|
|
246
|
+
app.post('/android/open-intent', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openIntent({ ...(req.body || {}), signal })));
|
|
247
|
+
app.post('/android/tap', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').tap({ ...(req.body || {}), signal })));
|
|
248
|
+
app.post('/android/long-press', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').longPress({ ...(req.body || {}), signal })));
|
|
249
|
+
app.post('/android/type', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').type({ ...(req.body || {}), signal })));
|
|
250
|
+
app.post('/android/swipe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').swipe({ ...(req.body || {}), signal })));
|
|
251
|
+
app.post('/android/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').pressKey({ ...(req.body || {}), signal })));
|
|
252
|
+
app.post('/android/wait-for', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').waitFor({ ...(req.body || {}), signal })));
|
|
253
|
+
app.post('/android/shell', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').shell({ ...(req.body || {}), signal })));
|
|
232
254
|
app.post('/android/install-apk', async (req, res) => {
|
|
233
255
|
await handle(res, async () => {
|
|
234
256
|
requireCapability(androidController, 'android');
|
|
@@ -145,6 +145,28 @@ function createSessionMiddleware({ secureCookies, trustProxy }) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function attachRequestSignal(req, res, next) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const abort = () => {
|
|
151
|
+
if (!res.writableEnded && !controller.signal.aborted) {
|
|
152
|
+
const error = new Error('HTTP client disconnected.');
|
|
153
|
+
error.name = 'AbortError';
|
|
154
|
+
error.code = 'ABORT_ERR';
|
|
155
|
+
controller.abort(error);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
req.removeListener('aborted', abort);
|
|
160
|
+
res.removeListener('close', abort);
|
|
161
|
+
res.removeListener('finish', cleanup);
|
|
162
|
+
};
|
|
163
|
+
req.signal = controller.signal;
|
|
164
|
+
req.once('aborted', abort);
|
|
165
|
+
res.once('close', abort);
|
|
166
|
+
res.once('finish', cleanup);
|
|
167
|
+
next();
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware, validateOrigin }) {
|
|
149
171
|
const jsonBody = require('express').json({
|
|
150
172
|
limit: '10mb',
|
|
@@ -179,6 +201,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
|
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
app.use(helmet(buildHelmetOptions({ secureCookies })));
|
|
204
|
+
app.use(attachRequestSignal);
|
|
182
205
|
app.use(
|
|
183
206
|
cors((req, callback) => {
|
|
184
207
|
const requestPath = `${req.originalUrl || req.url || req.path || ''}`.split('?')[0];
|
|
@@ -255,6 +278,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
|
|
|
255
278
|
}
|
|
256
279
|
|
|
257
280
|
module.exports = {
|
|
281
|
+
attachRequestSignal,
|
|
258
282
|
applyHttpMiddleware,
|
|
259
283
|
createSessionMiddleware
|
|
260
284
|
};
|
package/server/http/routes.js
CHANGED
|
@@ -185,24 +185,22 @@ function registerApiRoutes(app) {
|
|
|
185
185
|
app.get('/api/system/test/cli', requireAuth, async (req, res) => {
|
|
186
186
|
const userId = req.session?.userId;
|
|
187
187
|
const runtimeManager = req.app?.locals?.runtimeManager;
|
|
188
|
-
if (!runtimeManager || typeof runtimeManager.
|
|
188
|
+
if (!runtimeManager || typeof runtimeManager.executeCliCommand !== 'function') {
|
|
189
189
|
return res.json({ passed: false, backendUsed: 'vm', detail: 'Runtime not configured on this server.' });
|
|
190
190
|
}
|
|
191
|
-
// Note: executeCommand always routes through the VM backend regardless of
|
|
192
|
-
// the cli_backend setting — desktop CLI routing is not yet implemented.
|
|
193
191
|
try {
|
|
194
|
-
const result = await runtimeManager.
|
|
192
|
+
const result = await runtimeManager.executeCliCommand(userId, 'echo "cli_test_ok"', { timeout: 15000 });
|
|
195
193
|
const exitOk = result?.exitCode === 0;
|
|
196
194
|
const outputOk = String(result?.stdout || '').includes('cli_test_ok');
|
|
197
195
|
return res.json({
|
|
198
196
|
passed: exitOk && outputOk,
|
|
199
|
-
backendUsed: '
|
|
197
|
+
backendUsed: result?.backend || 'unknown',
|
|
200
198
|
detail: exitOk && outputOk
|
|
201
199
|
? 'Command executed successfully'
|
|
202
200
|
: `Exit ${result?.exitCode ?? '?'}: ${String(result?.stderr || result?.stdout || '').slice(0, 120)}`,
|
|
203
201
|
});
|
|
204
202
|
} catch (err) {
|
|
205
|
-
return res.json({ passed: false, backendUsed: '
|
|
203
|
+
return res.json({ passed: false, backendUsed: 'unknown', detail: String(err?.message || err).slice(0, 120) });
|
|
206
204
|
}
|
|
207
205
|
});
|
|
208
206
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
602b98932a60cda4e154d8497aa43085
|
|
Binary file
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"69c8c61792f04cc809dfef0c910414fb9afc06
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "593865678" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|