This commit is contained in:
ZacharyZhang-NY committed 2026-03-20 00:12:38 -04:00
1 parent 9510e1c858
commit 6b47e95d36
19 files changed
+195 -1409

No files matched your search

+26 -2
View File
@@ -10,6 +10,29 @@ import type { AppDatabase } from "./database";
import { dispatchAgentMessage } from "./message-dispatch";
import type { DesktopEvent } from "@shared/types";
/**
* Decode an HTTP request body to a UTF-8 string, handling the common Windows
* encoding issue where Chinese/CJK characters arrive in GBK (CP936) instead
* of UTF-8. On Chinese Windows the system ANSI code page is GBK; tools like
* curl encode command-line arguments using that code page when stdout is piped
* (chcp 65001 does not help because there is no real console). We try strict
* UTF-8 first and, only if the bytes are invalid UTF-8, fall back to GBK.
*/
function decodeBodyToUtf8(raw: Buffer): string {
if (raw.length === 0) return "";
try {
return new TextDecoder("utf-8", { fatal: true }).decode(raw);
} catch {
// Bytes are not valid UTF-8 — most likely GBK from Chinese Windows.
try {
return new TextDecoder("gbk").decode(raw);
} catch {
// Last resort: lossy UTF-8 (replaces invalid bytes with U+FFFD).
return raw.toString("utf8");
}
}
}
type RequestHandler = (
req: IncomingMessage,
res: ServerResponse,
@@ -234,7 +257,8 @@ export class AgentApiServer {
});
req.on("end", () => {
try {
const data = Buffer.concat(chunks).toString("utf8");
const raw = Buffer.concat(chunks);
const data = decodeBodyToUtf8(raw);
resolve(data ? JSON.parse(data) : null);
}
catch { reject(new Error("Invalid JSON")); }
@@ -244,7 +268,7 @@ export class AgentApiServer {
}
private json(res: ServerResponse, status: number, data: unknown) {
res.writeHead(status, { "Content-Type": "application/json" });
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(data));
}
+33 -4
View File
@@ -76,6 +76,32 @@ function safeRealpath(targetPath: string) {
}
}
/**
* Find the last complete UTF-8 character boundary in a buffer.
* Returns the number of bytes up to (and including) the last complete character.
* This prevents corrupting multi-byte CJK characters when slicing at chunk edges.
*/
function findUtf8Boundary(buffer: Buffer, bytesRead: number): number {
if (bytesRead === 0) return 0;
// Walk backwards past continuation bytes (10xxxxxx)
let pos = bytesRead - 1;
while (pos >= 0 && (buffer[pos] & 0xc0) === 0x80) {
pos--;
}
if (pos < 0) return bytesRead;
const lead = buffer[pos];
let charLen: number;
if ((lead & 0x80) === 0) charLen = 1;
else if ((lead & 0xe0) === 0xc0) charLen = 2;
else if ((lead & 0xf0) === 0xe0) charLen = 3;
else if ((lead & 0xf8) === 0xf0) charLen = 4;
else return bytesRead;
// If the character is complete within the buffer, keep all bytes
if (pos + charLen <= bytesRead) return bytesRead;
// Otherwise truncate before the incomplete character
return pos;
}
function taskPriorityOrderSql(columnName: string) {
return `case ${columnName}
when 'critical' then 4
@@ -1156,14 +1182,14 @@ export class AppDatabase {
.run(id, input.companyId, input.taskId, input.agentId, input.workspaceId, input.connectorId, logPath, ts, ts);
this.db.prepare("update tasks set active_run_id = ?, updated_at = ? where id = ?").run(id, ts, input.taskId);
});
writeFileSync(logPath, "");
writeFileSync(logPath, "", "utf8");
return { id, logPath };
}
appendRunLog(runId: string, stream: "stdout" | "stderr", chunk: string) {
const run = this.db.prepare("select log_path as logPath from runs where id = ?").get(runId) as { logPath: string | null } | undefined;
if (!run?.logPath) return;
return appendFile(run.logPath, `[${new Date().toISOString()}][${stream}] ${chunk}`);
return appendFile(run.logPath, `[${new Date().toISOString()}][${stream}] ${chunk}`, "utf8");
}
readRunLog(runId: string) {
@@ -1207,9 +1233,12 @@ export class AppDatabase {
try {
const buffer = Buffer.allocUnsafe(bytesToRead);
const bytesRead = readSync(fileDescriptor, buffer, 0, bytesToRead, safeOffset);
const nextOffset = safeOffset + bytesRead;
// Find the last complete UTF-8 character boundary to avoid corrupting
// multi-byte characters (e.g. Chinese/CJK 3-byte sequences) at the edge.
const safeBytesRead = findUtf8Boundary(buffer, bytesRead);
const nextOffset = safeOffset + safeBytesRead;
return {
content: buffer.subarray(0, bytesRead).toString("utf8"),
content: buffer.subarray(0, safeBytesRead).toString("utf8"),
offset: safeOffset,
nextOffset,
totalBytes,
+22 -2
View File
@@ -1,4 +1,5 @@
import { existsSync } from "node:fs";
import { extname } from "node:path";
import { pathToFileURL } from "node:url";
import { resolve, sep } from "node:path";
import {
@@ -81,14 +82,33 @@ export function createDesktopShellService({
}
function registerProtocol() {
protocol.handle("app", (request) => {
const textMimeTypes: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml; charset=utf-8",
};
protocol.handle("app", async (request) => {
const url = new URL(request.url);
const relativePath = decodeURIComponent(url.pathname === "/" ? "index.html" : url.pathname.replace(/^\/+/, ""));
const targetPath = resolve(rendererDist, relativePath);
if (!isWithinDirectory(rendererDist, targetPath) || !existsSync(targetPath)) {
return new Response("Not found", { status: 404 });
}
return net.fetch(pathToFileURL(targetPath).toString());
const response = await net.fetch(pathToFileURL(targetPath).toString());
const mime = textMimeTypes[extname(targetPath).toLowerCase()];
if (mime) {
const headers = new Headers(response.headers);
headers.set("Content-Type", mime);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
return response;
});
}
+27 -18
View File
@@ -451,38 +451,47 @@ Do not print or echo AGENT_COMPANY_API_KEY into comments, summaries, or external
## Authentication & Request Format
Every API call MUST include these headers:
Authorization: Bearer $AGENT_COMPANY_API_KEY
Content-Type: application/json
Content-Type: application/json; charset=utf-8
X-Agent-Company-Run-Id: ${ctx.runId}
Example using curl:
curl -s -X GET "$AGENT_COMPANY_API_URL/api/agents/me" \\
-H "Authorization: Bearer $AGENT_COMPANY_API_KEY" \\
-H "Content-Type: application/json" \\
-H "X-Agent-Company-Run-Id: ${ctx.runId}"
IMPORTANT: On Windows, prefer using Node.js fetch() for API calls instead of curl,
because curl on Windows may corrupt non-ASCII characters (Chinese, Japanese, etc.).
If you must use curl, write the JSON body to a temp file first and use curl -d @file.
Example using fetch (Node.js):
Example using fetch (Node.js) — PREFERRED:
const res = await fetch(\`\${process.env.AGENT_COMPANY_API_URL}/api/agents/me\`, {
headers: {
"Authorization": \`Bearer \${process.env.AGENT_COMPANY_API_KEY}\`,
"Content-Type": "application/json",
"Content-Type": "application/json; charset=utf-8",
"X-Agent-Company-Run-Id": "${ctx.runId}"
}
});
const data = await res.json();
Example POST using curl:
Example POST using fetch (Node.js) — PREFERRED for non-ASCII content:
const res = await fetch(\`\${process.env.AGENT_COMPANY_API_URL}/api/companies/${ctx.agent.companyId}/issues\`, {
method: "POST",
headers: {
"Authorization": \`Bearer \${process.env.AGENT_COMPANY_API_KEY}\`,
"Content-Type": "application/json; charset=utf-8",
"X-Agent-Company-Run-Id": "${ctx.runId}"
},
body: JSON.stringify({title:"Task title",description:"Details",assigneeAgentId:"agent-id",priority:"medium",status:"todo"})
});
Example GET using curl:
curl -s -X GET "$AGENT_COMPANY_API_URL/api/agents/me" \\
-H "Authorization: Bearer $AGENT_COMPANY_API_KEY" \\
-H "Content-Type: application/json; charset=utf-8" \\
-H "X-Agent-Company-Run-Id: ${ctx.runId}"
Example POST using curl (write body to temp file to avoid encoding issues):
echo '{"title":"Task title","description":"Details","priority":"medium","status":"todo"}' > /tmp/body.json
curl -s -X POST "$AGENT_COMPANY_API_URL/api/companies/${ctx.agent.companyId}/issues" \\
-H "Authorization: Bearer $AGENT_COMPANY_API_KEY" \\
-H "Content-Type: application/json" \\
-H "Content-Type: application/json; charset=utf-8" \\
-H "X-Agent-Company-Run-Id: ${ctx.runId}" \\
-d '{"title":"Task title","description":"Details","assigneeAgentId":"agent-id","priority":"medium","status":"todo"}'
Example PATCH using curl:
curl -s -X PATCH "$AGENT_COMPANY_API_URL/api/issues/ISSUE_ID" \\
-H "Authorization: Bearer $AGENT_COMPANY_API_KEY" \\
-H "Content-Type: application/json" \\
-H "X-Agent-Company-Run-Id: ${ctx.runId}" \\
-d '{"status":"in_review","comment":"Work completed. Summary of changes: ..."}'
-d @/tmp/body.json
IMPORTANT: Always check the HTTP response status. 200/201 = success, 4xx = client error (read the error message), 5xx = server error (retry once then report).
+4
View File
@@ -323,6 +323,10 @@ export function App() {
}
}, [snapshot, locale]);
useEffect(() => {
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
}, [locale]);
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
setLocale(newLocale);
try {
+10 -2
View File
@@ -1,5 +1,13 @@
@import "tailwindcss";
@theme {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB",
"Noto Sans CJK SC", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono Variable", "Microsoft YaHei", ui-monospace,
SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:root {
color-scheme: light;
--bg: #fbfbfb;
@@ -82,7 +90,7 @@ body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", Helvetica, Arial, sans-serif;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
@@ -120,7 +128,7 @@ select {
}
.mono {
font-family: "JetBrains Mono Variable", "Microsoft YaHei", monospace;
font-family: var(--font-mono);
}
.surface {