neoagent 3.0.1-beta.21 → 3.0.1-beta.23
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/landing/index.html +3 -3
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- 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 +5 -5
- package/server/services/ai/loop/conversation_loop.js +15 -9
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/runtime/guest_image.js +6 -0
- package/server/services/social_video/service.js +60 -10
|
Binary file
|
|
@@ -33,10 +33,10 @@ addEventListener("message", eventListener);
|
|
|
33
33
|
if (!window._flutter) {
|
|
34
34
|
window._flutter = {};
|
|
35
35
|
}
|
|
36
|
-
_flutter.buildConfig = {"engineRevision":"
|
|
36
|
+
_flutter.buildConfig = {"engineRevision":"a4ce257c68517c1410f4b48ac9852ab5642a3f8d","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2640036147" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -40951,7 +40951,7 @@ $S:4}
|
|
|
40951
40951
|
A.bgv.prototype={
|
|
40952
40952
|
$1(a){var s=A.fb().b
|
|
40953
40953
|
s=s==null?null:s.canvasKitBaseUrl
|
|
40954
|
-
return(s==null?"https://www.gstatic.com/flutter-canvaskit/
|
|
40954
|
+
return(s==null?"https://www.gstatic.com/flutter-canvaskit/a4ce257c68517c1410f4b48ac9852ab5642a3f8d/":s)+a},
|
|
40955
40955
|
$S:45}
|
|
40956
40956
|
A.a_n.prototype={
|
|
40957
40957
|
gH(a){var s=this.a
|
|
@@ -138284,7 +138284,7 @@ r===$&&A.b()
|
|
|
138284
138284
|
p.push(A.iR(q,A.jp(!1,new A.Z(B.vj,A.cX(new A.cA(B.jP,new A.a8Q(r,q),q),q,q),q),!1,B.I,!0),q,q,0,0,0,q))}r=!1
|
|
138285
138285
|
if(!s.ay)if(!s.ch){r=s.e
|
|
138286
138286
|
r===$&&A.b()
|
|
138287
|
-
r=B.b.v("
|
|
138287
|
+
r=B.b.v("mqqp9zyc-84cecca").length!==0&&r.b}if(r){r=s.d
|
|
138288
138288
|
r===$&&A.b()
|
|
138289
138289
|
r=r.aD&&!r.au?84:0
|
|
138290
138290
|
s=s.e
|
|
@@ -144158,7 +144158,7 @@ $S:0}
|
|
|
144158
144158
|
A.a00.prototype={}
|
|
144159
144159
|
A.TK.prototype={
|
|
144160
144160
|
no(a){var s=this
|
|
144161
|
-
if(B.b.v("
|
|
144161
|
+
if(B.b.v("mqqp9zyc-84cecca").length===0||s.a!=null)return
|
|
144162
144162
|
s.Bg()
|
|
144163
144163
|
s.a=A.m0(B.SS,new A.bfB(s))},
|
|
144164
144164
|
Bg(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -144176,7 +144176,7 @@ if(!t.f.b(k)){s=1
|
|
|
144176
144176
|
break}i=J.a1(k,"buildId")
|
|
144177
144177
|
h=i==null?null:B.b.v(J.p(i))
|
|
144178
144178
|
j=h==null?"":h
|
|
144179
|
-
if(J.bd(j)===0||J.f(j,"
|
|
144179
|
+
if(J.bd(j)===0||J.f(j,"mqqp9zyc-84cecca")){s=1
|
|
144180
144180
|
break}n.b=!0
|
|
144181
144181
|
n.E()
|
|
144182
144182
|
p=2
|
|
@@ -144193,7 +144193,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
144193
144193
|
return A.k($async$Bg,r)},
|
|
144194
144194
|
vW(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
144195
144195
|
var $async$vW=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
144196
|
-
s=p}for(;;)switch(s){case 0:if(B.b.v("
|
|
144196
|
+
s=p}for(;;)switch(s){case 0:if(B.b.v("mqqp9zyc-84cecca").length===0||n.c){s=1
|
|
144197
144197
|
break}n.c=!0
|
|
144198
144198
|
n.E()
|
|
144199
144199
|
p=4
|
|
@@ -145,6 +145,7 @@ const {
|
|
|
145
145
|
} = require('../messagingFallback');
|
|
146
146
|
const {
|
|
147
147
|
classifyToolExecution,
|
|
148
|
+
gatheredNewEvidence,
|
|
148
149
|
isSubstantiveProgressToolName,
|
|
149
150
|
summarizeProgressToolExecutions,
|
|
150
151
|
summarizeToolExecutions,
|
|
@@ -292,11 +293,11 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
292
293
|
|
|
293
294
|
function buildNoProgressWrapupPrompt({ readOnlyCount = 0, alreadyRead = '', platform = null } = {}) {
|
|
294
295
|
return [
|
|
295
|
-
`
|
|
296
|
-
alreadyRead ? `Already
|
|
297
|
-
'
|
|
298
|
-
'
|
|
299
|
-
'
|
|
296
|
+
`This is the final turn for this run (no further tool calls; ${Math.max(0, Number(readOnlyCount) || 0)} read-only turns without a state change).`,
|
|
297
|
+
alreadyRead ? `Already gathered: ${alreadyRead}.` : '',
|
|
298
|
+
'Write the answer now from everything you have already gathered in this conversation. Deliver the useful result you DO have — calendar, weather, emails, search findings, whatever was collected — formatted as the actual answer to the original request.',
|
|
299
|
+
'If one part could not be retrieved, still deliver everything else and note the missing part in at most one short clause. Never withhold a useful answer because a single detail is missing.',
|
|
300
|
+
'Only report a pure blocker if you genuinely gathered nothing usable at all. Do not describe the result as unfinished, unconfirmed, "blocked", or "still working" when you have something useful — this IS the final answer.',
|
|
300
301
|
buildMaxIterationWrapupPrompt(platform),
|
|
301
302
|
].filter(Boolean).join('\n\n');
|
|
302
303
|
}
|
|
@@ -1671,6 +1672,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1671
1672
|
options,
|
|
1672
1673
|
});
|
|
1673
1674
|
stepIndex = batch.endingStepIndex;
|
|
1675
|
+
let batchGatheredNewEvidence = false;
|
|
1674
1676
|
for (const item of batch.results) {
|
|
1675
1677
|
const execution = classifyToolExecution(
|
|
1676
1678
|
item.toolName,
|
|
@@ -1681,7 +1683,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1681
1683
|
execution.input = item.toolArgs;
|
|
1682
1684
|
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
1683
1685
|
toolExecutions.push(execution);
|
|
1684
|
-
engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
1686
|
+
const observation = engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
1687
|
+
if (gatheredNewEvidence(execution, observation)) batchGatheredNewEvidence = true;
|
|
1685
1688
|
if (item.error) failedStepCount += 1;
|
|
1686
1689
|
const modelPayload = compactPayloadForModel(item.toolName, item.result);
|
|
1687
1690
|
const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
|
|
@@ -1720,7 +1723,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1720
1723
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1721
1724
|
const iterMeta = engine.getRunMeta(runId);
|
|
1722
1725
|
if (iterMeta) {
|
|
1723
|
-
iterMeta.consecutiveReadOnlyIterations =
|
|
1726
|
+
iterMeta.consecutiveReadOnlyIterations = batchGatheredNewEvidence
|
|
1727
|
+
? 0
|
|
1728
|
+
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1724
1729
|
}
|
|
1725
1730
|
}
|
|
1726
1731
|
continue;
|
|
@@ -1941,10 +1946,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1941
1946
|
|
|
1942
1947
|
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
1943
1948
|
execution.input = toolArgs;
|
|
1944
|
-
|
|
1949
|
+
const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1950
|
+
if ((execution.stateChanged && isProgressToolCall(toolName, toolArgs))
|
|
1951
|
+
|| gatheredNewEvidence(execution, repetitionObservation)) {
|
|
1945
1952
|
iterationConcreteProgress = true;
|
|
1946
1953
|
}
|
|
1947
|
-
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1948
1954
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
1949
1955
|
toolExecutions.push(execution);
|
|
1950
1956
|
if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
|
|
@@ -153,6 +153,20 @@ function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
|
153
153
|
}).join('\n');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// A read-only turn that pulls in NEW information is real progress, even though
|
|
157
|
+
// it changes no state — research, browsing, reading, and searching are how an
|
|
158
|
+
// agent makes headway on "find out X" tasks. Treating those as "no progress"
|
|
159
|
+
// (and force-wrapping the run) is what guillotines legitimate research. Genuine
|
|
160
|
+
// churn — failed calls, pure `think`, or re-running an identical call that
|
|
161
|
+
// returns an unchanged result — is excluded, so the read-only guard still fires
|
|
162
|
+
// on real spinning.
|
|
163
|
+
function gatheredNewEvidence(execution, repetitionObservation = null) {
|
|
164
|
+
if (!execution || execution.ok !== true) return false;
|
|
165
|
+
if (!execution.evidenceRelevant) return false;
|
|
166
|
+
if (repetitionObservation && repetitionObservation.unchangedCount >= 2) return false;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
function isSubstantiveProgressToolName(toolName = '') {
|
|
157
171
|
const name = String(toolName || '').trim();
|
|
158
172
|
if (!name) return false;
|
|
@@ -235,6 +249,7 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
|
|
|
235
249
|
module.exports = {
|
|
236
250
|
classifyToolExecution,
|
|
237
251
|
deriveEvidenceSource,
|
|
252
|
+
gatheredNewEvidence,
|
|
238
253
|
isSubstantiveProgressEvidence,
|
|
239
254
|
isSubstantiveProgressToolName,
|
|
240
255
|
summarizeProgressToolExecutions,
|
|
@@ -323,6 +323,46 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
|
323
323
|
});
|
|
324
324
|
break;
|
|
325
325
|
|
|
326
|
+
case 'google_workspace_calendar_list_events': {
|
|
327
|
+
// The raw result leads with a verbose `nextTimedEvent` plus full event
|
|
328
|
+
// objects (description/htmlLink/attendees), so generic JSON truncation
|
|
329
|
+
// cuts the actual `events` array off mid-object — the model never sees
|
|
330
|
+
// what is scheduled and re-queries adjacent windows forever. Emit a
|
|
331
|
+
// compact, COMPLETE digest instead: every event the model needs to reason
|
|
332
|
+
// about "what's coming up", none of the noise it doesn't.
|
|
333
|
+
const events = Array.isArray(toolResult?.events)
|
|
334
|
+
? toolResult.events
|
|
335
|
+
: [
|
|
336
|
+
...(Array.isArray(toolResult?.timedEvents) ? toolResult.timedEvents : []),
|
|
337
|
+
...(Array.isArray(toolResult?.allDayEvents) ? toolResult.allDayEvents : []),
|
|
338
|
+
];
|
|
339
|
+
const timed = events.filter((event) => !event?.allDay);
|
|
340
|
+
const allDay = events.filter((event) => event?.allDay);
|
|
341
|
+
envelope = trimObject({
|
|
342
|
+
tool: toolName,
|
|
343
|
+
count: typeof toolResult?.count === 'number' ? toolResult.count : events.length,
|
|
344
|
+
timedCount: typeof toolResult?.timedCount === 'number' ? toolResult.timedCount : timed.length,
|
|
345
|
+
allDayCount: typeof toolResult?.allDayCount === 'number' ? toolResult.allDayCount : allDay.length,
|
|
346
|
+
hasOnlyAllDayEvents: toolResult?.hasOnlyAllDayEvents === true ? true : undefined,
|
|
347
|
+
// Timed appointments are what "starting soon" cares about — keep them all,
|
|
348
|
+
// with the fields a reminder actually uses.
|
|
349
|
+
timed: timed.map((event) => trimObject({
|
|
350
|
+
summary: clampText(event?.summary || '(no title)', 140),
|
|
351
|
+
start: event?.start || null,
|
|
352
|
+
end: event?.end || null,
|
|
353
|
+
location: event?.location ? clampText(event.location, 80) : undefined,
|
|
354
|
+
status: event?.status && event.status !== 'confirmed' ? event.status : undefined,
|
|
355
|
+
})),
|
|
356
|
+
// All-day entries are usually birthdays / markers — names + dates suffice.
|
|
357
|
+
allDay: allDay.map((event) => trimObject({
|
|
358
|
+
summary: clampText(event?.summary || '(no title)', 140),
|
|
359
|
+
start: event?.start || null,
|
|
360
|
+
end: event?.end || null,
|
|
361
|
+
})),
|
|
362
|
+
});
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
326
366
|
default:
|
|
327
367
|
envelope = trimObject({
|
|
328
368
|
tool: toolName,
|
|
@@ -57,6 +57,12 @@ function dockerfileFor(profile) {
|
|
|
57
57
|
// container runs as root (macOS) or as the host uid:gid (Linux, for shared
|
|
58
58
|
// file ownership). Chromium needs PLAYWRIGHT_BROWSERS_PATH readable by all.
|
|
59
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',
|
|
60
66
|
'CMD ["node", "server/guest_agent.js"]',
|
|
61
67
|
'',
|
|
62
68
|
].join('\n');
|
|
@@ -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
|
}
|