feat: add onWebhook handler for case-status events
Adds the onWebhook lifecycle hook to the definePlugin() call. When legal-ai POSTs to /webhooks/case-status, the handler finds the linked Paperclip issue (via plugin state scan), posts a Hebrew status comment, and wakes the CEO agent on qa_failed. Hoists PluginContext and CEO_AGENT_IDS to module scope so onWebhook can access them after setup(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
127
src/worker.ts
127
src/worker.ts
@@ -1,8 +1,19 @@
|
||||
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
|
||||
import type { PluginContext, PluginWebhookInput } from "@paperclipai/plugin-sdk";
|
||||
import { LegalApi } from "./legal-api.js";
|
||||
|
||||
// Hoisted so onWebhook can access the context after setup() completes.
|
||||
let pluginCtx: PluginContext | null = null;
|
||||
|
||||
// Per-company CEO agent IDs (shared between setup and onWebhook).
|
||||
const CEO_AGENT_IDS: Record<string, string> = {
|
||||
"42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33", // CMP (רישוי ובניה)
|
||||
"8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", // CMPA (היטלי השבחה)
|
||||
};
|
||||
|
||||
const plugin = definePlugin({
|
||||
async setup(ctx) {
|
||||
pluginCtx = ctx; // save for onWebhook
|
||||
const config = (await ctx.config.get()) as {
|
||||
legalApiBaseUrl?: string;
|
||||
} | null;
|
||||
@@ -517,8 +528,7 @@ const plugin = definePlugin({
|
||||
}
|
||||
});
|
||||
|
||||
// Route user comments through CEO agent
|
||||
const CEO_AGENT_ID = "752cebdd-6748-4a04-aacd-c7ab0294ef33";
|
||||
// Route user comments through CEO agent — per company
|
||||
|
||||
ctx.events.on("issue.comment.created", async (event) => {
|
||||
// Only intercept human comments — not agent comments (prevents loops)
|
||||
@@ -570,10 +580,18 @@ const plugin = definePlugin({
|
||||
}
|
||||
}
|
||||
|
||||
// Wake the CEO agent with the comment context
|
||||
// Wake the CEO agent for this company
|
||||
const ceoAgentId = CEO_AGENT_IDS[event.companyId];
|
||||
if (!ceoAgentId) {
|
||||
ctx.logger.warn("No CEO agent mapped for company", {
|
||||
companyId: event.companyId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { runId } = await ctx.agents.invoke(
|
||||
CEO_AGENT_ID,
|
||||
ceoAgentId,
|
||||
event.companyId,
|
||||
{
|
||||
prompt: [
|
||||
@@ -694,6 +712,107 @@ const plugin = definePlugin({
|
||||
async onHealth() {
|
||||
return { status: "ok" as const };
|
||||
},
|
||||
|
||||
async onWebhook(input: PluginWebhookInput): Promise<void> {
|
||||
if (!pluginCtx) return; // not yet initialized
|
||||
|
||||
const { endpointKey, parsedBody } = input;
|
||||
|
||||
if (endpointKey !== "case-status") return;
|
||||
|
||||
const payload = parsedBody as {
|
||||
caseNumber: string;
|
||||
oldStatus: string;
|
||||
newStatus: string;
|
||||
companyId: string | null;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
const { caseNumber, oldStatus, newStatus, companyId } = payload;
|
||||
pluginCtx.logger.info(`Webhook: case ${caseNumber} ${oldStatus} → ${newStatus}`, {
|
||||
companyId,
|
||||
});
|
||||
|
||||
if (!companyId) {
|
||||
pluginCtx.logger.warn("onWebhook: missing companyId in payload", { caseNumber });
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the Paperclip issue linked to this case number by scanning plugin state.
|
||||
// State stores: issue.id → case_number (scopeKind=issue, stateKey=legal-case-number)
|
||||
const issues = await pluginCtx.issues.list({ companyId });
|
||||
let linkedIssueId: string | null = null;
|
||||
for (const issue of issues) {
|
||||
const linkedCase = await pluginCtx.state.get({
|
||||
scopeKind: "issue",
|
||||
scopeId: issue.id,
|
||||
stateKey: "legal-case-number",
|
||||
});
|
||||
if (linkedCase === caseNumber) {
|
||||
linkedIssueId = issue.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedIssueId) {
|
||||
pluginCtx.logger.warn(`onWebhook: no Paperclip issue linked to case ${caseNumber}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Status label map (Hebrew)
|
||||
const statusLabels: Record<string, string> = {
|
||||
new: "📂 תיק חדש",
|
||||
uploading: "📤 העלאת מסמכים",
|
||||
processing: "⚙️ עיבוד מסמכים",
|
||||
documents_ready: "📁 מסמכים מוכנים",
|
||||
outcome_set: "🎯 תוצאה הוזנה",
|
||||
brainstorming: "💡 סיעור מוחות",
|
||||
direction_approved: "✅ כיוון אושר",
|
||||
drafting: "✍️ כתיבה בתהליך",
|
||||
qa_review: "🔍 בדיקת איכות",
|
||||
in_progress: "🔄 בעבודה",
|
||||
drafted: "✍️ טיוטה מוכנה",
|
||||
qa_failed: "❌ QA נכשל",
|
||||
exported: "📄 יוצא ל-DOCX",
|
||||
reviewed: "✅ נבדק",
|
||||
final: "🎯 סופי",
|
||||
};
|
||||
const label = statusLabels[newStatus] ?? newStatus;
|
||||
|
||||
// Post a Hebrew status comment on the linked issue
|
||||
await pluginCtx.issues.createComment(
|
||||
linkedIssueId,
|
||||
`**עדכון סטטוס תיק ${caseNumber}:** ${label} (היה: ${oldStatus})`,
|
||||
companyId,
|
||||
);
|
||||
|
||||
// Wake the CEO agent if QA failed
|
||||
if (newStatus === "qa_failed") {
|
||||
const ceoId = CEO_AGENT_IDS[companyId];
|
||||
if (ceoId) {
|
||||
try {
|
||||
const { runId } = await pluginCtx.agents.invoke(ceoId, companyId, {
|
||||
prompt: `תיק ${caseNumber} נכשל בבדיקת QA. עיין בתוצאות QA ותקן את הבעיות לפני שתמשיך.`,
|
||||
reason: "qa_failed webhook",
|
||||
});
|
||||
pluginCtx.logger.info(`Invoked CEO agent for qa_failed`, {
|
||||
caseNumber,
|
||||
ceoId,
|
||||
runId,
|
||||
});
|
||||
} catch (err) {
|
||||
pluginCtx.logger.error("Failed to invoke CEO agent for qa_failed", {
|
||||
caseNumber,
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
pluginCtx.logger.warn("onWebhook: no CEO agent mapped for company", {
|
||||
companyId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default plugin;
|
||||
|
||||
Reference in New Issue
Block a user