Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ yarn.lock
.claude

CLAUDE.md
.omc

test-results
playwright-report
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"@eslint/compat": "^1.4.1",
"@eslint/js": "9.39.2",
"@playwright/test": "^1.58.2",
"@rspack/cli": "^1.7.6",
"@rspack/core": "^1.6.8",
"@rspack/cli": "^1.7.11",
"@rspack/core": "^1.7.11",
"@swc/helpers": "^0.5.17",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
Expand Down Expand Up @@ -107,7 +107,7 @@
"unocss": "66.5.4",
"vitest": "^4.0.18"
},
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.33.0",
"sideEffects": [
"**/*.css",
"**/*.scss",
Expand Down
1,453 changes: 430 additions & 1,023 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minimumReleaseAge: 10080
41 changes: 40 additions & 1 deletion src/app/service/agent/core/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export function parseOpenAIStream(
// 标记是否已通过 [DONE] 信号发出了 done 事件,避免 .then() 再次发出
let doneSent = false;

// 跨 chunk 追踪 <think>...</think> 块状态(用于把思考混在 content 里的模型)
let inThinkBlock = false;
Comment on lines +153 to +154

return readSSEStream(
reader,
signal,
Expand Down Expand Up @@ -196,7 +199,43 @@ export function parseOpenAIStream(
}
}
} else {
onEvent({ type: "content_delta", delta: delta.content });
// 处理 <think>...</think> 内联标签(reasoning 模型)
// 思考内容路由为 thinking_delta,避免裸露标签出现在对话里
let remaining: string = delta.content;

while (remaining.length > 0) {
if (inThinkBlock) {
// 已在 think 块内,找结束标签
const endIdx = remaining.indexOf("</think>");
if (endIdx === -1) {
// 整段都是思考内容
onEvent({ type: "thinking_delta", delta: remaining });
remaining = "";
} else {
// 结束标签之前是思考内容,之后是正文
if (endIdx > 0) {
onEvent({ type: "thinking_delta", delta: remaining.slice(0, endIdx) });
}
inThinkBlock = false;
remaining = remaining.slice(endIdx + "</think>".length);
}
} else {
// 不在 think 块内,找开始标签
const startIdx = remaining.indexOf("<think>");
if (startIdx === -1) {
// 整段都是正文
onEvent({ type: "content_delta", delta: remaining });
remaining = "";
} else {
// 开始标签之前是正文,之后进入思考块
if (startIdx > 0) {
onEvent({ type: "content_delta", delta: remaining.slice(0, startIdx) });
}
inThinkBlock = true;
remaining = remaining.slice(startIdx + "<think>".length);
}
}
}
Comment on lines +227 to +259
Comment on lines 153 to +259
Comment on lines +227 to +259
}
}

Expand Down
98 changes: 98 additions & 0 deletions src/app/service/service_worker/resource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { initTestEnv } from "@Tests/utils";
import { ResourceService } from "./resource";
import { vi, describe, it, expect, beforeEach } from "vitest";
import type { Group } from "@Packages/message/server";
import type { IMessageQueue } from "@Packages/message/message_queue";

initTestEnv();

// mock fetch
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

// 创建文本 blob 和二进制 blob 的辅助函数
function textBlob(content: string, contentType = "text/plain") {
return new Blob([content], { type: contentType });
}

function binaryBlob(bytes: number[]) {
return new Blob([new Uint8Array(bytes)], { type: "application/octet-stream" });
}

function mockResponse(blob: Blob, status = 200, contentType?: string) {
return {
status,
blob: () => Promise.resolve(blob),
headers: new Headers(contentType ? { "content-type": contentType } : {}),
} as unknown as Response;
}

describe("ResourceService - loadByUrl", () => {
let service: ResourceService;

beforeEach(() => {
vi.clearAllMocks();
const mockGroup = {} as Group;
const mockMQ = {} as IMessageQueue;
service = new ResourceService(mockGroup, mockMQ);
// calculateHash 不影响核心逻辑,直接 mock
vi.spyOn(service, "calculateHash").mockResolvedValue({
md5: "mock-md5",
sha1: "",
sha256: "",
sha384: "",
sha512: "",
});
});

it("加载文本资源(require)时应设置 content", async () => {
const jsCode = "console.log('hello');";
mockFetch.mockResolvedValue(mockResponse(textBlob(jsCode), 200, "application/javascript; charset=utf-8"));

const res = await service.loadByUrl("https://example.com/lib.js", "require");

expect(res.url).toBe("https://example.com/lib.js");
expect(res.content).toBeTruthy();
expect(res.contentType).toBe("application/javascript");
expect(res.base64).toBeTruthy();
expect(res.type).toBe("require");
});

it("加载文本资源(resource)时应通过 blob.text() 设置 content", async () => {
const text = "plain text content";
mockFetch.mockResolvedValue(mockResponse(textBlob(text), 200, "text/plain"));

const res = await service.loadByUrl("https://example.com/data.txt", "resource");

expect(res.content).toBe(text);
expect(res.type).toBe("resource");
});

it("加载二进制资源时 content 应为空", async () => {
// 包含 null 字节的二进制数据,isText 会返回 false
const bytes = [0x89, 0x50, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00];
mockFetch.mockResolvedValue(mockResponse(binaryBlob(bytes), 200, "image/png"));

const res = await service.loadByUrl("https://example.com/img.png", "resource");

expect(res.content).toBe("");
expect(res.base64).toBeTruthy();
expect(res.contentType).toBe("image/png");
});

it("响应非200时应抛出异常", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob(""), 404));

await expect(service.loadByUrl("https://example.com/404", "require")).rejects.toThrow(
"resource response status not 200: 404"
);
});

it("没有 content-type 时应默认为 application/octet-stream", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob("data"), 200));

const res = await service.loadByUrl("https://example.com/noct", "resource");

expect(res.contentType).toBe("application/octet-stream");
});
});
3 changes: 1 addition & 2 deletions src/app/service/service_worker/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export class ResourceService {
throw new Error(`resource response status not 200: ${resp.status}`);
}
const data = await resp.blob();
const [hash, arrayBuffer, base64] = await Promise.all([
const [hash, uint8Array, base64] = await Promise.all([
this.calculateHash(data),
blobToUint8Array(data),
blobToBase64(data),
Expand All @@ -280,7 +280,6 @@ export class ResourceService {
type,
createtime: Date.now(),
};
const uint8Array = new Uint8Array(arrayBuffer);
if (isText(uint8Array)) {
if (type === "require" || type === "require-css") {
resource.content = await readBlobContent(data, contentType); // @require和@require-css 是会转换成代码运行的,可以进行解码
Expand Down
13 changes: 11 additions & 2 deletions src/pages/options/routes/AgentProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ function AgentProvider() {
let chatUrl: string;
let body: string;

const systemMessage = "Reply in one brief sentence only. No thinking or reasoning.";
const userMessage = "Greet the user warmly in a short, concise sentence.";

if (editingModel.provider === "anthropic") {
chatUrl = `${baseUrl}/v1/messages`;
headers["x-api-key"] = editingModel.apiKey;
Expand All @@ -385,7 +388,9 @@ function AgentProvider() {
body = JSON.stringify({
model: editingModel.model || "claude-sonnet-4-20250514",
max_tokens: 256,
messages: [{ role: "user", content: "hi" }],
system: systemMessage,
messages: [{ role: "user", content: userMessage }],
stream: false,
});
} else {
chatUrl = `${baseUrl}/chat/completions`;
Expand All @@ -396,7 +401,11 @@ function AgentProvider() {
body = JSON.stringify({
model: editingModel.model || defaultModel,
max_tokens: 256,
messages: [{ role: "user", content: "hi" }],
messages: [
{ role: "system", content: systemMessage },
{ role: "user", content: userMessage },
],
stream: false,
});
Comment on lines 401 to 409
}

Expand Down
20 changes: 10 additions & 10 deletions src/pkg/utils/monaco-editor/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("findGlobalInsertionInfo", () => {
});

it("应该处理包含global关键字的多行块注释", () => {
const model = createMockModel(["/* global jQuery,", " axios */", "const x = 1;"]);
const model = createMockModel(["/* global jQuery,", " moment */", "const x = 1;"]);
const result = findGlobalInsertionInfo(model);
expect(result).toEqual({ insertLine: 3, globalLine: 1 });
});
Expand Down Expand Up @@ -106,8 +106,8 @@ describe("updateGlobalCommentLine", () => {

it("应该在注释末尾添加新的全局变量", () => {
const line = "/* global jQuery */";
const result = updateGlobalCommentLine(line, "axios");
expect(result).toBe("/* global jQuery, axios */");
const result = updateGlobalCommentLine(line, "moment");
expect(result).toBe("/* global jQuery, moment */");
});

it("应该在只有global关键字的注释后添加变量", () => {
Expand All @@ -118,8 +118,8 @@ describe("updateGlobalCommentLine", () => {

it("应该处理以逗号结尾的注释", () => {
const line = "/* global jQuery, */";
const result = updateGlobalCommentLine(line, "axios");
expect(result).toBe("/* global jQuery, axios */");
const result = updateGlobalCommentLine(line, "moment");
expect(result).toBe("/* global jQuery, moment */");
});

it("应该处理多个已存在的全局变量", () => {
Expand All @@ -130,18 +130,18 @@ describe("updateGlobalCommentLine", () => {

it("应该处理注释后有额外内容的情况", () => {
const line = "/* global jQuery */ // some comment";
const result = updateGlobalCommentLine(line, "axios");
expect(result).toBe("/* global jQuery, axios */ // some comment");
const result = updateGlobalCommentLine(line, "moment");
expect(result).toBe("/* global jQuery, moment */ // some comment");
});

it("应该处理格式不正确的注释(缺少*/)", () => {
const line = "/* global jQuery";
const result = updateGlobalCommentLine(line, "axios");
expect(result).toBe("/* global jQuery, axios");
const result = updateGlobalCommentLine(line, "moment");
expect(result).toBe("/* global jQuery, moment");
});

it("应该避免重复添加相同的全局变量", () => {
const line = "/* global jQuery, axios */";
const line = "/* global jQuery, moment */";
const result = updateGlobalCommentLine(line, "jQuery");
expect(result).toBe(line);
});
Expand Down
Loading