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.
180 lines
6.8 KiB
TypeScript
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 });
|
|
});
|
|
}
|