feat: pending-finish proposals and QQ image input
Proposal semantics: worker results no longer distinguish success/fail; only an explicit finish settles a pending proposal. Pending owner input follows up by resuming the original worker session. Dirty pending blocks start_next globally; clean pending only blocks its owner. QQ adapter now downloads image attachments and passes them as ACP image content blocks; video/file attachments degrade to link text.
This commit is contained in:
+53
-8
@@ -3,6 +3,7 @@ import type { GatewayPolicy } from "../config.js";
|
||||
import type { PlatformAdapter } from "./adapter.js";
|
||||
import { CommandRouter, type ParsedCommand } from "./command-router.js";
|
||||
import { chatKeyFor } from "./durable-session-store.js";
|
||||
import type { Proposal } from "./proposal-store.js";
|
||||
import type { IncomingMessage, MessageTarget } from "./types.js";
|
||||
|
||||
export interface GatewayResult { ok: boolean; reply?: string; ignored?: boolean; error?: string }
|
||||
@@ -114,7 +115,8 @@ export class Gateway {
|
||||
chatId: message.chatId,
|
||||
userId: message.userId,
|
||||
text: message.text,
|
||||
messageId: message.messageId
|
||||
messageId: message.messageId,
|
||||
attachments: message.attachments
|
||||
})).text;
|
||||
await this.replyStream(chatKey, message, adapter).enqueue(reply);
|
||||
return { ok: true, reply };
|
||||
@@ -237,7 +239,8 @@ export class Gateway {
|
||||
chatId: message.chatId,
|
||||
userId: message.userId,
|
||||
text: message.text,
|
||||
messageId: message.messageId
|
||||
messageId: message.messageId,
|
||||
attachments: message.attachments
|
||||
})).text;
|
||||
return { ok: true, reply };
|
||||
} catch (error) {
|
||||
@@ -256,17 +259,19 @@ export class Gateway {
|
||||
}
|
||||
case "list": {
|
||||
if (!this.runtime.listProposals) return "当前运行时不支持列出提案。";
|
||||
const proposals = this.runtime.listProposals(message.platform, message.chatId, message.userId);
|
||||
if (proposals.length === 0) return "当前没有提案。";
|
||||
return proposals.map((proposal) => `${proposal.id} [${proposal.status}] ${proposal.title}`).join("\n");
|
||||
return renderProposalPanel(this.runtime.listProposals(message.platform, message.chatId, message.userId));
|
||||
}
|
||||
case "confirm": {
|
||||
if (!this.runtime.confirm) return "当前运行时不支持确认操作。";
|
||||
return await this.runtime.confirm(message.platform, message.chatId, message.userId) ? "已确认,继续执行。" : "当前没有待确认的提案。";
|
||||
return await this.runtime.confirm(message.platform, message.chatId, message.userId) ? "已确认,加入队列。" : "当前没有待确认的提案。";
|
||||
}
|
||||
case "finish": {
|
||||
if (!this.runtime.finish) return "当前运行时不支持结束操作。";
|
||||
return await this.runtime.finish(message.platform, message.chatId, message.userId) ? "已结束该任务。" : "当前没有待结束的任务。";
|
||||
}
|
||||
case "stop": {
|
||||
if (!this.runtime.stop) return "当前运行时不支持停止操作。";
|
||||
return await this.runtime.stop(message.platform, message.chatId, message.userId) ? "已停止当前任务。" : "当前没有正在执行的任务。";
|
||||
return await this.runtime.stop(message.platform, message.chatId, message.userId) ? "已停止当前任务,任务转为待确认。" : "当前没有正在执行的任务。";
|
||||
}
|
||||
case "cancel":
|
||||
return await this.runtime.cancel(message.platform, message.chatId, message.userId) ? "已取消最近的提案。" : "没有可取消的提案。";
|
||||
@@ -282,6 +287,45 @@ export class Gateway {
|
||||
|
||||
}
|
||||
|
||||
function renderProposalPanel(proposals: Proposal[]): string {
|
||||
if (proposals.length === 0) return "当前没有提案。";
|
||||
const pending = proposals.filter((proposal) => proposal.status === "pending");
|
||||
const working = proposals.filter((proposal) => proposal.status === "working");
|
||||
const queued = proposals.filter((proposal) => proposal.status === "queued");
|
||||
const proposed = proposals.filter((proposal) => proposal.status === "proposed");
|
||||
const finished = proposals.filter((proposal) => proposal.status === "finished")
|
||||
.sort((left, right) => (right.finishedAt ?? right.updatedAt) - (left.finishedAt ?? left.updatedAt))
|
||||
.slice(0, 3);
|
||||
const sections: string[] = [];
|
||||
if (pending.length > 0) {
|
||||
sections.push("待确认(等你处理):");
|
||||
for (const proposal of pending) {
|
||||
const card = proposal.pending;
|
||||
sections.push(`- ${proposal.id}「${proposal.title}」${card ? `:${card.summary}${card.question ? `(问你:${card.question})` : ""}` : ""}(说 /finish 结束,或直接说要求继续改)`);
|
||||
}
|
||||
}
|
||||
if (working.length > 0) {
|
||||
sections.push("进行中:");
|
||||
for (const proposal of working) sections.push(`- ${proposal.id}「${proposal.title}」正在执行(/stop 可中止)`);
|
||||
}
|
||||
if (queued.length > 0) {
|
||||
sections.push("排队中:");
|
||||
for (const proposal of queued) sections.push(`- ${proposal.id}「${proposal.title}」`);
|
||||
}
|
||||
if (proposed.length > 0) {
|
||||
sections.push("未确认(proposed):");
|
||||
for (const proposal of proposed) sections.push(`- ${proposal.id}「${proposal.title}」(/confirm 确认后排队)`);
|
||||
}
|
||||
if (finished.length > 0) {
|
||||
sections.push("最近结束:");
|
||||
for (const proposal of finished) {
|
||||
const state = proposal.finishKind === "cancelled" ? "已取消" : "已完成";
|
||||
sections.push(`- ${proposal.id}「${proposal.title}」${state}${proposal.finishNote ? `(${proposal.finishNote})` : ""}`);
|
||||
}
|
||||
}
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
function safeEventError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
const http = /HTTP (\d{3})/.exec(message);
|
||||
@@ -296,7 +340,8 @@ const HELP_TEXT = [
|
||||
"兜底命令:",
|
||||
"/list 列出当前提案",
|
||||
"/confirm 确认最近待确认的提案",
|
||||
"/finish 结束最近待确认的任务",
|
||||
"/stop 停止正在执行的任务",
|
||||
"/cancel 取消最近未开始的提案",
|
||||
"/cancel 取消最近未开始或待确认的提案",
|
||||
"/status 查看运行状态"
|
||||
].join("\n");
|
||||
|
||||
Reference in New Issue
Block a user