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:
+41
-4
@@ -31,10 +31,12 @@ class FakeRuntime implements ConversationRuntime {
|
||||
readonly turns: PendingTurn[] = [];
|
||||
cancelled = 0;
|
||||
confirmed = 0;
|
||||
finished = 0;
|
||||
stopped = 0;
|
||||
listed = 0;
|
||||
cancelResult = true;
|
||||
confirmResult = true;
|
||||
finishResult = true;
|
||||
stopResult = true;
|
||||
proposals: Proposal[] = [];
|
||||
commandUsers: Record<string, string | undefined> = {};
|
||||
@@ -50,6 +52,7 @@ class FakeRuntime implements ConversationRuntime {
|
||||
}
|
||||
async cancel(_platform: string, _chatId: string, userId: string) { this.cancelled++; this.commandUsers.cancel = userId; return this.cancelResult; }
|
||||
async confirm(_platform: string, _chatId: string, userId: string) { this.confirmed++; this.commandUsers.confirm = userId; return this.confirmResult; }
|
||||
async finish(_platform: string, _chatId: string, userId: string) { this.finished++; this.commandUsers.finish = userId; return this.finishResult; }
|
||||
async stop(_platform: string, _chatId: string, userId: string) { this.stopped++; this.commandUsers.stop = userId; return this.stopResult; }
|
||||
listProposals(_platform: string, _chatId: string, userId: string) { this.listed++; this.commandUsers.list = userId; return this.proposals; }
|
||||
async reset() {}
|
||||
@@ -434,20 +437,22 @@ test("status and help reach the runtime or reply directly", async () => {
|
||||
const help = await gateway.receive(message("/help"), adapter, { synchronous: true });
|
||||
assert.match(help.reply || "", /自然语言/);
|
||||
assert.match(help.reply || "", /\/confirm/);
|
||||
assert.match(help.reply || "", /\/finish/);
|
||||
assert.match(help.reply || "", /\/stop/);
|
||||
assert.match(help.reply || "", /\/cancel/);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
});
|
||||
|
||||
test("list, confirm, stop, and cancel forward to the runtime with the chat identity", async () => {
|
||||
test("list, confirm, finish, stop, and cancel forward to the runtime with the chat identity", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
runtime.proposals = [fakeProposal()];
|
||||
|
||||
const list = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.listed, 1);
|
||||
assert.match(list.reply || "", /proposal-1 \[proposed\] 修复登录页/);
|
||||
assert.match(list.reply || "", /未确认(proposed)/);
|
||||
assert.match(list.reply || "", /proposal-1「修复登录页」/);
|
||||
|
||||
runtime.proposals = [];
|
||||
const emptyList = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
@@ -455,11 +460,15 @@ test("list, confirm, stop, and cancel forward to the runtime with the chat ident
|
||||
|
||||
const confirm = await gateway.receive(message("/confirm"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.confirmed, 1);
|
||||
assert.equal(confirm.reply, "已确认,继续执行。");
|
||||
assert.equal(confirm.reply, "已确认,加入队列。");
|
||||
|
||||
const finish = await gateway.receive(message("/finish"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.finished, 1);
|
||||
assert.equal(finish.reply, "已结束该任务。");
|
||||
|
||||
const stop = await gateway.receive(message("/stop"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.stopped, 1);
|
||||
assert.equal(stop.reply, "已停止当前任务。");
|
||||
assert.equal(stop.reply, "已停止当前任务,任务转为待确认。");
|
||||
|
||||
const cancel = await gateway.receive(message("/cancel"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.cancelled, 1);
|
||||
@@ -467,17 +476,45 @@ test("list, confirm, stop, and cancel forward to the runtime with the chat ident
|
||||
|
||||
assert.equal(runtime.commandUsers.list, "user");
|
||||
assert.equal(runtime.commandUsers.confirm, "user");
|
||||
assert.equal(runtime.commandUsers.finish, "user");
|
||||
assert.equal(runtime.commandUsers.stop, "user");
|
||||
assert.equal(runtime.commandUsers.cancel, "user");
|
||||
|
||||
runtime.confirmResult = false;
|
||||
runtime.finishResult = false;
|
||||
runtime.stopResult = false;
|
||||
runtime.cancelResult = false;
|
||||
assert.equal((await gateway.receive(message("/confirm"), adapter, { synchronous: true })).reply, "当前没有待确认的提案。");
|
||||
assert.equal((await gateway.receive(message("/finish"), adapter, { synchronous: true })).reply, "当前没有待结束的任务。");
|
||||
assert.equal((await gateway.receive(message("/stop"), adapter, { synchronous: true })).reply, "当前没有正在执行的任务。");
|
||||
assert.equal((await gateway.receive(message("/cancel"), adapter, { synchronous: true })).reply, "没有可取消的提案。");
|
||||
});
|
||||
|
||||
test("the list panel orders pending first, then working, queued, proposed, and recent finished", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
runtime.proposals = [
|
||||
fakeProposal({ id: "p-finished", title: "旧任务", status: "finished", finishKind: "done", finishedAt: 10 }),
|
||||
fakeProposal({ id: "p-proposed", title: "新想法", status: "proposed" }),
|
||||
fakeProposal({ id: "p-working", title: "干活中", status: "working" }),
|
||||
fakeProposal({ id: "p-queued", title: "排队任务", status: "queued" }),
|
||||
fakeProposal({ id: "p-pending", title: "待确认任务", status: "pending", pending: { summary: "做了一半", question: "继续吗?", receivedAt: 5 } }),
|
||||
fakeProposal({ id: "p-cancelled", title: "取消任务", status: "finished", finishKind: "cancelled", finishedAt: 20 })
|
||||
];
|
||||
|
||||
const list = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
const reply = list.reply || "";
|
||||
const order = ["待确认(等你处理)", "进行中", "排队中", "未确认(proposed)", "最近结束"]
|
||||
.map((section) => reply.indexOf(section));
|
||||
assert.ok(order.every((index) => index >= 0), reply);
|
||||
assert.deepEqual([...order].sort((a, b) => a - b), order);
|
||||
assert.match(reply, /p-pending「待确认任务」:做了一半(问你:继续吗?)/);
|
||||
assert.match(reply, /\/finish 结束/);
|
||||
assert.match(reply, /p-working「干活中」正在执行/);
|
||||
assert.match(reply, /p-cancelled「取消任务」已取消/);
|
||||
assert.match(reply, /p-finished「旧任务」已完成/);
|
||||
});
|
||||
|
||||
test("synchronous work sends no delivery messages", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
|
||||
Reference in New Issue
Block a user