Files
AgentCompany/src/main/ipc-browser-handlers.ts
T
ZacharyZhang-NY 118e3d8795 Remove final AppDatabase pass-through methods with ESLint guard (C1 part 3/3)
Migrate all remaining pass-through call sites (social, skills, budget,
entityCrud, documents, knowledge, automation, wakeup, backup, threads,
settings, snapshot, connectors, workspaces, activity) to their owning
domain modules. `AppDatabase` now carries only three methods:

  - createHireRequest (multi-module composite: agents + approvalsMod)
  - rollbackConfigRevision (multi-module composite: documents + entityCrud)
  - queryAll (raw-SQL escape hatch, @internal)

The vault/env-binding infrastructure previously passed argument-by-
argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at
module construction, letting callers invoke `db.entityCrud.saveSecret({...})`
without carrying vault plumbing.

Structural types that narrowed AppDatabase to a bag of flat methods
(`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in
budget-enforcement / budget-incidents / task-quality-gates /
failure-escalation / task-quality-gates-sweep) are reshaped to the new
namespace. `connectorPolicyDepsFromDb` replaces the old pattern of
handing the whole AppDatabase to `validateConnectorModelSelection`.

A new ESLint rule in eslint.config.mjs blocks new top-level methods on
AppDatabase so the pass-through layer cannot silently grow back.
`AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost
the 270-line trailing pass-through block.
2026-04-20 13:31:39 -04:00

135 lines
4.9 KiB
TypeScript

import {
IPC_CHANNELS,
browserActionQuerySchema,
cancelBrowserActionSchema,
deleteEntitySchema,
socialAccountInputSchema,
triggerBrowserLoginSchema,
} from "@shared/contracts";
import { fail, ok, type RegisterDomainHandlersDependencies } from "./ipc-domain-common";
const inflightBrowserLogins = new Set<string>();
export function registerBrowserHandlers({
registerHandle,
db,
logger,
ensureCompanyExists,
publishDomainChanged,
getBrowserActions,
}: Pick<
RegisterDomainHandlersDependencies,
| "registerHandle"
| "db"
| "logger"
| "ensureCompanyExists"
| "publishDomainChanged"
| "getBrowserActions"
>) {
registerHandle(IPC_CHANNELS.saveSocialAccount, (_event, payload) => {
const input = socialAccountInputSchema.parse(payload);
const err = ensureCompanyExists(input.companyId);
if (err) return err;
const id = db.social.saveSocialAccount(input);
publishDomainChanged();
return ok(id);
});
registerHandle(IPC_CHANNELS.deleteSocialAccount, async (_event, payload) => {
const input = deleteEntitySchema.parse(payload);
const err = ensureCompanyExists(input.companyId);
if (err) return err;
const browserActions = db.social.listBrowserActions(input.companyId, { socialAccountId: input.id, limit: 500 });
await getBrowserActions()?.clearSocialAccountData(
input.id,
browserActions.flatMap((action: { screenshotPath: string | null }) => (action.screenshotPath ? [action.screenshotPath] : [])),
);
db.social.deleteSocialAccount(input.id, input.companyId);
publishDomainChanged();
return ok(true);
});
registerHandle(IPC_CHANNELS.listBrowserActions, (_event, payload) => {
const input = browserActionQuerySchema.parse(payload);
const err = ensureCompanyExists(input.companyId);
if (err) return err;
return ok(db.social.listBrowserActions(input.companyId, {
socialAccountId: input.socialAccountId,
status: input.status,
limit: input.limit,
}));
});
registerHandle(IPC_CHANNELS.cancelBrowserAction, (_event, payload) => {
const input = cancelBrowserActionSchema.parse(payload);
const action = db.social.getBrowserAction(input.actionId, input.companyId);
if (!action) {
return fail("BROWSER_ACTION_NOT_FOUND", "Browser action not found.");
}
getBrowserActions()?.cancelAction(input.actionId);
db.social.updateBrowserAction(input.actionId, {
status: "cancelled",
resultSummary: "Browser action cancelled from the desktop control surface.",
errorMessage: "Action cancelled.",
finishedAt: new Date().toISOString(),
});
publishDomainChanged();
return ok(true);
});
registerHandle(IPC_CHANNELS.triggerBrowserLogin, async (_event, payload) => {
const input = triggerBrowserLoginSchema.parse(payload);
const err = ensureCompanyExists(input.companyId);
if (err) return err;
const browserActions = getBrowserActions();
if (!browserActions) {
return fail("BROWSER_NOT_READY", "Browser manager not initialized");
}
const account = db.social.getSocialAccount(input.socialAccountId, input.companyId);
if (!account) {
return fail("ACCOUNT_NOT_FOUND", "Social account not found");
}
if (inflightBrowserLogins.has(input.socialAccountId)) {
return fail("LOGIN_IN_PROGRESS", "A login for this account is already in progress.");
}
inflightBrowserLogins.add(input.socialAccountId);
browserActions.launchLoginBrowser(input.socialAccountId, account.platform).then(async (result) => {
if (result.success) {
const latestAccount = db.social.getSocialAccount(input.socialAccountId, account.companyId);
if (!latestAccount) {
return;
}
let detectedName = "";
let detectedDisplay = "";
try {
const profile = await browserActions.extractProfileFromSession(input.socialAccountId, account.platform);
detectedName = profile.accountName;
detectedDisplay = profile.displayName;
} catch (err) {
logger.warn(`[ipc/browser] extractProfileFromSession failed for social account ${input.socialAccountId}:`, err instanceof Error ? err.message : String(err));
}
db.social.saveSocialAccount({
id: latestAccount.id,
companyId: latestAccount.companyId,
platform: latestAccount.platform,
accountName: detectedName || latestAccount.accountName,
displayName: detectedDisplay || latestAccount.displayName,
profileUrl: latestAccount.profileUrl,
credentialSecretId: latestAccount.credentialSecretId,
status: "logged_in",
requireApproval: latestAccount.requireApproval,
});
publishDomainChanged();
}
}).catch((error) => {
logger.error(`[browser] Login failed: ${error}`);
}).finally(() => {
inflightBrowserLogins.delete(input.socialAccountId);
});
return ok(true);
});
}