feat: expose bot task progress
This commit is contained in:
+59
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user