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:
@@ -31,6 +31,125 @@ test("group messages without member_openid fall back to user_openid", async () =
|
||||
assert.equal(received[1].userId, "mem4");
|
||||
});
|
||||
|
||||
test("image attachments are downloaded to base64 while video and file attachments degrade to text links", async () => {
|
||||
const received: any[] = [];
|
||||
const gateway = { receive: async (message: unknown) => { received.push(message); } };
|
||||
const adapter = new QqAdapter(config, gateway as never);
|
||||
const pngBytes = Buffer.from("fake-png-bytes");
|
||||
const urls: string[] = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
urls.push(String(input));
|
||||
return new Response(pngBytes, { status: 200 });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await adapter.handleWebhook({
|
||||
body: {
|
||||
op: 0, t: "C2C_MESSAGE_CREATE",
|
||||
d: {
|
||||
id: "m10", author: { user_openid: "u10" }, content: "看看这些",
|
||||
attachments: [
|
||||
{ content_type: "image/png", filename: "a.png", size: pngBytes.length, url: "//cdn.example.com/a.png" },
|
||||
{ content_type: "video/mp4", filename: "v.mp4", size: 1024, url: "https://cdn.example.com/v.mp4" },
|
||||
{ content_type: "application/pdf", filename: "f.pdf", size: 1024, url: "https://cdn.example.com/f.pdf" }
|
||||
]
|
||||
}
|
||||
},
|
||||
headers: {}, query: {}, req: {} as never
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(received.length, 1);
|
||||
const message = received[0];
|
||||
assert.deepEqual(urls, ["https://cdn.example.com/a.png"]);
|
||||
assert.equal(message.attachments.length, 1);
|
||||
assert.equal(message.attachments[0].mimeType, "image/png");
|
||||
assert.equal(message.attachments[0].filename, "a.png");
|
||||
assert.equal(message.attachments[0].data, pngBytes.toString("base64"));
|
||||
assert.match(message.text, /^看看这些/);
|
||||
assert.match(message.text, /\[视频\] https:\/\/cdn\.example\.com\/v\.mp4/);
|
||||
assert.match(message.text, /\[文件\] https:\/\/cdn\.example\.com\/f\.pdf/);
|
||||
} finally { globalThis.fetch = original; }
|
||||
});
|
||||
|
||||
test("oversized images are skipped and images beyond the per-message cap are not downloaded", async () => {
|
||||
const received: any[] = [];
|
||||
const gateway = { receive: async (message: unknown) => { received.push(message); } };
|
||||
const adapter = new QqAdapter(config, gateway as never);
|
||||
const urls: string[] = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
urls.push(String(input));
|
||||
return new Response(Buffer.from("x"), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await adapter.handleWebhook({
|
||||
body: {
|
||||
op: 0, t: "C2C_MESSAGE_CREATE",
|
||||
d: {
|
||||
id: "m11", author: { user_openid: "u11" }, content: "hi",
|
||||
attachments: [
|
||||
{ content_type: "image/png", size: 6 * 1024 * 1024, url: "https://cdn.example.com/big.png" },
|
||||
{ content_type: "image/png", size: 10, url: "https://cdn.example.com/1.png" },
|
||||
{ content_type: "image/png", size: 10, url: "https://cdn.example.com/2.png" },
|
||||
{ content_type: "image/png", size: 10, url: "https://cdn.example.com/3.png" },
|
||||
{ content_type: "image/png", size: 10, url: "https://cdn.example.com/4.png" }
|
||||
]
|
||||
}
|
||||
},
|
||||
headers: {}, query: {}, req: {} as never
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(received.length, 1);
|
||||
assert.deepEqual(urls, [
|
||||
"https://cdn.example.com/1.png",
|
||||
"https://cdn.example.com/2.png",
|
||||
"https://cdn.example.com/3.png"
|
||||
]);
|
||||
assert.equal(received[0].attachments.length, 3);
|
||||
} finally { globalThis.fetch = original; }
|
||||
});
|
||||
|
||||
test("a pure image message uses placeholder text and failed downloads degrade", async () => {
|
||||
const received: any[] = [];
|
||||
const gateway = { receive: async (message: unknown) => { received.push(message); } };
|
||||
const adapter = new QqAdapter(config, gateway as never);
|
||||
const original = globalThis.fetch;
|
||||
let fail = false;
|
||||
globalThis.fetch = (async () => {
|
||||
if (fail) throw new Error("network down");
|
||||
return new Response(Buffer.from("img"), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await adapter.handleWebhook({
|
||||
body: { op: 0, t: "C2C_MESSAGE_CREATE", d: { id: "m12", author: { user_openid: "u12" }, content: "", attachments: [{ content_type: "image/jpeg", url: "https://cdn.example.com/a.jpg" }] } },
|
||||
headers: {}, query: {}, req: {} as never
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(received.length, 1);
|
||||
assert.equal(received[0].text, "(发来一张图片)");
|
||||
assert.equal(received[0].attachments.length, 1);
|
||||
|
||||
// Every download failing with no text keeps the message ignored.
|
||||
fail = true;
|
||||
await adapter.handleWebhook({
|
||||
body: { op: 0, t: "C2C_MESSAGE_CREATE", d: { id: "m13", author: { user_openid: "u12" }, content: "", attachments: [{ content_type: "image/jpeg", url: "https://cdn.example.com/b.jpg" }] } },
|
||||
headers: {}, query: {}, req: {} as never
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(received.length, 1);
|
||||
|
||||
// Failed downloads with text still deliver the text without attachments.
|
||||
await adapter.handleWebhook({
|
||||
body: { op: 0, t: "C2C_MESSAGE_CREATE", d: { id: "m14", author: { user_openid: "u12" }, content: "看图", attachments: [{ content_type: "image/jpeg", url: "https://cdn.example.com/c.jpg" }] } },
|
||||
headers: {}, query: {}, req: {} as never
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(received.length, 2);
|
||||
assert.equal(received[1].text, "看图");
|
||||
assert.equal(received[1].attachments, undefined);
|
||||
} finally { globalThis.fetch = original; }
|
||||
});
|
||||
|
||||
test("sendMessage uses C2C endpoint and forwards reply sequences as msg_seq", async () => {
|
||||
const original = globalThis.fetch; const urls: string[] = []; const bodies: Array<Record<string, unknown>> = [];
|
||||
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
|
||||
Reference in New Issue
Block a user