neoagent 3.2.1-beta.9 → 3.3.1-beta.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/LICENSE +67 -80
- package/README.md +7 -1
- package/docs/licensing.md +44 -0
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_app_shell.dart +186 -40
- package/flutter_app/lib/main_chat.dart +542 -80
- package/flutter_app/lib/main_controller.dart +66 -5
- package/flutter_app/lib/main_install.dart +9 -7
- package/flutter_app/lib/main_models.dart +171 -22
- package/flutter_app/lib/main_operations.dart +0 -49
- package/flutter_app/lib/main_settings.dart +338 -0
- package/flutter_app/lib/src/backend_client.dart +19 -0
- package/flutter_app/lib/src/local_runtime_paths.dart +60 -0
- package/lib/manager.js +10 -2
- package/package.json +1 -1
- package/runtime/paths.js +70 -0
- package/server/db/database.js +2 -2
- package/server/http/routes.js +1 -0
- 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 +84940 -83737
- package/server/routes/behavior.js +80 -0
- package/server/routes/settings.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/history.js +1 -1
- package/server/services/ai/loop/agent_engine_core.js +110 -17
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/completion_judge.js +226 -33
- package/server/services/ai/loop/conversation_loop.js +92 -34
- package/server/services/ai/loop/messaging_delivery.js +47 -0
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loopPolicy.js +24 -2
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_failure_cache.js +7 -0
- package/server/services/ai/systemPrompt.js +31 -122
- package/server/services/ai/taskAnalysis.js +86 -12
- package/server/services/ai/toolEvidence.js +68 -224
- package/server/services/ai/tools.js +60 -19
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +172 -0
- package/server/services/behavior/modules/persona_prompt.js +238 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +118 -0
- package/server/services/behavior/modules/turn_taking.js +238 -0
- package/server/services/behavior/pipeline.js +341 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/memory/manager.js +14 -33
- package/server/services/messaging/access_policy.js +269 -74
- package/server/services/messaging/automation.js +158 -27
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +73 -28
- package/server/services/messaging/inbound_queue.js +74 -13
- package/server/services/messaging/inbound_store.js +33 -0
- package/server/services/messaging/manager.js +57 -5
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/whatsapp.js +11 -0
- package/server/services/social_reach/channels/social_video.js +1 -1
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +194 -29
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/runtime.js +2 -2
- package/server/utils/logger.js +19 -0
- package/server/services/ai/terminal_reply.js +0 -57
|
@@ -229,6 +229,36 @@ function firstFileMatching(dirPath, startsWith) {
|
|
|
229
229
|
return path.join(dirPath, match);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function pickDownloadedCaptionFile(dirPath, preferredLanguages = []) {
|
|
233
|
+
const captionExts = new Set(['.vtt', '.webvtt', '.srt', '.ttml', '.xml', '.json3', '.json']);
|
|
234
|
+
const captionGroups = {};
|
|
235
|
+
for (const name of fs.readdirSync(dirPath)
|
|
236
|
+
.filter((name) => name.startsWith('captions.'))
|
|
237
|
+
.sort()) {
|
|
238
|
+
const fullPath = path.join(dirPath, name);
|
|
239
|
+
const extension = path.extname(name).toLowerCase();
|
|
240
|
+
if (!captionExts.has(extension) || !fileExists(fullPath)) continue;
|
|
241
|
+
const base = path.basename(name, extension);
|
|
242
|
+
const language = base.slice('captions.'.length).toLowerCase();
|
|
243
|
+
captionGroups[language] ||= [];
|
|
244
|
+
captionGroups[language].push({
|
|
245
|
+
url: fullPath,
|
|
246
|
+
ext: extension.replace(/^\./, ''),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const track = pickCaptionTrack(captionGroups, preferredLanguages);
|
|
250
|
+
if (!track) return null;
|
|
251
|
+
return {
|
|
252
|
+
path: track.url,
|
|
253
|
+
language: track.language,
|
|
254
|
+
ext: track.ext,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cookieArg(cookieFilePath) {
|
|
259
|
+
return cookieFilePath ? ` --cookies ${shellEscape(cookieFilePath)}` : '';
|
|
260
|
+
}
|
|
261
|
+
|
|
232
262
|
function classifyExtractionError(error) {
|
|
233
263
|
const message = String(error?.message || error || '').trim();
|
|
234
264
|
const normalized = message.toLowerCase();
|
|
@@ -275,6 +305,7 @@ class SocialVideoService {
|
|
|
275
305
|
this.artifactStore = options.artifactStore || null;
|
|
276
306
|
this.runtimeManager = options.runtimeManager || null;
|
|
277
307
|
this.cliExecutor = options.cliExecutor || new CLIExecutor();
|
|
308
|
+
this.publicResourceFetcher = options.publicResourceFetcher || fetchPublicResource;
|
|
278
309
|
this.voiceTranscriber = options.voiceTranscriber || transcribeVoiceInput;
|
|
279
310
|
this.voiceSettingsResolver = options.voiceSettingsResolver || ((userId, agentId) => this.#resolveVoiceSttConfig(userId, agentId));
|
|
280
311
|
this.ytDlpBin = String(process.env.YT_DLP_BIN || 'yt-dlp').trim() || 'yt-dlp';
|
|
@@ -299,10 +330,13 @@ class SocialVideoService {
|
|
|
299
330
|
]);
|
|
300
331
|
|
|
301
332
|
const health = {
|
|
302
|
-
ready: ytDlp.available
|
|
303
|
-
dependencies: [
|
|
333
|
+
ready: ytDlp.available,
|
|
334
|
+
dependencies: [
|
|
335
|
+
{ ...ytDlp, required: true },
|
|
336
|
+
{ ...ffmpeg, required: false },
|
|
337
|
+
],
|
|
304
338
|
speechToText: {
|
|
305
|
-
note: '
|
|
339
|
+
note: 'Captions are downloaded directly with yt-dlp without social platform API keys. If captions are unavailable, the configured voice STT provider transcribes the media.',
|
|
306
340
|
},
|
|
307
341
|
checkedAt: new Date().toISOString(),
|
|
308
342
|
};
|
|
@@ -325,7 +359,9 @@ class SocialVideoService {
|
|
|
325
359
|
throwIfAborted(options.signal, 'Social video extraction aborted.');
|
|
326
360
|
const health = await this.getHealthStatus({ signal: options.signal });
|
|
327
361
|
if (!health.ready) {
|
|
328
|
-
const missing = health.dependencies
|
|
362
|
+
const missing = health.dependencies
|
|
363
|
+
.filter((item) => item.required !== false && !item.available)
|
|
364
|
+
.map((item) => item.name);
|
|
329
365
|
throw new Error(`Missing required dependency: ${missing.join(', ')}`);
|
|
330
366
|
}
|
|
331
367
|
|
|
@@ -340,7 +376,11 @@ class SocialVideoService {
|
|
|
340
376
|
normalizedUrl,
|
|
341
377
|
warnings,
|
|
342
378
|
options.signal,
|
|
343
|
-
)
|
|
379
|
+
).catch((error) => {
|
|
380
|
+
rethrowCancellation(error, options.signal);
|
|
381
|
+
warnings.push(`Public page metadata was unavailable: ${error.message}`);
|
|
382
|
+
return {};
|
|
383
|
+
});
|
|
344
384
|
throwIfAborted(options.signal, 'Social video extraction aborted.');
|
|
345
385
|
jobDir = await fsp.mkdtemp(path.join(SOCIAL_VIDEO_TMP_DIR, `${platform}-${Date.now()}-`));
|
|
346
386
|
const cookieFilePath = await this.#resolveCookieFile({
|
|
@@ -377,6 +417,7 @@ class SocialVideoService {
|
|
|
377
417
|
sourceUrl: normalizedUrl,
|
|
378
418
|
mediaInfo,
|
|
379
419
|
captionTrack,
|
|
420
|
+
preferredLanguages,
|
|
380
421
|
transcriptDecision,
|
|
381
422
|
jobDir,
|
|
382
423
|
cookieFilePath,
|
|
@@ -520,7 +561,7 @@ class SocialVideoService {
|
|
|
520
561
|
return browserMetadata;
|
|
521
562
|
}
|
|
522
563
|
|
|
523
|
-
const response = await
|
|
564
|
+
const response = await this.publicResourceFetcher(normalizedUrl, {
|
|
524
565
|
signal,
|
|
525
566
|
maxResponseBytes: MAX_PAGE_HTML_BYTES,
|
|
526
567
|
accept: 'text/html,*/*',
|
|
@@ -585,8 +626,7 @@ class SocialVideoService {
|
|
|
585
626
|
async #readMediaInfo(normalizedUrl, jobDir, cookieFilePath = null, signal = null) {
|
|
586
627
|
const infoTemplate = path.join(jobDir, 'media.%(ext)s');
|
|
587
628
|
const infoPath = path.join(jobDir, 'media.info.json');
|
|
588
|
-
const
|
|
589
|
-
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)}`;
|
|
629
|
+
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()} --skip-download --write-info-json --no-clean-infojson${cookieArg(cookieFilePath)} -o ${shellEscape(infoTemplate)} -- ${shellEscape(normalizedUrl)}`;
|
|
590
630
|
await this.#runCommand(command, { cwd: jobDir, timeout: 4 * 60 * 1000, signal });
|
|
591
631
|
if (!fileExists(infoPath)) {
|
|
592
632
|
throw new Error('yt-dlp did not produce an info JSON artifact.');
|
|
@@ -603,11 +643,15 @@ class SocialVideoService {
|
|
|
603
643
|
}
|
|
604
644
|
|
|
605
645
|
async #resolveTranscript(context) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
646
|
+
// Prefer captions over STT and never depend on social platform API keys.
|
|
647
|
+
// Caption URLs from metadata are often signed/short-lived or cookie-gated, and
|
|
648
|
+
// auto-captions may exist even when info JSON omits them, so we always attempt
|
|
649
|
+
// caption acquisition before spending STT quota (unless forceStt is set).
|
|
650
|
+
const forceStt = context.transcriptDecision?.mode === 'stt'
|
|
651
|
+
&& context.transcriptDecision?.reason === 'forced';
|
|
652
|
+
|
|
653
|
+
if (!forceStt) {
|
|
654
|
+
const captionText = await this.#resolveCaptionTranscript(context).catch((error) => {
|
|
611
655
|
rethrowCancellation(error, context.signal);
|
|
612
656
|
context.warnings.push(`Caption transcript failed: ${error.message}`);
|
|
613
657
|
return '';
|
|
@@ -618,7 +662,9 @@ class SocialVideoService {
|
|
|
618
662
|
source: 'captions',
|
|
619
663
|
};
|
|
620
664
|
}
|
|
621
|
-
context.
|
|
665
|
+
if (context.transcriptDecision?.mode === 'captions') {
|
|
666
|
+
context.warnings.push('Caption extraction did not yield usable text. Falling back to speech-to-text.');
|
|
667
|
+
}
|
|
622
668
|
}
|
|
623
669
|
|
|
624
670
|
const transcript = await this.#transcribeViaStt(context).catch((error) => {
|
|
@@ -632,40 +678,159 @@ class SocialVideoService {
|
|
|
632
678
|
};
|
|
633
679
|
}
|
|
634
680
|
|
|
635
|
-
async #
|
|
636
|
-
|
|
681
|
+
async #resolveCaptionTranscript(context) {
|
|
682
|
+
if (context.captionTrack) {
|
|
683
|
+
const direct = await this.#readTranscriptFromCaption(
|
|
684
|
+
context.captionTrack,
|
|
685
|
+
context.sourceUrl,
|
|
686
|
+
context.signal,
|
|
687
|
+
).catch((error) => {
|
|
688
|
+
rethrowCancellation(error, context.signal);
|
|
689
|
+
context.warnings.push(`Direct caption fetch failed: ${error.message}`);
|
|
690
|
+
return '';
|
|
691
|
+
});
|
|
692
|
+
if (direct) {
|
|
693
|
+
return direct;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return this.#downloadCaptionsViaYtDlp(context);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async #readTranscriptFromCaption(captionTrack, sourceUrl, signal = null) {
|
|
701
|
+
const response = await this.publicResourceFetcher(captionTrack.url, {
|
|
637
702
|
signal,
|
|
638
703
|
maxResponseBytes: MAX_VTT_BYTES,
|
|
639
704
|
accept: 'text/vtt,text/plain,application/json,application/xml,*/*',
|
|
705
|
+
headers: {
|
|
706
|
+
referer: String(sourceUrl || ''),
|
|
707
|
+
},
|
|
640
708
|
});
|
|
641
709
|
return parseCaptionText(response.body, captionTrack.ext);
|
|
642
710
|
}
|
|
643
711
|
|
|
644
|
-
async #
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
712
|
+
async #downloadCaptionsViaYtDlp(context) {
|
|
713
|
+
const preferredLanguages = Array.isArray(context.preferredLanguages)
|
|
714
|
+
? context.preferredLanguages
|
|
715
|
+
: [];
|
|
716
|
+
const languageHints = preferredLanguages.length > 0
|
|
717
|
+
? preferredLanguages.join(',')
|
|
718
|
+
: 'en.*,en';
|
|
719
|
+
// Download both manual and auto-generated captions without platform API keys.
|
|
720
|
+
// Prefer formats we can parse directly so caption extraction does not require
|
|
721
|
+
// ffmpeg just to perform a subtitle conversion.
|
|
722
|
+
const outputTemplate = path.join(context.jobDir, 'captions');
|
|
723
|
+
const command = [
|
|
724
|
+
shellEscape(this.ytDlpBin),
|
|
725
|
+
'--quiet',
|
|
726
|
+
'--no-warnings',
|
|
727
|
+
'--no-playlist',
|
|
728
|
+
this.#networkFlags(),
|
|
729
|
+
cookieArg(context.cookieFilePath),
|
|
730
|
+
'--skip-download',
|
|
731
|
+
'--write-subs',
|
|
732
|
+
'--write-auto-subs',
|
|
733
|
+
'--sub-langs', shellEscape(`${languageHints},all,-live_chat`),
|
|
734
|
+
'--sub-format', shellEscape('vtt/srt/ttml/json3/best'),
|
|
735
|
+
'-o', shellEscape(outputTemplate),
|
|
736
|
+
'--',
|
|
737
|
+
shellEscape(context.sourceUrl),
|
|
738
|
+
].filter(Boolean).join(' ');
|
|
739
|
+
|
|
648
740
|
await this.#runCommand(command, {
|
|
649
741
|
cwd: context.jobDir,
|
|
650
|
-
timeout:
|
|
742
|
+
timeout: 4 * 60 * 1000,
|
|
651
743
|
signal: context.signal,
|
|
652
744
|
});
|
|
653
745
|
|
|
654
|
-
const
|
|
655
|
-
if (!
|
|
656
|
-
throw new Error('
|
|
746
|
+
const captionFile = pickDownloadedCaptionFile(context.jobDir, preferredLanguages);
|
|
747
|
+
if (!captionFile) {
|
|
748
|
+
throw new Error('yt-dlp did not produce a usable caption file.');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const raw = await fsp.readFile(captionFile.path, 'utf8');
|
|
752
|
+
throwIfAborted(context.signal, 'Social video extraction aborted.');
|
|
753
|
+
const text = parseCaptionText(raw, captionFile.ext);
|
|
754
|
+
if (!text) {
|
|
755
|
+
throw new Error(`Caption file ${path.basename(captionFile.path)} parsed to empty text.`);
|
|
657
756
|
}
|
|
757
|
+
return text;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async #transcribeViaStt(context) {
|
|
761
|
+
const audioPath = await this.#downloadAudioForStt(context);
|
|
762
|
+
const preparedPath = await this.#prepareAudioForStt(audioPath, context);
|
|
658
763
|
|
|
659
764
|
const sttConfig = await Promise.resolve(
|
|
660
765
|
this.voiceSettingsResolver(context.userId, context.agentId),
|
|
661
766
|
);
|
|
662
767
|
throwIfAborted(context.signal, 'Social video transcription aborted.');
|
|
663
|
-
return this.voiceTranscriber(
|
|
768
|
+
return this.voiceTranscriber(preparedPath, {
|
|
664
769
|
provider: sttConfig?.provider || 'openai',
|
|
665
770
|
model: sttConfig?.model || '',
|
|
666
|
-
mimeType: detectMimeFromFile(
|
|
771
|
+
mimeType: detectMimeFromFile(preparedPath),
|
|
772
|
+
signal: context.signal,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async #downloadAudioForStt(context) {
|
|
777
|
+
const template = path.join(context.jobDir, 'audio.%(ext)s');
|
|
778
|
+
const command = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg(context.cookieFilePath)} -o ${shellEscape(template)} -f "bestaudio/best" --extract-audio --audio-format mp3 --audio-quality 0 -- ${shellEscape(context.sourceUrl)}`;
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
await this.#runCommand(command, {
|
|
782
|
+
cwd: context.jobDir,
|
|
783
|
+
timeout: 10 * 60 * 1000,
|
|
784
|
+
signal: context.signal,
|
|
785
|
+
});
|
|
786
|
+
} catch (error) {
|
|
787
|
+
rethrowCancellation(error, context.signal);
|
|
788
|
+
// Some extractors reject --extract-audio; fall back to raw bestaudio/best download.
|
|
789
|
+
context.warnings.push(`Audio extract-audio path failed (${error.message}). Retrying raw media download.`);
|
|
790
|
+
const fallbackCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg(context.cookieFilePath)} -o ${shellEscape(template)} -f "bestaudio/best" -- ${shellEscape(context.sourceUrl)}`;
|
|
791
|
+
await this.#runCommand(fallbackCommand, {
|
|
792
|
+
cwd: context.jobDir,
|
|
793
|
+
timeout: 10 * 60 * 1000,
|
|
794
|
+
signal: context.signal,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const audioPath = firstFileMatching(context.jobDir, 'audio.');
|
|
799
|
+
if (!audioPath || !fileExists(audioPath)) {
|
|
800
|
+
throw new Error('Audio download succeeded but no audio file was created.');
|
|
801
|
+
}
|
|
802
|
+
return audioPath;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async #prepareAudioForStt(audioPath, context) {
|
|
806
|
+
const ext = path.extname(String(audioPath || '')).toLowerCase();
|
|
807
|
+
const mimeType = detectMimeFromFile(audioPath);
|
|
808
|
+
// Prefer formats STT providers accept directly. Normalize anything else
|
|
809
|
+
// (including video containers from bestaudio/best fallbacks) to mono mp3.
|
|
810
|
+
const readyAsIs = (
|
|
811
|
+
mimeType === 'audio/mpeg'
|
|
812
|
+
|| mimeType === 'audio/mp4'
|
|
813
|
+
|| mimeType === 'audio/wav'
|
|
814
|
+
|| mimeType === 'audio/webm'
|
|
815
|
+
|| mimeType === 'audio/ogg'
|
|
816
|
+
|| mimeType === 'audio/opus'
|
|
817
|
+
) && !['.mp4', '.mkv', '.mov', '.avi'].includes(ext);
|
|
818
|
+
|
|
819
|
+
if (readyAsIs) {
|
|
820
|
+
return audioPath;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const normalizedPath = path.join(context.jobDir, 'audio.stt.mp3');
|
|
824
|
+
const command = `${shellEscape(this.ffmpegBin)} -hwaccel none -y -hide_banner -loglevel error -i ${shellEscape(audioPath)} -vn -ac 1 -ar 16000 -b:a 64k ${shellEscape(normalizedPath)}`;
|
|
825
|
+
await this.#runCommand(command, {
|
|
826
|
+
cwd: context.jobDir,
|
|
827
|
+
timeout: 3 * 60 * 1000,
|
|
667
828
|
signal: context.signal,
|
|
668
829
|
});
|
|
830
|
+
if (!fileExists(normalizedPath)) {
|
|
831
|
+
throw new Error('ffmpeg did not produce a normalized audio file for transcription.');
|
|
832
|
+
}
|
|
833
|
+
return normalizedPath;
|
|
669
834
|
}
|
|
670
835
|
|
|
671
836
|
async #resolveVoiceSttConfig(userId, agentId) {
|
|
@@ -738,8 +903,7 @@ class SocialVideoService {
|
|
|
738
903
|
|
|
739
904
|
async #extractFrameFromVideo(context) {
|
|
740
905
|
const template = path.join(context.jobDir, 'video.%(ext)s');
|
|
741
|
-
const
|
|
742
|
-
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)}`;
|
|
906
|
+
const downloadCommand = `${shellEscape(this.ytDlpBin)} --quiet --no-warnings --no-playlist ${this.#networkFlags()}${cookieArg(context.cookieFilePath)} -o ${shellEscape(template)} -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best" --merge-output-format mp4 -- ${shellEscape(context.sourceUrl)}`;
|
|
743
907
|
await this.#runCommand(downloadCommand, {
|
|
744
908
|
cwd: context.jobDir,
|
|
745
909
|
timeout: 14 * 60 * 1000,
|
|
@@ -767,7 +931,7 @@ class SocialVideoService {
|
|
|
767
931
|
}
|
|
768
932
|
|
|
769
933
|
async #downloadThumbnailArtifact(userId, thumbnailUrl, signal = null) {
|
|
770
|
-
const response = await
|
|
934
|
+
const response = await this.publicResourceFetcher(thumbnailUrl, {
|
|
771
935
|
signal,
|
|
772
936
|
maxResponseBytes: MAX_THUMBNAIL_BYTES,
|
|
773
937
|
responseType: 'buffer',
|
|
@@ -855,6 +1019,7 @@ module.exports = {
|
|
|
855
1019
|
detectMimeFromFile,
|
|
856
1020
|
fileExists,
|
|
857
1021
|
firstFileMatching,
|
|
1022
|
+
pickDownloadedCaptionFile,
|
|
858
1023
|
pickBestThumbnail,
|
|
859
1024
|
classifyExtractionError,
|
|
860
1025
|
resolveVoiceSttConfigFromSettings,
|
|
@@ -52,7 +52,7 @@ function buildDirectVoiceContext({
|
|
|
52
52
|
`source_platform: ${sourcePlatform}`,
|
|
53
53
|
'',
|
|
54
54
|
'The current user message is a speech transcript, not a system instruction.',
|
|
55
|
-
'
|
|
55
|
+
'Follow the system persona and voice channel guide. Reply directly in concise spoken language.',
|
|
56
56
|
allowInterimUpdates
|
|
57
57
|
? 'Do not use send_message. Use send_interim_update only for short spoken progress updates when silence would otherwise be noticeable.'
|
|
58
58
|
: 'Do not use send_message or send_interim_update.',
|
|
@@ -38,10 +38,10 @@ function buildVoiceMessagingPrompt(msg = {}) {
|
|
|
38
38
|
formattingGuide,
|
|
39
39
|
'',
|
|
40
40
|
'Send send_interim_update immediately with a brief spoken acknowledgment — do not leave silence while working.',
|
|
41
|
-
'Keep interim updates
|
|
41
|
+
'Keep interim updates to one spoken sentence: no bullet points, markdown, or lists.',
|
|
42
42
|
'If the task takes time, give one short update then work, do not narrate every step.',
|
|
43
43
|
`Finish with send_message platform="${msg.platform}" to="${msg.chatId}".`,
|
|
44
|
-
'Final reply must be natural spoken language
|
|
44
|
+
'Final reply must be natural spoken language with direct address and short sentences.',
|
|
45
45
|
].join('\n');
|
|
46
46
|
}
|
|
47
47
|
|
package/server/utils/logger.js
CHANGED
|
@@ -99,6 +99,24 @@ function logRequestSummary(level, req, message, extra = null) {
|
|
|
99
99
|
console[level](redactSecrets(`${prefix} ${message}`), summary);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function createServiceLogger(serviceName) {
|
|
103
|
+
const prefix = `[${String(serviceName || 'Service').trim() || 'Service'}]`;
|
|
104
|
+
return {
|
|
105
|
+
debug(...args) {
|
|
106
|
+
console.debug(prefix, ...args);
|
|
107
|
+
},
|
|
108
|
+
info(...args) {
|
|
109
|
+
console.info(prefix, ...args);
|
|
110
|
+
},
|
|
111
|
+
warn(...args) {
|
|
112
|
+
console.warn(prefix, ...args);
|
|
113
|
+
},
|
|
114
|
+
error(...args) {
|
|
115
|
+
console.error(prefix, ...args);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
function getLogFile() {
|
|
103
121
|
try {
|
|
104
122
|
const { DATA_DIR } = require('../../runtime/paths');
|
|
@@ -180,6 +198,7 @@ function setupConsoleInterceptor(io) {
|
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
module.exports = {
|
|
201
|
+
createServiceLogger,
|
|
183
202
|
formatLogArgs,
|
|
184
203
|
logRequestSummary,
|
|
185
204
|
setupConsoleInterceptor
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function normalizeReply(content) {
|
|
4
|
-
return String(content || '')
|
|
5
|
-
.replace(/[\u2018\u2019]/g, "'")
|
|
6
|
-
.replace(/^[\s>*_`#-]+/, '')
|
|
7
|
-
.replace(/\s+/g, ' ')
|
|
8
|
-
.trim()
|
|
9
|
-
.toLowerCase();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function hasExternalBlocker(text) {
|
|
13
|
-
return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve)|blockiert|kann nicht|konnte nicht|mir fehlt|ich brauche von dir|warte auf dein(?:e|en)?|bitte (?:gib|schick|sende|nenn|bestätige|waehle|wähle|genehmige))\b/.test(text);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function isTerminalQuestionOrBlockerReply(content) {
|
|
17
|
-
const text = normalizeReply(content);
|
|
18
|
-
if (!text) return false;
|
|
19
|
-
return /[??]/.test(text) || hasExternalBlocker(text);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Detect replies that only announce or promise work which has not happened yet.
|
|
24
|
-
* This is intentionally conservative and supplements the model completion judge;
|
|
25
|
-
* it is a deterministic last line of defence against terminating after a status
|
|
26
|
-
* phrase such as "I'm working on it" or "let me check".
|
|
27
|
-
*/
|
|
28
|
-
function isDeferredWorkReply(content) {
|
|
29
|
-
const text = normalizeReply(content);
|
|
30
|
-
if (!text || hasExternalBlocker(text)) return false;
|
|
31
|
-
|
|
32
|
-
const acknowledgement = '(?:(?:sure|okay|ok|alright|absolutely|of course|got it|understood)[,.!]?\\s+)?';
|
|
33
|
-
const taskTarget = '(?:it|this|that|your\\s+(?:request|task|issue)|the\\s+(?:request|task|issue|problem|code|logs?|repository|repo|build|tests?))';
|
|
34
|
-
const activeWork = `(?:working\\s+(?:on|through)\\s+${taskTarget}|checking(?:\\s+${taskTarget})?|looking\\s+(?:into|at)\\s+${taskTarget}|investigating\\s+${taskTarget}|reviewing\\s+${taskTarget}|researching\\s+${taskTarget}|testing\\s+${taskTarget}|debugging\\s+${taskTarget}|running\\s+${taskTarget}|processing\\s+${taskTarget}|handling\\s+${taskTarget}|starting\\s+${taskTarget}|continuing\\s+${taskTarget})`;
|
|
35
|
-
const promisedWork = '(?:check|look\\s+into|investigate|review|research|test|debug|run|work\\s+on|handle|start|continue|fix|send|create|update|delete|install|restart|deploy|publish|do\\s+that|take\\s+care\\s+of)';
|
|
36
|
-
const patterns = [
|
|
37
|
-
new RegExp(`^${acknowledgement}i(?:'m| am)\\s+(?:(?:still|currently|now|already)\\s+)?${activeWork}\\b`),
|
|
38
|
-
new RegExp(`^${acknowledgement}i(?:'ll| will)\\s+(?:now\\s+)?${promisedWork}\\b`),
|
|
39
|
-
new RegExp(`^${acknowledgement}(?:let me|allow me to)\\s+${promisedWork}\\b`),
|
|
40
|
-
new RegExp(`^${acknowledgement}(?:i(?:'m| am)\\s+going to)\\s+${promisedWork}\\b`),
|
|
41
|
-
/^(?:(?:please )?give me\s+(?:(?:a|one|another)\s+)?(?:moment|minute|bit)|hang tight|please wait|one moment|bear with me)\b/,
|
|
42
|
-
/^(?:working on it|checking now|on it)[.!…]*$/,
|
|
43
|
-
/\bi(?:'ll| will)\s+(?:get back to you|update you|report back|let you know|keep you posted)\b/,
|
|
44
|
-
/\b(?:i(?:'ll| will)\s+follow up|stay tuned)\b/,
|
|
45
|
-
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:(?:arbeite|pruefe|prüfe|schaue|untersuche|teste|debugge|starte)\s+(?:(?:gerade|aktuell|jetzt|noch)\b|(?:das|dies|die logs?|den code|deine anfrage|deinen auftrag)\b)|(?:kuemmere|kümmere)\s+mich\s+(?:gerade|aktuell|jetzt|noch)\b)/,
|
|
46
|
-
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:werde|wuerde|würde)\s+(?:jetzt\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen|starten|fixen|erledigen)\b/,
|
|
47
|
-
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?(?:lass|lasst)\s+mich\s+(?:(?:das|dies|die logs?|den code)\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen)\b/,
|
|
48
|
-
/^(?:gib mir|gebt mir)\s+(?:einen\s+)?(?:moment|augenblick)|^(?:bin dran|mache ich)[.!…]*$/,
|
|
49
|
-
/\bich\s+(?:melde mich|gebe dir bescheid|halte dich auf dem laufenden)\b/,
|
|
50
|
-
];
|
|
51
|
-
return patterns.some((pattern) => pattern.test(text));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
module.exports = {
|
|
55
|
-
isDeferredWorkReply,
|
|
56
|
-
isTerminalQuestionOrBlockerReply,
|
|
57
|
-
};
|