From e440dd277368ad7c92ec877a7d4d1c3a6bd7ef64 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 11:38:06 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E7=9A=84=E6=98=A0=E5=B0=84=E5=92=8Cicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/toolIcons.ts | 5 +++++ src/views/messageArea.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/constants/toolIcons.ts b/src/constants/toolIcons.ts index f551a16..dc24f97 100644 --- a/src/constants/toolIcons.ts +++ b/src/constants/toolIcons.ts @@ -180,3 +180,8 @@ export const userQuestionIconSvg = ``; + +/** + * 更新阶段图标 SVG + */ +export const updateStageIconSvg = ``; diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 8350e2c..8897fde 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -24,6 +24,7 @@ import { knowledgeLoadIconSvg, stateTransitionIconSvg, userQuestionIconSvg, + updateStageIconSvg, } from "../constants/toolIcons"; import { getWaveformPreviewContent, @@ -670,6 +671,7 @@ export function getMessageAreaScript(): string { const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`; const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`; const userQuestionIconSvg = \`${userQuestionIconSvg}\`; + const updateStageIconSvg = \`${updateStageIconSvg}\`; ${getAgentCardScript()} @@ -724,6 +726,7 @@ export function getMessageAreaScript(): string { 'updateNode': fileWriteIconSvg, 'addStateTransition': stateTransitionIconSvg, 'askUser': userQuestionIconSvg, + 'updatePhase': updateStageIconSvg, }; return iconMap[toolName] || ''; } @@ -756,6 +759,7 @@ export function getMessageAreaScript(): string { 'spawnExplorer': '代码探索', 'spawnDebugger': '波形调试', 'askUser': '用户提问', + 'updatePhase': '已更新阶段', }; return toolNameMap[toolName] || toolName; } From faa7b63aee3454a59f9fda525034e6c3b5332a85 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 12:01:16 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=E4=BF=AE=E5=A4=8D=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E6=B3=A2=E5=BD=A2=E5=8F=AA=E8=83=BD=E5=B1=95=E7=A4=BA=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=9A=84bug=20+=20iverilog=E5=B7=A5=E5=85=B7=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/contextButton.ts | 4 +--- src/views/messageArea.ts | 1 + src/views/waveformPreviewContent.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/views/contextButton.ts b/src/views/contextButton.ts index 621069b..20bb559 100644 --- a/src/views/contextButton.ts +++ b/src/views/contextButton.ts @@ -14,9 +14,7 @@ export function getContextButtonContent(): string { 添加上下文 - - - + 添加文件、文件夹、图片或文档作为上下文 diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 8897fde..698ee4f 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -760,6 +760,7 @@ export function getMessageAreaScript(): string { 'spawnDebugger': '波形调试', 'askUser': '用户提问', 'updatePhase': '已更新阶段', + 'iverilog': '已完成编译', }; return toolNameMap[toolName] || toolName; } diff --git a/src/views/waveformPreviewContent.ts b/src/views/waveformPreviewContent.ts index 73135ea..83568dd 100644 --- a/src/views/waveformPreviewContent.ts +++ b/src/views/waveformPreviewContent.ts @@ -174,7 +174,7 @@ export function getWaveformPreviewScript(): string { const content = document.createElement('div'); content.className = 'waveform-preview-content'; - const miniViewerId = 'waveform-mini-' + Date.now(); + const miniViewerId = 'waveform-mini-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); const miniViewer = document.createElement('div'); miniViewer.id = miniViewerId; miniViewer.className = 'waveform-mini-viewer'; From d08f9a73669feb9be1bbc383a8ec7d9782b05a0e Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 14:03:20 +0800 Subject: [PATCH 3/8] =?UTF-8?q?style:spec=E6=96=87=E6=A1=A3=E7=9A=84markdo?= =?UTF-8?q?wn=E6=A0=B7=E5=BC=8F=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/planCard.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/views/planCard.ts b/src/views/planCard.ts index c061e08..bdfafbf 100644 --- a/src/views/planCard.ts +++ b/src/views/planCard.ts @@ -62,9 +62,9 @@ export function getPlanCardStyles(): string { .plan-summary p { margin: 8px 0; } .plan-summary ul, .plan-summary ol { margin: 8px 0; - padding-left: 24px; + padding-left: 0; } - .plan-summary li { margin: 4px 0; } + .plan-summary li { margin: 4px 0 4px 27px; } .plan-summary code { background: var(--vscode-textCodeBlock-background); padding: 2px 6px; @@ -392,6 +392,12 @@ export function getPlanCardScript(): string { .replace(//g, '>'); + // 标题(必须在转义之后、其他处理之前) + html = html.replace(/^#### (.+)$/gm, '

$1

'); + html = html.replace(/^### (.+)$/gm, '

$1

'); + html = html.replace(/^## (.+)$/gm, '

$1

'); + html = html.replace(/^# (.+)$/gm, '

$1

'); + // 代码块 (\`\`\`code\`\`\`) html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '
$1
'); @@ -417,12 +423,6 @@ export function getPlanCardScript(): string { return table; }); - // 标题 - html = html.replace(/^#### (.+)$/gm, '

$1

'); - html = html.replace(/^### (.+)$/gm, '

$1

'); - html = html.replace(/^## (.+)$/gm, '

$1

'); - html = html.replace(/^# (.+)$/gm, '

$1

'); - // 粗体和斜体 html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1'); html = html.replace(/\\*(.+?)\\*/g, '$1'); From 43189e144a04f157b1349f28cc27edd0b0c9f739 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 18:00:54 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat=EF=BC=9A=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E4=B8=8B=E9=9D=A2=E5=B1=95=E7=A4=BA=E6=A1=88=E4=BE=8B=E5=92=8C?= =?UTF-8?q?web=E7=AB=AF=E7=9A=84=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/exampleShowcase.ts | 216 +++++++++++++++++++++++++++++++++++ src/views/inputArea.ts | 19 ++- 2 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 src/views/exampleShowcase.ts diff --git a/src/views/exampleShowcase.ts b/src/views/exampleShowcase.ts new file mode 100644 index 0000000..1d8ff39 --- /dev/null +++ b/src/views/exampleShowcase.ts @@ -0,0 +1,216 @@ +/** + * 获取展示区域的 HTML 内容 + */ +export function getExampleShowcaseContent(): string { + return ` +
+
展示
+
+
+
📝
+
+
代码生成
+
生成一个 8 位全加器的 Verilog 代码
+
+
+ +
+
🔍
+
+
代码分析
+
分析当前项目中的时序逻辑设计
+
+
+
+ + +
+ `; +} + +/** + * 获取展示区域的样式 + */ +export function getExampleShowcaseStyles(): string { + return ` + .example-showcase { + margin-top: 24px; + padding: 0; + opacity: 1; + transition: opacity 0.3s ease; + } + + .example-showcase.hidden { + display: none; + } + + .showcase-title { + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 12px; + text-align: left; + } + + .example-cards { + display: flex; + flex-direction: row; + gap: 12px; + margin-bottom: 20px; + } + + .example-card { + flex: 1; + min-width: 0; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 8px; + padding: 14px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + } + + .example-card:hover { + border-color: var(--vscode-focusBorder); + background: var(--vscode-list-hoverBackground); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + .example-icon { + font-size: 28px; + line-height: 1; + flex-shrink: 0; + } + + .example-content { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-width: 0; + } + + .example-title { + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .example-desc { + font-size: 11px; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .web-link { + display: flex; + justify-content: center; + padding-top: 20px; + border-top: 1px solid var(--vscode-panel-border); + margin-top: 8px; + } + + .web-link-button { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: transparent; + border: none; + text-decoration: none; + font-size: 14px; + font-weight: 600; + transition: all 0.2s ease; + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + outline: none; + } + + .web-link-button:focus { + outline: none; + } + + .web-link-button:hover { + transform: translateY(-1px); + opacity: 0.8; + } + + .link-icon { + font-size: 16px; + } + + .link-arrow { + font-size: 16px; + transition: transform 0.2s ease; + } + + .web-link-button:hover .link-arrow { + transform: translateX(3px); + } + `; +} + +/** + * 获取展示区域的脚本 + */ +export function getExampleShowcaseScript(): string { + return ` + // 示例文本数组 + const exampleTexts = [ + '生成一个 8 位全加器的 Verilog 代码', + '分析当前项目中的时序逻辑设计' + ]; + + // 填充示例到输入框 + function fillExample(index) { + const messageInput = document.getElementById('messageInput'); + if (messageInput && exampleTexts[index]) { + messageInput.value = exampleTexts[index]; + messageInput.focus(); + // 触发自动调整高度 + if (typeof autoResizeTextarea === 'function') { + autoResizeTextarea(); + } + } + } + + // 监听消息变化,自动隐藏/显示展示区域 + function updateShowcaseVisibility() { + const showcase = document.getElementById('exampleShowcase'); + if (showcase) { + if (hasMessages) { + showcase.classList.add('hidden'); + } else { + showcase.classList.remove('hidden'); + } + } + } + + // 扩展原有的布局更新函数 + const originalUpdateInputAreaLayout = updateInputAreaLayout; + updateInputAreaLayout = function() { + if (originalUpdateInputAreaLayout) { + originalUpdateInputAreaLayout(); + } + updateShowcaseVisibility(); + }; + `; +} diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts index 8aa6815..9baa1bd 100644 --- a/src/views/inputArea.ts +++ b/src/views/inputArea.ts @@ -29,16 +29,21 @@ import { getOptimizeButtonStyles, getOptimizeButtonScript, } from "./optimizeButton"; +import { + getExampleShowcaseContent, + getExampleShowcaseStyles, + getExampleShowcaseScript, +} from "./exampleShowcase"; import { sendIconSvg, stopIconSvg } from "../constants/toolIcons"; /** * 获取输入区域的 HTML 内容 */ export function getInputAreaContent( - autoIcon: string = '', - liteIcon: string = '', - syIcon: string = '', - maxIcon: string = '' + autoIcon: string = "", + liteIcon: string = "", + syIcon: string = "", + maxIcon: string = "" ): string { return `
@@ -71,6 +76,8 @@ export function getInputAreaContent(
+ + ${getExampleShowcaseContent()} `; } @@ -86,6 +93,7 @@ export function getInputAreaStyles(): string { ${getContextDisplayStyles()} ${getContextCompressStyles()} ${getOptimizeButtonStyles()} + ${getExampleShowcaseStyles()} .input-area { border-top: 1px solid var(--vscode-panel-border); padding-top: 15px; @@ -95,7 +103,7 @@ export function getInputAreaStyles(): string { /* 居中模式:未发起对话时 */ .input-area.centered { position: absolute; - top: 50%; + top: 55%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 40px); @@ -292,6 +300,7 @@ export function getInputAreaScript(): string { ${getContextDisplayScript()} ${getContextCompressScript()} ${getOptimizeButtonScript()} + ${getExampleShowcaseScript()} // 对话状态管理 let isConversationActive = false; From 25966bc1e2996f30b96bcc765cc69786247c4bea Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 19:09:19 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat:=E6=98=BE=E7=A4=BA=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=82=B9=20-=20=E7=99=BB=E5=BD=95=E4=B9=8B=E5=90=8E=E5=B0=B1?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E8=B5=84=E6=BA=90=E7=82=B9=E5=B9=B6=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=20-=20=E6=98=BE=E7=A4=BA=E5=89=A9=E4=BD=99?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E7=82=B9=E5=88=B0=E9=A1=B5=E9=9D=A2=E4=B8=8A?= =?UTF-8?q?=20-=20=E4=B8=80=E8=BD=AE=E5=AF=B9=E8=AF=9D=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=B9=8B=E5=90=8E=E9=87=8D=E6=96=B0=E8=8E=B7=E5=8F=96=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E7=82=B9=E5=B9=B6=E4=B8=94=E6=9B=B4=E6=96=B0=E7=BC=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 4 ++++ src/panels/ICHelperPanel.ts | 10 +++++--- src/services/creditsService.ts | 36 +++++++++++++++++++++++++++- src/services/userService.ts | 43 ++++++++++++++++++++++++++++++---- src/utils/messageHandler.ts | 13 +++++++++- src/views/userInfoComponent.ts | 15 +++++++++++- src/views/webviewContent.ts | 7 +++++- 7 files changed, 116 insertions(+), 12 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index e1c67b4..80f49c3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { ChatHistoryManager } from "./utils/chatHistoryManager"; import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider"; import { VCDFileServer } from "./services/vcdFileServer"; import { initUserService } from "./services/userService"; +import { initCreditsService } from "./services/creditsService"; export function activate(context: vscode.ExtensionContext) { console.log("🎉 IC Coder 插件已激活!"); @@ -13,6 +14,9 @@ export function activate(context: vscode.ExtensionContext) { // 初始化用户服务 initUserService(context); + // 初始化 Credits 服务 + initCreditsService(context); + // 初始化 VCD 文件服务器 const vcdFileServer = new VCDFileServer(context.extensionUri); vcdFileServer.start().then((port) => { diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index c8f2007..3a239d9 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -147,16 +147,20 @@ export async function showICHelperPanel( if (userInfo) { // 使用缓存的用户信息 console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo); + console.log('[ICHelperPanel] Credits 余额:', userInfo.credits); const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode); - panel.webview.postMessage({ + const messageData = { command: 'updateUserInfo', userInfo: { userId: userInfo.userId, nickname: userInfo.nickname, - username: userInfo.username + username: userInfo.username, + credits: userInfo.credits }, tierIconUrl: tierIconUrl - }); + }; + console.log('[ICHelperPanel] 发送用户信息到前端:', messageData); + panel.webview.postMessage(messageData); } else { // 如果没有缓存,从 session 中获取 const session = await vscode.authentication.getSession("iccoder", [], { diff --git a/src/services/creditsService.ts b/src/services/creditsService.ts index 09a3a9f..475d379 100644 --- a/src/services/creditsService.ts +++ b/src/services/creditsService.ts @@ -22,6 +22,33 @@ let lastUpdateTime: number = 0; /** 缓存有效期(5分钟) */ const CACHE_TTL_MS = 5 * 60 * 1000; +/** ExtensionContext 用于持久化存储 */ +let extensionContext: vscode.ExtensionContext | null = null; + +/** + * 初始化 Credits 服务(设置 context) + */ +export function initCreditsService(context: vscode.ExtensionContext): void { + extensionContext = context; + // 从持久化存储加载余额 + const savedBalance = extensionContext.globalState.get('icCoderCreditsBalance'); + if (savedBalance !== undefined) { + cachedBalance = savedBalance; + lastUpdateTime = Date.now(); + console.log('[CreditsService] 从持久化存储加载余额:', savedBalance); + } +} + +/** + * 保存余额到持久化存储 + */ +async function saveBalance(balance: number): Promise { + if (extensionContext) { + await extensionContext.globalState.update('icCoderCreditsBalance', balance); + console.log('[CreditsService] 余额已保存到持久化存储:', balance); + } +} + /** * 更新缓存的余额(从 SSE credit_update 事件调用) */ @@ -29,6 +56,10 @@ export function updateCachedBalance(balance: number): void { cachedBalance = balance; lastUpdateTime = Date.now(); console.log('[CreditsService] 余额已更新:', balance); + // 异步保存到持久化存储 + saveBalance(balance).catch(err => { + console.error('[CreditsService] 保存余额失败:', err); + }); } /** @@ -203,8 +234,11 @@ export async function checkBalanceBeforeSend(): Promise<{ /** * 清除缓存(登出时调用) */ -export function clearBalanceCache(): void { +export async function clearBalanceCache(): Promise { cachedBalance = null; lastUpdateTime = 0; + if (extensionContext) { + await extensionContext.globalState.update('icCoderCreditsBalance', undefined); + } console.log('[CreditsService] 余额缓存已清除'); } diff --git a/src/services/userService.ts b/src/services/userService.ts index c02a2e6..e91d641 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -8,6 +8,7 @@ import { URL } from 'url'; import * as vscode from 'vscode'; import { getStrangeLoopApiUrl, getConfig } from '../config/settings'; import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api'; +import { fetchBalance, getCachedBalance } from './creditsService'; /** * HTTP 请求选项 @@ -114,6 +115,8 @@ export interface UserInfo { remainingDays?: number; monthlyCredits?: number; }; + // Credits 余额 + credits?: number; } /** @@ -221,12 +224,13 @@ function getHighestTierMembership(allMemberships?: MembershipItemVO[]): Membersh */ export async function onTokenReceived(token: string): Promise { try { - console.log('[UserService] Token 已获取,正在获取用户信息和会员信息...'); + console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...'); - // 并行获取用户信息和会员信息 - const [userInfo, membershipInfo] = await Promise.all([ + // 并行获取用户信息、会员信息和余额 + const [userInfo, membershipInfo, credits] = await Promise.all([ getUserInfo(token), - getMembershipInfo(token) + getMembershipInfo(token), + fetchBalance() ]); if (!userInfo) { @@ -234,6 +238,15 @@ export async function onTokenReceived(token: string): Promise { return null; } + // 添加 Credits 余额到用户信息 + console.log('[UserService] 获取到的 Credits 余额:', credits); + if (credits !== null) { + userInfo.credits = credits; + console.log('[UserService] Credits 已添加到用户信息'); + } else { + console.warn('[UserService] Credits 余额为 null,未添加到用户信息'); + } + // 打印用户信息到控制台 console.log('='.repeat(60)); console.log('用户信息详情:'); @@ -286,6 +299,15 @@ export async function onTokenReceived(token: string): Promise { } } + // 打印 Credits 余额 + console.log(''); + console.log('资源点余额:'); + if (userInfo.credits !== undefined) { + console.log(`当前余额: ${userInfo.credits} Credits`); + } else { + console.log('当前余额: 未获取到余额信息'); + } + console.log('='.repeat(60)); // 保存到持久化存储 @@ -329,7 +351,18 @@ export function getCachedUserInfo(): UserInfo | null { console.warn('[UserService] ExtensionContext 未初始化'); return null; } - return extensionContext.globalState.get('icCoderUserInfo') || null; + const userInfo = extensionContext.globalState.get('icCoderUserInfo') || null; + + // 从 creditsService 加载余额并合并到用户信息中 + if (userInfo) { + const cachedCredits = getCachedBalance(); + if (cachedCredits !== null) { + userInfo.credits = cachedCredits; + console.log('[UserService] 从 creditsService 加载余额:', cachedCredits); + } + } + + return userInfo; } /** diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index b0ee6b1..6e73e34 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -18,7 +18,7 @@ import { ChatHistoryManager } from "./chatHistoryManager"; import { dialogManager, DialogSession } from "../services/dialogService"; import { userInteractionManager } from "../services/userInteraction"; import { healthCheck } from "../services/apiClient"; -import { checkBalanceBeforeSend } from "../services/creditsService"; +import { checkBalanceBeforeSend, fetchBalance } from "../services/creditsService"; import type { RunMode, ServiceTier } from "../types/api"; @@ -200,6 +200,17 @@ async function handleUserMessageWithBackend( // 最后一次发送完整的段落 console.log("[MessageHandler] 对话完成, 段落数:", segments.length); + // 对话完成后重新获取余额(因为已经消耗了 Credits) + try { + console.log("[MessageHandler] 对话完成,重新获取余额..."); + const newBalance = await fetchBalance(); + if (newBalance !== null) { + console.log("[MessageHandler] 余额已更新:", newBalance); + } + } catch (error) { + console.error("[MessageHandler] 获取余额失败:", error); + } + const result = await panel.webview.postMessage({ command: "updateSegments", segments: segments, diff --git a/src/views/userInfoComponent.ts b/src/views/userInfoComponent.ts index 444ad59..95bb043 100644 --- a/src/views/userInfoComponent.ts +++ b/src/views/userInfoComponent.ts @@ -250,14 +250,27 @@ export function getUserInfoComponentScript(): string { // 更新剩余 Credits const creditsDetail = document.getElementById('creditsDetail'); + console.log('[UserInfoComponent] 更新 Credits 显示'); + console.log('[UserInfoComponent] currentUserInfo.credits:', currentUserInfo.credits); + console.log('[UserInfoComponent] creditsDetail 元素:', creditsDetail); if (creditsDetail) { - creditsDetail.textContent = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-'; + const creditsText = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-'; + creditsDetail.textContent = creditsText; + console.log('[UserInfoComponent] Credits 已更新为:', creditsText); + } else { + console.warn('[UserInfoComponent] creditsDetail 元素未找到'); } } // 更新用户信息显示 function updateUserInfoDisplay(userInfo) { currentUserInfo = userInfo; + console.log('[UserInfoComponent] 更新用户信息:', userInfo); + // 如果下拉面板已打开,立即更新显示 + const dropdown = document.getElementById('userDetailDropdown'); + if (dropdown && dropdown.classList.contains('active')) { + updateUserDetailModal(); + } } // 绑定下拉面板事件 diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 9cd5e20..a4d03be 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -588,20 +588,25 @@ export function getWebviewContent( case 'updateUserInfo': // 更新用户信息 console.log('[WebView] 收到用户信息:', message.userInfo); + console.log('[WebView] Credits 字段值:', message.userInfo?.credits); if (message.userInfo) { const userInfoData = { nickname: message.userInfo.nickname || message.userInfo.username || '用户', userId: message.userInfo.userId || message.userInfo.id, tierName: message.userInfo.tierName, tierIconUrl: message.tierIconUrl, - registerTime: message.userInfo.registerTime || message.userInfo.createdAt + registerTime: message.userInfo.registerTime || message.userInfo.createdAt, + credits: message.userInfo.credits }; console.log('[WebView] 显示用户信息:', userInfoData); + console.log('[WebView] userInfoData.credits:', userInfoData.credits); // 调用更新用户头像图标按钮的函数 if (typeof updateUserAvatarIconButton === 'function') { updateUserAvatarIconButton(userInfoData); + } else { + console.warn('[WebView] updateUserAvatarIconButton 函数不存在'); } } break; From 58113fb1090c1f905034c389fbcd8f083d37b2b8 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 12 Jan 2026 21:03:37 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat:Credits=E4=B8=8D=E8=B6=B3=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E8=B7=B3=E8=BD=AC=E5=88=B0web=E7=AB=AF=E7=9A=84?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/dialogService.ts | 531 ++++++++++++++++++++++------------ src/utils/messageHandler.ts | 45 ++- 2 files changed, 388 insertions(+), 188 deletions(-) diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index fab077f..bb4d01c 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -2,27 +2,44 @@ * 对话服务 * 整合 SSE 通信、工具执行、用户交互 */ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler'; -import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; -import { userInteractionManager } from './userInteraction'; -import { getConfig } from '../config/settings'; -import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; -import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; -import { ChatHistoryManager } from '../utils/chatHistoryManager'; -import { getUserIdFromToken } from '../utils/jwtUtils'; -import { updateCachedBalance } from './creditsService'; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { + startStreamDialog, + generateTaskId, + SSEController, + SSECallbacks, +} from "./sseHandler"; +import { + executeToolCall, + createToolExecutorContext, + ToolExecutorContext, +} from "./toolExecutor"; +import { userInteractionManager } from "./userInteraction"; +import { getConfig } from "../config/settings"; +import type { + DialogRequest, + ToolCallRequest, + AskUserEvent, + RunMode, + ServiceTier, + ToolConfirmEvent, + PlanConfirmEvent, +} from "../types/api"; +import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient"; +import { ChatHistoryManager } from "../utils/chatHistoryManager"; +import { getUserIdFromToken } from "../utils/jwtUtils"; +import { updateCachedBalance } from "./creditsService"; /** * 消息段落类型 */ export interface MessageSegment { - type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress'; + type: "text" | "tool" | "question" | "agent" | "plan" | "progress"; content?: string; toolName?: string; - toolStatus?: 'running' | 'success' | 'error'; + toolStatus?: "running" | "success" | "error"; toolResult?: string; askId?: string; question?: string; @@ -30,15 +47,15 @@ export interface MessageSegment { // 智能体相关字段 agentId?: string; agentName?: string; - agentStatus?: 'running' | 'completed' | 'error'; + agentStatus?: "running" | "completed" | "error"; agentSteps?: AgentStep[]; // 计划相关字段 planTitle?: string; - planPhases?: import('../types/api').PlanPhase[]; + planPhases?: import("../types/api").PlanPhase[]; planSteps?: string[]; planSummary?: string; // 进度条相关字段(独立于 plan,用于执行模式) - progressPhases?: import('../types/api').PlanPhase[]; + progressPhases?: import("../types/api").PlanPhase[]; } /** @@ -49,7 +66,7 @@ export interface AgentStep { toolName: string; toolInput?: unknown; toolResult?: string; - status: 'running' | 'completed' | 'error'; + status: "running" | "completed" | "error"; } /** @@ -65,9 +82,19 @@ export interface DialogCallbacks { /** 工具执行错误 */ onToolError?: (toolName: string, error: string) => void; /** 工具确认请求(Ask 模式) */ - onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record) => void; + onToolConfirm?: ( + confirmId: number, + toolName: string, + toolInput: Record + ) => void; /** 计划确认请求(Plan 模式) */ - onPlanConfirm?: (confirmId: number, title: string, phases: import('../types/api').PlanPhase[] | undefined, steps: string[] | undefined, summary: string) => void; + onPlanConfirm?: ( + confirmId: number, + title: string, + phases: import("../types/api").PlanPhase[] | undefined, + steps: string[] | undefined, + summary: string + ) => void; /** 显示问题(ask_user) */ onQuestion?: (askId: string, question: string, options: string[]) => void; /** 实时更新段落(流式过程中) */ @@ -79,7 +106,11 @@ export interface DialogCallbacks { /** 通知消息 */ onNotification?: (message: string) => void; /** 上下文使用量更新 */ - onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void; + onContextUsage?: (data: { + currentTokens: number; + maxTokens: number; + percentage: number; + }) => void; /** 阶段进度更新 */ onPhaseProgress?: (phaseId: string, status: string) => void; } @@ -91,12 +122,13 @@ export class DialogSession { private taskId: string; private sseController: SSEController | null = null; private toolContext: ToolExecutorContext; - private accumulatedText = ''; + private accumulatedText = ""; private isActive = false; - private hasCompleted = false; // 标记是否已收到 complete 事件 + private hasCompleted = false; // 标记是否已收到 complete 事件 private segments: MessageSegment[] = []; private currentTextSegment: MessageSegment | null = null; - private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发 + private completeCallback: ((segments: MessageSegment[]) => void) | null = + null; // 保存完成回调,用于 abort 时触发 constructor(extensionPath: string, existingTaskId?: string) { // 支持复用现有 taskId(用于 Plan 模式确认后继续执行) @@ -109,10 +141,11 @@ export class DialogSession { */ private appendText(text: string): void { if (!this.currentTextSegment) { - this.currentTextSegment = { type: 'text', content: '' }; + this.currentTextSegment = { type: "text", content: "" }; this.segments.push(this.currentTextSegment); } - this.currentTextSegment.content = (this.currentTextSegment.content || '') + text; + this.currentTextSegment.content = + (this.currentTextSegment.content || "") + text; } /** @@ -125,13 +158,17 @@ export class DialogSession { /** * 添加工具段落 */ - private addToolSegment(toolName: string, status: 'running' | 'success' | 'error', result?: string): MessageSegment { + private addToolSegment( + toolName: string, + status: "running" | "success" | "error", + result?: string + ): MessageSegment { this.finalizeTextSegment(); const segment: MessageSegment = { - type: 'tool', + type: "tool", toolName, toolStatus: status, - toolResult: result + toolResult: result, }; this.segments.push(segment); return segment; @@ -140,11 +177,19 @@ export class DialogSession { /** * 更新工具段落状态 */ - private updateToolSegment(toolName: string, status: 'success' | 'error', result?: string): void { + private updateToolSegment( + toolName: string, + status: "success" | "error", + result?: string + ): void { // 找到最后一个匹配的工具段落 for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'tool' && seg.toolName === toolName && seg.toolStatus === 'running') { + if ( + seg.type === "tool" && + seg.toolName === toolName && + seg.toolStatus === "running" + ) { seg.toolStatus = status; seg.toolResult = result; break; @@ -171,19 +216,23 @@ export class DialogSession { * 从 .iccoder/knowledge.json 读取 */ private async loadKnowledgeData(): Promise { - console.log('[DialogSession] loadKnowledgeData 开始执行'); + console.log("[DialogSession] loadKnowledgeData 开始执行"); // 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空) const workspaceFolders = await this.waitForWorkspaceFolders(); if (!workspaceFolders || workspaceFolders.length === 0) { - console.log('[DialogSession] 没有工作区文件夹'); + console.log("[DialogSession] 没有工作区文件夹"); return null; } // 多根工作区场景:优先读取实际存在 knowledge.json 的根目录 for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) { - const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json'); - console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString()); + const knowledgeUri = vscode.Uri.joinPath( + folder.uri, + ".iccoder", + "knowledge.json" + ); + console.log("[DialogSession] 知识图谱 URI:", knowledgeUri.toString()); try { const content = await this.readTextFileWithRetry(knowledgeUri, 5); @@ -196,32 +245,40 @@ export class DialogSession { const parsed = JSON.parse(content) as any; // 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败 - if (parsed && typeof parsed === 'object' && 'empty' in parsed) { + if (parsed && typeof parsed === "object" && "empty" in parsed) { delete parsed.empty; } const sanitized = JSON.stringify(parsed); - console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length); + console.log( + "[DialogSession] 知识图谱已清洗, sanitizedLen:", + sanitized.length + ); return sanitized; } catch (e) { - console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e); + console.warn( + "[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:", + e + ); continue; } } catch (error) { - console.warn('[DialogSession] 加载知识图谱失败:', error); + console.warn("[DialogSession] 加载知识图谱失败:", error); } } return null; } - private async waitForWorkspaceFolders(): Promise { + private async waitForWorkspaceFolders(): Promise< + readonly vscode.WorkspaceFolder[] | undefined + > { for (let i = 0; i < 10; i++) { const folders = vscode.workspace.workspaceFolders; if (folders && folders.length > 0) { return folders; } - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } return vscode.workspace.workspaceFolders; } @@ -233,14 +290,16 @@ export class DialogSession { // 1) 当前激活文件所在的 workspace folder(如果有) const activeUri = vscode.window.activeTextEditor?.document?.uri; - const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + const activeFolder = activeUri + ? vscode.workspace.getWorkspaceFolder(activeUri) + : undefined; if (activeFolder) { result.push(activeFolder); } // 2) 其它 workspace folders(去重) for (const folder of workspaceFolders) { - if (!result.some(f => f.uri.toString() === folder.uri.toString())) { + if (!result.some((f) => f.uri.toString() === folder.uri.toString())) { result.push(folder); } } @@ -248,32 +307,42 @@ export class DialogSession { return result; } - private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise { + private async readTextFileWithRetry( + uri: vscode.Uri, + maxAttempts: number + ): Promise { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const bytes = await vscode.workspace.fs.readFile(uri); - const text = Buffer.from(bytes).toString('utf-8'); + const text = Buffer.from(bytes).toString("utf-8"); if (!text || !text.trim()) { return null; } return text; } catch (error) { // 文件不存在:不是错误,直接返回 null - if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + if ( + error instanceof vscode.FileSystemError && + error.code === "FileNotFound" + ) { return null; } const retryable = - (error instanceof vscode.FileSystemError && error.code === 'Unavailable') || - (typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code)); + (error instanceof vscode.FileSystemError && + error.code === "Unavailable") || + (typeof (error as any)?.code === "string" && + ["EBUSY", "EPERM", "EACCES"].includes((error as any).code)); if (!retryable || attempt >= maxAttempts) { throw error; } const delayMs = 50 * attempt; - console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`); - await new Promise(resolve => setTimeout(resolve, delayMs)); + console.log( + `[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms` + ); + await new Promise((resolve) => setTimeout(resolve, delayMs)); } } @@ -283,31 +352,38 @@ export class DialogSession { /** * 获取工具操作描述(用于确认对话框) */ - private getToolDescription(toolName: string, toolInput: Record): string { + private getToolDescription( + toolName: string, + toolInput: Record + ): string { const lines: string[] = []; switch (toolName) { - case 'file_write': - lines.push(`文件路径: ${toolInput.path || '未知'}`); + case "file_write": + lines.push(`文件路径: ${toolInput.path || "未知"}`); if (toolInput.content) { const content = String(toolInput.content); lines.push(`内容长度: ${content.length} 字符`); - lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`); + lines.push( + `内容预览: ${content.substring(0, 100)}${ + content.length > 100 ? "..." : "" + }` + ); } break; - case 'file_delete': - lines.push(`删除文件: ${toolInput.path || '未知'}`); + case "file_delete": + lines.push(`删除文件: ${toolInput.path || "未知"}`); break; - case 'syntax_check': - lines.push('执行语法检查'); + case "syntax_check": + lines.push("执行语法检查"); if (toolInput.code) { const code = String(toolInput.code); lines.push(`代码长度: ${code.length} 字符`); } break; - case 'simulation': - lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`); - lines.push(`TB文件: ${toolInput.tbPath || '未知'}`); + case "simulation": + lines.push(`RTL文件: ${toolInput.rtlPath || "未知"}`); + lines.push(`TB文件: ${toolInput.tbPath || "未知"}`); if (toolInput.duration) { lines.push(`仿真时长: ${toolInput.duration}`); } @@ -316,7 +392,7 @@ export class DialogSession { lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`); } - return lines.join('\n'); + return lines.join("\n"); } /** @@ -326,41 +402,52 @@ export class DialogSession { message: string, callbacks: DialogCallbacks, mode?: RunMode, - serviceTier?: ServiceTier // 新增:服务等级参数 + serviceTier?: ServiceTier // 新增:服务等级参数 ): Promise { if (this.isActive) { - callbacks.onError?.('当前有对话正在进行中'); + callbacks.onError?.("当前有对话正在进行中"); return; } this.isActive = true; - this.hasCompleted = false; // 重置完成标志 - this.accumulatedText = ''; + this.hasCompleted = false; // 重置完成标志 + this.accumulatedText = ""; this.segments = []; this.currentTextSegment = null; - this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发 + this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发 const config = getConfig(); // 从登录 session 获取真实 userId - let userId = config.userId; // 默认值 + let userId = config.userId; // 默认值 try { - console.log('[DialogSession] 尝试获取登录 session...'); - const session = await vscode.authentication.getSession('iccoder', [], { silent: true }); - console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined'); + console.log("[DialogSession] 尝试获取登录 session..."); + const session = await vscode.authentication.getSession("iccoder", [], { + silent: true, + }); + console.log( + "[DialogSession] session 结果:", + session ? "已获取" : "null/undefined" + ); if (session?.accessToken) { - console.log('[DialogSession] accessToken 长度:', session.accessToken.length); + console.log( + "[DialogSession] accessToken 长度:", + session.accessToken.length + ); const parsedUserId = getUserIdFromToken(session.accessToken); - console.log('[DialogSession] 解析的 userId:', parsedUserId); + console.log("[DialogSession] 解析的 userId:", parsedUserId); if (parsedUserId) { userId = parsedUserId; - console.log('[DialogSession] 使用真实 userId:', userId); + console.log("[DialogSession] 使用真实 userId:", userId); } } else { - console.log('[DialogSession] 未获取到 accessToken,使用默认 userId:', userId); + console.log( + "[DialogSession] 未获取到 accessToken,使用默认 userId:", + userId + ); } } catch (error) { - console.warn('[DialogSession] 获取登录 session 失败:', error); + console.warn("[DialogSession] 获取登录 session 失败:", error); } // 获取压缩数据和新消息(用于后端重启后恢复) @@ -370,19 +457,27 @@ export class DialogSession { // 加载知识图谱数据 const knowledgeData = await this.loadKnowledgeData(); - console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null'); + console.log( + "[DialogSession] knowledgeData 加载结果:", + knowledgeData ? `${knowledgeData.length} 字符` : "null" + ); - console.log('[DialogSession] serviceTier 参数:', serviceTier, '-> 使用:', serviceTier || config.serviceTier); + console.log( + "[DialogSession] serviceTier 参数:", + serviceTier, + "-> 使用:", + serviceTier || config.serviceTier + ); const request: DialogRequest = { taskId: this.taskId, message, userId, - mode: mode || 'agent', - serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数 + mode: mode || "agent", + serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数 compactedData: compactedData || undefined, newMessages: newMessages.length > 0 ? newMessages : undefined, - knowledgeData: knowledgeData || undefined + knowledgeData: knowledgeData || undefined, }; // 追踪用户消息 @@ -392,7 +487,10 @@ export class DialogSession { onTextDelta: (data) => { this.accumulatedText += data.text; this.appendText(data.text); - console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); + console.log( + "[DialogSession] onTextDelta, 累积文本长度:", + this.accumulatedText.length + ); callbacks.onText?.(this.accumulatedText, true); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); @@ -400,22 +498,34 @@ export class DialogSession { onToolCall: async (data: ToolCallRequest) => { const toolName = data.params.name; - console.log('[DialogSession] onToolCall:', toolName); + console.log("[DialogSession] onToolCall:", toolName); // 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示) const hasActiveAgent = this.segments.some( - s => s.type === 'agent' && s.agentStatus === 'running' + (s) => s.type === "agent" && s.agentStatus === "running" ); if (hasActiveAgent) { - console.log('[DialogSession] onToolCall: 智能体执行中,跳过工具段落:', toolName); + console.log( + "[DialogSession] onToolCall: 智能体执行中,跳过工具段落:", + toolName + ); } else { // 检查是否已经有相同的工具段落(可能由 onToolStart 添加) - const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); - if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') { - console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); + const lastToolSegment = this.segments + .filter((s) => s.type === "tool") + .pop(); + if ( + lastToolSegment && + lastToolSegment.toolName === toolName && + lastToolSegment.toolStatus === "running" + ) { + console.log( + "[DialogSession] onToolCall: 跳过重复的工具段落:", + toolName + ); } else { - this.addToolSegment(toolName, 'running'); + this.addToolSegment(toolName, "running"); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); } @@ -425,15 +535,15 @@ export class DialogSession { try { await executeToolCall(data, this.toolContext); if (!hasActiveAgent) { - this.updateToolSegment(toolName, 'success', '执行完成'); + this.updateToolSegment(toolName, "success", "执行完成"); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); } // 也不调用 callbacks.onToolComplete,避免重复 } catch (error) { - const errorMsg = error instanceof Error ? error.message : '未知错误'; + const errorMsg = error instanceof Error ? error.message : "未知错误"; if (!hasActiveAgent) { - this.updateToolSegment(toolName, 'error', errorMsg); + this.updateToolSegment(toolName, "error", errorMsg); callbacks.onSegmentUpdate?.(this.segments); } callbacks.onToolError?.(toolName, errorMsg); @@ -441,22 +551,28 @@ export class DialogSession { }, onToolStart: (data) => { - console.log('[DialogSession] onToolStart:', data.tool_name); + console.log("[DialogSession] onToolStart:", data.tool_name); // 检查是否已经有相同的工具段落(可能由 onToolCall 添加) - const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); - if (lastToolSegment && lastToolSegment.toolName === data.tool_name && lastToolSegment.toolStatus === 'running') { - console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name); + const lastToolSegment = this.segments + .filter((s) => s.type === "tool") + .pop(); + if ( + lastToolSegment && + lastToolSegment.toolName === data.tool_name && + lastToolSegment.toolStatus === "running" + ) { + console.log("[DialogSession] 跳过重复的工具段落:", data.tool_name); } else { - this.addToolSegment(data.tool_name, 'running'); + this.addToolSegment(data.tool_name, "running"); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); } - console.log('[DialogSession] segments 数量:', this.segments.length); + console.log("[DialogSession] segments 数量:", this.segments.length); callbacks.onToolStart?.(data.tool_name); }, onToolComplete: (data) => { - this.updateToolSegment(data.tool_name, 'success', data.result); + this.updateToolSegment(data.tool_name, "success", data.result); callbacks.onToolComplete?.(data.tool_name, data.result); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); @@ -465,7 +581,7 @@ export class DialogSession { }, onToolError: (data) => { - this.updateToolSegment(data.tool_name, 'error', data.error); + this.updateToolSegment(data.tool_name, "error", data.error); callbacks.onToolError?.(data.tool_name, data.error); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); @@ -474,20 +590,27 @@ export class DialogSession { }, onToolConfirm: async (data: ToolConfirmEvent) => { - console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId); + console.log( + "[DialogSession] onToolConfirm:", + data.toolName, + data.confirmId + ); // 结束当前文本段落 this.finalizeTextSegment(); // 生成工具描述 - const toolDescription = this.getToolDescription(data.toolName, data.toolInput); + const toolDescription = this.getToolDescription( + data.toolName, + data.toolInput + ); // 构建问题文本 const toolNameMap: Record = { - 'file_write': '写入文件', - 'file_delete': '删除文件', - 'syntax_check': '语法检查', - 'simulation': '运行仿真' + file_write: "写入文件", + file_delete: "删除文件", + syntax_check: "语法检查", + simulation: "运行仿真", }; const toolDisplayName = toolNameMap[data.toolName] || data.toolName; const question = `确认执行操作:${toolDisplayName}\n\n${toolDescription}`; @@ -497,17 +620,21 @@ export class DialogSession { // 添加问题段落到聊天界面 this.segments.push({ - type: 'question', + type: "question", askId: askId, question: question, - options: ['确认执行', '取消'] + options: ["确认执行", "取消"], }); // 实时发送段落更新 callbacks.onSegmentUpdate?.(this.segments); // 调用回调通知 UI - callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput); + callbacks.onToolConfirm?.( + data.confirmId, + data.toolName, + data.toolInput + ); // 使用 userInteractionManager 等待用户回答 try { @@ -515,7 +642,7 @@ export class DialogSession { { askId: askId, question: question, - options: ['确认执行', '取消'] + options: ["确认执行", "取消"], } as AskUserEvent, this.taskId ); @@ -523,22 +650,22 @@ export class DialogSession { // 注意:用户回答后,需要在 receiveAnswer 中处理 tool_confirm 类型的 askId // 这里不直接调用 submitToolConfirm,而是在 userInteractionManager 中统一处理 } catch (error) { - console.error('[DialogSession] 处理工具确认失败:', error); + console.error("[DialogSession] 处理工具确认失败:", error); // 如果出错,默认取消执行 try { await submitToolConfirm({ confirmId: data.confirmId, taskId: this.taskId, - approved: false + approved: false, }); } catch (submitError) { - console.error('[DialogSession] 发送取消响应失败:', submitError); + console.error("[DialogSession] 发送取消响应失败:", submitError); } } }, onPlanConfirm: async (data: PlanConfirmEvent) => { - console.log('[DialogSession] onPlanConfirm:', data.title); + console.log("[DialogSession] onPlanConfirm:", data.title); // 结束当前文本段落 this.finalizeTextSegment(); @@ -548,12 +675,12 @@ export class DialogSession { // 添加计划段落到聊天界面(包含 askId 用于响应) // 支持新格式(phases)和旧格式(steps) this.segments.push({ - type: 'plan', + type: "plan", askId: askId, planTitle: data.title, planPhases: data.phases, planSteps: data.steps, - planSummary: data.summary + planSummary: data.summary, }); // 实时发送段落更新 @@ -563,26 +690,39 @@ export class DialogSession { const planEvent = { askId: askId, question: `请确认执行计划:${data.title}`, - options: ['确认执行', '修改计划', '取消'] + options: ["确认执行", "修改计划", "取消"], }; try { - await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId); + await userInteractionManager.handleAskUser( + planEvent as AskUserEvent, + this.taskId + ); } catch (error) { - console.error('[DialogSession] 处理计划确认失败:', error); + console.error("[DialogSession] 处理计划确认失败:", error); } // 调用回调通知 UI - callbacks.onPlanConfirm?.(data.confirmId, data.title, data.phases, data.steps, data.summary); + callbacks.onPlanConfirm?.( + data.confirmId, + data.title, + data.phases, + data.steps, + data.summary + ); }, - onPhaseProgress: (data: import('../types/api').PhaseProgressEvent) => { - console.log('[DialogSession] onPhaseProgress:', data.phaseId, data.status); + onPhaseProgress: (data: import("../types/api").PhaseProgressEvent) => { + console.log( + "[DialogSession] onPhaseProgress:", + data.phaseId, + data.status + ); // 1. 尝试更新 plan segment(兼容旧逻辑) for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'plan' && seg.planPhases) { - seg.planPhases = seg.planPhases.map(phase => { + if (seg.type === "plan" && seg.planPhases) { + seg.planPhases = seg.planPhases.map((phase) => { if (phase.id === data.phaseId) { return { ...phase, status: data.status }; } @@ -597,13 +737,13 @@ export class DialogSession { callbacks.onPhaseProgress?.(data.phaseId, data.status); }, - onPlanStepAdd: (data: import('../types/api').PlanStepAddEvent) => { - console.log('[DialogSession] onPlanStepAdd:', data.phaseId, data.step); + onPlanStepAdd: (data: import("../types/api").PlanStepAddEvent) => { + console.log("[DialogSession] onPlanStepAdd:", data.phaseId, data.step); for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'plan' && seg.planPhases) { - seg.planPhases = seg.planPhases.map(phase => { + if (seg.type === "plan" && seg.planPhases) { + seg.planPhases = seg.planPhases.map((phase) => { if (phase.id === data.phaseId) { const newSteps = [...(phase.steps || [])]; if (data.index >= 0 && data.index < newSteps.length) { @@ -621,13 +761,17 @@ export class DialogSession { callbacks.onSegmentUpdate?.(this.segments); }, - onPlanStepRemove: (data: import('../types/api').PlanStepRemoveEvent) => { - console.log('[DialogSession] onPlanStepRemove:', data.phaseId, data.stepIndex); + onPlanStepRemove: (data: import("../types/api").PlanStepRemoveEvent) => { + console.log( + "[DialogSession] onPlanStepRemove:", + data.phaseId, + data.stepIndex + ); for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'plan' && seg.planPhases) { - seg.planPhases = seg.planPhases.map(phase => { + if (seg.type === "plan" && seg.planPhases) { + seg.planPhases = seg.planPhases.map((phase) => { if (phase.id === data.phaseId && phase.steps) { const newSteps = [...phase.steps]; newSteps.splice(data.stepIndex, 1); @@ -641,13 +785,17 @@ export class DialogSession { callbacks.onSegmentUpdate?.(this.segments); }, - onPlanStepUpdate: (data: import('../types/api').PlanStepUpdateEvent) => { - console.log('[DialogSession] onPlanStepUpdate:', data.phaseId, data.stepIndex); + onPlanStepUpdate: (data: import("../types/api").PlanStepUpdateEvent) => { + console.log( + "[DialogSession] onPlanStepUpdate:", + data.phaseId, + data.stepIndex + ); for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'plan' && seg.planPhases) { - seg.planPhases = seg.planPhases.map(phase => { + if (seg.type === "plan" && seg.planPhases) { + seg.planPhases = seg.planPhases.map((phase) => { if (phase.id === data.phaseId && phase.steps) { const newSteps = [...phase.steps]; if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) { @@ -663,12 +811,14 @@ export class DialogSession { callbacks.onSegmentUpdate?.(this.segments); }, - onPlanSummaryUpdate: (data: import('../types/api').PlanSummaryUpdateEvent) => { - console.log('[DialogSession] onPlanSummaryUpdate'); + onPlanSummaryUpdate: ( + data: import("../types/api").PlanSummaryUpdateEvent + ) => { + console.log("[DialogSession] onPlanSummaryUpdate"); for (let i = this.segments.length - 1; i >= 0; i--) { const seg = this.segments[i]; - if (seg.type === 'plan') { + if (seg.type === "plan") { seg.planSummary = data.summary; break; } @@ -679,10 +829,10 @@ export class DialogSession { onAskUser: async (data: AskUserEvent) => { this.finalizeTextSegment(); this.segments.push({ - type: 'question', + type: "question", askId: data.askId, question: data.question, - options: data.options + options: data.options, }); // 实时发送段落更新(包含问题) callbacks.onSegmentUpdate?.(this.segments); @@ -691,13 +841,13 @@ export class DialogSession { try { await userInteractionManager.handleAskUser(data, this.taskId); } catch (error) { - console.error('[DialogSession] 处理用户问题失败:', error); + console.error("[DialogSession] 处理用户问题失败:", error); } }, onComplete: (data) => { this.isActive = false; - this.hasCompleted = true; // 标记已收到 complete 事件 + this.hasCompleted = true; // 标记已收到 complete 事件 this.finalizeTextSegment(); // 追踪 AI 消息(用于后端重启后恢复) @@ -724,34 +874,41 @@ export class DialogSession { // 智能体事件处理 onAgentStart: (data) => { - console.log('[DialogSession] onAgentStart:', data.agentId); + console.log("[DialogSession] onAgentStart:", data.agentId); this.finalizeTextSegment(); this.segments.push({ - type: 'agent', + type: "agent", agentId: data.agentId, agentName: data.agentName, content: data.instruction, - agentStatus: 'running', - agentSteps: [] + agentStatus: "running", + agentSteps: [], }); callbacks.onSegmentUpdate?.(this.segments); }, onAgentProgress: (data) => { - console.log('[DialogSession] onAgentProgress:', data.agentId, data.step, data.status); + console.log( + "[DialogSession] onAgentProgress:", + data.agentId, + data.step, + data.status + ); const agentSegment = this.segments.find( - s => s.type === 'agent' && s.agentId === data.agentId + (s) => s.type === "agent" && s.agentId === data.agentId ); if (agentSegment && agentSegment.agentSteps) { - if (data.status === 'running') { + if (data.status === "running") { agentSegment.agentSteps.push({ step: data.step, toolName: data.toolName, toolInput: data.toolInput, - status: 'running' + status: "running", }); } else { - const step = agentSegment.agentSteps.find(s => s.step === data.step); + const step = agentSegment.agentSteps.find( + (s) => s.step === data.step + ); if (step) { step.status = data.status; step.toolResult = data.toolResult; @@ -762,68 +919,84 @@ export class DialogSession { }, onAgentComplete: (data) => { - console.log('[DialogSession] onAgentComplete:', data.agentId); + console.log("[DialogSession] onAgentComplete:", data.agentId); const agentSegment = this.segments.find( - s => s.type === 'agent' && s.agentId === data.agentId + (s) => s.type === "agent" && s.agentId === data.agentId ); if (agentSegment) { - agentSegment.agentStatus = 'completed'; + agentSegment.agentStatus = "completed"; agentSegment.content = data.summary; callbacks.onSegmentUpdate?.(this.segments); } }, onAgentError: (data) => { - console.log('[DialogSession] onAgentError:', data.agentId, data.error); + console.log("[DialogSession] onAgentError:", data.agentId, data.error); const agentSegment = this.segments.find( - s => s.type === 'agent' && s.agentId === data.agentId + (s) => s.type === "agent" && s.agentId === data.agentId ); if (agentSegment) { - agentSegment.agentStatus = 'error'; + agentSegment.agentStatus = "error"; agentSegment.content = data.error; callbacks.onSegmentUpdate?.(this.segments); } }, onMemoryCompacted: async (data) => { - console.log('[DialogSession] onMemoryCompacted:', data.taskId); + console.log("[DialogSession] onMemoryCompacted:", data.taskId); // 保存压缩数据到本地 await historyManager.saveCompactedData(data.compactedData); }, onContextUsage: (data) => { - console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens); + console.log( + "[DialogSession] onContextUsage:", + data.currentTokens, + "/", + data.maxTokens + ); callbacks.onContextUsage?.(data); }, onCreditUpdate: (data) => { - console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits); + console.log( + "[DialogSession] onCreditUpdate: 扣除", + data.deductedCredits, + "剩余", + data.remainingCredits + ); // 更新余额缓存 updateCachedBalance(data.remainingCredits); // 资源点余额低于阈值时弹窗提醒 const LOW_CREDIT_THRESHOLD = 5; if (data.remainingCredits < LOW_CREDIT_THRESHOLD) { - vscode.window.showWarningMessage( - `资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(2)} 点,请及时充值。`, - '去充值' - ).then(selection => { - if (selection === '去充值') { - // 打开充值页面 - vscode.env.openExternal(vscode.Uri.parse('https://iccoder.com/recharge')); - } - }); + vscode.window + .showWarningMessage( + `资源点余额不足!当前剩余 ${data.remainingCredits.toFixed( + 2 + )} 点,请及时充值。`, + "去充值" + ) + .then((selection) => { + if (selection === "去充值") { + // 打开充值页面 + vscode.env.openExternal( + vscode.Uri.parse("https://iccoder.com/memberCenter") + ); + } + }); } }, onOpen: () => { - console.log('[DialogSession] SSE 连接已建立'); + console.log("[DialogSession] SSE 连接已建立"); }, onClose: () => { - console.log('[DialogSession] SSE 连接已关闭'); + console.log("[DialogSession] SSE 连接已关闭"); // 如果没有收到 complete 事件,需要补充完成逻辑 if (!this.hasCompleted && this.isActive) { - console.log('[DialogSession] 未收到 complete 事件,补充完成处理'); + console.log("[DialogSession] 未收到 complete 事件,补充完成处理"); this.finalizeTextSegment(); if (this.accumulatedText) { historyManager.trackAiMessage(this.accumulatedText); @@ -831,14 +1004,14 @@ export class DialogSession { callbacks.onComplete?.(this.segments); } this.isActive = false; - } + }, }; try { this.sseController = await startStreamDialog(request, sseCallbacks); } catch (error) { this.isActive = false; - const errorMsg = error instanceof Error ? error.message : '连接失败'; + const errorMsg = error instanceof Error ? error.message : "连接失败"; callbacks.onError?.(errorMsg); throw error; } @@ -862,14 +1035,14 @@ export class DialogSession { // 如果之前是活跃状态,触发完成回调以结束 Promise if (wasActive && this.completeCallback) { this.finalizeTextSegment(); - console.log('[DialogSession] abort 触发完成回调'); + console.log("[DialogSession] abort 触发完成回调"); this.completeCallback(this.segments); this.completeCallback = null; } // 通知后端停止处理 - stopDialog(this.taskId).catch(err => { - console.warn('[DialogSession] 停止对话请求失败:', err); + stopDialog(this.taskId).catch((err) => { + console.warn("[DialogSession] 停止对话请求失败:", err); }); } @@ -931,7 +1104,7 @@ class DialogManager { */ abortCurrentSession(): void { this.currentSession?.abort(); - this.currentSession = null; // 清空会话,确保下次创建新会话 + this.currentSession = null; // 清空会话,确保下次创建新会话 } } diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 6e73e34..adfc75c 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -18,7 +18,10 @@ import { ChatHistoryManager } from "./chatHistoryManager"; import { dialogManager, DialogSession } from "../services/dialogService"; import { userInteractionManager } from "../services/userInteraction"; import { healthCheck } from "../services/apiClient"; -import { checkBalanceBeforeSend, fetchBalance } from "../services/creditsService"; +import { + checkBalanceBeforeSend, + fetchBalance, +} from "../services/creditsService"; import type { RunMode, ServiceTier } from "../types/api"; @@ -39,7 +42,7 @@ export async function handleUserMessage( text: string, extensionPath?: string, mode?: RunMode, - serviceTier?: ServiceTier // 新增:服务等级参数 + serviceTier?: ServiceTier // 新增:服务等级参数 ) { console.log("收到用户消息:", text); @@ -78,7 +81,9 @@ export async function handleUserMessage( "去充值" ); if (selection === "去充值") { - vscode.env.openExternal(vscode.Uri.parse("https://iccoder.com/recharge")); + vscode.env.openExternal( + vscode.Uri.parse("https://iccoder.com/memberCenter") + ); } // 恢复输入状态 panel.webview.postMessage({ @@ -92,7 +97,14 @@ export async function handleUserMessage( // 尝试使用后端服务 if (useBackendService && extensionPath) { try { - await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier); + await handleUserMessageWithBackend( + panel, + text, + extensionPath, + mode, + undefined, + serviceTier + ); return; } catch (error) { console.error("后端服务不可用:", error); @@ -128,7 +140,7 @@ async function handleUserMessageWithBackend( extensionPath: string, mode?: RunMode, reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行) - serviceTier?: ServiceTier // 新增:服务等级参数 + serviceTier?: ServiceTier // 新增:服务等级参数 ): Promise { const historyManager = ChatHistoryManager.getInstance(); @@ -137,10 +149,18 @@ async function handleUserMessageWithBackend( const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId(); // 创建会话(dialogManager 会自动处理旧会话的中止) - currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined); + currentSession = dialogManager.createSession( + extensionPath, + taskIdToUse || undefined + ); // 保存 taskId 用于后续操作(如压缩) lastTaskId = currentSession.getTaskId(); - console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成"); + console.log( + "[MessageHandler] 创建会话: taskId=", + lastTaskId, + "来源=", + taskIdToUse ? "historyManager" : "新生成" + ); // 显示状态栏 panel.webview.postMessage({ @@ -297,7 +317,7 @@ async function handleUserMessageWithBackend( }, }, mode, - serviceTier // 传递服务等级 + serviceTier // 传递服务等级 ); }); } @@ -380,7 +400,14 @@ export async function handlePlanAction( extensionPath: string, serviceTier?: ServiceTier ): Promise { - console.log("[handlePlanAction] action:", action, "planTitle:", planTitle, "serviceTier:", serviceTier); + console.log( + "[handlePlanAction] action:", + action, + "planTitle:", + planTitle, + "serviceTier:", + serviceTier + ); switch (action) { case "confirm": From c571cd9137d4c86a574bd0200fc24d2feedc5ed7 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:01:35 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Gateway=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev 环境: localhost:8080/iccoder - test 环境: 192.168.1.108:2029/iccoder - prod 环境: api.iccoder.com/iccoder --- src/config/settings.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config/settings.ts b/src/config/settings.ts index 2829944..769ba9c 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -8,7 +8,7 @@ import * as vscode from "vscode"; type Environment = "dev" | "test" | "prod"; /** 当前环境 - 修改这里切换环境 */ -const CURRENT_ENV: Environment = "dev"; +const CURRENT_ENV: Environment = "test"; /** 服务等级类型 */ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto"; @@ -31,28 +31,28 @@ export interface IccoderConfig { /** 环境配置 */ const ENV_CONFIG: Record = { - /** 本地开发环境 */ + /** 本地开发环境 - 通过 Gateway 路由 */ dev: { - backendUrl: "http://localhost:2233", - backendUrlStrongeLoop: "http://192.168.1.108:2029", + backendUrl: "http://localhost:8080/iccoder", + backendUrlStrongeLoop: "http://localhost:8080", loginUrl: "http://localhost/login", timeout: 300000, userId: "default-user", serviceTier: "max", // 默认使用 max }, - /** 测试服务器环境 */ + /** 测试服务器环境 - 通过 Gateway 路由 */ test: { - backendUrl: "http://192.168.1.108:2233", + backendUrl: "http://192.168.1.108:2029/iccoder", backendUrlStrongeLoop: "http://192.168.1.108:2029", loginUrl: "http://192.168.1.108:2005/login", timeout: 60000, userId: "default-user", serviceTier: "max", }, - /** 生产环境 */ + /** 生产环境 - 通过 Gateway 路由 */ prod: { - backendUrl: "https://api.iccoder.com", - backendUrlStrongeLoop: "http://api.iccoder.com:2029", + backendUrl: "https://api.iccoder.com/iccoder", + backendUrlStrongeLoop: "https://api.iccoder.com", loginUrl: "https://iccoder.com/login", timeout: 60000, userId: "default-user", From 4b2da8244fe7bcb1ccd3e92f3ac73faf3577acb5 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:20:55 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复登录时VSCode弹出"账户不一致"确认框的问题 - 添加SSE业务错误码401检测,正确触发重新登录流程 - 修复侧边栏登录状态不刷新的问题,添加onDidChangeSessions监听 --- src/extension.ts | 19 ++++++++++-------- src/services/sseHandler.ts | 19 ++++++++++++++++++ src/views/ICViewProvider.ts | 39 ++++++++++++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 935026c..6bb9387 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -128,15 +128,18 @@ export function activate(context: vscode.ExtensionContext) { "ic-coder.login", async () => { try { - // 检查是否有现有 session - const existingSession = await vscode.authentication.getSession("iccoder", [], { createIfNone: false }); - if (existingSession) { - // 有旧 session,使用 forceNewSession 强制创建新 session - await vscode.authentication.getSession("iccoder", [], { forceNewSession: true }); - } else { - // 没有旧 session,使用 createIfNone 创建新 session - await vscode.authentication.getSession("iccoder", [], { createIfNone: true }); + // 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框 + try { + await vscode.authentication.getSession("iccoder", [], { + clearSessionPreference: true, + createIfNone: false + }); + } catch { + // 忽略错误 } + + // 创建新 session + await vscode.authentication.getSession("iccoder", [], { createIfNone: true }); } catch (error) { vscode.window.showErrorMessage(`登录失败: ${error}`); } diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index 1858965..e5828eb 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -238,6 +238,25 @@ export async function startStreamDialog( res.on('data', (chunk: string) => { if (!controller.aborted) { console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200)); + + // 检查是否是业务错误码(Gateway 返回 HTTP 200 但响应体是错误 JSON) + try { + const trimmed = chunk.trim(); + if (trimmed.startsWith('{') && trimmed.includes('"code"')) { + const json = JSON.parse(trimmed); + if (json.code === 401 || json.msg?.includes('登录状态已过期')) { + console.log('[SSE] 检测到登录过期业务错误'); + const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录'); + callbacks.onError?.({ message: error.message }); + controller.abort(); + reject(error); + return; + } + } + } catch { + // 不是 JSON 格式,继续正常处理 + } + parser.feed(chunk); } }); diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index c028a2a..02eb3b6 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -128,10 +128,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { * 侧边栏视图提供者 */ export class ICViewProvider implements vscode.WebviewViewProvider { + private _view?: vscode.WebviewView; + constructor( private readonly extensionUri: vscode.Uri, private readonly context: vscode.ExtensionContext - ) {} + ) { + // 监听认证状态变化 + this.context.subscriptions.push( + vscode.authentication.onDidChangeSessions((e) => { + if (e.provider.id === "iccoder") { + this.refreshLoginStatus(); + } + }) + ); + } + + /** + * 刷新登录状态并更新视图 + */ + private async refreshLoginStatus(): Promise { + if (this._view) { + const isLoggedIn = await this.checkLoginStatus(); + this._view.webview.html = this.getWebviewContent( + this._view.webview, + isLoggedIn + ); + } + } /** * 检查登录状态(使用 Authentication API) @@ -139,24 +163,29 @@ export class ICViewProvider implements vscode.WebviewViewProvider { private async checkLoginStatus(): Promise { try { const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false }); + console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在"); if (!session) { return false; } // 检查 token 是否过期 const expired = isTokenExpired(session.accessToken); - // 如果已过期或无法判断(null),都认为未登录 - if (expired === true || expired === null) { - console.log("Token 已过期或无法判断过期状态"); + console.log("[ICViewProvider] token 过期检查结果:", expired); + // 只有明确过期才认为未登录,无法判断时认为已登录 + if (expired === true) { + console.log("[ICViewProvider] Token 已过期"); return false; } return true; } catch (error) { - console.log("检查登录状态失败:", error); + console.log("[ICViewProvider] 检查登录状态失败:", error); return false; } } resolveWebviewView(webviewView: vscode.WebviewView) { + // 保存引用以便后续刷新 + this._view = webviewView; + webviewView.webview.options = { enableScripts: true, localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],