@myrialabs/clopen 0.1.9 → 0.2.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/README.md +23 -1
- package/backend/index.ts +25 -1
- package/backend/lib/auth/auth-service.ts +484 -0
- package/backend/lib/auth/index.ts +4 -0
- package/backend/lib/auth/permissions.ts +63 -0
- package/backend/lib/auth/rate-limiter.ts +145 -0
- package/backend/lib/auth/tokens.ts +53 -0
- package/backend/lib/chat/stream-manager.ts +4 -1
- package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
- package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
- package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
- package/backend/lib/database/migrations/index.ts +21 -0
- package/backend/lib/database/queries/auth-queries.ts +201 -0
- package/backend/lib/database/queries/index.ts +2 -1
- package/backend/lib/database/queries/session-queries.ts +13 -0
- package/backend/lib/database/queries/snapshot-queries.ts +1 -1
- package/backend/lib/engine/adapters/opencode/server.ts +9 -1
- package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
- package/backend/lib/mcp/config.ts +13 -18
- package/backend/lib/mcp/index.ts +9 -0
- package/backend/lib/mcp/remote-server.ts +132 -0
- package/backend/lib/mcp/servers/helper.ts +49 -3
- package/backend/lib/mcp/servers/index.ts +3 -2
- package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
- package/backend/lib/preview/browser/browser-pool.ts +73 -176
- package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
- package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
- package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
- package/backend/lib/snapshot/helpers.ts +22 -49
- package/backend/lib/snapshot/snapshot-service.ts +148 -83
- package/backend/lib/utils/ws.ts +65 -1
- package/backend/ws/auth/index.ts +17 -0
- package/backend/ws/auth/invites.ts +84 -0
- package/backend/ws/auth/login.ts +269 -0
- package/backend/ws/auth/status.ts +41 -0
- package/backend/ws/auth/users.ts +32 -0
- package/backend/ws/chat/stream.ts +13 -0
- package/backend/ws/engine/claude/accounts.ts +3 -1
- package/backend/ws/engine/utils.ts +38 -6
- package/backend/ws/index.ts +4 -4
- package/backend/ws/preview/browser/interact.ts +27 -5
- package/backend/ws/snapshot/restore.ts +111 -12
- package/backend/ws/snapshot/timeline.ts +56 -29
- package/bin/clopen.ts +56 -1
- package/bun.lock +113 -51
- package/frontend/App.svelte +47 -29
- package/frontend/lib/components/auth/InvitePage.svelte +215 -0
- package/frontend/lib/components/auth/LoginPage.svelte +129 -0
- package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
- package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
- package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
- package/frontend/lib/components/git/CommitForm.svelte +6 -4
- package/frontend/lib/components/history/HistoryModal.svelte +1 -1
- package/frontend/lib/components/history/HistoryView.svelte +1 -1
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
- package/frontend/lib/components/settings/SettingsView.svelte +21 -7
- package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
- package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
- package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
- package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
- package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
- package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
- package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
- package/frontend/lib/stores/features/auth.svelte.ts +296 -0
- package/frontend/lib/stores/features/settings.svelte.ts +53 -9
- package/frontend/lib/stores/features/user.svelte.ts +26 -68
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
- package/frontend/lib/stores/ui/update.svelte.ts +2 -14
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
- package/package.json +8 -6
- package/shared/types/stores/settings.ts +16 -2
- package/shared/utils/logger.ts +1 -0
- package/shared/utils/ws-client.ts +30 -13
- package/shared/utils/ws-server.ts +42 -4
- package/backend/lib/mcp/stdio-server.ts +0 -103
- package/backend/ws/mcp/index.ts +0 -61
|
@@ -623,41 +623,115 @@ export class BrowserTabManager extends EventEmitter {
|
|
|
623
623
|
page.setDefaultNavigationTimeout(30000);
|
|
624
624
|
|
|
625
625
|
// Configure page for stability
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
// Optimize font loading to prevent screenshot timeouts
|
|
632
|
-
await page.evaluateOnNewDocument(() => {
|
|
633
|
-
// Disable font loading wait
|
|
634
|
-
Object.defineProperty(document, 'fonts', {
|
|
635
|
-
value: {
|
|
636
|
-
ready: Promise.resolve(),
|
|
637
|
-
load: () => Promise.resolve([]),
|
|
638
|
-
check: () => true,
|
|
639
|
-
addEventListener: () => {},
|
|
640
|
-
removeEventListener: () => {}
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
});
|
|
626
|
+
// Note: Do NOT set HTTP headers manually here (like Accept-Language).
|
|
627
|
+
// Setting extra headers alters the HTTP/2 pseudo-header order and capitalization
|
|
628
|
+
// which immediately gets flagged by Cloudflare's TLS/Fingerprint matching algorithms.
|
|
644
629
|
|
|
645
|
-
//
|
|
646
|
-
|
|
630
|
+
// Audio capture is injected post-navigation in BrowserVideoCapture.startStreaming()
|
|
631
|
+
// to avoid Cloudflare fingerprint detection of AudioContext constructor patching.
|
|
647
632
|
|
|
648
633
|
// Simplified cursor tracking for visual feedback only
|
|
649
634
|
await this.injectCursorTracking(page);
|
|
635
|
+
|
|
636
|
+
// Suppress Cloudflare Turnstile error callbacks to prevent sites from showing
|
|
637
|
+
// "CAPTCHA verification failed" popups in headless Chrome (error 600010).
|
|
638
|
+
//
|
|
639
|
+
// Strategy: Let the real Turnstile script load and define window.turnstile normally
|
|
640
|
+
// (needed for CF Managed Challenge auto-pass via StealthPlugin fingerprinting).
|
|
641
|
+
// Intercept window.turnstile assignment via getter/setter and patch render()/execute()
|
|
642
|
+
// to replace error-callback/expired-callback with no-ops before they are registered.
|
|
643
|
+
// Also strip data-error-callback attributes from DOM elements via MutationObserver
|
|
644
|
+
// to cover implicit render (data-sitekey) usage.
|
|
645
|
+
//
|
|
646
|
+
// This does NOT block challenges.cloudflare.com — CF Managed Challenge needs that
|
|
647
|
+
// URL to run its JS verification. Only the error reporting path is suppressed.
|
|
648
|
+
await page.evaluateOnNewDocument(function () {
|
|
649
|
+
(function () {
|
|
650
|
+
|
|
651
|
+
let _turnstile: any;
|
|
652
|
+
|
|
653
|
+
function patchOptions(options: Record<string, unknown>) {
|
|
654
|
+
return Object.assign({}, options, {
|
|
655
|
+
'error-callback': function () {},
|
|
656
|
+
'expired-callback': function () {}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
function patchApi(api: any) {
|
|
662
|
+
if (!api || typeof api !== 'object') return api;
|
|
663
|
+
['render', 'execute'].forEach(function (method: string) {
|
|
664
|
+
if (typeof api[method] === 'function') {
|
|
665
|
+
const orig = api[method].bind(api);
|
|
666
|
+
api[method] = function (container: unknown, opts: Record<string, unknown>) {
|
|
667
|
+
return orig(container, patchOptions(opts || {}));
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
return api;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
Object.defineProperty(window, 'turnstile', {
|
|
676
|
+
configurable: true,
|
|
677
|
+
enumerable: true,
|
|
678
|
+
|
|
679
|
+
get() { return _turnstile; },
|
|
680
|
+
|
|
681
|
+
set(val: any) { _turnstile = patchApi(val); }
|
|
682
|
+
});
|
|
683
|
+
} catch {
|
|
684
|
+
// Property already defined or can't be intercepted
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Strip data-error-callback / data-expired-callback from Turnstile elements
|
|
688
|
+
// before implicit render reads them, so no error handler is registered.
|
|
689
|
+
function stripErrorAttrs(el: Element) {
|
|
690
|
+
el.removeAttribute('data-error-callback');
|
|
691
|
+
el.removeAttribute('data-expired-callback');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const mo = new MutationObserver(function (mutations) {
|
|
695
|
+
mutations.forEach(function (m) {
|
|
696
|
+
m.addedNodes.forEach(function (node) {
|
|
697
|
+
if (!(node instanceof Element)) return;
|
|
698
|
+
if (node.hasAttribute('data-error-callback') || node.hasAttribute('data-expired-callback')) {
|
|
699
|
+
stripErrorAttrs(node);
|
|
700
|
+
}
|
|
701
|
+
node.querySelectorAll('[data-error-callback],[data-expired-callback]').forEach(stripErrorAttrs);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
if (document.documentElement) {
|
|
706
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
707
|
+
}
|
|
708
|
+
})();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Auto-dismiss native browser dialogs to prevent page hang in headless mode.
|
|
712
|
+
// alert() → accept (one-way informational, safe to dismiss)
|
|
713
|
+
// confirm()/prompt() → dismiss/cancel (avoid unintended side effects)
|
|
714
|
+
// CAPTCHA-related alerts are silently dismissed.
|
|
715
|
+
page.on('dialog', async (dialog) => {
|
|
716
|
+
const type = dialog.type();
|
|
717
|
+
if (type === 'alert') {
|
|
718
|
+
await dialog.accept().catch(() => {});
|
|
719
|
+
} else {
|
|
720
|
+
await dialog.dismiss().catch(() => {});
|
|
721
|
+
}
|
|
722
|
+
});
|
|
650
723
|
}
|
|
651
724
|
|
|
652
725
|
/**
|
|
653
726
|
* Inject cursor tracking script
|
|
654
727
|
*/
|
|
655
728
|
private async injectCursorTracking(page: Page) {
|
|
656
|
-
|
|
729
|
+
// Temporarily disabled mapping logic as CloudFlare frequently flags evaluateOnNewDocument injected tracking events
|
|
730
|
+
// await page.evaluateOnNewDocument(cursorTrackingScript);
|
|
657
731
|
}
|
|
658
732
|
|
|
659
733
|
/**
|
|
660
|
-
* Navigate with retry
|
|
734
|
+
* Navigate with retry, including Cloudflare auto-pass detection and CAPTCHA popup dismissal.
|
|
661
735
|
*/
|
|
662
736
|
private async navigateWithRetry(page: Page, url: string): Promise<string> {
|
|
663
737
|
let retries = 3;
|
|
@@ -669,7 +743,9 @@ export class BrowserTabManager extends EventEmitter {
|
|
|
669
743
|
waitUntil: 'domcontentloaded',
|
|
670
744
|
timeout: 30000
|
|
671
745
|
});
|
|
672
|
-
actualUrl =
|
|
746
|
+
actualUrl = await this.waitForCloudflareIfPresent(page);
|
|
747
|
+
// Dismiss any CAPTCHA failure popups from embedded Turnstile widgets
|
|
748
|
+
await this.dismissCaptchaPopupsIfPresent(page);
|
|
673
749
|
break;
|
|
674
750
|
} catch (error) {
|
|
675
751
|
retries--;
|
|
@@ -684,6 +760,168 @@ export class BrowserTabManager extends EventEmitter {
|
|
|
684
760
|
return actualUrl;
|
|
685
761
|
}
|
|
686
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Detect Cloudflare challenge page and wait for auto-pass redirect.
|
|
765
|
+
* Loops up to MAX_CF_RETRIES times to handle infinite verify loops where
|
|
766
|
+
* Cloudflare keeps redirecting back to a new challenge after each pass.
|
|
767
|
+
*/
|
|
768
|
+
private async waitForCloudflareIfPresent(page: Page): Promise<string> {
|
|
769
|
+
const MAX_CF_RETRIES = 5;
|
|
770
|
+
|
|
771
|
+
for (let attempt = 0; attempt < MAX_CF_RETRIES; attempt++) {
|
|
772
|
+
let isChallenge = false;
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
isChallenge = await page.evaluate(() => {
|
|
776
|
+
const title = document.title;
|
|
777
|
+
const bodyText = (document.body?.innerText || '').slice(0, 500).toLowerCase();
|
|
778
|
+
return (
|
|
779
|
+
// Old automated CF challenge
|
|
780
|
+
title === 'Just a moment...' ||
|
|
781
|
+
// Newer interactive CF challenge ("Performing security verification")
|
|
782
|
+
title.toLowerCase().includes('security verification') ||
|
|
783
|
+
bodyText.includes('verify you are human') ||
|
|
784
|
+
bodyText.includes('performing security verification') ||
|
|
785
|
+
// CF challenge DOM elements (reliable, present on challenge pages only)
|
|
786
|
+
document.getElementById('challenge-running') !== null ||
|
|
787
|
+
document.getElementById('cf-challenge-running') !== null ||
|
|
788
|
+
document.getElementById('challenge-form') !== null ||
|
|
789
|
+
document.querySelector('#challenge-stage') !== null
|
|
790
|
+
);
|
|
791
|
+
});
|
|
792
|
+
} catch {
|
|
793
|
+
// Page not evaluable (navigating, closed) — not a CF challenge
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!isChallenge) {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
debug.log('preview', `🛡️ Cloudflare challenge detected (attempt ${attempt + 1}/${MAX_CF_RETRIES}), waiting for auto-pass...`);
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
await page.waitForNavigation({
|
|
805
|
+
waitUntil: 'domcontentloaded',
|
|
806
|
+
timeout: 20000
|
|
807
|
+
});
|
|
808
|
+
debug.log('preview', `✅ Cloudflare navigation → ${page.url()}`);
|
|
809
|
+
} catch {
|
|
810
|
+
debug.warn('preview', `⚠️ Cloudflare auto-pass timed out on attempt ${attempt + 1}, proceeding`);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return page.url();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Inject a persistent non-blocking watcher into the page that auto-dismisses
|
|
820
|
+
* CAPTCHA failure popups whenever they appear (Cloudflare Turnstile error 600010,
|
|
821
|
+
* reCAPTCHA failures, etc.).
|
|
822
|
+
*
|
|
823
|
+
* Uses MutationObserver + setInterval so it catches popups regardless of when they
|
|
824
|
+
* appear after page load. Returns immediately — the watcher runs inside the page.
|
|
825
|
+
*/
|
|
826
|
+
private async dismissCaptchaPopupsIfPresent(page: Page): Promise<void> {
|
|
827
|
+
try {
|
|
828
|
+
await page.evaluate(() => {
|
|
829
|
+
const CAPTCHA_WORDS = ['captcha', 'turnstile', 'human verification', 'robot', 'bot detected'];
|
|
830
|
+
const FAIL_WORDS = ['failed', 'error', 'invalid', 'verification failed', 'try again', 'unable to verify'];
|
|
831
|
+
const DISMISS_LABELS = ['ok', 'close', 'dismiss', 'cancel', 'retry', 'try again', 'continue', 'got it'];
|
|
832
|
+
|
|
833
|
+
const isCaptchaText = (text: string): boolean => {
|
|
834
|
+
const t = text.toLowerCase();
|
|
835
|
+
return (
|
|
836
|
+
CAPTCHA_WORDS.some(w => t.includes(w)) &&
|
|
837
|
+
FAIL_WORDS.some(w => t.includes(w))
|
|
838
|
+
);
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const tryDismiss = (): boolean => {
|
|
842
|
+
// Strategy 1: click a dismiss button whose ancestor contains CAPTCHA failure text
|
|
843
|
+
const buttons = Array.from(document.querySelectorAll<HTMLElement>(
|
|
844
|
+
'button, input[type="button"], input[type="submit"], a[role="button"]'
|
|
845
|
+
));
|
|
846
|
+
for (const btn of buttons) {
|
|
847
|
+
const label = (
|
|
848
|
+
btn instanceof HTMLInputElement ? btn.value : btn.innerText || btn.textContent || ''
|
|
849
|
+
).trim().toLowerCase();
|
|
850
|
+
if (!DISMISS_LABELS.includes(label)) continue;
|
|
851
|
+
|
|
852
|
+
let el: Element | null = btn.parentElement;
|
|
853
|
+
while (el && el !== document.body) {
|
|
854
|
+
if (isCaptchaText((el as HTMLElement).innerText || '')) {
|
|
855
|
+
(btn as HTMLElement).click();
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
el = el.parentElement;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Strategy 2: hide any visible modal/overlay containing CAPTCHA failure text
|
|
863
|
+
const overlaySelectors = [
|
|
864
|
+
'[class*="modal"]', '[class*="popup"]', '[class*="dialog"]',
|
|
865
|
+
'[class*="overlay"]', '[class*="alert"]', '[class*="notification"]',
|
|
866
|
+
'[role="dialog"]', '[role="alertdialog"]', '[role="alert"]'
|
|
867
|
+
];
|
|
868
|
+
const overlays = document.querySelectorAll<HTMLElement>(overlaySelectors.join(','));
|
|
869
|
+
for (const overlay of overlays) {
|
|
870
|
+
if (!isCaptchaText(overlay.innerText || '')) continue;
|
|
871
|
+
const style = overlay.style;
|
|
872
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
873
|
+
|
|
874
|
+
// Try clicking a close button first
|
|
875
|
+
const closeBtn = overlay.querySelector<HTMLElement>(
|
|
876
|
+
'button, [class*="close"], [aria-label*="lose"], [aria-label*="ismiss"]'
|
|
877
|
+
);
|
|
878
|
+
if (closeBtn) {
|
|
879
|
+
closeBtn.click();
|
|
880
|
+
} else {
|
|
881
|
+
style.display = 'none';
|
|
882
|
+
}
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return false;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// Run immediately in case popup is already present
|
|
890
|
+
if (tryDismiss()) return;
|
|
891
|
+
|
|
892
|
+
// Set up persistent watcher — fires on any DOM mutation
|
|
893
|
+
const observer = new MutationObserver(() => {
|
|
894
|
+
if (tryDismiss()) {
|
|
895
|
+
observer.disconnect();
|
|
896
|
+
clearInterval(ticker);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Also poll via interval as safety net (MutationObserver may miss text changes)
|
|
901
|
+
const ticker = setInterval(() => {
|
|
902
|
+
if (tryDismiss()) {
|
|
903
|
+
clearInterval(ticker);
|
|
904
|
+
observer.disconnect();
|
|
905
|
+
}
|
|
906
|
+
}, 400);
|
|
907
|
+
|
|
908
|
+
if (document.body) {
|
|
909
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Self-cleanup after 30 seconds to avoid memory leaks
|
|
913
|
+
setTimeout(() => {
|
|
914
|
+
observer.disconnect();
|
|
915
|
+
clearInterval(ticker);
|
|
916
|
+
}, 30000);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
debug.log('preview', '🔔 CAPTCHA auto-dismiss watcher injected into page');
|
|
920
|
+
} catch {
|
|
921
|
+
// Page closed or navigated away — ignore
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
687
925
|
/**
|
|
688
926
|
* Setup browser event handlers
|
|
689
927
|
*/
|
|
@@ -31,6 +31,7 @@ import type { Page } from 'puppeteer';
|
|
|
31
31
|
import type { BrowserTab, StreamingConfig } from './types';
|
|
32
32
|
import { DEFAULT_STREAMING_CONFIG } from './types';
|
|
33
33
|
import { videoEncoderScript } from './scripts/video-stream';
|
|
34
|
+
import { audioCaptureScript } from './scripts/audio-stream';
|
|
34
35
|
import { debug } from '$shared/utils/logger';
|
|
35
36
|
|
|
36
37
|
interface VideoStreamSession {
|
|
@@ -157,7 +158,8 @@ export class BrowserVideoCapture extends EventEmitter {
|
|
|
157
158
|
// Inject persistent video encoder script (survives navigation)
|
|
158
159
|
// Only inject once per page instance
|
|
159
160
|
if (!videoSession.scriptInjected) {
|
|
160
|
-
|
|
161
|
+
// Temporarily disable evaluateOnNewDocument for evasion test
|
|
162
|
+
// await page.evaluateOnNewDocument(videoEncoderScript, videoConfig);
|
|
161
163
|
videoSession.scriptInjected = true;
|
|
162
164
|
debug.log('webcodecs', `Persistent video encoder script injected for ${sessionId}`);
|
|
163
165
|
}
|
|
@@ -166,6 +168,12 @@ export class BrowserVideoCapture extends EventEmitter {
|
|
|
166
168
|
// (evaluateOnNewDocument only runs on NEXT navigation)
|
|
167
169
|
await page.evaluate(videoEncoderScript, videoConfig);
|
|
168
170
|
|
|
171
|
+
// Inject audio capture script post-navigation to avoid CF detection.
|
|
172
|
+
// Using page.evaluate() instead of evaluateOnNewDocument() ensures
|
|
173
|
+
// AudioContext patching happens AFTER Cloudflare challenges pass,
|
|
174
|
+
// preventing fingerprint detection of constructor interception.
|
|
175
|
+
await page.evaluate(audioCaptureScript, config.audio);
|
|
176
|
+
|
|
169
177
|
// Verify peer was created
|
|
170
178
|
const peerExists = await page.evaluate(() => {
|
|
171
179
|
return typeof (window as any).__webCodecsPeer?.startStreaming === 'function';
|
|
@@ -570,6 +578,9 @@ export class BrowserVideoCapture extends EventEmitter {
|
|
|
570
578
|
|
|
571
579
|
await page.evaluate(videoEncoderScript, videoConfig);
|
|
572
580
|
|
|
581
|
+
// Re-inject audio capture script for new page context (post-navigation)
|
|
582
|
+
await page.evaluate(audioCaptureScript, config.audio);
|
|
583
|
+
|
|
573
584
|
// Verify peer was re-created
|
|
574
585
|
const peerExists = await page.evaluate(() => {
|
|
575
586
|
return typeof (window as any).__webCodecsPeer?.startStreaming === 'function';
|
|
@@ -590,6 +601,25 @@ export class BrowserVideoCapture extends EventEmitter {
|
|
|
590
601
|
return false;
|
|
591
602
|
}
|
|
592
603
|
|
|
604
|
+
// Re-initialize and start audio capture after navigation
|
|
605
|
+
try {
|
|
606
|
+
const audioReady = await page.evaluate(async () => {
|
|
607
|
+
const encoder = (window as any).__audioEncoder;
|
|
608
|
+
if (!encoder) return false;
|
|
609
|
+
const initiated = await encoder.init();
|
|
610
|
+
if (initiated) return encoder.start();
|
|
611
|
+
return false;
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (audioReady) {
|
|
615
|
+
debug.log('webcodecs', 'Audio re-initialized after navigation');
|
|
616
|
+
} else {
|
|
617
|
+
debug.warn('webcodecs', 'Audio not available after navigation, continuing with video only');
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
debug.warn('webcodecs', 'Audio re-init failed after navigation, continuing with video only');
|
|
621
|
+
}
|
|
622
|
+
|
|
593
623
|
// Restart CDP screencast
|
|
594
624
|
const cdp = (session as any).__webCodecsCdp;
|
|
595
625
|
if (cdp) {
|
|
@@ -631,6 +661,11 @@ export class BrowserVideoCapture extends EventEmitter {
|
|
|
631
661
|
|
|
632
662
|
if (session?.page && !session.page.isClosed()) {
|
|
633
663
|
try {
|
|
664
|
+
// Stop audio encoder
|
|
665
|
+
await session.page.evaluate(() => {
|
|
666
|
+
(window as any).__audioEncoder?.stop();
|
|
667
|
+
}).catch(() => {});
|
|
668
|
+
|
|
634
669
|
// Stop peer
|
|
635
670
|
await session.page.evaluate(() => {
|
|
636
671
|
(window as any).__webCodecsPeer?.stopStreaming();
|
|
@@ -7,6 +7,9 @@ import type { DatabaseMessage } from '$shared/types/database/schema';
|
|
|
7
7
|
* Snapshot domain helper functions
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/** Sentinel ID for the "initial state" node (before any chat messages) */
|
|
11
|
+
export const INITIAL_NODE_ID = '__initial__';
|
|
12
|
+
|
|
10
13
|
export interface CheckpointNode {
|
|
11
14
|
id: string;
|
|
12
15
|
messageId: string;
|
|
@@ -18,6 +21,7 @@ export interface CheckpointNode {
|
|
|
18
21
|
isOrphaned: boolean; // descendant of current active checkpoint
|
|
19
22
|
isCurrent: boolean; // this is the current active checkpoint
|
|
20
23
|
hasSnapshot: boolean;
|
|
24
|
+
isInitial?: boolean; // true for the "initial state" node
|
|
21
25
|
senderName?: string | null;
|
|
22
26
|
// File change statistics (git-like)
|
|
23
27
|
filesChanged?: number;
|
|
@@ -335,62 +339,31 @@ export function isDescendant(
|
|
|
335
339
|
}
|
|
336
340
|
|
|
337
341
|
/**
|
|
338
|
-
* Get file change stats for a checkpoint
|
|
339
|
-
*
|
|
342
|
+
* Get file change stats for a checkpoint.
|
|
343
|
+
* The snapshot associated with the checkpoint message itself contains the stats
|
|
344
|
+
* (file changes the assistant made in response to this user message).
|
|
340
345
|
*/
|
|
341
346
|
export function getCheckpointFileStats(
|
|
342
|
-
checkpointMsg: DatabaseMessage
|
|
343
|
-
allMessages: DatabaseMessage[],
|
|
344
|
-
nextCheckpointTimestamp?: string
|
|
347
|
+
checkpointMsg: DatabaseMessage
|
|
345
348
|
): { filesChanged: number; insertions: number; deletions: number } {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const checkpointTimestamp = checkpointMsg.timestamp;
|
|
351
|
-
|
|
352
|
-
const laterMessages = allMessages
|
|
353
|
-
.filter(m => {
|
|
354
|
-
if (m.timestamp <= checkpointTimestamp) return false;
|
|
355
|
-
if (nextCheckpointTimestamp && m.timestamp >= nextCheckpointTimestamp) return false;
|
|
356
|
-
return true;
|
|
357
|
-
})
|
|
358
|
-
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
349
|
+
const snapshot = snapshotQueries.getByMessageId(checkpointMsg.id);
|
|
350
|
+
if (!snapshot) {
|
|
351
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
352
|
+
}
|
|
359
353
|
|
|
360
|
-
|
|
361
|
-
const
|
|
354
|
+
let filesChanged = snapshot.files_changed || 0;
|
|
355
|
+
const insertions = snapshot.insertions || 0;
|
|
356
|
+
const deletions = snapshot.deletions || 0;
|
|
362
357
|
|
|
363
|
-
|
|
358
|
+
// Try to get a more accurate file count from session_changes
|
|
359
|
+
if (snapshot.session_changes) {
|
|
364
360
|
try {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!userSnapshot) continue;
|
|
370
|
-
|
|
371
|
-
const fc = userSnapshot.files_changed || 0;
|
|
372
|
-
const ins = userSnapshot.insertions || 0;
|
|
373
|
-
const del = userSnapshot.deletions || 0;
|
|
374
|
-
|
|
375
|
-
if (fc > 0 || ins > 0 || del > 0) {
|
|
376
|
-
statsInRange.push({ files: fc, ins, del });
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (userSnapshot.delta_changes) {
|
|
380
|
-
try {
|
|
381
|
-
const delta = JSON.parse(userSnapshot.delta_changes);
|
|
382
|
-
if (delta.added) Object.keys(delta.added).forEach(f => allChangedFiles.add(f));
|
|
383
|
-
if (delta.modified) Object.keys(delta.modified).forEach(f => allChangedFiles.add(f));
|
|
384
|
-
if (delta.deleted && Array.isArray(delta.deleted)) delta.deleted.forEach((f: string) => allChangedFiles.add(f));
|
|
385
|
-
} catch { /* skip */ }
|
|
361
|
+
const changes = JSON.parse(snapshot.session_changes as string);
|
|
362
|
+
const changeCount = Object.keys(changes).length;
|
|
363
|
+
if (changeCount > 0) {
|
|
364
|
+
filesChanged = changeCount;
|
|
386
365
|
}
|
|
387
|
-
} catch { /*
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (statsInRange.length > 0) {
|
|
391
|
-
filesChanged = allChangedFiles.size > 0 ? allChangedFiles.size : Math.max(...statsInRange.map(s => s.files));
|
|
392
|
-
insertions = statsInRange.reduce((sum, s) => sum + s.ins, 0);
|
|
393
|
-
deletions = statsInRange.reduce((sum, s) => sum + s.del, 0);
|
|
366
|
+
} catch { /* use files_changed from DB */ }
|
|
394
367
|
}
|
|
395
368
|
|
|
396
369
|
return { filesChanged, insertions, deletions };
|