From 940584e1ea6e5e2cbbe9c115df25b72765038361 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Fri, 9 Jan 2026 15:26:33 +0800 Subject: [PATCH] =?UTF-8?q?feat/=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF+=E5=B1=95=E7=A4=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/settings.ts | 17 ++ src/extension.ts | 4 + src/panels/ICHelperPanel.ts | 38 +++++ src/services/apiClient.ts | 13 +- src/services/icCoderAuthProvider.ts | 11 +- src/services/userService.ts | 251 ++++++++++++++++++++++++++++ src/types/api.ts | 35 ++++ src/views/conversationHistoryBar.ts | 49 +++++- src/views/webviewContent.ts | 13 ++ 9 files changed, 423 insertions(+), 8 deletions(-) create mode 100644 src/services/userService.ts diff --git a/src/config/settings.ts b/src/config/settings.ts index b760a4e..10ce0c9 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -17,6 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto"; export interface IccoderConfig { /** 后端服务地址 */ backendUrl: string; + /** 后端服务地址(strangeLoop) */ + backendUrlStrongeLoop: string; /** 请求超时时间(毫秒) */ timeout: number; /** 用户ID(临时使用,后续对接认证) */ @@ -30,6 +32,7 @@ const ENV_CONFIG: Record = { /** 本地开发环境 */ dev: { backendUrl: "http://localhost:2233", + backendUrlStrongeLoop: "http://192.168.1.108:2029", timeout: 300000, userId: "default-user", serviceTier: "max", // 默认使用 max @@ -37,6 +40,7 @@ const ENV_CONFIG: Record = { /** 测试服务器环境 */ test: { backendUrl: "http://192.168.1.108:2233", + backendUrlStrongeLoop: "http://192.168.1.108:2029", timeout: 60000, userId: "default-user", serviceTier: "max", @@ -44,6 +48,7 @@ const ENV_CONFIG: Record = { /** 生产环境 */ prod: { backendUrl: "https://api.iccoder.com", + backendUrlStrongeLoop: "http://api.iccoder.com:2029", timeout: 60000, userId: "default-user", serviceTier: "auto", @@ -75,3 +80,15 @@ export function getApiUrl(path: string): string { const apiPath = path.startsWith("/") ? path : `/${path}`; return `${baseUrl}${apiPath}`; } + +/** + * 获取 StrangeLoop 服务 API 地址(用于用户信息等) + */ +export function getStrangeLoopApiUrl(path: string): string { + const { backendUrlStrongeLoop } = getConfig(); + const baseUrl = backendUrlStrongeLoop.endsWith("/") + ? backendUrlStrongeLoop.slice(0, -1) + : backendUrlStrongeLoop; + const apiPath = path.startsWith("/") ? path : `/${path}`; + return `${baseUrl}${apiPath}`; +} diff --git a/src/extension.ts b/src/extension.ts index 3603e8a..a430c3c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,10 +5,14 @@ import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel import { ChatHistoryManager } from "./utils/chatHistoryManager"; import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider"; import { VCDFileServer } from "./services/vcdFileServer"; +import { initUserService } from "./services/userService"; export function activate(context: vscode.ExtensionContext) { console.log("🎉 IC Coder 插件已激活!"); + // 初始化用户服务 + initUserService(context); + // 初始化 VCD 文件服务器 const vcdFileServer = new VCDFileServer(); vcdFileServer.start().then((port) => { diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 2d7978e..80bed3f 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -18,6 +18,7 @@ import { compactDialog } from "../services/apiClient"; import { VCDViewerPanel } from "./VCDViewerPanel"; import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { MessageType } from "../types/chatHistory"; +import { getCachedUserInfo } from "../services/userService"; /** * 创建并显示 IC 助手面板 @@ -108,6 +109,43 @@ export async function showICHelperPanel( maxIconUri.toString() ); + // 获取并发送用户信息到 webview + try { + // 优先使用缓存的用户信息 + let userInfo = getCachedUserInfo(); + + if (userInfo) { + // 使用缓存的用户信息 + console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo); + panel.webview.postMessage({ + command: 'updateUserInfo', + userInfo: { + userId: userInfo.userId, + nickname: userInfo.nickname, + username: userInfo.username + } + }); + } else { + // 如果没有缓存,从 session 中获取 + const session = await vscode.authentication.getSession("iccoder", [], { + createIfNone: false, + }); + if (session) { + console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account); + panel.webview.postMessage({ + command: 'updateUserInfo', + userInfo: { + userId: session.account.id, + nickname: session.account.label, + username: session.account.label + } + }); + } + } + } catch (error) { + console.error('[ICHelperPanel] 获取用户信息失败:', error); + } + // 处理消息 panel.webview.onDidReceiveMessage( async (message) => { diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index bbc312d..c94d577 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -6,7 +6,7 @@ import * as https from 'https'; import * as http from 'http'; import { URL } from 'url'; import { getApiUrl, getConfig } from '../config/settings'; -import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api'; +import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api'; /** * HTTP 请求选项 @@ -213,3 +213,14 @@ export function createSystemErrorResult(id: number, code: number, message: strin error: { code, message } }; } + +/** + * 获取用户信息 + * GET /system/user/getInfo + */ +export async function getUserInfo(): Promise { + console.log('[API] 获取用户信息'); + return request('/system/user/getInfo', { + method: 'GET' + }); +} diff --git a/src/services/icCoderAuthProvider.ts b/src/services/icCoderAuthProvider.ts index 05c8a3c..8f46e01 100644 --- a/src/services/icCoderAuthProvider.ts +++ b/src/services/icCoderAuthProvider.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import * as http from "http"; import * as path from "path"; import * as fs from "fs"; +import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService"; /** * IC Coder Authentication Provider @@ -62,13 +63,16 @@ export class ICCoderAuthenticationProvider try { const token = await this.login(); + // 获取到 token 后立即调用用户信息接口 + const userInfo = await onTokenReceived(token); + // 创建会话 const session: vscode.AuthenticationSession = { id: this.generateSessionId(), accessToken: token, account: { - id: "iccoder-user", - label: "IC Coder 用户", + id: userInfo?.userId || "iccoder-user", + label: userInfo?.nickname || userInfo?.username || "IC Coder 用户", }, scopes: [...scopes], }; @@ -109,6 +113,9 @@ export class ICCoderAuthenticationProvider this._sessions.splice(sessionIndex, 1); await this.saveSessions(); + // 清除用户信息缓存 + await clearUserInfo(); + // 触发会话变化事件 this._onDidChangeSessions.fire({ added: [], diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 0000000..351f04c --- /dev/null +++ b/src/services/userService.ts @@ -0,0 +1,251 @@ +/** + * 用户服务 + * 管理用户信息和认证相关的 API 调用 + */ +import * as https from 'https'; +import * as http from 'http'; +import { URL } from 'url'; +import * as vscode from 'vscode'; +import { getStrangeLoopApiUrl, getConfig } from '../config/settings'; +import type { UserInfoResponse } from '../types/api'; + +/** + * HTTP 请求选项 + */ +interface RequestOptions { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + headers?: Record; + body?: unknown; + timeout?: number; + token?: string; +} + +/** + * 发送 HTTP 请求(带 token) + */ +async function request(path: string, options: RequestOptions): Promise { + const url = new URL(getStrangeLoopApiUrl(path)); + const { timeout } = getConfig(); + + const isHttps = url.protocol === 'https:'; + const httpModule = isHttps ? https : http; + + const headers: Record = { + 'Content-Type': 'application/json', + ...options.headers + }; + + // 如果有 token,添加到请求头 + if (options.token) { + headers['Authorization'] = `Bearer ${options.token}`; + } + + const requestOptions: http.RequestOptions = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method: options.method, + headers, + timeout: options.timeout || timeout + }; + + return new Promise((resolve, reject) => { + const req = httpModule.request(requestOptions, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + console.log(`[HTTP] 响应状态码: ${res.statusCode}`); + console.log(`[HTTP] 响应内容: ${data}`); + + try { + const json = JSON.parse(data); + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + resolve(json as T); + } else { + reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`)); + } + } catch (e) { + // 如果不是 JSON,直接返回原始内容 + reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`)); + } + }); + }); + + req.on('error', (error) => { + reject(error); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('请求超时')); + }); + + if (options.body) { + req.write(JSON.stringify(options.body)); + } + + req.end(); + }); +} + +/** + * 用户信息数据结构(实际返回的数据) + */ +export interface UserInfo { + userId: string; + username: string; + nickname: string; + email?: string; + phonenumber?: string; + avatar?: string; + roles?: string[]; + permissions?: string[]; + createTime?: string; + loginDate?: string; +} + +/** + * 获取用户信息 + * GET /system/user/getInfo + */ +export async function getUserInfo(token: string): Promise { + const apiPath = '/system/user/getInfo'; + const fullUrl = getStrangeLoopApiUrl(apiPath); + console.log('[UserService] 获取用户信息'); + console.log('[UserService] 请求地址:', fullUrl); + console.log('[UserService] Token:', token ? '已提供' : '未提供'); + + try { + const response = await request(apiPath, { + method: 'GET', + token + }); + + // 处理响应数据 - 检查 code 是否为 200 + if (response.code === 200 && response.user) { + const user = response.user; + return { + userId: String(user.userId), + username: user.userName, + nickname: user.nickName, + email: user.email, + phonenumber: user.phonenumber, + avatar: user.avatar, + roles: response.roles, + permissions: response.permissions, + createTime: user.createTime, + loginDate: user.loginDate + }; + } + + console.error('[UserService] 获取用户信息失败:', response); + return null; + } catch (error) { + console.error('[UserService] 请求失败:', error); + return null; + } +} + +/** + * 当获取到 token 时自动调用此函数 + * 用于在登录成功后立即获取用户信息 + */ +export async function onTokenReceived(token: string): Promise { + try { + console.log('[UserService] Token 已获取,正在获取用户信息...'); + const userInfo = await getUserInfo(token); + + if (!userInfo) { + console.warn('[UserService] 未能获取到用户信息'); + return null; + } + + // 打印用户信息到控制台 + console.log('='.repeat(60)); + console.log('用户信息详情:'); + console.log('='.repeat(60)); + console.log(`用户ID: ${userInfo.userId}`); + console.log(`用户名: ${userInfo.username}`); + console.log(`昵称: ${userInfo.nickname}`); + if (userInfo.email) { + console.log(`邮箱: ${userInfo.email}`); + } + if (userInfo.phonenumber) { + console.log(`手机号: ${userInfo.phonenumber}`); + } + if (userInfo.avatar) { + console.log(`头像: ${userInfo.avatar}`); + } + if (userInfo.roles && userInfo.roles.length > 0) { + console.log(`角色: ${userInfo.roles.join(', ')}`); + } + if (userInfo.permissions && userInfo.permissions.length > 0) { + console.log(`权限: ${userInfo.permissions.join(', ')}`); + } + if (userInfo.createTime) { + console.log(`创建时间: ${userInfo.createTime}`); + } + if (userInfo.loginDate) { + console.log(`最后登录: ${userInfo.loginDate}`); + } + console.log('='.repeat(60)); + + // 保存到持久化存储 + await saveUserInfo(userInfo); + + return userInfo; + } catch (error) { + console.error('[UserService] 获取用户信息失败:', error); + return null; + } +} + +// ============== 持久化存储 ============== + +let extensionContext: vscode.ExtensionContext | null = null; + +/** + * 初始化用户服务(设置 context) + */ +export function initUserService(context: vscode.ExtensionContext): void { + extensionContext = context; +} + +/** + * 保存用户信息到持久化存储 + */ +export async function saveUserInfo(userInfo: UserInfo): Promise { + if (!extensionContext) { + console.warn('[UserService] ExtensionContext 未初始化'); + return; + } + await extensionContext.globalState.update('icCoderUserInfo', userInfo); + console.log('[UserService] 用户信息已保存到持久化存储'); +} + +/** + * 从持久化存储获取用户信息 + */ +export function getCachedUserInfo(): UserInfo | null { + if (!extensionContext) { + console.warn('[UserService] ExtensionContext 未初始化'); + return null; + } + return extensionContext.globalState.get('icCoderUserInfo') || null; +} + +/** + * 清除持久化存储的用户信息 + */ +export async function clearUserInfo(): Promise { + if (!extensionContext) { + console.warn('[UserService] ExtensionContext 未初始化'); + return; + } + await extensionContext.globalState.update('icCoderUserInfo', undefined); + console.log('[UserService] 用户信息已清除'); +} diff --git a/src/types/api.ts b/src/types/api.ts index 34bb475..3aec7ec 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -310,6 +310,41 @@ export interface ToolConfirmResponse { approved: boolean; } +// ============== 用户信息 ============== + +/** + * 用户信息响应 + * GET /system/user/getInfo + */ +export interface UserInfoResponse { + /** 响应消息 */ + msg: string; + /** 响应代码 (200 表示成功) */ + code: number; + /** 权限列表 */ + permissions: string[]; + /** 角色列表 */ + roles: string[]; + /** 是否默认修改密码 */ + isDefaultModifyPwd: boolean; + /** 密码是否过期 */ + isPasswordExpired: boolean; + /** 用户信息 */ + user: { + userId: number; + userName: string; + nickName: string; + email?: string; + phonenumber?: string; + sex?: string; + avatar?: string; + status?: string; + createTime?: string; + loginDate?: string; + [key: string]: any; + }; +} + // ============== 辅助类型 ============== /** 后端工具名称 */ diff --git a/src/views/conversationHistoryBar.ts b/src/views/conversationHistoryBar.ts index b429620..b16bf9b 100644 --- a/src/views/conversationHistoryBar.ts +++ b/src/views/conversationHistoryBar.ts @@ -19,11 +19,20 @@ export function getConversationHistoryBarContent(): string { - +
+ + + +
`; } @@ -49,6 +58,36 @@ export function getConversationHistoryBarStyles(): string { flex: 1; } + .right-actions { + display: flex; + align-items: center; + gap: 12px; + } + + .user-info { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + background: var(--vscode-button-secondaryBackground); + border-radius: 4px; + font-size: 13px; + color: var(--vscode-button-secondaryForeground); + } + + .user-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + .user-nickname { + white-space: nowrap; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + } + .history-dropdown-button { display: inline-flex; align-items: center; diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 90eda69..a9d09d6 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -585,6 +585,19 @@ export function getWebviewContent( } break; + case 'updateUserInfo': + // 更新用户信息 + console.log('[WebView] 收到用户信息:', message.userInfo); + const userInfo = document.getElementById('userInfo'); + const userNickname = document.getElementById('userNickname'); + if (userInfo && userNickname && message.userInfo) { + const displayName = message.userInfo.nickname || message.userInfo.username || '用户'; + console.log('[WebView] 显示用户名:', displayName); + userNickname.textContent = displayName; + userInfo.style.display = 'flex'; + } + break; + case 'resetSegmentedMessage': // 重置分段消息容器(停止对话时调用) console.log('[WebView] 重置分段消息容器');