production: reminder cron, dashboard overhaul, shadcn components, setup wizard

- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback
- /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate)
- /api/pledges: GET handler with filtering, search, pagination, sort by dueDate
- Dashboard overview: stats, collection progress bar, needs attention, upcoming payments
- Dashboard pledges: proper table with status tabs, search, actions, pagination
- New shadcn components: Table, Tabs, DropdownMenu, Progress
- Setup wizard: 4-step onboarding (org → bank → event → QR code)
- Settings API: PUT handler for org create/update
- Org resolver: single-tenant fallback to first org
- Cron jobs installed: reminders every 15min, overdue check at 6am
- Auto-generates installment dates when not provided
- HOSTNAME=0.0.0.0 in compose for multi-network binding
This commit is contained in:
2026-03-03 05:11:17 +08:00
parent 250221b530
commit c79b9bcabc
61 changed files with 3547 additions and 534 deletions

198
lib/agent-worker.ts Normal file
View File

@@ -0,0 +1,198 @@
import Anthropic from "@anthropic-ai/sdk";
import { toolDefs, executeTool } from "./tools.js";
import * as store from "./store.js";
import * as tg from "./telegram.js";
const client = new Anthropic();
const MODEL = "claude-sonnet-4-20250514";
const MAX_TURNS = 50;
// Map of agentId -> resolve function for when user replies
const waitingForUser = new Map<number, (response: string) => void>();
export function isWaitingForUser(agentId: number): boolean {
return waitingForUser.has(agentId);
}
export function resolveUserResponse(agentId: number, response: string): void {
const resolve = waitingForUser.get(agentId);
if (resolve) {
waitingForUser.delete(agentId);
resolve(response);
}
}
function buildSystemPrompt(task: string): string {
return `You are an autonomous coding agent. You have been assigned a specific task.
YOUR TASK:
${task}
GUIDELINES:
- Work independently to complete the task
- Use the bash tool for running commands, git, etc.
- Use read_file, write_file, edit_file for file operations
- When you're done, call the "done" tool with a summary
- If you need user input or a decision, call "ask_user"
- Be efficient — don't explain what you're about to do, just do it
- If something fails, try to fix it yourself before asking the user
WORKING DIRECTORY: ${process.cwd()}`;
}
export async function runAgent(agentId: number): Promise<void> {
const agent = store.getAgent(agentId);
if (!agent) return;
store.updateAgent(agentId, { status: "working" });
store.addLog(agentId, "system", `Agent started: ${agent.task}`);
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: agent.task },
];
let turns = 0;
try {
while (turns < MAX_TURNS) {
turns++;
const response = await client.messages.create({
model: MODEL,
max_tokens: 8096,
system: buildSystemPrompt(agent.task),
tools: toolDefs as any,
messages,
});
// Collect assistant content
const assistantContent = response.content;
messages.push({ role: "assistant", content: assistantContent });
// Log text blocks (no Telegram notification — reduces noise)
for (const block of assistantContent) {
if (block.type === "text" && block.text.trim()) {
store.addLog(agentId, "assistant", block.text);
}
}
// If no tool use, we're done
if (response.stop_reason !== "tool_use") {
store.updateAgent(agentId, {
status: "done",
summary: "Completed (no more actions)",
});
store.addLog(agentId, "system", "Agent finished (end_turn)");
await tg.send(
`✅ *Agent #${agentId}* finished.\nTask: ${agent.task}`,
agent.chat_id,
{ reply_to: agent.thread_msg_id || undefined }
);
return;
}
// Process tool calls
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of assistantContent) {
if (block.type !== "tool_use") continue;
const toolName = block.name;
const toolInput = block.input as Record<string, unknown>;
store.addLog(
agentId,
"tool",
`${toolName}: ${JSON.stringify(toolInput).slice(0, 500)}`
);
if (toolName === "ask_user") {
// Pause and wait for user response
store.updateAgent(agentId, { status: "waiting" });
await tg.send(
`❓ *Agent #${agentId}* needs your input:\n\n${toolInput.question}`,
agent.chat_id,
{
reply_to: agent.thread_msg_id || undefined,
keyboard: [
[{ text: "💬 Reply", callback_data: `talk_${agentId}` }],
],
}
);
store.addLog(agentId, "system", `Waiting for user: ${toolInput.question}`);
// Wait for user response
const userResponse = await new Promise<string>((resolve) => {
waitingForUser.set(agentId, resolve);
});
store.updateAgent(agentId, { status: "working" });
store.addLog(agentId, "user", `User replied: ${userResponse}`);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `User responded: ${userResponse}`,
});
continue;
}
if (toolName === "done") {
const summary = (toolInput.summary as string) || "Task completed";
store.updateAgent(agentId, { status: "done", summary });
store.addLog(agentId, "system", `Done: ${summary}`);
await tg.send(
`✅ *Agent #${agentId}* completed!\n\n*Summary:* ${summary}\n*Task:* ${agent.task}`,
agent.chat_id,
{ reply_to: agent.thread_msg_id || undefined }
);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: summary,
});
// Push tool results and stop
messages.push({ role: "user", content: toolResults });
return;
}
// Execute the tool
const { result } = executeTool(toolName, toolInput);
store.addLog(
agentId,
"tool_result",
`${toolName}${result.slice(0, 500)}`
);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result,
});
}
messages.push({ role: "user", content: toolResults });
}
// Hit max turns
store.updateAgent(agentId, {
status: "done",
summary: `Stopped after ${MAX_TURNS} turns`,
});
await tg.send(
`⚠️ *Agent #${agentId}* hit max turns (${MAX_TURNS}). Task: ${agent.task}`,
agent.chat_id,
{ reply_to: agent.thread_msg_id || undefined }
);
} catch (e: any) {
console.error(`[agent ${agentId}] error:`, e);
store.updateAgent(agentId, { status: "error", error: e.message });
store.addLog(agentId, "error", e.message);
await tg.send(
`❌ *Agent #${agentId}* error:\n${e.message?.slice(0, 500)}`,
agent.chat_id,
{ reply_to: agent.thread_msg_id || undefined }
);
}
}