From 1879f5ce322651da479b43a82c3d28c1333af437 Mon Sep 17 00:00:00 2001 From: houakang Date: Sun, 12 Apr 2026 17:18:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E8=8E=B7=E5=8F=96=E5=8F=8A?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=86=99=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加获取用户信息的API接口和Pinia存储 在登录流程中增加用户信息获取和配置写入操作 新增opencode配置写入的IPC通信功能 --- src/main/index.js | 43 +++++++++++++++++++++++++ src/preload/index.js | 4 +++ src/renderer/components/LoginDialog.vue | 39 ++++++++++++++++++---- src/renderer/http/api.js | 3 ++ src/renderer/http/url.js | 2 ++ src/renderer/stores/user.js | 25 ++++++++++++++ 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/renderer/stores/user.js diff --git a/src/main/index.js b/src/main/index.js index f1de3be..da1959a 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,6 +1,7 @@ import { app, BrowserWindow, shell, ipcMain, Menu } from 'electron'; import path from 'node:path'; import fs from 'node:fs'; +import os from 'node:os'; import net from 'node:net'; import { spawn } from 'node:child_process'; import started from 'electron-squirrel-startup'; @@ -225,6 +226,48 @@ function registerIpcHandlers() { // Bonjour ipcMain.handle('bonjour:get-services', () => getDiscoveredServices()); + // opencode 配置写入 + ipcMain.handle('opencode:write-config', async (_e, { modelInfo, deviceHost, devicePort }) => { + const configDir = path.join(os.homedir(), '.config', 'opencode'); + const configPath = path.join(configDir, 'opencode.json'); + + await fs.promises.mkdir(configDir, { recursive: true }); + + const config = { + $schema: 'https://opencode.ai/config.json', + provider: { + zhiju: { + name: 'Zhiju AI', + env: ['ZHIJU_API_KEY'], + options: { + baseURL: `http://${deviceHost}:${modelInfo.port}/v1`, + apiKey: `${modelInfo.apiKey}`, + }, + models: { + [modelInfo.model_name]: { + name: modelInfo.model_name, + family: 'openai', + status: modelInfo.status || 'beta', + capabilities: modelInfo.capabilities || { + reasoning: false, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + }, + limit: modelInfo.limit || { context: 128000, output: 4096 }, + }, + }, + }, + }, + model: `zhiju/${modelInfo.model_name}`, + }; + + await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + console.log('[opencode] config written to:', configPath); + return true; + }); + // 窗口控制 ipcMain.on('window:minimize', (event) => { const win = BrowserWindow.fromWebContents(event.sender); diff --git a/src/preload/index.js b/src/preload/index.js index 07e6aa7..2874aaf 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -26,3 +26,7 @@ contextBridge.exposeInMainWorld('bonjour', { return () => ipcRenderer.removeListener('bonjour:services-updated', listener); }, }); + +contextBridge.exposeInMainWorld('opencodeConfig', { + write: (params) => ipcRenderer.invoke('opencode:write-config', params), +}); diff --git a/src/renderer/components/LoginDialog.vue b/src/renderer/components/LoginDialog.vue index b7d5bbd..971838f 100644 --- a/src/renderer/components/LoginDialog.vue +++ b/src/renderer/components/LoginDialog.vue @@ -67,9 +67,11 @@ import { ref, watch } from 'vue'; import { User } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; import { useSparkStore } from '@/stores/spark'; -import { loginAction } from '@/http/api.js'; +import { loginAction, getUserInfoAction } from '@/http/api.js'; +import { useUserStore } from '@/stores/user'; const sparkStore = useSparkStore(); +const userStore = useUserStore(); const props = defineProps({ modelValue: { @@ -115,13 +117,38 @@ async function handleLogin() { const device = sparkStore.devices.find((d) => d.name === form.value.sparkDevice); if (device) sparkStore.selectDevice(device); - const url = sparkStore.selectedDeviceUrl; - console.log('[Login] spark device:', device); - console.log('[Login] target url:', url); + const selectedDevice = sparkStore.selectedDevice; + console.log('[Login] spark device:', selectedDevice); + console.log('[Login] target url:', sparkStore.selectedDeviceUrl); await loginAction({ email: form.value.username, password: form.value.password }); - ElMessage.success(`登录成功 | ${url ?? '未选择设备'}`); - emit('login-success', { username: form.value.username, device }); + + // 登录成功后获取用户信息并保存 + const userRes = await getUserInfoAction(); + const modelInfo = userRes.data?.xuanjian_model_info; + userStore.setUserInfo({ nickname: userRes.data?.nickname, email: userRes.data?.email }); + + // 写入 opencode 配置文件 + if (modelInfo && selectedDevice) { + try { + const deviceHost = selectedDevice.host; + console.log('[Config] modelInfo:', modelInfo); + console.log('[Config] deviceHost:', deviceHost, 'port:', selectedDevice.port); + await window.opencodeConfig.write({ + modelInfo, + deviceHost: selectedDevice.host, + devicePort: selectedDevice.port, + }); + console.log('[Config] 写入成功'); + } catch (configErr) { + console.error('[Config] 写入失败:', configErr); + } + } else { + console.warn('[Config] 跳过写入,modelInfo:', modelInfo, 'selectedDevice:', selectedDevice); + } + + ElMessage.success(`登录成功 | ${sparkStore.selectedDeviceUrl ?? '未选择设备'}`); + emit('login-success', { username: form.value.username, device: selectedDevice }); visible.value = false; } catch (err) { ElMessage.error('登录失败,请重试'); diff --git a/src/renderer/http/api.js b/src/renderer/http/api.js index b06b458..9d357eb 100644 --- a/src/renderer/http/api.js +++ b/src/renderer/http/api.js @@ -8,6 +8,9 @@ export const getHealthAction = () => getAction(url.health); // 用户登录 export const loginAction = (data) => postAction(url.user.login, { email: data.email, password: encryptPassword(data.password) }); +// 获取用户信息 +export const getUserInfoAction = () => getAction(url.user.getUserInfo); + // 会话 export const createSessionAction = (data) => postAction(url.session.create, data); export const getSessionAction = (id) => getAction(url.session.detail(id)); diff --git a/src/renderer/http/url.js b/src/renderer/http/url.js index 476cf4e..ff1dc2c 100644 --- a/src/renderer/http/url.js +++ b/src/renderer/http/url.js @@ -25,6 +25,8 @@ const url = { // 用户 user: { login: '/v1/user/login', + // 获取用户信息接口 + getUserInfo: '/v1/user/info', }, // SSE 事件流 diff --git a/src/renderer/stores/user.js b/src/renderer/stores/user.js new file mode 100644 index 0000000..9ec765c --- /dev/null +++ b/src/renderer/stores/user.js @@ -0,0 +1,25 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; + +const STORAGE_KEY = 'user_info'; + +export const useUserStore = defineStore('user', () => { + const userInfo = ref(JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null')); + + const nickname = computed(() => userInfo.value?.nickname || ''); + const email = computed(() => userInfo.value?.email || ''); + const isLoggedIn = computed(() => !!userInfo.value); + + function setUserInfo(info) { + userInfo.value = info; + localStorage.setItem(STORAGE_KEY, JSON.stringify(info)); + } + + function clearUserInfo() { + userInfo.value = null; + localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem('Authorization'); + } + + return { userInfo, nickname, email, isLoggedIn, setUserInfo, clearUserInfo }; +});