diff --git a/src/StdUtils.ts b/src/StdUtils.ts index 3d9e2159..9bd36600 100644 --- a/src/StdUtils.ts +++ b/src/StdUtils.ts @@ -1,4 +1,5 @@ import {Readable, Writable} from "node:stream"; +import {StringDecoder} from "node:string_decoder"; import {Emitter} from "vscode-jsonrpc/node"; import type {DataCallback, Disposable, Message, MessageReader, MessageWriter, PartialMessageInfo} from "vscode-jsonrpc/node"; import * as acp from "@agentclientprotocol/sdk"; @@ -32,9 +33,10 @@ export function createJSONRPCWriter(writable: Writable): MessageWriter { export function createJSONRPCReader(readable: Readable): MessageReader { return { listen(callback: DataCallback): Disposable { + const decoder = new StringDecoder('utf8'); let buf = ''; - const onData = (chunk: Buffer) => { - buf += chunk.toString(); + const onData = (chunk: Buffer | string) => { + buf += typeof chunk === 'string' ? chunk : decoder.write(chunk); for (;;) { const i = buf.indexOf('\n'); if (i < 0) break; diff --git a/src/__tests__/StdUtils.test.ts b/src/__tests__/StdUtils.test.ts new file mode 100644 index 00000000..97e302d1 --- /dev/null +++ b/src/__tests__/StdUtils.test.ts @@ -0,0 +1,27 @@ +import {PassThrough} from "node:stream"; +import {describe, expect, it} from "vitest"; +import {createJSONRPCReader} from "../StdUtils"; + +describe("createJSONRPCReader", () => { + it("preserves UTF-8 characters split across chunks", () => { + const readable = new PassThrough(); + const messages: unknown[] = []; + const listener = createJSONRPCReader(readable).listen((message) => messages.push(message)); + const payload = Buffer.from(JSON.stringify({id: 1, result: {text: "服务治理需求"}}) + "\n"); + const character = Buffer.from("需"); + const characterOffset = payload.indexOf(character); + + readable.write(payload.subarray(0, characterOffset + 1)); + readable.write(payload.subarray(characterOffset + 1, characterOffset + 2)); + readable.write(payload.subarray(characterOffset + 2)); + + expect(messages).toEqual([{ + jsonrpc: "2.0", + id: 1, + result: {text: "服务治理需求"}, + }]); + + listener.dispose(); + readable.destroy(); + }); +});