mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 08:54:23 +08:00
feat: rename package to @basketikun/canvas-agent and update related configurations
This commit is contained in:
+176
-9
@@ -11,6 +11,8 @@ import type { AgentAttachment, AgentEmit } from "./types.js";
|
||||
type Json = Record<string, unknown>;
|
||||
type AgentEvent = Json & { type: string; usage?: unknown };
|
||||
type PendingRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void };
|
||||
type CodexRunOptions = { threadId?: string; cwd?: string };
|
||||
type AgentHistoryMessage = { id: string; role: "user" | "assistant" | "tool" | "error"; title?: string; text: string; detail?: unknown; streamId?: string };
|
||||
|
||||
let codexQueue: Promise<unknown> = Promise.resolve();
|
||||
let codexApp: CodexAppClient | null = null;
|
||||
@@ -22,19 +24,19 @@ export function withAgentPrompt(prompt: string) {
|
||||
return prompt.trim() ? `${AGENT_PROMPT}\n\n用户请求:${prompt}` : "";
|
||||
}
|
||||
|
||||
export async function runCodexTurn(prompt: string, emit: AgentEmit, attachments: AgentAttachment[] = []) {
|
||||
export async function runCodexTurn(prompt: string, emit: AgentEmit, attachments: AgentAttachment[] = [], options: CodexRunOptions = {}) {
|
||||
if (!prompt.trim()) return;
|
||||
codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments));
|
||||
codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments, options));
|
||||
await codexQueue;
|
||||
}
|
||||
|
||||
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[]) {
|
||||
async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: AgentAttachment[], options: CodexRunOptions) {
|
||||
let files: string[] = [];
|
||||
try {
|
||||
files = await writeAttachmentFiles(attachments);
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
codexThreadId ||= await codexApp.startThread();
|
||||
await codexApp.startTurn(codexThreadId, prompt, files);
|
||||
const threadId = await ensureCodexThread(codexApp, options);
|
||||
await codexApp.startTurn(threadId, prompt, files);
|
||||
} catch (error) {
|
||||
emit("agent_error", { message: errorMessage(error) });
|
||||
} finally {
|
||||
@@ -42,6 +44,46 @@ async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: Age
|
||||
}
|
||||
}
|
||||
|
||||
export async function startCodexThread(emit: AgentEmit, cwd?: string) {
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
const thread = await codexApp.startThread(cwd);
|
||||
codexThreadId = String(field(thread, "id") || "");
|
||||
return thread;
|
||||
}
|
||||
|
||||
export async function resumeCodexThread(emit: AgentEmit, threadId: string, cwd?: string) {
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
const thread = await codexApp.resumeThread(threadId, cwd);
|
||||
codexThreadId = String(field(thread, "id") || threadId);
|
||||
return { thread, messages: threadMessages(thread) };
|
||||
}
|
||||
|
||||
export async function listCodexThreads(emit: AgentEmit, options: { cwd?: string; all?: boolean; searchTerm?: string; limit?: number }) {
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
const result = await codexApp.listThreads({
|
||||
limit: options.limit || 40,
|
||||
sortKey: "updated_at",
|
||||
sortDirection: "desc",
|
||||
sourceKinds: ["cli", "vscode", "appServer", "exec"],
|
||||
...(options.all ? {} : { cwd: options.cwd }),
|
||||
...(options.searchTerm ? { searchTerm: options.searchTerm } : {}),
|
||||
});
|
||||
const data = Array.isArray(field(result, "data")) ? (field(result, "data") as unknown[]).map(summarizeCodexThread) : [];
|
||||
return { data, nextCursor: field(result, "nextCursor") || null, backwardsCursor: field(result, "backwardsCursor") || null };
|
||||
}
|
||||
|
||||
export async function readCodexThread(emit: AgentEmit, threadId: string) {
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
const result = await codexApp.readThread(threadId);
|
||||
const thread = field(result, "thread") || {};
|
||||
return { thread: summarizeCodexThread(thread), messages: threadMessages(thread) };
|
||||
}
|
||||
|
||||
export async function archiveCodexThread(emit: AgentEmit, threadId: string) {
|
||||
codexApp ||= await CodexAppClient.start(emit);
|
||||
await codexApp.archiveThread(threadId);
|
||||
}
|
||||
|
||||
export function runClaudeTurn(prompt: string, emit: AgentEmit) {
|
||||
if (!prompt.trim()) return;
|
||||
const child = spawnAgent("claude", ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--allowedTools", "mcp__infinite-canvas__*", prompt], ["ignore", "pipe", "pipe"], emit);
|
||||
@@ -49,6 +91,21 @@ export function runClaudeTurn(prompt: string, emit: AgentEmit) {
|
||||
pipeJsonLines(child, emit, "claude");
|
||||
}
|
||||
|
||||
async function ensureCodexThread(app: CodexAppClient, options: CodexRunOptions) {
|
||||
if (options.threadId) {
|
||||
if (codexThreadId !== options.threadId) {
|
||||
const thread = await app.resumeThread(options.threadId, options.cwd);
|
||||
codexThreadId = String(field(thread, "id") || options.threadId);
|
||||
}
|
||||
return codexThreadId;
|
||||
}
|
||||
if (!codexThreadId) {
|
||||
const thread = await app.startThread(options.cwd);
|
||||
codexThreadId = String(field(thread, "id") || "");
|
||||
}
|
||||
return codexThreadId;
|
||||
}
|
||||
|
||||
class CodexAppClient {
|
||||
private nextId = 1;
|
||||
private buffer = "";
|
||||
@@ -78,11 +135,32 @@ class CodexAppClient {
|
||||
return client;
|
||||
}
|
||||
|
||||
async startThread() {
|
||||
const result = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), threadSource: "user" });
|
||||
const id = String(field(field(result, "thread"), "id") || "");
|
||||
async startThread(cwd?: string) {
|
||||
const result = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}), threadSource: "user" });
|
||||
const thread = field(result, "thread") as Json | undefined;
|
||||
const id = String(field(thread, "id") || "");
|
||||
if (!id) throw new Error("Codex app-server 没有返回 thread id");
|
||||
return id;
|
||||
return thread || {};
|
||||
}
|
||||
|
||||
async resumeThread(threadId: string, cwd?: string) {
|
||||
const result = await this.request("thread/resume", { threadId, approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}) });
|
||||
const thread = field(result, "thread") as Json | undefined;
|
||||
const id = String(field(thread, "id") || "");
|
||||
if (!id) throw new Error("Codex app-server 没有返回 thread id");
|
||||
return thread || {};
|
||||
}
|
||||
|
||||
listThreads(params: Json) {
|
||||
return this.request("thread/list", params);
|
||||
}
|
||||
|
||||
readThread(threadId: string) {
|
||||
return this.request("thread/read", { threadId, includeTurns: true });
|
||||
}
|
||||
|
||||
archiveThread(threadId: string) {
|
||||
return this.request("thread/archive", { threadId });
|
||||
}
|
||||
|
||||
async startTurn(threadId: string, prompt: string, images: string[]) {
|
||||
@@ -245,6 +323,95 @@ function field(value: unknown, key: string) {
|
||||
return value && typeof value === "object" ? (value as Json)[key] : undefined;
|
||||
}
|
||||
|
||||
export function summarizeCodexThread(thread: unknown) {
|
||||
return {
|
||||
id: String(field(thread, "id") || ""),
|
||||
sessionId: String(field(thread, "sessionId") || ""),
|
||||
preview: displayUserText(String(field(thread, "preview") || "")),
|
||||
name: stringOrNull(field(thread, "name")),
|
||||
cwd: String(field(thread, "cwd") || ""),
|
||||
status: String(field(thread, "status") || ""),
|
||||
source: field(thread, "source"),
|
||||
threadSource: field(thread, "threadSource"),
|
||||
createdAt: Number(field(thread, "createdAt") || 0),
|
||||
updatedAt: Number(field(thread, "updatedAt") || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function threadMessages(thread: unknown): AgentHistoryMessage[] {
|
||||
const turns = arrayValue(field(thread, "turns"));
|
||||
const messages: AgentHistoryMessage[] = [];
|
||||
turns.forEach((turn, turnIndex) => {
|
||||
arrayValue(field(turn, "items")).forEach((item, itemIndex) => {
|
||||
const type = String(field(item, "type") || "");
|
||||
const id = String(field(item, "id") || `${turnIndex}-${itemIndex}`);
|
||||
if (type === "userMessage") {
|
||||
const text = displayUserText(userInputText(field(item, "content")));
|
||||
if (text) messages.push({ id, role: "user", text });
|
||||
}
|
||||
if (type === "agentMessage") {
|
||||
const text = String(field(item, "text") || "").trim();
|
||||
if (text) messages.push({ id, role: "assistant", title: "Codex", text, streamId: id });
|
||||
}
|
||||
if (type === "mcpToolCall") {
|
||||
const tool = String(field(item, "tool") || "工具调用");
|
||||
const error = field(field(item, "error"), "message");
|
||||
messages.push({ id, role: error ? "error" : "tool", title: toolName(tool), text: error ? String(error) : `${toolName(tool)} ${String(field(item, "status") || "完成")}`, detail: item });
|
||||
}
|
||||
if (type === "commandExecution") {
|
||||
const command = String(field(item, "command") || "").trim();
|
||||
if (command) messages.push({ id, role: "tool", title: "命令", text: command, detail: { cwd: field(item, "cwd"), status: field(item, "status"), exitCode: field(item, "exitCode") } });
|
||||
}
|
||||
if (type === "fileChange") messages.push({ id, role: "tool", title: "文件变更", text: "Codex 修改了文件", detail: item });
|
||||
});
|
||||
});
|
||||
return messages.filter((item) => item.text).slice(-120);
|
||||
}
|
||||
|
||||
function userInputText(content: unknown) {
|
||||
return arrayValue(content)
|
||||
.map((item) => {
|
||||
const type = String(field(item, "type") || "");
|
||||
if (type === "text") return String(field(item, "text") || "");
|
||||
if (type === "image" || type === "localImage") return "图片附件";
|
||||
if (type === "mention") return `@${String(field(item, "name") || "文件")}`;
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function displayUserText(text: string) {
|
||||
const value = text.trim();
|
||||
const marker = "用户请求:";
|
||||
const index = value.lastIndexOf(marker);
|
||||
return (index >= 0 ? value.slice(index + marker.length) : value).trim();
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function stringOrNull(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function toolName(name: string) {
|
||||
if (name === "canvas_apply_ops") return "画布操作";
|
||||
if (name === "canvas_get_state") return "读取画布";
|
||||
if (name === "canvas_get_selection") return "读取选区";
|
||||
if (name === "canvas_export_snapshot") return "导出快照";
|
||||
if (name === "canvas_create_text_node") return "创建文本";
|
||||
if (name === "canvas_create_image_prompt_flow") return "创建生图流程";
|
||||
if (name === "canvas_create_generation_flow") return "创建生成流程";
|
||||
if (name === "canvas_generate_text") return "生成文本";
|
||||
if (name === "canvas_generate_image") return "生成图片";
|
||||
if (name === "canvas_generate_video") return "生成视频";
|
||||
if (name === "canvas_generate_audio") return "生成音频";
|
||||
if (name === "canvas_run_generation") return "触发生成";
|
||||
return name;
|
||||
}
|
||||
|
||||
async function writeAttachmentFiles(attachments: AgentAttachment[]) {
|
||||
return await Promise.all(attachments.filter((item) => item.dataUrl?.startsWith("data:image/")).map(writeAttachmentFile));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user