neoagent 3.0.1-beta.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { DATA_DIR } = require('../../../runtime/paths');
|
|
8
|
+
const { stageGuestPayload, normalizeRuntimeProfile } = require('./guest_bootstrap');
|
|
9
|
+
|
|
10
|
+
// Where build contexts are staged. One directory per profile.
|
|
11
|
+
const BUILD_ROOT = path.join(DATA_DIR, 'runtime-vms', 'guest-image');
|
|
12
|
+
// Slim official Node base — Playwright browsers + OS deps are added at build time
|
|
13
|
+
// via `playwright install --with-deps`, so the image is self-contained and the
|
|
14
|
+
// browser version always matches the pinned `playwright-chromium` dependency.
|
|
15
|
+
const BASE_IMAGE = String(process.env.NEOAGENT_GUEST_BASE_IMAGE || 'node:20-bookworm-slim').trim();
|
|
16
|
+
const IMAGE_REPO = 'neoagent-guest-agent';
|
|
17
|
+
const BUILD_TIMEOUT_MS = Number(process.env.NEOAGENT_GUEST_IMAGE_BUILD_TIMEOUT_MS || 20 * 60 * 1000);
|
|
18
|
+
|
|
19
|
+
// The build context is the staged guest payload (package.json + runtime/ + server/).
|
|
20
|
+
// Dependencies and browsers are installed once, at image build time — never per
|
|
21
|
+
// container start. Copy package.json first so the dependency layer stays cached
|
|
22
|
+
// across source-only changes.
|
|
23
|
+
function dockerfileFor(profile) {
|
|
24
|
+
if (normalizeRuntimeProfile(profile) === 'android') {
|
|
25
|
+
return [
|
|
26
|
+
`FROM ${BASE_IMAGE}`,
|
|
27
|
+
'ENV NODE_ENV=production',
|
|
28
|
+
'ENV NEOAGENT_GUEST_PROFILE=android',
|
|
29
|
+
'WORKDIR /opt/neoagent',
|
|
30
|
+
'COPY package.json ./',
|
|
31
|
+
'RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force',
|
|
32
|
+
'COPY runtime ./runtime',
|
|
33
|
+
'COPY server ./server',
|
|
34
|
+
// World-writable runtime dir so the agent works whether the container runs
|
|
35
|
+
// as root (macOS) or as the host uid:gid (Linux, for shared file ownership).
|
|
36
|
+
'RUN mkdir -p /opt/neoagent/.runtime && chmod -R 0777 /opt/neoagent/.runtime',
|
|
37
|
+
'CMD ["node", "server/guest_agent.js"]',
|
|
38
|
+
'',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
`FROM ${BASE_IMAGE}`,
|
|
43
|
+
'ENV NODE_ENV=production',
|
|
44
|
+
'ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
|
|
45
|
+
'ENV NEOAGENT_GUEST_PROFILE=browser_cli',
|
|
46
|
+
'WORKDIR /opt/neoagent',
|
|
47
|
+
'COPY package.json ./',
|
|
48
|
+
// Install deps without running package postinstall scripts, then fetch the
|
|
49
|
+
// matching Chromium and its OS dependencies explicitly. This mirrors the
|
|
50
|
+
// proven QEMU guest bootstrap and keeps the browser revision deterministic.
|
|
51
|
+
'RUN npm install --omit=dev --ignore-scripts --no-audit --no-fund \\',
|
|
52
|
+
' && npx playwright install --with-deps chromium \\',
|
|
53
|
+
' && npm cache clean --force',
|
|
54
|
+
'COPY runtime ./runtime',
|
|
55
|
+
'COPY server ./server',
|
|
56
|
+
// World-writable runtime dir + browser cache so the agent works whether the
|
|
57
|
+
// container runs as root (macOS) or as the host uid:gid (Linux, for shared
|
|
58
|
+
// file ownership). Chromium needs PLAYWRIGHT_BROWSERS_PATH readable by all.
|
|
59
|
+
'RUN mkdir -p /opt/neoagent/.runtime && chmod -R 0777 /opt/neoagent/.runtime && chmod -R a+rX /ms-playwright',
|
|
60
|
+
// Chromium is baked in at build time, so the runtime-ready marker the browser
|
|
61
|
+
// controller polls for (BROWSER_READY_MARKER in server/services/browser/controller.js)
|
|
62
|
+
// is already satisfied. Create it here so ensureBrowser() launches immediately
|
|
63
|
+
// instead of waiting out its full 10-minute timeout for a marker that only the
|
|
64
|
+
// legacy QEMU per-boot bootstrap ever writes.
|
|
65
|
+
'RUN mkdir -p /var/lib/neoagent && touch /var/lib/neoagent/browser-runtime-ready',
|
|
66
|
+
'CMD ["node", "server/guest_agent.js"]',
|
|
67
|
+
'',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Stable content hash over every staged file plus the Dockerfile, so the image
|
|
72
|
+
// tag changes whenever the guest source, its dependencies, or the base image
|
|
73
|
+
// change. A new tag forces a rebuild; an unchanged tag reuses the cached image.
|
|
74
|
+
function hashBuildContext(contextDir, dockerfile) {
|
|
75
|
+
const hash = crypto.createHash('sha256');
|
|
76
|
+
hash.update(dockerfile);
|
|
77
|
+
const walk = (dir) => {
|
|
78
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
79
|
+
const full = path.join(dir, name);
|
|
80
|
+
const stat = fs.statSync(full);
|
|
81
|
+
if (stat.isDirectory()) {
|
|
82
|
+
walk(full);
|
|
83
|
+
} else {
|
|
84
|
+
hash.update(path.relative(contextDir, full));
|
|
85
|
+
hash.update(fs.readFileSync(full));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
walk(contextDir);
|
|
90
|
+
return hash.digest('hex').slice(0, 12);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function docker(args, opts = {}) {
|
|
94
|
+
return spawnSync('docker', args, {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
timeout: opts.timeout || 30000,
|
|
98
|
+
...opts,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dockerAvailable() {
|
|
103
|
+
const result = docker(['info'], { timeout: 5000 });
|
|
104
|
+
return !result.error && result.status === 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Builds and caches the per-profile guest-agent Docker image. The image bakes in
|
|
108
|
+
// the guest agent, its dependencies, and (for browser_cli) the Chromium browser,
|
|
109
|
+
// so containers start the agent directly with no runtime installation step.
|
|
110
|
+
class GuestImageBuilder {
|
|
111
|
+
#buildPromise = null;
|
|
112
|
+
#cachedTag = null;
|
|
113
|
+
#cachedBuiltAt = 0;
|
|
114
|
+
|
|
115
|
+
constructor(options = {}) {
|
|
116
|
+
this.profile = normalizeRuntimeProfile(options.runtimeProfile || 'browser_cli');
|
|
117
|
+
this.contextDir = path.join(BUILD_ROOT, this.profile);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Stage the build context on disk and return its content-addressed image tag.
|
|
121
|
+
prepareContext() {
|
|
122
|
+
const dockerfile = dockerfileFor(this.profile);
|
|
123
|
+
stageGuestPayload(this.contextDir, this.profile);
|
|
124
|
+
fs.writeFileSync(path.join(this.contextDir, 'Dockerfile'), dockerfile);
|
|
125
|
+
fs.writeFileSync(path.join(this.contextDir, '.dockerignore'), 'node_modules\n');
|
|
126
|
+
const tag = `${IMAGE_REPO}:${this.profile}-${hashBuildContext(this.contextDir, dockerfile)}`;
|
|
127
|
+
this.#cachedTag = tag;
|
|
128
|
+
return tag;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
imageExists(tag) {
|
|
132
|
+
const result = docker(['image', 'inspect', tag], { timeout: 10000 });
|
|
133
|
+
return !result.error && result.status === 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Returns { dockerAvailable, imageBuilt, image } without triggering a build.
|
|
137
|
+
// Cheap enough to call from readiness polling (only stages the context once).
|
|
138
|
+
getState() {
|
|
139
|
+
if (!dockerAvailable()) {
|
|
140
|
+
return { dockerAvailable: false, imageBuilt: false, image: this.#cachedTag };
|
|
141
|
+
}
|
|
142
|
+
let tag = this.#cachedTag;
|
|
143
|
+
try {
|
|
144
|
+
if (!tag) tag = this.prepareContext();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.warn(`[GuestImage:${this.profile}] Failed to stage build context: ${err.message}`);
|
|
147
|
+
return { dockerAvailable: true, imageBuilt: false, image: null };
|
|
148
|
+
}
|
|
149
|
+
return { dockerAvailable: true, imageBuilt: this.imageExists(tag), image: tag };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ensure the image exists, building it if necessary. Concurrent callers share
|
|
153
|
+
// the same in-flight build. Returns the resolved image tag.
|
|
154
|
+
async ensure() {
|
|
155
|
+
const tag = this.prepareContext();
|
|
156
|
+
if (this.imageExists(tag)) {
|
|
157
|
+
this.#cachedBuiltAt = Date.now();
|
|
158
|
+
return tag;
|
|
159
|
+
}
|
|
160
|
+
if (this.#buildPromise) return this.#buildPromise;
|
|
161
|
+
this.#buildPromise = this.#build(tag).finally(() => { this.#buildPromise = null; });
|
|
162
|
+
return this.#buildPromise;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async #build(tag) {
|
|
166
|
+
console.log(`[GuestImage:${this.profile}] Building guest image ${tag} (one-time; downloads browser + deps)…`);
|
|
167
|
+
const started = Date.now();
|
|
168
|
+
const result = docker(['build', '-t', tag, this.contextDir], {
|
|
169
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
170
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
171
|
+
});
|
|
172
|
+
if (result.error) {
|
|
173
|
+
throw new Error(`Guest image build failed to start: ${result.error.message}`);
|
|
174
|
+
}
|
|
175
|
+
if (result.status !== 0) {
|
|
176
|
+
throw new Error(`Guest image build for ${tag} exited with status ${result.status}`);
|
|
177
|
+
}
|
|
178
|
+
this.#cachedBuiltAt = Date.now();
|
|
179
|
+
console.log(`[GuestImage:${this.profile}] Built ${tag} in ${Math.round((Date.now() - started) / 1000)}s`);
|
|
180
|
+
return tag;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
GuestImageBuilder,
|
|
186
|
+
dockerAvailable,
|
|
187
|
+
BASE_IMAGE,
|
|
188
|
+
};
|
|
@@ -11,14 +11,9 @@ function getRuntimeValidation(runtimeManager) {
|
|
|
11
11
|
|
|
12
12
|
if (policy.profile === 'prod' || nodeEnvIsProd) {
|
|
13
13
|
if (!browserVmReadiness) {
|
|
14
|
-
issues.push('prod profile requires a working
|
|
15
|
-
} else if (!browserVmReadiness.
|
|
16
|
-
|
|
17
|
-
issues.push(`prod profile requires QEMU (${browserVmReadiness.qemuBinary}) to be installed for browser/CLI.`);
|
|
18
|
-
}
|
|
19
|
-
if (!browserVmReadiness.baseImageExists && !browserVmReadiness.downloadConfigured) {
|
|
20
|
-
issues.push('prod profile requires a VM base image or a downloadable base image URL for browser/CLI.');
|
|
21
|
-
}
|
|
14
|
+
issues.push('prod profile requires a working container runtime for browser/CLI.');
|
|
15
|
+
} else if (!browserVmReadiness.dockerAvailable) {
|
|
16
|
+
issues.push('prod profile requires Docker to be installed and running for the browser/CLI runtime.');
|
|
22
17
|
}
|
|
23
18
|
}
|
|
24
19
|
|
|
@@ -22,6 +22,41 @@ fs.mkdirSync(SOCIAL_VIDEO_TMP_DIR, { recursive: true });
|
|
|
22
22
|
|
|
23
23
|
const HEALTH_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
24
24
|
|
|
25
|
+
// A realistic desktop browser UA. Social platforms frequently return 403/bot
|
|
26
|
+
// challenge pages to the default Node/yt-dlp user agents, so we present a
|
|
27
|
+
// browser-like identity for every outbound request.
|
|
28
|
+
const BROWSER_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
|
|
29
|
+
+ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
30
|
+
|
|
31
|
+
// Network hardening flags applied to every yt-dlp invocation. Transient
|
|
32
|
+
// failures (rate limits, dropped fragments, slow extractors) are the most
|
|
33
|
+
// common cause of unreliable extraction, so we retry aggressively and cap
|
|
34
|
+
// socket waits instead of failing on the first hiccup.
|
|
35
|
+
const YT_DLP_NETWORK_FLAGS = [
|
|
36
|
+
'--retries', '5',
|
|
37
|
+
'--fragment-retries', '5',
|
|
38
|
+
'--extractor-retries', '3',
|
|
39
|
+
'--socket-timeout', '30',
|
|
40
|
+
'--user-agent', BROWSER_USER_AGENT,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Platforms that gate most video content behind authentication or aggressive
|
|
44
|
+
// bot detection. For these we always try to attach the user's browser cookies.
|
|
45
|
+
const COOKIE_ASSISTED_PLATFORMS = new Set(['instagram', 'youtube', 'tiktok', 'x']);
|
|
46
|
+
|
|
47
|
+
async function fetchWithBrowserHeaders(url, options = {}) {
|
|
48
|
+
return fetch(url, {
|
|
49
|
+
redirect: 'follow',
|
|
50
|
+
...options,
|
|
51
|
+
headers: {
|
|
52
|
+
'user-agent': BROWSER_USER_AGENT,
|
|
53
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
54
|
+
accept: '*/*',
|
|
55
|
+
...(options.headers || {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
function shellEscape(value) {
|
|
26
61
|
const text = String(value ?? '');
|
|
27
62
|
if (!text.length) return process.platform === 'win32' ? '""' : "''";
|
|
@@ -178,12 +213,21 @@ function classifyExtractionError(error) {
|
|
|
178
213
|
if (/unsupported social video url|unsupported url/.test(normalized)) {
|
|
179
214
|
return { code: 'unsupported_url', message };
|
|
180
215
|
}
|
|
181
|
-
if (/private|login required|sign in to confirm/.test(normalized)) {
|
|
216
|
+
if (/private|login required|sign in to confirm|requested content is not available|account is private/.test(normalized)) {
|
|
182
217
|
return { code: 'private_or_auth_required', message };
|
|
183
218
|
}
|
|
184
|
-
if (
|
|
219
|
+
if (/\b429\b|rate.?limit|too many requests/.test(normalized)) {
|
|
220
|
+
return { code: 'rate_limited', message };
|
|
221
|
+
}
|
|
222
|
+
if (/\b403\b|forbidden|blocked|not a bot/.test(normalized)) {
|
|
185
223
|
return { code: 'blocked_or_unavailable', message };
|
|
186
224
|
}
|
|
225
|
+
if (/timed out|timeout|etimedout|socket hang up|network is unreachable/.test(normalized)) {
|
|
226
|
+
return { code: 'network_error', message };
|
|
227
|
+
}
|
|
228
|
+
if (/this video is unavailable|video unavailable|removed|404|not found/.test(normalized)) {
|
|
229
|
+
return { code: 'content_unavailable', message };
|
|
230
|
+
}
|
|
187
231
|
return { code: 'social_video_extract_failed', message };
|
|
188
232
|
}
|
|
189
233
|
|
|
@@ -358,6 +402,10 @@ class SocialVideoService {
|
|
|
358
402
|
}
|
|
359
403
|
}
|
|
360
404
|
|
|
405
|
+
#networkFlags() {
|
|
406
|
+
return YT_DLP_NETWORK_FLAGS.map(shellEscape).join(' ');
|
|
407
|
+
}
|
|
408
|
+
|
|
361
409
|
async #runCommand(command, options = {}) {
|
|
362
410
|
const result = await this.cliExecutor.execute(command, {
|
|
363
411
|
cwd: options.cwd || process.cwd(),
|
|
@@ -423,7 +471,7 @@ class SocialVideoService {
|
|
|
423
471
|
return browserMetadata;
|
|
424
472
|
}
|
|
425
473
|
|
|
426
|
-
const response = await
|
|
474
|
+
const response = await fetchWithBrowserHeaders(normalizedUrl);
|
|
427
475
|
const html = await response.text();
|
|
428
476
|
const metadata = extractPublicMetadataFromHtml(html, response.url || normalizedUrl);
|
|
429
477
|
return {
|
|
@@ -473,7 +521,7 @@ class SocialVideoService {
|
|
|
473
521
|
const infoTemplate = path.join(jobDir, 'media.%(ext)s');
|
|
474
522
|
const infoPath = path.join(jobDir, 'media.info.json');
|
|
475
523
|
const cookieArg = cookieFilePath ? ` --cookies ${shellEscape(cookieFilePath)}` : '';
|
|
476
|
-
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist --skip-download --write-info-json --no-clean-infojson${cookieArg} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
|
|
524
|
+
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()} --skip-download --write-info-json --no-clean-infojson${cookieArg} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
|
|
477
525
|
await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000 });
|
|
478
526
|
if (!fileExists(infoPath)) {
|
|
479
527
|
throw new Error('yt-dlp did not produce an info JSON artifact.');
|
|
@@ -514,7 +562,7 @@ class SocialVideoService {
|
|
|
514
562
|
}
|
|
515
563
|
|
|
516
564
|
async #readTranscriptFromCaption(captionTrack) {
|
|
517
|
-
const response = await
|
|
565
|
+
const response = await fetchWithBrowserHeaders(captionTrack.url);
|
|
518
566
|
if (!response.ok) {
|
|
519
567
|
throw new Error(`Caption request failed (${response.status}).`);
|
|
520
568
|
}
|
|
@@ -525,7 +573,7 @@ class SocialVideoService {
|
|
|
525
573
|
async #transcribeViaStt(context) {
|
|
526
574
|
const template = path.join(context.jobDir, 'audio.%(ext)s');
|
|
527
575
|
const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
|
|
528
|
-
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f bestaudio -- ${shellEscape(context.sourceUrl)}`;
|
|
576
|
+
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg} -o ${shellEscape(template)} -f bestaudio/best -- ${shellEscape(context.sourceUrl)}`;
|
|
529
577
|
await this.#runCommand(command, { cwd: context.jobDir, timeout: 10 * 60 * 1000 });
|
|
530
578
|
|
|
531
579
|
const audioPath = firstFileMatching(context.jobDir, 'audio.');
|
|
@@ -561,7 +609,7 @@ class SocialVideoService {
|
|
|
561
609
|
}
|
|
562
610
|
|
|
563
611
|
async #resolveCookieFile(context) {
|
|
564
|
-
if (context.platform
|
|
612
|
+
if (!COOKIE_ASSISTED_PLATFORMS.has(context.platform)) {
|
|
565
613
|
return null;
|
|
566
614
|
}
|
|
567
615
|
if (!this.runtimeManager || typeof this.runtimeManager.getBrowserProviderForUser !== 'function') {
|
|
@@ -581,7 +629,9 @@ class SocialVideoService {
|
|
|
581
629
|
});
|
|
582
630
|
const cookies = Array.isArray(payload?.cookies) ? payload.cookies : [];
|
|
583
631
|
if (cookies.length === 0) {
|
|
584
|
-
|
|
632
|
+
// Not fatal: many videos are public. yt-dlp will simply attempt the
|
|
633
|
+
// extraction without an authenticated session.
|
|
634
|
+
context.warnings.push(`Browser cookie export returned no cookies for ${context.platform}.`);
|
|
585
635
|
return null;
|
|
586
636
|
}
|
|
587
637
|
|
|
@@ -610,7 +660,7 @@ class SocialVideoService {
|
|
|
610
660
|
async #extractFrameFromVideo(context) {
|
|
611
661
|
const template = path.join(context.jobDir, 'video.%(ext)s');
|
|
612
662
|
const cookieArg = context.cookieFilePath ? ` --cookies ${shellEscape(context.cookieFilePath)}` : '';
|
|
613
|
-
const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist${cookieArg} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
|
|
663
|
+
const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
|
|
614
664
|
await this.#runCommand(downloadCommand, { cwd: context.jobDir, timeout: 14 * 60 * 1000 });
|
|
615
665
|
|
|
616
666
|
const videoPath = firstFileMatching(context.jobDir, 'video.');
|
|
@@ -630,7 +680,7 @@ class SocialVideoService {
|
|
|
630
680
|
}
|
|
631
681
|
|
|
632
682
|
async #downloadThumbnailArtifact(userId, thumbnailUrl) {
|
|
633
|
-
const response = await
|
|
683
|
+
const response = await fetchWithBrowserHeaders(thumbnailUrl);
|
|
634
684
|
if (!response.ok) {
|
|
635
685
|
throw new Error(`Thumbnail request failed (${response.status}).`);
|
|
636
686
|
}
|