适配新的 opencode Agent 框架

This commit is contained in:
2026-04-29 15:33:08 +08:00
parent 49fd4f5eb1
commit 3b5a493cda
10 changed files with 53 additions and 53 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ NEXTAUTH_URL="https://demo.waternetwork.cn/"
# 为前端暴露的变量添加 NEXT_PUBLIC_ 前缀 # 为前端暴露的变量添加 NEXT_PUBLIC_ 前缀
NEXT_PUBLIC_BACKEND_URL="https://server.waternetwork.cn" NEXT_PUBLIC_BACKEND_URL="https://server.waternetwork.cn"
NEXT_PUBLIC_COPILOT_URL="https://agent.waternetwork.cn" NEXT_PUBLIC_AGENT_URL="https://agent.waternetwork.cn"
NEXT_PUBLIC_AUDIO_SERVICE_URL="https://tts.waternetwork.cn" NEXT_PUBLIC_AUDIO_SERVICE_URL="https://tts.waternetwork.cn"
NEXT_PUBLIC_MAP_URL="https://geoserver.waternetwork.cn/geoserver" NEXT_PUBLIC_MAP_URL="https://geoserver.waternetwork.cn/geoserver"
NEXT_PUBLIC_MAP_WORKSPACE="tjwater" NEXT_PUBLIC_MAP_WORKSPACE="tjwater"
+1 -1
View File
@@ -102,7 +102,7 @@ jobs:
-t "${IMAGE_NAME}:${IMAGE_TAG}" \ -t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \ -t "${IMAGE_NAME}:latest" \
--build-arg NEXT_PUBLIC_BACKEND_URL="${{ vars.NEXT_PUBLIC_BACKEND_URL }}" \ --build-arg NEXT_PUBLIC_BACKEND_URL="${{ vars.NEXT_PUBLIC_BACKEND_URL }}" \
--build-arg NEXT_PUBLIC_COPILOT_URL="${{ vars.NEXT_PUBLIC_COPILOT_URL }}" \ --build-arg NEXT_PUBLIC_AGENT_URL="${{ vars.NEXT_PUBLIC_AGENT_URL }}" \
--build-arg NEXT_PUBLIC_AUDIO_SERVICE_URL="${{ vars.NEXT_PUBLIC_AUDIO_SERVICE_URL }}" \ --build-arg NEXT_PUBLIC_AUDIO_SERVICE_URL="${{ vars.NEXT_PUBLIC_AUDIO_SERVICE_URL }}" \
--build-arg NEXT_PUBLIC_MAP_URL="${{ vars.NEXT_PUBLIC_MAP_URL }}" \ --build-arg NEXT_PUBLIC_MAP_URL="${{ vars.NEXT_PUBLIC_MAP_URL }}" \
--build-arg NEXT_PUBLIC_MAP_WORKSPACE="${{ vars.NEXT_PUBLIC_MAP_WORKSPACE }}" \ --build-arg NEXT_PUBLIC_MAP_WORKSPACE="${{ vars.NEXT_PUBLIC_MAP_WORKSPACE }}" \
+1 -1
View File
@@ -18,7 +18,7 @@ FROM base AS builder
# 只定义 ARG 接收来自构建命令或 docker-compose.yaml 的参数 # 只定义 ARG 接收来自构建命令或 docker-compose.yaml 的参数
# Next.js 在 build 时会自动读取同名的 ARG 作为环境变量 # Next.js 在 build 时会自动读取同名的 ARG 作为环境变量
ARG NEXT_PUBLIC_BACKEND_URL ARG NEXT_PUBLIC_BACKEND_URL
ARG NEXT_PUBLIC_COPILOT_URL ARG NEXT_PUBLIC_AGENT_URL
ARG NEXT_PUBLIC_AUDIO_SERVICE_URL ARG NEXT_PUBLIC_AUDIO_SERVICE_URL
ARG NEXT_PUBLIC_MAP_URL ARG NEXT_PUBLIC_MAP_URL
ARG NEXT_PUBLIC_MAP_WORKSPACE ARG NEXT_PUBLIC_MAP_WORKSPACE
+1 -1
View File
@@ -8,7 +8,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL} NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL}
NEXT_PUBLIC_COPILOT_URL: ${NEXT_PUBLIC_COPILOT_URL} NEXT_PUBLIC_AGENT_URL: ${NEXT_PUBLIC_AGENT_URL}
NEXT_PUBLIC_AUDIO_SERVICE_URL: ${NEXT_PUBLIC_AUDIO_SERVICE_URL} NEXT_PUBLIC_AUDIO_SERVICE_URL: ${NEXT_PUBLIC_AUDIO_SERVICE_URL}
NEXT_PUBLIC_MAP_URL: ${NEXT_PUBLIC_MAP_URL} NEXT_PUBLIC_MAP_URL: ${NEXT_PUBLIC_MAP_URL}
NEXT_PUBLIC_MAP_WORKSPACE: ${NEXT_PUBLIC_MAP_WORKSPACE} NEXT_PUBLIC_MAP_WORKSPACE: ${NEXT_PUBLIC_MAP_WORKSPACE}
+13 -13
View File
@@ -32,7 +32,7 @@ import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRound
import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
// Logic // Logic
import { streamCopilotChat } from "@/lib/chatStream"; import { streamAgentChat } from "@/lib/chatStream";
import type { StreamEvent } from "@/lib/chatStream"; import type { StreamEvent } from "@/lib/chatStream";
import { import {
useChatToolStore, useChatToolStore,
@@ -60,8 +60,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const [isStreaming, setIsStreaming] = useState(false); const [isStreaming, setIsStreaming] = useState(false);
const [width, setWidth] = useState(480); const [width, setWidth] = useState(480);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const [conversationId, setConversationId] = useState<string | undefined>( const [sessionId, setSessionId] = useState<string | undefined>(
initialChatStateRef.current.conversationId initialChatStateRef.current.sessionId
); );
const [headerMenuAnchorEl, setHeaderMenuAnchorEl] = useState<HTMLElement | null>(null); const [headerMenuAnchorEl, setHeaderMenuAnchorEl] = useState<HTMLElement | null>(null);
const [isPresetPanelOpen, setIsPresetPanelOpen] = useState(false); const [isPresetPanelOpen, setIsPresetPanelOpen] = useState(false);
@@ -117,13 +117,13 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
const state: PersistedChatState = { messages, conversationId }; const state: PersistedChatState = { messages, sessionId };
try { try {
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state)); window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state));
} catch (error) { } catch (error) {
console.error("[GlobalChatbox] Failed to persist chat state:", error); console.error("[GlobalChatbox] Failed to persist chat state:", error);
} }
}, [messages, conversationId]); }, [messages, sessionId]);
const sendPrompt = useCallback( const sendPrompt = useCallback(
async (rawPrompt: string) => { async (rawPrompt: string) => {
@@ -291,13 +291,13 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
}; };
try { try {
await streamCopilotChat({ await streamAgentChat({
message: prompt, message: prompt,
conversationId, sessionId,
signal: controller.signal, signal: controller.signal,
onEvent: (event) => { onEvent: (event) => {
if (event.type === "token") { if (event.type === "token") {
if (!conversationId && event.conversationId) setConversationId(event.conversationId); if (!sessionId && event.sessionId) setSessionId(event.sessionId);
const normalizedToken = normalizeThoughtTagToken(event.content); const normalizedToken = normalizeThoughtTagToken(event.content);
setMessages((prev) => setMessages((prev) =>
prev.map((m) => prev.map((m) =>
@@ -307,13 +307,13 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
) )
); );
} else if (event.type === "done") { } else if (event.type === "done") {
if (!conversationId && event.conversationId) setConversationId(event.conversationId); if (!sessionId && event.sessionId) setSessionId(event.sessionId);
setMessages((prev) => setMessages((prev) =>
prev.map((m) => prev.map((m) =>
m.id === assistantId && m.content.trim().length === 0 m.id === assistantId && m.content.trim().length === 0
? { ? {
...m, ...m,
content: "⚠️ **错误:** Copilot 未返回内容,请稍后重试。", content: "⚠️ **错误:** Agent 未返回内容,请稍后重试。",
isError: true, isError: true,
} }
: m : m
@@ -358,7 +358,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
setIsStreaming(false); setIsStreaming(false);
} }
}, },
[conversationId, isStreaming, stopListening, dispatchToolAction], [sessionId, isStreaming, stopListening, dispatchToolAction],
); );
const handleSend = async () => { const handleSend = async () => {
@@ -573,7 +573,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
<Box> <Box>
<Typography variant="h6" fontWeight={800} sx={{ background: `linear-gradient(90deg, ${theme.palette.primary.dark}, ${theme.palette.secondary.dark})`, backgroundClip: "text", color: "transparent", letterSpacing: -0.5 }}> <Typography variant="h6" fontWeight={800} sx={{ background: `linear-gradient(90deg, ${theme.palette.primary.dark}, ${theme.palette.secondary.dark})`, backgroundClip: "text", color: "transparent", letterSpacing: -0.5 }}>
Copilot Agent
</Typography> </Typography>
<Typography variant="caption" color="text.secondary" fontWeight={500}> <Typography variant="caption" color="text.secondary" fontWeight={500}>
AI AI
@@ -834,7 +834,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
void handleSend(); void handleSend();
} }
}} }}
placeholder="输入消息给 Copilot..." placeholder="输入消息给 Agent..."
fullWidth fullWidth
multiline multiline
maxRows={3} maxRows={3}
+1 -1
View File
@@ -14,5 +14,5 @@ export type SpeechState = "idle" | "playing" | "paused";
export type PersistedChatState = { export type PersistedChatState = {
messages: Message[]; messages: Message[];
conversationId?: string; sessionId?: string;
}; };
+6 -6
View File
@@ -2,7 +2,7 @@ import type { PersistedChatState } from "./GlobalChatbox.types";
export const createId = () => export const createId = () =>
`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
export const CHAT_STORAGE_KEY = "tjwater_copilot_chat_state_v1"; export const CHAT_STORAGE_KEY = "tjwater_agent_chat_state_v1";
const THINK_TAG_ALIAS_PATTERN = const THINK_TAG_ALIAS_PATTERN =
/<\s*(\/?)\s*(thinking|reasoning|thought)\b[^>]*>/gi; /<\s*(\/?)\s*(thinking|reasoning|thought)\b[^>]*>/gi;
export const PRESET_PROMPTS = [ export const PRESET_PROMPTS = [
@@ -36,24 +36,24 @@ export const stripMarkdown = (md: string): string =>
export const getInitialChatState = (): PersistedChatState => { export const getInitialChatState = (): PersistedChatState => {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return { messages: [], conversationId: undefined }; return { messages: [], sessionId: undefined };
} }
try { try {
const storedRaw = window.localStorage.getItem(CHAT_STORAGE_KEY); const storedRaw = window.localStorage.getItem(CHAT_STORAGE_KEY);
if (!storedRaw) return { messages: [], conversationId: undefined }; if (!storedRaw) return { messages: [], sessionId: undefined };
const parsed = JSON.parse(storedRaw) as PersistedChatState; const parsed = JSON.parse(storedRaw) as PersistedChatState;
if (!Array.isArray(parsed.messages)) { if (!Array.isArray(parsed.messages)) {
console.error("[GlobalChatbox] Invalid persisted messages format."); console.error("[GlobalChatbox] Invalid persisted messages format.");
window.localStorage.removeItem(CHAT_STORAGE_KEY); window.localStorage.removeItem(CHAT_STORAGE_KEY);
return { messages: [], conversationId: undefined }; return { messages: [], sessionId: undefined };
} }
return { messages: parsed.messages, conversationId: parsed.conversationId }; return { messages: parsed.messages, sessionId: parsed.sessionId };
} catch (error) { } catch (error) {
console.error( console.error(
"[GlobalChatbox] Failed to read persisted chat state:", "[GlobalChatbox] Failed to read persisted chat state:",
error, error,
); );
window.localStorage.removeItem(CHAT_STORAGE_KEY); window.localStorage.removeItem(CHAT_STORAGE_KEY);
return { messages: [], conversationId: undefined }; return { messages: [], sessionId: undefined };
} }
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
export const config = { export const config = {
BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL || "http://127.0.0.1:8000", BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL || "http://127.0.0.1:8000",
COPILOT_URL: process.env.NEXT_PUBLIC_COPILOT_URL || "http://127.0.0.1:8787", AGENT_URL: process.env.NEXT_PUBLIC_AGENT_URL || "http://127.0.0.1:8788",
AUDIO_SERVICE_URL: AUDIO_SERVICE_URL:
process.env.NEXT_PUBLIC_AUDIO_SERVICE_URL || "http://127.0.0.1:18083", process.env.NEXT_PUBLIC_AUDIO_SERVICE_URL || "http://127.0.0.1:18083",
MAP_URL: process.env.NEXT_PUBLIC_MAP_URL || "http://127.0.0.1:8080/geoserver", MAP_URL: process.env.NEXT_PUBLIC_MAP_URL || "http://127.0.0.1:8080/geoserver",
+14 -14
View File
@@ -1,4 +1,4 @@
import { streamCopilotChat } from "./chatStream"; import { streamAgentChat } from "./chatStream";
import { ReadableStream } from "stream/web"; import { ReadableStream } from "stream/web";
import { TextEncoder, TextDecoder } from "util"; import { TextEncoder, TextDecoder } from "util";
@@ -32,7 +32,7 @@ const makeStream = (chunks: string[]) =>
}, },
}); });
describe("streamCopilotChat", () => { describe("streamAgentChat", () => {
beforeEach(() => { beforeEach(() => {
apiFetch.mockReset(); apiFetch.mockReset();
}); });
@@ -41,21 +41,21 @@ describe("streamCopilotChat", () => {
apiFetch.mockResolvedValue({ apiFetch.mockResolvedValue({
ok: true, ok: true,
body: makeStream([ body: makeStream([
'event: token\ndata: {"conversationId":"c1","content":"he"}\n\n', 'event: token\ndata: {"session_id":"s1","content":"he"}\n\n',
'event: token\ndata: {"conversationId":"c1","content":"llo"}\n\n', 'event: token\ndata: {"session_id":"s1","content":"llo"}\n\n',
'event: done\ndata: {"conversationId":"c1"}\n\n', 'event: done\ndata: {"session_id":"s1"}\n\n',
]), ]),
}); });
const events: Array<{ type: string; content?: string; conversationId?: string }> = []; const events: Array<{ type: string; content?: string; sessionId?: string }> = [];
await streamCopilotChat({ await streamAgentChat({
message: "hi", message: "hi",
onEvent: (event) => events.push(event), onEvent: (event) => events.push(event),
}); });
expect(apiFetch).toHaveBeenCalledWith( expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/copilot/chat/stream"), expect.stringContaining("/api/v1/agent/chat/stream"),
expect.objectContaining({ expect.objectContaining({
method: "POST", method: "POST",
projectHeaderMode: "include", projectHeaderMode: "include",
@@ -64,9 +64,9 @@ describe("streamCopilotChat", () => {
); );
expect(events).toEqual([ expect(events).toEqual([
{ type: "token", conversationId: "c1", content: "he" }, { type: "token", sessionId: "s1", content: "he" },
{ type: "token", conversationId: "c1", content: "llo" }, { type: "token", sessionId: "s1", content: "llo" },
{ type: "done", conversationId: "c1" }, { type: "done", sessionId: "s1" },
]); ]);
}); });
@@ -78,7 +78,7 @@ describe("streamCopilotChat", () => {
}); });
const events: Array<{ type: string; message?: string; detail?: string }> = []; const events: Array<{ type: string; message?: string; detail?: string }> = [];
await streamCopilotChat({ await streamAgentChat({
message: "hi", message: "hi",
onEvent: (event) => events.push(event), onEvent: (event) => events.push(event),
}); });
@@ -97,7 +97,7 @@ describe("streamCopilotChat", () => {
}); });
const events: Array<{ type: string; message?: string; detail?: string }> = []; const events: Array<{ type: string; message?: string; detail?: string }> = [];
await streamCopilotChat({ await streamAgentChat({
message: "hi", message: "hi",
onEvent: (event) => events.push(event), onEvent: (event) => events.push(event),
}); });
@@ -111,7 +111,7 @@ describe("streamCopilotChat", () => {
apiFetch.mockRejectedValue(new TypeError("Failed to fetch")); apiFetch.mockRejectedValue(new TypeError("Failed to fetch"));
const events: Array<{ type: string; message?: string; detail?: string }> = []; const events: Array<{ type: string; message?: string; detail?: string }> = [];
await streamCopilotChat({ await streamAgentChat({
message: "hi", message: "hi",
onEvent: (event) => events.push(event), onEvent: (event) => events.push(event),
}); });
+14 -14
View File
@@ -2,24 +2,24 @@ import { apiFetch } from "@/lib/apiFetch";
import { config } from "@config/config"; import { config } from "@config/config";
export type StreamEvent = export type StreamEvent =
| { type: "token"; conversationId: string; content: string } | { type: "token"; sessionId: string; content: string }
| { type: "done"; conversationId: string } | { type: "done"; sessionId: string }
| { | {
type: "error"; type: "error";
conversationId?: string; sessionId?: string;
message: string; message: string;
detail?: string; detail?: string;
} }
| { | {
type: "tool_call"; type: "tool_call";
conversationId: string; sessionId: string;
tool: string; tool: string;
params: Record<string, unknown>; params: Record<string, unknown>;
}; };
type StreamOptions = { type StreamOptions = {
message: string; message: string;
conversationId?: string; sessionId?: string;
signal?: AbortSignal; signal?: AbortSignal;
onEvent: (event: StreamEvent) => void; onEvent: (event: StreamEvent) => void;
}; };
@@ -43,16 +43,16 @@ const parseEventBlock = (block: string): { event?: string; data?: string } => {
}; };
}; };
export const streamCopilotChat = async ({ export const streamAgentChat = async ({
message, message,
conversationId, sessionId,
signal, signal,
onEvent, onEvent,
}: StreamOptions) => { }: StreamOptions) => {
let response: Response; let response: Response;
try { try {
response = await apiFetch( response = await apiFetch(
`${config.COPILOT_URL}/api/v1/copilot/chat/stream`, `${config.AGENT_URL}/api/v1/agent/chat/stream`,
{ {
method: "POST", method: "POST",
signal, signal,
@@ -62,7 +62,7 @@ export const streamCopilotChat = async ({
}, },
body: JSON.stringify({ body: JSON.stringify({
message, message,
conversation_id: conversationId, session_id: sessionId,
}), }),
projectHeaderMode: "include", projectHeaderMode: "include",
skipAuthRedirect: true, skipAuthRedirect: true,
@@ -115,7 +115,7 @@ export const streamCopilotChat = async ({
try { try {
const parsed = JSON.parse(data) as { const parsed = JSON.parse(data) as {
conversationId?: string; session_id?: string;
content?: string; content?: string;
message?: string; message?: string;
detail?: string; detail?: string;
@@ -125,25 +125,25 @@ export const streamCopilotChat = async ({
if (event === "token") { if (event === "token") {
onEvent({ onEvent({
type: "token", type: "token",
conversationId: parsed.conversationId ?? "", sessionId: parsed.session_id ?? "",
content: parsed.content ?? "", content: parsed.content ?? "",
}); });
} else if (event === "done") { } else if (event === "done") {
onEvent({ onEvent({
type: "done", type: "done",
conversationId: parsed.conversationId ?? "", sessionId: parsed.session_id ?? "",
}); });
} else if (event === "error") { } else if (event === "error") {
onEvent({ onEvent({
type: "error", type: "error",
conversationId: parsed.conversationId, sessionId: parsed.session_id,
message: parsed.message ?? "unknown error", message: parsed.message ?? "unknown error",
detail: parsed.detail, detail: parsed.detail,
}); });
} else if (event === "tool_call") { } else if (event === "tool_call") {
onEvent({ onEvent({
type: "tool_call", type: "tool_call",
conversationId: parsed.conversationId ?? "", sessionId: parsed.session_id ?? "",
tool: parsed.tool ?? "", tool: parsed.tool ?? "",
params: parsed.params ?? {}, params: parsed.params ?? {},
}); });