Skip to content

Commit 1d8b6f4

Browse files
committed
♻️ AgentChatRepo 改为模块单例消除 setter fan-out
AgentChatRepo 是 OPFS 的无状态包装,原本通过 AgentService._repo 配合 setter 同步给 4 个子服务 (CompactService/LLMClient/ ToolLoopOrchestrator/ChatService),新增子服务容易遗漏同步。 改为模块级单例 agentChatRepo,子服务直接 import 使用,测试用 vi.hoisted + vi.mock 替换整个模块,彻底消除 setter 同步仪式。
1 parent 61a7aaa commit 1d8b6f4

File tree

8 files changed

+83
-80
lines changed

8 files changed

+83
-80
lines changed

src/app/repo/agent_chat.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,7 @@ export class AgentChatRepo extends OPFSRepo {
180180
await this.deleteFile(`${conversationId}.json`, tasksDir);
181181
}
182182
}
183+
184+
// 模块级单例:AgentChatRepo 是 OPFS 的无状态薄包装,无需每处 new。
185+
// 子服务直接 import 使用,测试通过 vi.mock 替换整个模块。
186+
export const agentChatRepo = new AgentChatRepo();

src/app/service/agent/core/agent.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import { AgentService } from "@App/app/service/agent/service_worker/agent";
88
import type { ToolRegistry } from "./tool_registry";
99
import type { ToolExecutor } from "./tool_registry";
1010

11+
// mock agent_chat repo 单例:子服务通过 import { agentChatRepo } 直接使用该 mock 对象
12+
const { mockChatRepo } = vi.hoisted(() => ({
13+
mockChatRepo: {} as any,
14+
}));
15+
16+
vi.mock("@App/app/repo/agent_chat", () => ({
17+
AgentChatRepo: class {},
18+
agentChatRepo: mockChatRepo,
19+
}));
20+
1121
// 模型配置
1222
const openaiConfig: AgentModelConfig = {
1323
id: "test-openai",
@@ -614,13 +624,16 @@ function makeToolCallSSE(
614624

615625
// 创建 mock AgentService 实例
616626
function createTestService() {
617-
const mockRepo = {
627+
// 重置 agent_chat 单例 mock 方法(保持对象身份不变,只替换 vi.fn)
628+
Object.assign(mockChatRepo, {
618629
appendMessage: vi.fn().mockResolvedValue(undefined),
619630
getMessages: vi.fn().mockResolvedValue([]),
620631
listConversations: vi.fn().mockResolvedValue([]),
621632
saveConversation: vi.fn().mockResolvedValue(undefined),
622633
saveMessages: vi.fn().mockResolvedValue(undefined),
623-
};
634+
getAttachment: vi.fn().mockResolvedValue(null),
635+
saveAttachment: vi.fn().mockResolvedValue(0),
636+
});
624637

625638
const mockGroup = {
626639
on: vi.fn(),
@@ -640,12 +653,9 @@ function createTestService() {
640653
getDefaultModelId: vi.fn().mockResolvedValue("test-openai"),
641654
};
642655

643-
// 替换 repo(避免 OPFS 调用)
644-
(service as any).repo = mockRepo;
645-
646656
const toolRegistry = (service as any).toolRegistry as ToolRegistry;
647657

648-
return { service, mockRepo, toolRegistry };
658+
return { service, mockRepo: mockChatRepo, toolRegistry };
649659
}
650660

651661
describe("callLLMWithToolLoop", () => {

src/app/service/agent/service_worker/agent.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
OPFSApiRequest,
1919
MCPApiRequest,
2020
} from "@App/app/service/agent/core/types";
21-
import { AgentChatRepo } from "@App/app/repo/agent_chat";
21+
import { agentChatRepo } from "@App/app/repo/agent_chat";
2222
import type { AgentModelRepo } from "@App/app/repo/agent_model";
2323
import type { SkillRepo } from "@App/app/repo/skill_repo";
2424
import { uuidv4 } from "@App/pkg/utils/uuid";
@@ -51,26 +51,6 @@ import { ChatService } from "./chat_service";
5151
export { isRetryableError, withRetry, classifyErrorCode } from "./retry_utils";
5252

5353
export class AgentService {
54-
private _repo = new AgentChatRepo();
55-
// 测试兼容性:透传访问 repo,setter 同步更新 compactService
56-
private get repo() {
57-
return this._repo;
58-
}
59-
private set repo(v: AgentChatRepo) {
60-
this._repo = v;
61-
if (this.compactService) {
62-
this.compactService.repo = v;
63-
}
64-
if (this.llmClient) {
65-
this.llmClient.repo = v;
66-
}
67-
if (this.toolLoopOrchestrator) {
68-
this.toolLoopOrchestrator.repo = v;
69-
}
70-
if (this.chatService) {
71-
this.chatService.repo = v;
72-
}
73-
}
7454
private toolRegistry = new ToolRegistry();
7555
// Skill 相关功能委托给 SkillService
7656
private skillService!: SkillService;
@@ -138,11 +118,11 @@ export class AgentService {
138118
this.skillService = new SkillService(sender, resourceService);
139119
this.modelService = new AgentModelService(group);
140120
this.opfsService = new AgentOPFSService(sender);
141-
this.llmClient = new LLMClient(this.repo);
142-
this.compactService = new CompactService(this.repo, this.modelService, {
121+
this.llmClient = new LLMClient();
122+
this.compactService = new CompactService(this.modelService, {
143123
callLLM: (model, params, sendEvent, signal) => this.llmClient.callLLM(model, params, sendEvent, signal),
144124
});
145-
this.toolLoopOrchestrator = new ToolLoopOrchestrator(this.toolRegistry, this.repo, {
125+
this.toolLoopOrchestrator = new ToolLoopOrchestrator(this.toolRegistry, {
146126
// callLLM 通过 lambda 注入,确保测试 spy 可以拦截 service.callLLM
147127
callLLM: (model, params, sendEvent, signal) => this.callLLM(model, params, sendEvent, signal),
148128
autoCompact: (convId, model, msgs, sendEvent, signal) =>
@@ -152,7 +132,6 @@ export class AgentService {
152132
callLLMWithToolLoop: (params) => this.callLLMWithToolLoop(params),
153133
});
154134
this.chatService = new ChatService(
155-
this.repo,
156135
this.toolRegistry,
157136
this.modelService,
158137
this.skillService,
@@ -185,7 +164,7 @@ export class AgentService {
185164

186165
init() {
187166
// 注入 chatRepo 到 ToolRegistry 用于保存附件
188-
this.toolRegistry.setChatRepo(this.repo);
167+
this.toolRegistry.setChatRepo(agentChatRepo);
189168
// 初始化 MCP Service
190169
this.mcpService = new MCPService(this.toolRegistry);
191170
this.mcpService.init();
@@ -229,7 +208,7 @@ export class AgentService {
229208
// 初始化 AgentTaskService(在 skillService 初始化后)
230209
this.agentTaskService = new AgentTaskService(
231210
this.sender,
232-
this.repo,
211+
agentChatRepo,
233212
this.toolRegistry,
234213
this.skillService,
235214
{
@@ -347,7 +326,7 @@ export class AgentService {
347326

348327
// 处理 CAT.agent.opfs API 请求,委托给 AgentOPFSService
349328
async handleOPFSApi(request: OPFSApiRequest, sender: IGetSender): Promise<unknown> {
350-
return this.opfsService.handleOPFSApi(request, sender, this.repo);
329+
return this.opfsService.handleOPFSApi(request, sender, agentChatRepo);
351330
}
352331

353332
// 解析对话关联的 skills(测试兼容:通过 (service as any).resolveSkills 访问)

src/app/service/agent/service_worker/chat_service.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
} from "@App/app/service/agent/core/types";
1212
import type { ScriptToolCallback, ToolExecutor } from "@App/app/service/agent/core/tool_registry";
1313
import type { ToolCall } from "@App/app/service/agent/core/types";
14-
import type { AgentChatRepo } from "@App/app/repo/agent_chat";
14+
import { agentChatRepo } from "@App/app/repo/agent_chat";
1515
import type { ToolRegistry } from "@App/app/service/agent/core/tool_registry";
1616
import type { SkillService } from "./skill_service";
1717
import type { CompactService } from "./compact_service";
@@ -55,7 +55,6 @@ export interface ChatServiceLLMDeps {
5555

5656
export class ChatService {
5757
constructor(
58-
public repo: AgentChatRepo,
5958
private toolRegistry: ToolRegistry,
6059
private modelService: AgentModelService,
6160
private skillService: SkillService,
@@ -74,12 +73,12 @@ export class ChatService {
7473
case "get":
7574
return this.getConversation(params.id);
7675
case "getMessages":
77-
return this.repo.getMessages(params.conversationId);
76+
return agentChatRepo.getMessages(params.conversationId);
7877
case "save":
7978
// 对话已经在 chat 过程中持久化,这里确保元数据也保存
8079
return true;
8180
case "clearMessages":
82-
await this.repo.saveMessages(params.conversationId, []);
81+
await agentChatRepo.saveMessages(params.conversationId, []);
8382
return true;
8483
default:
8584
throw new Error(`Unknown conversation action: ${(params as any).action}`);
@@ -97,12 +96,12 @@ export class ChatService {
9796
createtime: Date.now(),
9897
updatetime: Date.now(),
9998
};
100-
await this.repo.saveConversation(conv);
99+
await agentChatRepo.saveConversation(conv);
101100
return conv;
102101
}
103102

104103
private async getConversation(id: string): Promise<Conversation | null> {
105-
const conversations = await this.repo.listConversations();
104+
const conversations = await agentChatRepo.listConversations();
106105
return conversations.find((c) => c.id === id) || null;
107106
}
108107

@@ -280,7 +279,7 @@ export class ChatService {
280279
}
281280

282281
const model = await this.modelService.getModel(params.modelId || conv.modelId);
283-
const existingMessages = await this.repo.getMessages(params.conversationId);
282+
const existingMessages = await agentChatRepo.getMessages(params.conversationId);
284283

285284
if (existingMessages.filter((m) => m.role !== "system").length === 0) {
286285
sendEvent({ type: "error", message: "No messages to compact" });
@@ -322,7 +321,7 @@ export class ChatService {
322321
content: `[Conversation Summary]\n\n${summary}`,
323322
createtime: Date.now(),
324323
};
325-
await this.repo.saveMessages(params.conversationId, [summaryMessage]);
324+
await agentChatRepo.saveMessages(params.conversationId, [summaryMessage]);
326325

327326
sendEvent({ type: "compact_done", summary, originalCount });
328327
sendEvent({ type: "done", usage: result.usage });
@@ -348,7 +347,7 @@ export class ChatService {
348347
}
349348
if (needSave) {
350349
conv.updatetime = Date.now();
351-
await this.repo.saveConversation(conv);
350+
await agentChatRepo.saveConversation(conv);
352351
}
353352

354353
const model = await this.modelService.getModel(conv.modelId);
@@ -373,10 +372,10 @@ export class ChatService {
373372

374373
// 注册每次请求的临时工具
375374
// Task tools(从持久化加载,变更时保存并推送事件到 UI)
376-
const initialTasks = await this.repo.getTasks(params.conversationId);
375+
const initialTasks = await agentChatRepo.getTasks(params.conversationId);
377376
const { tools: taskToolDefs } = createTaskTools({
378377
initialTasks,
379-
onSave: (tasks) => this.repo.saveTasks(params.conversationId, tasks),
378+
onSave: (tasks) => agentChatRepo.saveTasks(params.conversationId, tasks),
380379
sendEvent,
381380
});
382381
for (const t of taskToolDefs) {
@@ -422,7 +421,7 @@ export class ChatService {
422421
}
423422

424423
// 加载历史消息
425-
const existingMessages = await this.repo.getMessages(params.conversationId);
424+
const existingMessages = await agentChatRepo.getMessages(params.conversationId);
426425

427426
// 扫描历史消息中的 load_skill 调用,预加载之前已加载的 skill 的工具
428427
if (enableTools && metaTools.length > 0) {
@@ -480,7 +479,7 @@ export class ChatService {
480479
if (!params.skipSaveUserMessage) {
481480
// 添加新用户消息到 LLM 上下文并持久化
482481
messages.push({ role: "user", content: params.message });
483-
await this.repo.appendMessage({
482+
await agentChatRepo.appendMessage({
484483
id: uuidv4(),
485484
conversationId: params.conversationId,
486485
role: "user",
@@ -494,7 +493,7 @@ export class ChatService {
494493
const titleText = getTextContent(params.message);
495494
conv.title = titleText.slice(0, 30) + (titleText.length > 30 ? "..." : "");
496495
conv.updatetime = Date.now();
497-
await this.repo.saveConversation(conv);
496+
await agentChatRepo.saveConversation(conv);
498497
}
499498

500499
try {
@@ -530,7 +529,7 @@ export class ChatService {
530529
// 持久化错误消息到 OPFS,确保刷新后仍可见
531530
if (params.conversationId && !params.ephemeral) {
532531
try {
533-
await this.repo.appendMessage({
532+
await agentChatRepo.appendMessage({
534533
id: uuidv4(),
535534
conversationId: params.conversationId,
536535
role: "assistant",

src/app/service/agent/service_worker/compact_service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AgentChatRepo } from "@App/app/repo/agent_chat";
1+
import { agentChatRepo } from "@App/app/repo/agent_chat";
22
import type {
33
AgentModelConfig,
44
ChatRequest,
@@ -41,7 +41,6 @@ export interface CompactOrchestrator {
4141

4242
export class CompactService {
4343
constructor(
44-
public repo: AgentChatRepo,
4544
private modelService: AgentModelService,
4645
private orchestrator: CompactOrchestrator
4746
) {}
@@ -89,7 +88,7 @@ export class CompactService {
8988
content: `[Conversation Summary]\n\n${summary}`,
9089
createtime: Date.now(),
9190
};
92-
await this.repo.saveMessages(conversationId, [summaryMessage]);
91+
await agentChatRepo.saveMessages(conversationId, [summaryMessage]);
9392

9493
// 通知 UI
9594
sendEvent({ type: "compact_done", summary, originalCount: -1 });

src/app/service/agent/service_worker/llm_client.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AgentChatRepo } from "@App/app/repo/agent_chat";
1+
import { agentChatRepo } from "@App/app/repo/agent_chat";
22
import type {
33
AgentModelConfig,
44
ChatRequest,
@@ -24,8 +24,6 @@ export interface LLMCallResult {
2424
}
2525

2626
export class LLMClient {
27-
constructor(public repo: AgentChatRepo) {}
28-
2927
/**
3028
* 调用 LLM 并收集完整响应(内部处理流式、重试与图片保存)
3129
*/
@@ -44,7 +42,9 @@ export class LLMClient {
4442
};
4543

4644
// 预解析消息中 ContentBlock 引用的 attachmentId → base64
47-
const attachmentResolver = await resolveAttachments(params.messages, model, (id) => this.repo.getAttachment(id));
45+
const attachmentResolver = await resolveAttachments(params.messages, model, (id) =>
46+
agentChatRepo.getAttachment(id)
47+
);
4848

4949
// zhipu 暂无独立实现,映射到 openai provider;独立实现后可移除此映射
5050
const providerName = model.provider === "zhipu" ? "openai" : model.provider;
@@ -187,7 +187,7 @@ export class LLMClient {
187187
const savedBlocks: ContentBlock[] = [];
188188
for (const pending of pendingImageSaves) {
189189
try {
190-
await this.repo.saveAttachment(pending.block.attachmentId, pending.data);
190+
await agentChatRepo.saveAttachment(pending.block.attachmentId, pending.data);
191191
savedBlocks.push(pending.block);
192192
// 转发不含 data 的 content_block_complete 事件给 UI
193193
sendEvent({ type: "content_block_complete", block: pending.block });
@@ -206,7 +206,7 @@ export class LLMClient {
206206
const ext = subtype || "png";
207207
const blockId = `img_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
208208
try {
209-
await this.repo.saveAttachment(blockId, dataUrl);
209+
await agentChatRepo.saveAttachment(blockId, dataUrl);
210210
const block: ContentBlock = {
211211
type: "image",
212212
attachmentId: blockId,

0 commit comments

Comments
 (0)