feat: send workspace images to QQ chats
Worker results may report image attachments stored inside the workspace; the runtime validates them (containment, png/jpg magic, size) and delivers them with the pending event. Assistants gain a send_image action so users can ask for an image later. QQ uploads via /files with base64 file_data and sends msg_type 7 rich media, sharing the same msg_id/msg_seq counter as text replies; non-image adapters flatten images to text.
This commit is contained in:
Vendored
+16
@@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import * as acp from "@agentclientprotocol/sdk";
|
||||
|
||||
const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
|
||||
const pending = new Map();
|
||||
const logFile = process.env.FAKE_ACP_LOG;
|
||||
const log = (entry) => { if (logFile) fs.appendFileSync(logFile, `${JSON.stringify(entry)}\n`); };
|
||||
@@ -84,6 +87,7 @@ const app = acp.agent({ name: "fake-acp-agent" })
|
||||
else if (userText === "finish") response = assistantEnvelope("finishing", [{ type: "finish" }]);
|
||||
else if (userText.startsWith("follow up:")) response = assistantEnvelope("following up", [{ type: "follow_up", instruction: userText.slice("follow up:".length).trim() }]);
|
||||
else if (userText.startsWith("adjust proposal:")) response = assistantEnvelope("adjusting", [{ type: "adjust_proposal", title: userText.slice("adjust proposal:".length).trim() }]);
|
||||
else if (userText.startsWith("send image:")) response = assistantEnvelope("sending image", [{ type: "send_image", path: userText.slice("send image:".length).trim() }]);
|
||||
else if (userText === "start next") response = assistantEnvelope("starting next", [{ type: "start_next" }]);
|
||||
else if (userText === "stop") response = assistantEnvelope("stopping", [{ type: "stop" }]);
|
||||
else if (userText === "cancel proposal") response = assistantEnvelope("cancelling", [{ type: "cancel" }]);
|
||||
@@ -117,6 +121,18 @@ const app = acp.agent({ name: "fake-acp-agent" })
|
||||
if (process.env.FAKE_ACP_WORKER_GATE_FILE) {
|
||||
while (!fs.existsSync(process.env.FAKE_ACP_WORKER_GATE_FILE)) await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
if (goal.includes("attachbad")) {
|
||||
await update(client, params.sessionId, workerEnvelope({ status: "PENDING", summary: "made a pic", attachments: [{ path: "/tmp/evil.png" }, { path: "not-an-image.txt" }] }));
|
||||
return { stopReason: "end_turn" };
|
||||
}
|
||||
if (goal.includes("attach")) {
|
||||
const outbox = path.join(process.cwd(), ".gori-outbox");
|
||||
fs.mkdirSync(outbox, { recursive: true });
|
||||
fs.writeFileSync(path.join(outbox, "shot.png"), Buffer.concat([PNG_HEADER, Buffer.from("fake-png-payload-v1")]));
|
||||
fs.writeFileSync(path.join(process.cwd(), "not-an-image.txt"), "plain text");
|
||||
await update(client, params.sessionId, workerEnvelope({ status: "PENDING", summary: "made a pic", attachments: [{ path: ".gori-outbox/shot.png", mimeType: "image/png" }] }));
|
||||
return { stopReason: "end_turn" };
|
||||
}
|
||||
const result = goal.includes("ask")
|
||||
? { status: "PENDING", summary: "hit a dirty target", question: "May I overwrite it?", workspaceDirty: true }
|
||||
: goal.includes("fail")
|
||||
|
||||
Reference in New Issue
Block a user