feat: expose bot task progress

This commit is contained in:
zenord
2026-08-17 14:37:51 +08:00
parent 91bf4e7a0c
commit 2795567f8c
13 changed files with 321 additions and 54 deletions
+59 -12
View File
@@ -7,8 +7,15 @@ import type { IncomingMessage } from "./types.js";
export interface GatewayResult { ok: boolean; reply?: string; ignored?: boolean; error?: string }
interface ChatWorkState {
running: boolean;
queued: number;
startedAt?: number;
}
export class Gateway {
private readonly locks = new Map<string, Promise<void>>();
private readonly chatWork = new Map<string, ChatWorkState>();
readonly commandRouter = new CommandRouter();
constructor(private readonly policy: GatewayPolicy, private readonly runtime: ConversationRuntime) {}
@@ -21,19 +28,28 @@ export class Gateway {
}
const command = this.commandRouter.parse(message.text);
if (command?.kind === "cancel") return this.reply(message, adapter, await this.cancelText(message), options);
if (command?.kind === "new") await this.runtime.cancel(message.platform, message.chatId);
if (command?.kind === "help" || command?.kind === "status" || command?.kind === "cancel") {
return this.reply(message, adapter, await this.executeCommand(command, message), options);
}
const chatKey = chatKeyFor(message.platform, message.chatId);
const existing = this.chatWork.get(chatKey);
const wasBusy = Boolean(existing?.running || existing?.queued);
const acknowledgement = !command && !options.synchronous
? this.sendAcknowledgement(message, adapter, wasBusy)
: Promise.resolve();
return this.withChatLock(chatKey, async () => {
await acknowledgement;
const replySequence = !command && !options.synchronous ? 2 : 1;
try {
const reply = command ? await this.executeCommand(command, message) : (await this.runtime.prompt({
platform: message.platform, chatId: message.chatId, userId: message.userId, text: message.text, messageId: message.messageId
})).text;
return this.reply(message, adapter, reply, options);
return this.reply(message, adapter, reply, options, replySequence);
} catch (error) {
const errorText = error instanceof Error ? error.message : String(error);
const reply = `Agent error: ${errorText}`;
if (!options.synchronous) await this.send(message, adapter, reply);
if (!options.synchronous) await this.send(message, adapter, reply, replySequence);
return { ok: false, error: errorText, reply };
}
});
@@ -45,7 +61,14 @@ export class Gateway {
switch (command.kind) {
case "help": return ["Commands:", "/status", "/cancel", "/new", "/help"].join("\n");
case "status": {
const status = this.runtime.status(message.platform, message.chatId);
const chatKey = chatKeyFor(message.platform, message.chatId);
const work = this.chatWork.get(chatKey);
const status = {
...this.runtime.status(message.platform, message.chatId),
gatewayRunning: Boolean(work?.running),
queued: work?.queued || 0,
gatewayRunningSeconds: work?.startedAt ? Math.max(0, Math.floor((Date.now() - work.startedAt) / 1000)) : 0
};
return `OK\n${Object.entries(status).map(([key, value]) => `${key}=${value}`).join("\n")}`;
}
case "new":
@@ -60,13 +83,26 @@ export class Gateway {
return await this.runtime.cancel(message.platform, message.chatId) ? "Cancellation requested." : "No active turn to cancel.";
}
private async reply(message: IncomingMessage, adapter: PlatformAdapter, reply: string, options: { synchronous?: boolean }): Promise<GatewayResult> {
if (!options.synchronous) await this.send(message, adapter, reply);
private async reply(message: IncomingMessage, adapter: PlatformAdapter, reply: string, options: { synchronous?: boolean }, replySequence = 1): Promise<GatewayResult> {
if (!options.synchronous) await this.send(message, adapter, reply, replySequence);
return { ok: true, reply };
}
private send(message: IncomingMessage, adapter: PlatformAdapter, text: string): Promise<void> {
return adapter.sendMessage({ target: { platform: message.platform, chatId: message.chatId, userId: message.userId, raw: message.raw }, text, replyTo: message.messageId });
private send(message: IncomingMessage, adapter: PlatformAdapter, text: string, replySequence: number): Promise<void> {
return adapter.sendMessage({
target: { platform: message.platform, chatId: message.chatId, userId: message.userId, raw: message.raw },
text,
replyTo: message.messageId,
replySequence
});
}
private async sendAcknowledgement(message: IncomingMessage, adapter: PlatformAdapter, queued: boolean): Promise<void> {
const text = queued
? "已收到,已排队。可随时发送 /status 查看状态。"
: "已收到,正在处理。可随时发送 /status 查看状态。";
try { await this.send(message, adapter, text, 1); }
catch { console.error(`Gateway acknowledgement send failed (platform=${message.platform})`); }
}
private checkPolicy(message: IncomingMessage): string | undefined {
@@ -78,14 +114,25 @@ export class Gateway {
private async withChatLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
const previous = this.locks.get(key) || Promise.resolve();
const state = this.chatWork.get(key) || { running: false, queued: 0 };
state.queued++;
this.chatWork.set(key, state);
let release!: () => void;
const current = new Promise<void>((resolve) => { release = resolve; });
const queued = previous.then(() => current);
this.locks.set(key, queued);
const lock = previous.then(() => current);
this.locks.set(key, lock);
await previous;
state.queued--;
state.running = true;
state.startedAt = Date.now();
try { return await fn(); } finally {
state.running = false;
state.startedAt = undefined;
release();
if (this.locks.get(key) === queued) this.locks.delete(key);
if (this.locks.get(key) === lock) {
this.locks.delete(key);
this.chatWork.delete(key);
}
}
}
}