diff --git a/src/modules/opencode-api.ts b/src/modules/opencode-api.ts
index 852b649..33b69bf 100644
--- a/src/modules/opencode-api.ts
+++ b/src/modules/opencode-api.ts
@@ -11,6 +11,7 @@ const api = {
"create": {
"path": "opencode",
"key": "create",
+ "id": "b662fba3c0a8a593",
"description": "创建 OpenCode 客户端",
"metadata": {
"tags": [
@@ -40,6 +41,7 @@ const api = {
"close": {
"path": "opencode",
"key": "close",
+ "id": "49672adea9daa837",
"description": "关闭 OpenCode 客户端",
"metadata": {
"tags": [
@@ -69,6 +71,7 @@ const api = {
"restart": {
"path": "opencode",
"key": "restart",
+ "id": "e0b1564a796ea88b",
"description": "重启 OpenCode 客户端",
"metadata": {
"tags": [
@@ -98,6 +101,7 @@ const api = {
"getUrl": {
"path": "opencode",
"key": "getUrl",
+ "id": "c611acf038e41279",
"description": "获取 OpenCode 服务 URL",
"metadata": {
"tags": [
@@ -121,6 +125,7 @@ const api = {
"ls-projects": {
"path": "opencode",
"key": "ls-projects",
+ "id": "ee72cd09da63d13d",
"metadata": {
"url": "/root/v1/cnb-dev",
"source": "query-proxy-api"
@@ -135,6 +140,7 @@ const api = {
"runProject": {
"path": "opencode",
"key": "runProject",
+ "id": "112127fa82fe1d9d",
"metadata": {
"tags": [
"opencode"
@@ -163,13 +169,17 @@ const api = {
* @param data.question - {string} 问题
* @param data.baseUrl - {string} OpenCode 服务地址,默认为 http://localhost:4096
* @param data.directory - {string} 运行目录,默认为根目录
- * @param data.messageID - {string} 消息 ID,选填
+ * @param data.messageId - {string} 消息 ID,选填
* @param data.sessionId - {string} 会话 ID,选填
+ * @param data.providerId - {string} 指定使用的提供商 ID,默认为空,表示使用默认提供商
+ * @param data.modelId - {string} 指定使用的模型 ID,默认为空,表示使用默认模型
* @param data.parts - {array} 消息内容的分块,优先于 question 参数
+ * @param data.awaitAnswer - {boolean} 是否等待回答完成,默认为 false,开启后会在回答完成后返回完整回答内容
*/
"question": {
"path": "opencode-cnb",
"key": "question",
+ "id": "193c9c63bc50cbbc",
"description": "创建 OpenCode 客户端",
"metadata": {
"args": {
@@ -190,7 +200,7 @@ const api = {
"type": "string",
"optional": true
},
- "messageID": {
+ "messageId": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "消息 ID,选填",
"type": "string",
@@ -202,17 +212,430 @@ const api = {
"type": "string",
"optional": true
},
+ "providerId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "指定使用的提供商 ID,默认为空,表示使用默认提供商",
+ "type": "string",
+ "optional": true
+ },
+ "modelId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "指定使用的模型 ID,默认为空,表示使用默认模型",
+ "type": "string",
+ "optional": true
+ },
"parts": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "消息内容的分块,优先于 question 参数",
"type": "array",
"items": {},
"optional": true
+ },
+ "awaitAnswer": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "是否等待回答完成,默认为 false,开启后会在回答完成后返回完整回答内容",
+ "type": "boolean",
+ "optional": true
}
},
"url": "/root/v1/cnb-dev",
"source": "query-proxy-api"
}
+ },
+ /**
+ * 获取 OpenCode 可用模型列表,返回 providerID 和 modelID
+ *
+ * @param data - Request parameters
+ * @param data.baseUrl - {string} OpenCode 服务地址,默认为 http://localhost:4096
+ */
+ "models": {
+ "path": "opencode-cnb",
+ "key": "models",
+ "id": "a66f19f8427e7085",
+ "description": "获取 OpenCode 可用模型列表,返回 providerID 和 modelID",
+ "metadata": {
+ "args": {
+ "baseUrl": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务地址,默认为 http://localhost:4096",
+ "type": "string",
+ "optional": true
+ }
+ },
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ }
+ },
+ "opencode-session": {
+ /**
+ * 在指定目录创建一个新的 OpenCode 会话
+ *
+ * @param data - Request parameters
+ * @param data.directory - {string} 工作目录,默认为 /workspace
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "create": {
+ "path": "opencode-session",
+ "key": "create",
+ "id": "f07990a69e2a1eaf",
+ "description": "创建 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "directory": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "工作目录,默认为 /workspace",
+ "type": "string",
+ "optional": true
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "create-opencode-session",
+ "title": "创建 Session",
+ "summary": "在指定目录创建一个新的 OpenCode 会话",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 更新指定 OpenCode 会话的属性,如标题
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.title - {string} 新的会话标题
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "update": {
+ "path": "opencode-session",
+ "key": "update",
+ "id": "0a6f7cd78fa5ff20",
+ "description": "更新 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "title": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "新的会话标题",
+ "type": "string",
+ "optional": true
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "update-opencode-session",
+ "title": "更新 Session",
+ "summary": "更新指定 OpenCode 会话的属性,如标题",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 根据 ID 删除指定的 OpenCode 会话及其所有数据
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "delete": {
+ "path": "opencode-session",
+ "key": "delete",
+ "id": "c7bd762b2eccdfc2",
+ "description": "删除 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "delete-opencode-session",
+ "title": "删除 Session",
+ "summary": "根据 ID 删除指定的 OpenCode 会话及其所有数据",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 中止正在运行的 OpenCode 会话
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "abort": {
+ "path": "opencode-session",
+ "key": "abort",
+ "id": "0b89922558164ffd",
+ "description": "中止 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "abort-opencode-session",
+ "title": "中止 Session",
+ "summary": "中止正在运行的 OpenCode 会话",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 对指定的 OpenCode 会话进行内容总结
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "summarize": {
+ "path": "opencode-session",
+ "key": "summarize",
+ "id": "c51ae8a43b269383",
+ "description": "总结 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "summarize-opencode-session",
+ "title": "总结 Session",
+ "summary": "对指定的 OpenCode 会话进行内容总结",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 获取当前 OpenCode 会话的运行状态,可按目录过滤
+ *
+ * @param data - Request parameters
+ * @param data.directory - {string} 工作目录
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "status": {
+ "path": "opencode-session",
+ "key": "status",
+ "id": "a2507055e8e1ed42",
+ "description": "获取 OpenCode Session 状态",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "directory": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "工作目录",
+ "type": "string",
+ "optional": true
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "get-opencode-session-status",
+ "title": "获取 Session 状态",
+ "summary": "获取当前 OpenCode 会话的运行状态,可按目录过滤",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 列出指定 OpenCode 会话的所有消息记录
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "messages": {
+ "path": "opencode-session",
+ "key": "messages",
+ "id": "04e78517e6e9144e",
+ "description": "列出 OpenCode Session 消息",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "list-opencode-session-messages",
+ "title": "列出 Session 消息",
+ "summary": "列出指定 OpenCode 会话的所有消息记录",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 列出 OpenCode 中的所有会话,可按目录过滤
+ *
+ * @param data - Request parameters
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "list": {
+ "path": "opencode-session",
+ "key": "list",
+ "id": "a44e79adfcb199dd",
+ "description": "列出所有 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "list-opencode-sessions",
+ "title": "列出所有 Session",
+ "summary": "列出 OpenCode 中的所有会话,可按目录过滤",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 根据 ID 获取指定的 OpenCode 会话信息
+ *
+ * @param data - Request parameters
+ * @param data.id - {string} Session ID
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "get": {
+ "path": "opencode-session",
+ "key": "get",
+ "id": "7acea53865affb10",
+ "description": "获取指定 OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "id": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "get-opencode-session",
+ "title": "获取 Session",
+ "summary": "根据 ID 获取指定的 OpenCode 会话信息",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
+ },
+ /**
+ * 从指定消息处 Fork 一个 OpenCode 会话
+ *
+ * @param data - Request parameters
+ * @param data.sessionId - {string} Session ID
+ * @param data.messageId - {string} 从该消息处开始 Fork
+ * @param data.port - {number} OpenCode 服务端口,默认为 4096
+ */
+ "fork": {
+ "path": "opencode-session",
+ "key": "fork",
+ "id": "d43a8e2282412078",
+ "description": "Fork OpenCode Session",
+ "metadata": {
+ "tags": [
+ "session"
+ ],
+ "args": {
+ "sessionId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "Session ID"
+ },
+ "messageId": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "string",
+ "description": "从该消息处开始 Fork"
+ },
+ "port": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "description": "OpenCode 服务端口,默认为 4096",
+ "type": "number",
+ "optional": true
+ }
+ },
+ "skill": "fork-opencode-session",
+ "title": "Fork Session",
+ "summary": "从指定消息处 Fork 一个 OpenCode 会话",
+ "url": "/root/v1/cnb-dev",
+ "source": "query-proxy-api"
+ }
}
}
} as const;
diff --git a/src/pages/chat-dev/components/OpencodeChat.tsx b/src/pages/chat-dev/components/OpencodeChat.tsx
new file mode 100644
index 0000000..d9b971d
--- /dev/null
+++ b/src/pages/chat-dev/components/OpencodeChat.tsx
@@ -0,0 +1,126 @@
+import { useEffect } from 'react';
+import { useShallow } from 'zustand/react/shallow';
+import { FileIcon, FolderIcon, RefreshCwIcon, TrashIcon } from 'lucide-react';
+import { useChatDevStore } from '../store';
+import { useCodeGraphStore } from '@/pages/code-graph/store';
+
+export const OpencodeChat = () => {
+ const {
+ question, projectInfo,
+ sessionId, isLoading,
+ saveSessionInfo, loadSessionInfo, fetchSession, fetchMessages, clearSession,
+ setData,
+ } = useChatDevStore(
+ useShallow((s) => ({
+ question: s.question,
+ projectInfo: s.projectInfo,
+ setData: s.setData,
+ sessionId: s.sessionId,
+ isLoading: s.isLoading,
+ saveSessionInfo: s.saveSessionInfo,
+ loadSessionInfo: s.loadSessionInfo,
+ fetchSession: s.fetchSession,
+ fetchMessages: s.fetchMessages,
+ clearSession: s.clearSession,
+ })),
+ );
+ const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
+ createQuestion: s.createQuestion,
+ url: s.url,
+ })));
+
+ // 初始化后尝试从 sessionStorage 恢复 opencode session 并加载历史消息
+ useEffect(() => {
+ if (!codeGraphStore.url) return;
+ const info = loadSessionInfo();
+ if (info) {
+ fetchSession(info.sessionId);
+ fetchMessages(info.sessionId);
+ }
+ }, [codeGraphStore.url]);
+
+ const relativePath = projectInfo
+ ? (projectInfo.filepath || '').replace((projectInfo.projectPath || '') + '/', '') || '/'
+ : null;
+
+ const onSend = async () => {
+ const res = await codeGraphStore.createQuestion({
+ question,
+ projectPath: projectInfo?.projectPath,
+ engine: 'opencode',
+ sessionId: sessionId || undefined,
+ });
+ console.log(res);
+ if (res?.code === 200 && res?.data) {
+ const { sessionId: newSessionId, messageId: newMessageId } = res.data as any;
+ if (newSessionId) {
+ saveSessionInfo(newSessionId, newMessageId || '');
+ fetchMessages(newSessionId);
+ }
+ }
+ };
+
+ return (
+
+ {/* Session 信息栏 */}
+ {sessionId && (
+
+
+
+ Session: {sessionId.slice(0, 8)}...
+
+
+
+
+
+ )}
+
+ {/* 内容区 */}
+
+ {/* 节点信息 */}
+ {relativePath && (
+
+
+
+ {relativePath}
+
+
+ )}
+ {projectInfo?.projectPath && !relativePath && (
+
+
+
+ {projectInfo.projectPath}
+
+
+ )}
+
+ {/* 问题输入区 */}
+
+
+ );
+};
diff --git a/src/pages/chat-dev/page.tsx b/src/pages/chat-dev/page.tsx
index 95b059e..cdb2078 100644
--- a/src/pages/chat-dev/page.tsx
+++ b/src/pages/chat-dev/page.tsx
@@ -1,35 +1,36 @@
import { useEffect } from 'react';
import { useSearch } from '@tanstack/react-router';
import { useShallow } from 'zustand/react/shallow';
-import { BotIcon, FileIcon, FolderIcon } from 'lucide-react';
+import { BotIcon } from 'lucide-react';
import { useChatDevStore } from './store';
import { BOT_KEYS, BotKey } from '@/pages/code-graph/store/bot-helper';
import openclawSvg from '@/pages/code-graph/assets/openclaw.svg';
import opencodePng from '@/pages/code-graph/assets/opencode.png';
import { useCodeGraphStore } from '../code-graph/store';
import { useLayoutStore } from '../auth/store';
+import { OpencodeChat } from './components/OpencodeChat';
const BOT_ICONS: Record = {
openclaw: openclawSvg,
opencode: opencodePng,
};
+const BOT_LABELS: Record = {
+ openclaw: 'Openclaw',
+ opencode: 'Opencode',
+};
+
export const App = () => {
const { timestamp } = useSearch({ from: '/chat-dev' });
- const { question, engine, projectInfo, initFromTimestamp, setData } = useChatDevStore(
+ const { engine, initFromTimestamp, setData } = useChatDevStore(
useShallow((s) => ({
- question: s.question,
engine: s.engine,
- projectInfo: s.projectInfo,
initFromTimestamp: s.initFromTimestamp,
setData: s.setData,
})),
);
- const layoutStore = useLayoutStore(useShallow((s) => ({
- me: s.me,
- })));
+ const layoutStore = useLayoutStore(useShallow((s) => ({ me: s.me })));
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
- createQuestion: s.createQuestion,
init: s.init,
})));
@@ -37,84 +38,45 @@ export const App = () => {
if (!layoutStore.me?.username) return;
codeGraphStore.init(layoutStore.me, { load: false });
}, [layoutStore.me]);
+
useEffect(() => {
if (timestamp) {
initFromTimestamp(timestamp);
}
}, [timestamp]);
- const relativePath = projectInfo
- ? (projectInfo.filepath || '').replace((projectInfo.projectPath || '') + '/', '') || '/'
- : null;
- const onSend = async () => {
- if (projectInfo) {
- const res = await codeGraphStore.createQuestion({
- question,
- projectPath: projectInfo.projectPath,
- engine,
- });
- console.log(res);
- }
- }
return (
-
-
- {/* 标题栏 */}
-
-
- AI 助手
-
-
- {/* 内容区 */}
-
- {/* 节点信息 + Bot 切换 */}
-
- {relativePath && (
-
-
-
- {relativePath}
-
-
- )}
- {projectInfo?.projectPath && !relativePath && (
-
-
-
- {projectInfo.projectPath}
-
-
- )}
- {/* Bot 切换按钮组 */}
-
- {BOT_KEYS.map((key) => (
-
- ))}
-
+
+
+ {/* Bot 切换按钮 —— 最顶层 */}
+
+
+
Bot
+
+ {BOT_KEYS.map((key) => (
+
+ ))}
-
- {/* 问题输入区 */}
-
+
+ {/* Bot 对应的聊天面板 */}
+ {engine === 'opencode' &&
}
+ {engine === 'openclaw' && (
+
+ Openclaw 即将支持,敬请期待
+
+ )}
);
diff --git a/src/pages/chat-dev/store/index.ts b/src/pages/chat-dev/store/index.ts
index 94cb7ad..4a4199e 100644
--- a/src/pages/chat-dev/store/index.ts
+++ b/src/pages/chat-dev/store/index.ts
@@ -1,35 +1,116 @@
import { create } from 'zustand';
import { BotKey } from '@/pages/code-graph/store/bot-helper';
+import { queryApi as opencodeApi } from '@/modules/opencode-api';
+import { getApiUrl } from '@/pages/code-graph/store';
export type ChatDevData = {
question: string;
engine: BotKey;
projectInfo: {
filepath: string;
projectPath: string;
+ kind: 'file' | 'dir' | 'root';
} | null;
};
+export type SessionMessage = {
+ info: any;
+ parts: Array
;
+};
+
+export type SessionInfo = {
+ sessionId: string;
+ messageId: string;
+};
+
type ChatDevState = {
question: string;
engine: BotKey;
projectInfo: {
filepath: string;
projectPath: string;
+ kind?: 'file' | 'dir' | 'root';
} | null;
+ sessionId: string | null;
+ messageId: string | null;
+ messages: SessionMessage[];
+ isLoading: boolean;
setData: (data: Partial) => void;
initFromTimestamp: (timestamp: string) => void;
+ saveSessionInfo: (sessionId: string, messageId: string) => void;
+ loadSessionInfo: () => SessionInfo | null;
+ fetchSession: (sessionId: string) => Promise;
+ fetchMessages: (sessionId: string) => Promise;
+ clearSession: () => void;
};
const SESSION_KEY_PREFIX = 'chat-dev-';
const SESSION_KEY = 'chat-dev';
+const SESSION_INFO_KEY = 'chat-dev-session-info';
-export const useChatDevStore = create()((set) => ({
+export const useChatDevStore = create()((set, get) => ({
question: '',
engine: 'opencode',
projectInfo: null,
+ sessionId: null,
+ messageId: null,
+ messages: [],
+ isLoading: false,
setData: (data) => set((s) => ({ ...s, ...data })),
+ saveSessionInfo: (sessionId: string, messageId: string) => {
+ const info: SessionInfo = { sessionId, messageId };
+ sessionStorage.setItem(SESSION_INFO_KEY, JSON.stringify(info));
+ set({ sessionId, messageId });
+ },
+
+ loadSessionInfo: () => {
+ const raw = sessionStorage.getItem(SESSION_INFO_KEY);
+ if (!raw) return null;
+ try {
+ return JSON.parse(raw) as SessionInfo;
+ } catch {
+ sessionStorage.removeItem(SESSION_INFO_KEY);
+ return null;
+ }
+ },
+
+ fetchSession: async (sessionId: string) => {
+ set({ isLoading: true });
+ try {
+ const url = getApiUrl();
+ const res = await opencodeApi['opencode-session'].get({ id: sessionId }, { url });
+ if (res.code === 200 && res.data) {
+ set({ sessionId });
+ }
+ } catch {
+ // 静默失败,不影响主流程
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+
+ fetchMessages: async (sessionId: string) => {
+ set({ isLoading: true });
+ try {
+ const url = getApiUrl();
+ const res = await opencodeApi['opencode-session'].messages({ sessionId: sessionId }, { url });
+ if (res.code === 200 && res.data) {
+ const msgs: SessionMessage[] = Array.isArray(res.data) ? res.data : (res.data as any).list ?? [];
+ set({ messages: msgs });
+ }
+ } catch {
+ // 静默失败,不影响主流程
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+
+ clearSession: () => {
+ sessionStorage.removeItem(SESSION_INFO_KEY);
+ set({ sessionId: null, messageId: null, messages: [] });
+ },
+
initFromTimestamp: (timestamp: string) => {
const key = SESSION_KEY_PREFIX + timestamp;
diff --git a/src/pages/code-graph/components/BotHelperModal.tsx b/src/pages/code-graph/components/BotHelperModal.tsx
index 0a8beb6..b74f356 100644
--- a/src/pages/code-graph/components/BotHelperModal.tsx
+++ b/src/pages/code-graph/components/BotHelperModal.tsx
@@ -31,23 +31,25 @@ export function BotHelperModal() {
projectInfo: s.projectInfo,
})),
);
- const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({
- nodeInfoData: s.nodeInfoData,
+ const { createQuestion } = useCodeGraphStore(useShallow((s) => ({
createQuestion: s.createQuestion,
})));
const location = useLocation();
- console.log('BotHelperModal render', location);
+ console.log('BotHelperModal render', botHelperStore.projectInfo);
const basename = location.publicHref.replace(location.href, '');
- const relativePath = nodeInfoData
- ? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/'
+ const projectPath = botHelperStore.projectInfo?.projectPath;
+ const filepath = botHelperStore.projectInfo?.filepath;
+ const relativePath = botHelperStore.projectInfo
+ ? filepath?.replace((projectPath || '') + '/', '') || '/'
: null;
-
+ const kind = botHelperStore.projectInfo?.kind || 'dir';
const handleConfirm = async () => {
- if (nodeInfoData) {
+ const projectPath = botHelperStore.projectInfo?.projectPath;
+ if (projectPath) {
const res = await createQuestion({
question: botHelperStore.input,
- projectPath: nodeInfoData.projectPath,
- filePath: nodeInfoData.kind === 'file' ? nodeInfoData.fullPath : undefined,
+ projectPath,
+ filePath: botHelperStore.projectInfo?.kind === 'file' ? botHelperStore.projectInfo.filepath : undefined,
engine: botHelperStore.activeKey,
});
console.log(res);
@@ -102,9 +104,9 @@ export function BotHelperModal() {
{/* 节点信息 + 按钮组 */}
- {nodeInfoData && relativePath && (
+ {botHelperStore.projectInfo && relativePath && (
-
+
diff --git a/src/pages/code-graph/components/Code3DGraph.tsx b/src/pages/code-graph/components/Code3DGraph.tsx
index e3b4f21..89bdf7a 100644
--- a/src/pages/code-graph/components/Code3DGraph.tsx
+++ b/src/pages/code-graph/components/Code3DGraph.tsx
@@ -185,7 +185,7 @@ interface Code3DGraphProps {
files: FileProjectData[];
className?: string;
type?: "map" | 'minimap';
- onProjectFocus?: (projectPath: string) => void;
+ onProjectFocus?: string;
}
// ─── 主组件 ───────────────────────────────────────────────────────────────────
diff --git a/src/pages/code-graph/components/NodeInfo.tsx b/src/pages/code-graph/components/NodeInfo.tsx
index 7d77f3b..5a11f1a 100644
--- a/src/pages/code-graph/components/NodeInfo.tsx
+++ b/src/pages/code-graph/components/NodeInfo.tsx
@@ -13,7 +13,7 @@ function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }
}
const KIND_LABEL: Record = {
- root: '项目根目录',
+ root: '根目录',
dir: '目录',
file: '文件',
};
@@ -57,7 +57,6 @@ export const NodeInfoContainer = () => {
codePodOpen: s.codePodOpen,
nodeInfoData: s.nodeInfoData,
nodeInfoPos: s.nodeInfoPos,
- setNodeInfoOpen: s.setNodeInfo,
closeNodeInfo: s.closeNodeInfo,
setCodePodOpen: s.setCodePodOpen,
setCodePodAttrs: s.setCodePodAttrs,
@@ -175,6 +174,7 @@ export const NodeInfoContainer = () => {
botHelperStore.setProjectInfo({
filepath: nodeInfoData.fullPath,
projectPath: nodeInfoData.projectPath,
+ kind: nodeInfoData.kind,
});
botHelperStore.openModal();
}}
diff --git a/src/pages/code-graph/components/ProjectPanel.tsx b/src/pages/code-graph/components/ProjectPanel.tsx
index a38340c..f46a1eb 100644
--- a/src/pages/code-graph/components/ProjectPanel.tsx
+++ b/src/pages/code-graph/components/ProjectPanel.tsx
@@ -1,14 +1,22 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useShallow } from 'zustand/react/shallow';
-import { GripVertical, X } from 'lucide-react';
+import { GripVertical, X, Sparkles, MoreHorizontal, StopCircle, PlayCircle } from 'lucide-react';
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { useCodeGraphStore } from '../store';
import { FileProjectData } from '../modules/tree';
+import { useBotHelperStore } from '../store/bot-helper';
interface ProjectPanelProps {
onProjectClick?: (projectPath: string, files: FileProjectData[]) => void;
+ onOpenCodePod?: (projectPath: string) => void;
+ onStopProject?: (projectPath: string) => void;
}
-export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
+export function ProjectPanel({
+ onProjectClick,
+ onOpenCodePod,
+ onStopProject,
+}: ProjectPanelProps) {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 20, y: 80 });
const dragOffset = useRef({ x: 0, y: 0 });
@@ -20,9 +28,69 @@ export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
files: s.files,
})),
);
+ const botHelperStore = useBotHelperStore(useShallow((s) => ({
+ openModal: s.openModal,
+ setProjectInfo: s.setProjectInfo,
+ })));
+ const codeGraphStore = useCodeGraphStore(useShallow((s) => {
+ return {
+ setCodePodAttrs: s.setCodePodAttrs,
+ setCodePodOpen: s.setCodePodOpen,
+ setNodeInfo: s.setNodeInfo
+ };
+ }));
const activeProjects = projects.filter((p) => p.status === 'active');
+ const handleMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ if (!panelRef.current) return;
+ const rect = panelRef.current.getBoundingClientRect();
+ dragOffset.current = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+ setIsDragging(true);
+ },
+ [],
+ );
+
+ const handleMouseMove = useCallback(
+ (e: MouseEvent) => {
+ if (!isDragging) return;
+ setPosition({
+ x: e.clientX - dragOffset.current.x,
+ y: e.clientY - dragOffset.current.y,
+ });
+ },
+ [isDragging],
+ );
+
+ const handleMouseUp = useCallback(() => {
+ setIsDragging(false);
+ }, []);
+
+ useEffect(() => {
+ if (isDragging) {
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ }
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, [isDragging, handleMouseMove, handleMouseUp]);
+
+ const handleProjectClick = useCallback(
+ (projectPath: string) => {
+ const project = projects.find((p) => p.path === projectPath);
+ if (project && onProjectClick) {
+ onProjectClick(projectPath, files);
+ }
+ },
+ [projects, files, onProjectClick],
+ );
+
const [isLargeScreen, setIsLargeScreen] = useState(false);
useEffect(() => {
@@ -67,16 +135,78 @@ export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
{activeProjects.map((project) => {
const projectName = project.name || project.path.split('/').pop() || project.path;
+ const nodeInfoData = {
+ fullPath: project.path,
+ projectPath: project.path,
+ kind: 'dir',
+ }
return (
-
+
+
+
+
+
+ e.stopPropagation()}
+ title='更多'
+ className='p-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/10 transition-colors'>
+
+
+
+
+ onStopProject?.(project.path)}
+ className='text-slate-300 focus:bg-white/5 focus:text-slate-100 cursor-pointer'>
+
+ 停止
+
+ {
+ const projectPath = project.path;
+ codeGraphStore.setCodePodAttrs({
+ label: projectPath.split('/').pop() || projectPath,
+ size: 0,
+ color: '',
+ x: 0,
+ y: 0,
+ fullPath: projectPath,
+ projectPath,
+ kind: 'dir',
+ });
+ codeGraphStore.setCodePodOpen(true);
+ }}
+ className='text-slate-300 focus:bg-white/5 focus:text-slate-100 cursor-pointer'>
+
+ 打开 CodePod
+
+
+
+
+
);
})}
diff --git a/src/pages/code-graph/page.tsx b/src/pages/code-graph/page.tsx
index 2747d67..b5b68db 100644
--- a/src/pages/code-graph/page.tsx
+++ b/src/pages/code-graph/page.tsx
@@ -1,16 +1,17 @@
import { useState, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { DatabaseIcon, RefreshCw } from 'lucide-react';
-import { CodePod } from './components/CodePod';
-import { useCodeGraphStore } from './store';
-import CodeGraphView from './components/CodeGraph';
-import { Code3DGraph } from './components/Code3DGraph';
-import { NodeInfoContainer } from './components/NodeInfo';
-import { ProjectDialog } from './components/ProjectDialog';
import { BotHelperModal } from './components/BotHelperModal';
import { ProjectPanel } from './components/ProjectPanel';
+import { useBotHelperStore } from './store/bot-helper';
import { useLayoutStore } from '../auth/store';
import type { FileProjectData } from './modules/tree';
+import { useCodeGraphStore } from './store';
+import { Code3DGraph } from './components/Code3DGraph';
+import CodeGraphView from './components/CodeGraph';
+import CodePod from './components/CodePod';
+import NodeInfoContainer from './components/NodeInfo';
+import { ProjectDialog } from './components/ProjectDialog';
type ViewMode = '2d' | '3d';
export default function CodeGraphPage() {
@@ -19,16 +20,18 @@ export default function CodeGraphPage() {
const layoutStore = useLayoutStore(useShallow((s) => ({
me: s.me,
})));
- const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, init, files, fetchProjects, setFiles } = useCodeGraphStore(
+ const { codePodOpen, setCodePodOpen, codePodAttrs, setCodePodAttrs, setProjectDialogOpen, init, files, fetchProjects, setFiles, toggleProjectStatus } = useCodeGraphStore(
useShallow((s) => ({
files: s.files,
setFiles: s.setFiles,
codePodOpen: s.codePodOpen,
setCodePodOpen: s.setCodePodOpen,
codePodAttrs: s.codePodAttrs,
+ setCodePodAttrs: s.setCodePodAttrs,
setProjectDialogOpen: s.setProjectDialogOpen,
init: s.init,
fetchProjects: s.fetchProjects,
+ toggleProjectStatus: s.toggleProjectStatus,
})),
);
@@ -92,7 +95,23 @@ export default function CodeGraphPage() {
{viewMode === '3d' ? (
<>
-
+
{
+ setCodePodAttrs({
+ label: projectPath.split('/').pop() || projectPath,
+ size: 0,
+ color: '',
+ x: 0,
+ y: 0,
+ fullPath: projectPath,
+ projectPath,
+ kind: 'dir',
+ });
+ setCodePodOpen(true);
+ }}
+ onStopProject={(projectPath) => toggleProjectStatus(projectPath)}
+ />
>
) : (
diff --git a/src/pages/code-graph/store/bot-helper.ts b/src/pages/code-graph/store/bot-helper.ts
index f8b826f..a0358a0 100644
--- a/src/pages/code-graph/store/bot-helper.ts
+++ b/src/pages/code-graph/store/bot-helper.ts
@@ -14,8 +14,9 @@ type BotHelperState = {
projectInfo: {
filepath: string;
projectPath: string;
+ kind: 'file' | 'dir' | 'root';
} | null;
- setProjectInfo: (info: { filepath: string; projectPath: string } | null) => void;
+ setProjectInfo: (info: { filepath: string; projectPath: string; kind: 'file' | 'dir' | 'root' } | null) => void;
openModal: () => void;
closeModal: () => void;
};
@@ -30,6 +31,8 @@ export const useBotHelperStore = create()((set) => ({
setActiveKey: (key) => set({ activeKey: key }),
projectInfo: null,
setProjectInfo: (info) => set({ projectInfo: info }),
- openModal: () => set({ open: true }),
+ openModal: () => set({
+ open: true,
+ }),
closeModal: () => set({ open: false, input: '', projectInfo: null }),
}));
diff --git a/src/pages/code-graph/store/index.ts b/src/pages/code-graph/store/index.ts
index a3c7bc4..43b1394 100644
--- a/src/pages/code-graph/store/index.ts
+++ b/src/pages/code-graph/store/index.ts
@@ -64,7 +64,7 @@ type State = {
projectPath?: string; // 项目路径,必填
getContent?: boolean; // 是否获取文件内容,默认为 false
}) => Promise>;
- createQuestion: (opts: { question: string, projectPath: string, filePath?: string, engine?: 'openclaw' | 'opencode', }) => any;
+ createQuestion: (opts: { question: string, projectPath?: string, filePath?: string, engine?: 'openclaw' | 'opencode', sessionId?: string }) => any;
saveFile: (filepath: string, content: string) => Promise;
isMobile: boolean;
setIsMobile: (isMobile: boolean) => void;
@@ -274,11 +274,13 @@ export const useCodeGraphStore = create()((set, get) => ({
isMobile: false,
setIsMobile: (isMobile) => set({ isMobile }),
createQuestion: async (opts) => {
- const { question, projectPath, filePath, engine = 'opencode' } = opts;
+ const { question, projectPath, filePath, engine = 'opencode', sessionId } = opts;
const url = get().url
- let q = `
- ${question}
- 项目路径: ${projectPath}`
+ let q = question;
+ if (projectPath) {
+ q += `
+ 项目路径: ${projectPath}`;
+ }
if (filePath && filePath !== projectPath) {
q += `
文件路径: ${filePath}`;
@@ -286,6 +288,7 @@ export const useCodeGraphStore = create()((set, get) => ({
const res = await opencodeApi["opencode-cnb"].question({
question: q,
directory: projectPath,
+ sessionId: sessionId || undefined,
}, {
url,
timeout: 60 * 1000 * 15, // 15分钟
@@ -293,3 +296,8 @@ export const useCodeGraphStore = create()((set, get) => ({
return res;
},
}));
+
+export const getApiUrl = () => {
+ const store = useCodeGraphStore.getState();
+ return store.url || API_URL;
+}
\ No newline at end of file