feat: 实现 Agent 与 DeepSeek 对话链路

This commit is contained in:
李岩岩 2026-07-30 17:44:52 +08:00
parent 0dec9e1a36
commit 381a88eca8
3 changed files with 194 additions and 0 deletions

View File

@ -12,6 +12,9 @@ pnpm dev
启动时自动加载项目根目录的 `.env`
其中设置 `DEEPSEEK_API_KEY`
也可用 `LLM_TO_AGENT_HOME` 修改数据根目录,用 `DEEPSEEK_MODEL` 修改模型。
文档:
- [架构总览](docs/architecture.md)

View File

@ -0,0 +1,95 @@
import { randomUUID } from "node:crypto";
import type {
Extension,
ExtensionRuntimeContext,
} from "../../../kernel";
import { Event, ExtensionId, Hook } from "../../catalog";
import {
type ChatMessage,
type WorkspaceService,
} from "../workspace";
export interface ModelProvider {
id: string;
chat(messages: ChatMessage[], signal?: AbortSignal): AsyncIterable<string>;
}
export interface AgentService {
chat(input: string, signal?: AbortSignal): AsyncIterable<string>;
}
export function createAgentExtension(): Extension {
let runtime: ExtensionRuntimeContext | undefined;
let workspace: WorkspaceService | undefined;
let model: ModelProvider | undefined;
const agent: AgentService = {
async *chat(input, signal) {
if (!runtime || !workspace || !model) {
throw new Error("Agent is not running.");
}
const activeRuntime = runtime;
const activeWorkspace = workspace;
const activeModel = model;
const runId = randomUUID();
const userMessage = await activeWorkspace.append("user", input);
await activeRuntime.emit(Event.MessageAdded, {
workspaceId: activeWorkspace.workspaceId,
conversationId: activeWorkspace.conversationId,
message: userMessage,
});
await activeRuntime.emit(Event.RunStarted, { runId, input });
let answer = "";
try {
const messages = await activeWorkspace.messages();
for await (const chunk of activeModel.chat(messages, signal)) {
answer += chunk;
yield chunk;
}
const assistantMessage = await activeWorkspace.append("assistant", answer);
await activeRuntime.emit(Event.MessageAdded, {
workspaceId: activeWorkspace.workspaceId,
conversationId: activeWorkspace.conversationId,
message: assistantMessage,
});
await activeRuntime.emit(Event.RunCompleted, { runId });
} catch (error) {
await activeRuntime.emit(Event.RunFailed, { runId, error: String(error) });
throw error;
}
},
};
return {
id: ExtensionId.Agent,
setup(context) {
context.add(Hook.Agent, agent);
},
start(context) {
const providers = context.all<ModelProvider>(Hook.ModelProviders);
if (providers.length === 0) {
throw new Error("Agent needs at least one model provider.");
}
runtime = context;
workspace = context.get<WorkspaceService>(Hook.Workspace);
model = providers[0];
},
stop() {
runtime = undefined;
workspace = undefined;
model = undefined;
},
};
}

View File

@ -0,0 +1,96 @@
import type { Extension } from "../../../kernel";
import { ExtensionId, Hook } from "../../catalog";
import type { ModelProvider } from "../agent";
export interface DeepSeekOptions {
apiKey?: string;
baseUrl?: string;
model?: string;
request?: (url: string, init: RequestInit) => Promise<Response>;
}
export function createDeepSeekExtension(options: DeepSeekOptions = {}): Extension {
const apiKey = options.apiKey ?? process.env.DEEPSEEK_API_KEY;
const baseUrl = (
options.baseUrl ??
process.env.DEEPSEEK_BASE_URL ??
"https://api.deepseek.com"
).replace(/\/+$/, "");
const model = options.model ?? process.env.DEEPSEEK_MODEL ?? "deepseek-v4-flash";
const request = options.request ?? fetch;
const provider: ModelProvider = {
id: "deepseek",
async *chat(messages, signal) {
const response = await request(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model,
messages: messages.map(({ role, content }) => ({ role, content })),
thinking: { type: "disabled" },
stream: true,
}),
signal,
});
if (!response.ok) {
throw new Error(
`DeepSeek request failed (${response.status}): ${await response.text()}`,
);
}
if (!response.body) {
throw new Error("DeepSeek returned an empty response body.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: !done });
const events = buffer.split(/\r?\n\r?\n/);
buffer = events.pop() ?? "";
for (const entry of events) {
for (const line of entry.split(/\r?\n/)) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") return;
if (!data) continue;
const chunk = JSON.parse(data);
const content = chunk.choices?.[0]?.delta?.content;
if (content) yield content;
}
}
if (done) break;
}
} finally {
reader.releaseLock();
}
},
};
return {
id: ExtensionId.DeepSeek,
setup(context) {
if (!apiKey) {
throw new Error("DEEPSEEK_API_KEY is required.");
}
context.add(Hook.ModelProviders, provider);
},
};
}