Files
AgentCompany/src/main/api-server/routes-messages.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

180 lines
6.8 KiB
TypeScript

import log from "electron-log/main.js";
import { agentMessageInputSchema } from "@shared/contracts";
import { dispatchAgentMessage } from "../messaging/dispatch";
import { requireParam, type AgentApiServer } from "./server-core";
export function registerMessageRoutes(server: AgentApiServer) {
const db = server.db;
const onDomainChanged = server.onDomainChanged;
const onWakeAgent = server.onWakeAgent;
const emitEvent = server.emitEvent;
server.addRoute("POST", "/api/issues/:issueId/comments", (_req, res, params, token, body) => {
const input = body as { body?: string } | null;
if (!input?.body || typeof input.body !== "string") {
server.json(res, 400, { error: "body is required" });
return;
}
if (input.body.length > 4000) {
server.json(res, 400, { error: "Comment body must not exceed 4000 characters" });
return;
}
try {
const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" });
return;
}
const agent = db.entityCrud.getAgent(token.agentId);
const comment = db.activityMod.addComment({
companyId: task.companyId,
taskId: task.id,
authorAgentId: token.agentId,
authorName: agent.name,
body: input.body,
});
onDomainChanged();
// Fire comment automation rules
server.callbacks.onCommentPosted(task.id, task.companyId, token.agentId);
const wokenForComment = new Set<string>();
if (task.assigneeAgentId && task.assigneeAgentId !== token.agentId) {
onWakeAgent(task.assigneeAgentId, task.companyId, "comment");
wokenForComment.add(task.assigneeAgentId);
}
const chain = db.agents.getChainOfCommand(token.agentId);
if (chain.length > 0 && chain[0].id !== token.agentId && !wokenForComment.has(chain[0].id)) {
onWakeAgent(chain[0].id, task.companyId, "comment");
wokenForComment.add(chain[0].id);
}
// @mention detection: wake any mentioned agents not already woken
const mentionedIds = db.agents.findMentionedAgentIds(task.companyId, input.body);
for (const mentionedId of mentionedIds) {
if (mentionedId !== token.agentId && !wokenForComment.has(mentionedId)) {
onWakeAgent(mentionedId, task.companyId, "comment_mentioned");
wokenForComment.add(mentionedId);
}
}
server.json(res, 201, comment);
} catch (err) {
log.warn(`[api/messages] addComment failed:`, err instanceof Error ? err.message : String(err));
server.json(res, 404, { error: "Task not found" });
}
});
server.addRoute("GET", "/api/issues/:issueId/comments", (_req, res, params, token) => {
try {
const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" });
return;
}
} catch (err) {
log.warn(`[api/messages] getTask failed for comments list:`, err instanceof Error ? err.message : String(err));
server.json(res, 404, { error: "Task not found" });
return;
}
const comments = db.activityMod.listComments(requireParam(params, "issueId"));
server.json(res, 200, comments);
});
server.addRoute("POST", "/api/companies/:companyId/messages", (_req, res, params, token, body) => {
const companyId = requireParam(params, "companyId");
if (companyId !== token.companyId) {
server.json(res, 403, { error: "Company mismatch" });
return;
}
const parsed = agentMessageInputSchema.safeParse({
...(body as Record<string, unknown>),
companyId,
fromAgentId: token.agentId,
});
if (!parsed.success) {
server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() });
return;
}
const dispatchResult = dispatchAgentMessage(
{
db,
emitEvent,
notify: server.callbacks.notify,
publishDomainChanged: onDomainChanged,
wakeAgentIfPossible: (agentId, cid, trigger) => {
if (!agentId) return null;
return onWakeAgent(agentId, cid, trigger);
},
injectRuntimeMessage: server.callbacks.injectRuntimeMessage,
onHumanMessage: server.callbacks.onHumanMessage,
},
parsed.data,
"message",
);
if (!dispatchResult.ok) {
server.json(res, 400, { error: dispatchResult.error.message, code: dispatchResult.error.code });
return;
}
server.json(res, 201, { id: dispatchResult.data.id });
});
server.addRoute("GET", "/api/companies/:companyId/messages", (req, res, params, token) => {
const companyId = requireParam(params, "companyId");
if (companyId !== token.companyId) {
server.json(res, 403, { error: "Company mismatch" });
return;
}
const url = new URL(req.url ?? "/", `http://127.0.0.1:${server.port}`);
const channelParam = url.searchParams.get("channel");
const channelTargetId = url.searchParams.get("channelTargetId");
const unreadOnly = url.searchParams.get("unreadOnly") === "true";
const limitStr = url.searchParams.get("limit");
const limit = limitStr ? Math.min(Math.max(parseInt(limitStr, 10) || 50, 1), 200) : 50;
const allowedChannels = new Set(["direct", "department", "company", "project", "incident"]);
if (channelParam && !allowedChannels.has(channelParam)) {
server.json(res, 400, { error: `Invalid channel filter: ${channelParam}` });
return;
}
const messages = db.messages.listAgentMessages(companyId, {
agentId: token.agentId,
channel: channelParam ?? undefined,
channelTargetId: channelTargetId && channelTargetId.length > 0 ? channelTargetId : undefined,
unreadOnly,
limit,
});
server.json(res, 200, messages);
});
server.addRoute("POST", "/api/companies/:companyId/messages/:messageId/read", (_req, res, params, token) => {
const companyId = requireParam(params, "companyId");
if (companyId !== token.companyId) {
server.json(res, 403, { error: "Company mismatch" });
return;
}
const message = db.messages.getAgentMessage(requireParam(params, "messageId"), companyId);
if (!message) {
server.json(res, 404, { error: "Message not found" });
return;
}
const visibleToAgent = db.messages.listAgentMessages(companyId, {
agentId: token.agentId,
messageId: message.id,
limit: 1,
}).some((entry) => entry.id === message.id);
if (!visibleToAgent) {
server.json(res, 403, { error: "This message is not visible to the current agent" });
return;
}
db.messages.markMessageReadForReader(requireParam(params, "messageId"), companyId, token.agentId);
onDomainChanged();
server.json(res, 200, { ok: true });
});
}