feat(用户): 实现用户信息获取及配置写入功能

添加获取用户信息的API接口和Pinia存储
在登录流程中增加用户信息获取和配置写入操作
新增opencode配置写入的IPC通信功能
This commit is contained in:
houakang
2026-04-12 17:18:19 +08:00
parent fa013e597e
commit 1879f5ce32
6 changed files with 110 additions and 6 deletions

View File

@@ -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);

View File

@@ -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),
});

View File

@@ -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('登录失败,请重试');

View File

@@ -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));

View File

@@ -25,6 +25,8 @@ const url = {
// 用户
user: {
login: '/v1/user/login',
// 获取用户信息接口
getUserInfo: '/v1/user/info',
},
// SSE 事件流

View File

@@ -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 };
});