feat/获取用户信息+展示用户名称

This commit is contained in:
Roe-xin
2026-01-09 15:26:33 +08:00
parent 4037e9e2d7
commit 940584e1ea
9 changed files with 423 additions and 8 deletions

View File

@ -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<Environment, IccoderConfig> = {
/** 本地开发环境 */
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<Environment, IccoderConfig> = {
/** 测试服务器环境 */
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<Environment, IccoderConfig> = {
/** 生产环境 */
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}`;
}

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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<UserInfoResponse> {
console.log('[API] 获取用户信息');
return request<UserInfoResponse>('/system/user/getInfo', {
method: 'GET'
});
}

View File

@ -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: [],

251
src/services/userService.ts Normal file
View File

@ -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<string, string>;
body?: unknown;
timeout?: number;
token?: string;
}
/**
* 发送 HTTP 请求(带 token
*/
async function request<T>(path: string, options: RequestOptions): Promise<T> {
const url = new URL(getStrangeLoopApiUrl(path));
const { timeout } = getConfig();
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const headers: Record<string, string> = {
'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<UserInfo | null> {
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<UserInfoResponse>(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<UserInfo | null> {
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<void> {
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<UserInfo>('icCoderUserInfo') || null;
}
/**
* 清除持久化存储的用户信息
*/
export async function clearUserInfo(): Promise<void> {
if (!extensionContext) {
console.warn('[UserService] ExtensionContext 未初始化');
return;
}
await extensionContext.globalState.update('icCoderUserInfo', undefined);
console.log('[UserService] 用户信息已清除');
}

View File

@ -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;
};
}
// ============== 辅助类型 ==============
/** 后端工具名称 */

View File

@ -19,11 +19,20 @@ export function getConversationHistoryBarContent(): string {
</div>
</div>
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg>
</button>
<div class="right-actions">
<div class="user-info" id="userInfo" style="display: none;">
<svg class="user-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
</svg>
<span class="user-nickname" id="userNickname">加载中...</span>
</div>
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg>
</button>
</div>
</div>
`;
}
@ -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;

View File

@ -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] 重置分段消息容器');