Build the current workspace snapshot on native CI runners

This branch snapshots the in-progress desktop workspace so GitHub-hosted Windows and Linux runners can package the same source tree that already passes local verification on macOS.

Constraint: Local Apple Silicon Docker/Wine packaging crashes before Windows artifacts are produced
Rejected: Publish from the incomplete base repo state | remote typecheck failed because current source additions were missing
Confidence: medium
Scope-risk: broad
Directive: Treat this branch as a build snapshot only; do not merge it without reviewing the unrelated workspace changes it contains
Tested: local typecheck, lint, workspace typecheck, unit tests, integration tests, e2e smoke, macOS signed release upload
Not-tested: Native Windows/Linux artifacts before this CI run
This commit is contained in:
ZacharyZhang-NY committed 2026-04-04 03:28:14 -04:00
1 parent 0e497cd892
commit b772e1c758
271 files changed
+35047 -22879

No files matched your search

+335
View File
@@ -0,0 +1,335 @@
import { BrowserWindow } from "electron";
import { existsSync, writeFileSync } from "node:fs";
import type { NativeImage } from "electron";
import log from "electron-log/main.js";
import type { BrowserAction, BrowserActionResult } from "./browser-types";
import { assertAllowedUrl, attachWindowSecurity, getComposeUrl, getFeedUrl, getSearchUrl } from "./browser-url-helpers";
export { getPlatformRules, assertAllowedUrl, attachWindowSecurity } from "./browser-url-helpers";
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function waitForLoad(win: BrowserWindow, timeoutMs = 15000): Promise<void> {
return new Promise((resolve) => {
if (win.webContents.isLoading()) {
const timer = setTimeout(resolve, timeoutMs);
win.webContents.once("did-finish-load", () => {
clearTimeout(timer);
resolve();
});
} else {
resolve();
}
});
}
export async function runAction(
action: BrowserAction,
screenshotDir: string,
session: Electron.Session,
cancelledActionIds: Set<string>,
activeActionWindows: Map<string, BrowserWindow>,
): Promise<BrowserActionResult> {
const payload = action.payload;
const screenshotPath = `${screenshotDir}/${action.id}.png`;
const platform = action.platform;
log.info(`[browser] Executing ${action.actionType} for ${platform}/${action.socialAccountId}`);
const win = new BrowserWindow({
width: 1280,
height: 860,
title: `AgentCompany — ${action.actionType}`,
show: true,
webPreferences: {
session,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
},
});
activeActionWindows.set(action.id, win);
attachWindowSecurity(win, platform);
win.setMenuBarVisibility(false);
try {
let summary = "Action completed";
const assertNotCancelled = () => {
if (cancelledActionIds.has(action.id)) {
throw new Error("BROWSER_ACTION_CANCELLED");
}
};
switch (action.actionType) {
case "navigate": {
assertNotCancelled();
const url = String(payload.url || "https://google.com");
assertAllowedUrl(platform, url, "navigate");
await win.loadURL(url);
await waitForLoad(win);
assertNotCancelled();
const title = await win.webContents.executeJavaScript("document.title");
summary = `Navigated to ${title}`;
break;
}
case "post": {
assertNotCancelled();
const content = String(payload.content || "");
const postUrl = getComposeUrl(platform);
await win.loadURL(postUrl);
await waitForLoad(win);
await delay(2000);
assertNotCancelled();
if (platform === "twitter") {
// Use clipboard-based approach since Twitter's React editor ignores execCommand
await win.webContents.executeJavaScript(`
(async () => {
const editor = document.querySelector('[data-testid="tweetTextarea_0"], [role="textbox"][contenteditable="true"]');
if (!editor) return;
editor.focus();
const dt = new DataTransfer();
dt.setData('text/plain', ${JSON.stringify(content)});
const pasteEvent = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
editor.dispatchEvent(pasteEvent);
await new Promise(r => setTimeout(r, 500));
if (!editor.textContent || editor.textContent.trim().length === 0) {
editor.textContent = ${JSON.stringify(content)};
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${JSON.stringify(content)} }));
}
})()
`);
await delay(1500);
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]');
if (btn && !btn.disabled) btn.click();
})()
`);
await delay(3000);
} else if (platform === "linkedin") {
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('button.share-box-feed-entry__trigger, button.artdeco-button--muted');
if (btn) btn.click();
})()
`);
await delay(2000);
await win.webContents.executeJavaScript(`
(async () => {
const editor = document.querySelector('[role="textbox"][contenteditable="true"], .ql-editor[contenteditable="true"]');
if (editor) {
editor.focus();
const dt = new DataTransfer();
dt.setData('text/plain', ${JSON.stringify(content)});
const pasteEvent = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
editor.dispatchEvent(pasteEvent);
await new Promise(r => setTimeout(r, 500));
if (!editor.textContent || editor.textContent.trim().length === 0) {
editor.textContent = ${JSON.stringify(content)};
editor.dispatchEvent(new InputEvent('input', { bubbles: true }));
}
}
})()
`);
await delay(1500);
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('button.share-actions__primary-action, button.artdeco-button--primary[type="submit"]');
if (btn && !btn.disabled) btn.click();
})()
`);
await delay(3000);
}
summary = `Posted to ${platform}: "${content.slice(0, 50)}..."`;
break;
}
case "reply": {
assertNotCancelled();
const targetUrl = String(payload.targetUrl || "");
assertAllowedUrl(platform, targetUrl, "reply");
const replyContent = String(payload.content || "");
await win.loadURL(targetUrl);
await waitForLoad(win);
await delay(2000);
assertNotCancelled();
await win.webContents.executeJavaScript(`
(async () => {
const editor = document.querySelector('[data-testid="tweetTextarea_0"], [role="textbox"][contenteditable="true"]');
if (editor) {
editor.focus();
const dt = new DataTransfer();
dt.setData('text/plain', ${JSON.stringify(replyContent)});
const pasteEvent = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
editor.dispatchEvent(pasteEvent);
await new Promise(r => setTimeout(r, 500));
if (!editor.textContent || editor.textContent.trim().length === 0) {
editor.textContent = ${JSON.stringify(replyContent)};
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${JSON.stringify(replyContent)} }));
}
}
})()
`);
await delay(1500);
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"], button[type="submit"]');
if (btn && !btn.disabled) btn.click();
})()
`);
await delay(3000);
summary = `Replied to ${targetUrl.slice(0, 50)}`;
break;
}
case "like": {
assertNotCancelled();
const targetUrl = String(payload.targetUrl || "");
assertAllowedUrl(platform, targetUrl, "like");
await win.loadURL(targetUrl);
await waitForLoad(win);
await delay(2000);
assertNotCancelled();
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('[data-testid="like"], button[aria-label*="Like"]');
if (btn) btn.click();
})()
`);
await delay(2000);
summary = `Liked post at ${targetUrl.slice(0, 50)}`;
break;
}
case "follow": {
assertNotCancelled();
const targetUrl = String(payload.targetUrl || payload.profileUrl || "");
assertAllowedUrl(platform, targetUrl, "follow");
await win.loadURL(targetUrl);
await waitForLoad(win);
await delay(2000);
assertNotCancelled();
await win.webContents.executeJavaScript(`
(() => {
const btn = document.querySelector('[data-testid*="follow"], button[aria-label*="Follow"]');
if (btn && !btn.textContent.includes('Following')) btn.click();
})()
`);
await delay(2000);
summary = `Followed user at ${targetUrl.slice(0, 50)}`;
break;
}
case "browse_feed": {
assertNotCancelled();
const feedUrl = String(payload.url || getFeedUrl(platform));
assertAllowedUrl(platform, feedUrl, "browse_feed");
const maxPosts = Number(payload.maxPosts || 10);
await win.loadURL(feedUrl);
await waitForLoad(win);
await delay(3000);
assertNotCancelled();
for (let i = 0; i < 3; i++) {
await win.webContents.executeJavaScript("window.scrollBy(0, window.innerHeight)");
await delay(1500);
}
const posts: string[] = await win.webContents.executeJavaScript(`
(() => {
const articles = document.querySelectorAll('article, [data-testid="tweet"], .feed-shared-update-v2');
return Array.from(articles).slice(0, ${maxPosts}).map(el => el.innerText.slice(0, 300));
})()
`);
summary = `Browsed feed — found ${posts.length} posts`;
break;
}
case "search": {
assertNotCancelled();
const query = String(payload.query || "");
const searchUrl = getSearchUrl(platform, query);
assertAllowedUrl(platform, searchUrl, "search");
await win.loadURL(searchUrl);
await waitForLoad(win);
await delay(3000);
assertNotCancelled();
const results: string[] = await win.webContents.executeJavaScript(`
(() => {
const items = document.querySelectorAll('article, [data-testid="tweet"], .search-result');
return Array.from(items).slice(0, 10).map(el => el.innerText.slice(0, 200));
})()
`);
summary = `Search "${query.slice(0, 30)}" — ${results.length} results`;
break;
}
case "screenshot": {
assertNotCancelled();
const url = String(payload.url || "about:blank");
if (url !== "about:blank") {
assertAllowedUrl(platform, url, "screenshot");
await win.loadURL(url);
await waitForLoad(win);
await delay(2000);
}
assertNotCancelled();
const title = await win.webContents.executeJavaScript("document.title");
summary = `Screenshot captured: ${title}`;
break;
}
default:
throw new Error(`Unsupported browser action type: ${action.actionType}`);
}
try {
const image: NativeImage = await win.webContents.capturePage();
if (!image.isEmpty()) {
writeFileSync(screenshotPath, image.toPNG());
}
} catch (err) {
log.warn(`[browser] Screenshot failed: ${err}`);
}
await delay(1000);
if (!win.isDestroyed()) win.close();
return {
success: true,
cancelled: false,
summary,
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
error: null,
};
} catch (error) {
const wasCancelled = cancelledActionIds.has(action.id) || (error instanceof Error && error.message === "BROWSER_ACTION_CANCELLED");
try {
const image: NativeImage = await win.webContents.capturePage();
if (!image.isEmpty()) writeFileSync(screenshotPath, image.toPNG());
} catch { /* ignore */ }
if (!win.isDestroyed()) win.close();
const msg = wasCancelled ? "Action cancelled." : error instanceof Error ? error.message : String(error);
if (!wasCancelled) {
log.error(`[browser] Action ${action.actionType} failed: ${msg}`);
}
return {
success: false,
cancelled: wasCancelled,
summary: wasCancelled ? "Action cancelled" : "Action failed",
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
error: msg,
};
} finally {
activeActionWindows.delete(action.id);
cancelledActionIds.delete(action.id);
}
}
+95
View File
@@ -0,0 +1,95 @@
export interface BrowserAction {
id: string;
socialAccountId: string;
platform: string;
actionType: string;
payload: Record<string, unknown>;
}
export interface BrowserActionResult {
success: boolean;
cancelled: boolean;
summary: string;
screenshotPath: string | null;
error: string | null;
}
export interface QueuedAction {
action: BrowserAction;
resolve: (result: BrowserActionResult) => void;
reject: (error: Error) => void;
}
export interface PlatformRule {
loginUrl: string;
authCheckUrl: string;
allowedHosts: string[];
cookieOrigins: string[];
unauthenticatedUrlMarkers: string[];
}
export const PLATFORM_RULES: Record<string, PlatformRule> = {
twitter: {
loginUrl: "https://x.com/i/flow/login",
authCheckUrl: "https://x.com/home",
allowedHosts: ["x.com", "twitter.com"],
cookieOrigins: ["https://x.com", "https://twitter.com"],
unauthenticatedUrlMarkers: ["/i/flow/login", "/login"],
},
linkedin: {
loginUrl: "https://www.linkedin.com/login",
authCheckUrl: "https://www.linkedin.com/feed/",
allowedHosts: ["linkedin.com"],
cookieOrigins: ["https://www.linkedin.com"],
unauthenticatedUrlMarkers: ["/login", "/checkpoint"],
},
instagram: {
loginUrl: "https://www.instagram.com/accounts/login/",
authCheckUrl: "https://www.instagram.com/",
allowedHosts: ["instagram.com"],
cookieOrigins: ["https://www.instagram.com"],
unauthenticatedUrlMarkers: ["/accounts/login"],
},
facebook: {
loginUrl: "https://www.facebook.com/login",
authCheckUrl: "https://www.facebook.com/",
allowedHosts: ["facebook.com"],
cookieOrigins: ["https://www.facebook.com"],
unauthenticatedUrlMarkers: ["/login"],
},
reddit: {
loginUrl: "https://www.reddit.com/login",
authCheckUrl: "https://www.reddit.com/",
allowedHosts: ["reddit.com"],
cookieOrigins: ["https://www.reddit.com"],
unauthenticatedUrlMarkers: ["/login"],
},
youtube: {
loginUrl: "https://accounts.google.com/signin",
authCheckUrl: "https://www.youtube.com/",
allowedHosts: ["youtube.com", "google.com"],
cookieOrigins: ["https://www.youtube.com", "https://accounts.google.com"],
unauthenticatedUrlMarkers: ["/signin", "/ServiceLogin"],
},
tiktok: {
loginUrl: "https://www.tiktok.com/login",
authCheckUrl: "https://www.tiktok.com/foryou",
allowedHosts: ["tiktok.com"],
cookieOrigins: ["https://www.tiktok.com"],
unauthenticatedUrlMarkers: ["/login"],
},
bluesky: {
loginUrl: "https://bsky.app",
authCheckUrl: "https://bsky.app/",
allowedHosts: ["bsky.app"],
cookieOrigins: ["https://bsky.app"],
unauthenticatedUrlMarkers: ["/login"],
},
other: {
loginUrl: "https://google.com",
authCheckUrl: "https://google.com",
allowedHosts: ["google.com"],
cookieOrigins: ["https://google.com"],
unauthenticatedUrlMarkers: ["/login"],
},
};
+100
View File
@@ -0,0 +1,100 @@
import { BrowserWindow } from "electron";
import log from "electron-log/main.js";
import { PLATFORM_RULES } from "./browser-types";
export function getPlatformRules(platform: string) {
return PLATFORM_RULES[platform] ?? PLATFORM_RULES.other;
}
export function assertAllowedUrl(platform: string, rawUrl: string, actionType: string) {
const allowedHosts = getPlatformRules(platform).allowedHosts;
let url: URL;
try {
url = new URL(rawUrl);
} catch {
throw new Error(`Invalid URL for ${actionType}: ${rawUrl}`);
}
if (url.protocol !== "https:") {
throw new Error(`Navigation blocked for ${actionType}. Only https URLs are allowed.`);
}
const host = url.hostname.toLowerCase();
const isAllowed = allowedHosts.some((allowedHost) => host === allowedHost || host.endsWith(`.${allowedHost}`));
if (!isAllowed) {
throw new Error(`Navigation blocked for ${actionType}. ${host} is outside the allowed ${platform} domains.`);
}
}
export function isAllowedNavigation(platform: string, rawUrl: string): boolean {
try {
assertAllowedUrl(platform, rawUrl, "navigate");
return true;
} catch {
return false;
}
}
export function attachWindowSecurity(win: BrowserWindow, platform: string) {
win.webContents.setWindowOpenHandler(({ url }) => {
if (!isAllowedNavigation(platform, url)) {
return { action: "deny" };
}
setImmediate(() => {
void win.loadURL(url).catch((error) => {
log.warn(`[browser] Failed to open allowed popup target for ${platform}: ${error instanceof Error ? error.message : String(error)}`);
});
});
return { action: "deny" };
});
win.webContents.on("will-navigate", (event, navigationUrl) => {
if (isAllowedNavigation(platform, navigationUrl)) {
return;
}
event.preventDefault();
log.warn(`[browser] Blocked navigation outside allowlist for ${platform}: ${navigationUrl}`);
});
win.webContents.on("will-redirect", (event, navigationUrl) => {
if (isAllowedNavigation(platform, navigationUrl)) {
return;
}
event.preventDefault();
log.warn(`[browser] Blocked redirect outside allowlist for ${platform}: ${navigationUrl}`);
});
}
export function getComposeUrl(platform: string): string {
const urls: Record<string, string> = {
twitter: "https://x.com/compose/tweet",
linkedin: "https://www.linkedin.com/feed/",
instagram: "https://www.instagram.com/",
facebook: "https://www.facebook.com/",
reddit: "https://www.reddit.com/submit",
bluesky: "https://bsky.app/",
youtube: "https://www.youtube.com/",
tiktok: "https://www.tiktok.com/",
};
return urls[platform] || getPlatformRules(platform).loginUrl;
}
export function getFeedUrl(platform: string): string {
const urls: Record<string, string> = {
twitter: "https://x.com/home",
linkedin: "https://www.linkedin.com/feed/",
instagram: "https://www.instagram.com/",
facebook: "https://www.facebook.com/",
reddit: "https://www.reddit.com/",
bluesky: "https://bsky.app/",
youtube: "https://www.youtube.com/",
tiktok: "https://www.tiktok.com/",
};
return urls[platform] || getPlatformRules(platform).loginUrl;
}
export function getSearchUrl(platform: string, query: string): string {
const q = encodeURIComponent(query);
const urls: Record<string, string> = {
twitter: `https://x.com/search?q=${q}`,
linkedin: `https://www.linkedin.com/search/results/all/?keywords=${q}`,
reddit: `https://www.reddit.com/search/?q=${q}`,
};
return urls[platform] || `https://google.com/search?q=${q}`;
}