Compare commits
33 Commits
9786b7141c
...
feat/back-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d1b8f7e26 | |||
| 5753e120ba | |||
| 21a8abd5cf | |||
| 4b2da8244f | |||
| c571cd9137 | |||
| 72a84ed9e2 | |||
| 58113fb109 | |||
| 25966bc1e2 | |||
| 3c93c07afd | |||
| 85a37b546c | |||
| 37a121c3de | |||
| 341b6540fa | |||
| 1d074e5a94 | |||
| 5a5d82eef8 | |||
| 43189e144a | |||
| fd11eadc19 | |||
| 1231ef0892 | |||
| a1e88d473b | |||
| d08f9a7366 | |||
| faa7b63aee | |||
| e440dd2773 | |||
| a02027e7c9 | |||
| 772b067202 | |||
| a3fd5df8e8 | |||
| bdc55c727a | |||
| 52e4522ed0 | |||
| d44b316c9a | |||
| 939768986c | |||
| 1e99f3cb20 | |||
| 2af79cf1dc | |||
| 5b225126f1 | |||
| 4abb979eab | |||
| 4a790b5aca |
@ -1,29 +0,0 @@
|
|||||||
# 排除开发文件
|
|
||||||
.vscode/**
|
|
||||||
.git/**
|
|
||||||
.gitignore
|
|
||||||
node_modules/**
|
|
||||||
src/**
|
|
||||||
**/*.ts
|
|
||||||
**/*.map
|
|
||||||
|
|
||||||
# 排除测试文件
|
|
||||||
test/**
|
|
||||||
**/*.test.js
|
|
||||||
|
|
||||||
# 排除文档
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
|
|
||||||
# 排除 waveform_trace Python 源码(只保留 exe)
|
|
||||||
tools/waveform_trace/src/**
|
|
||||||
tools/waveform_trace/build/**
|
|
||||||
tools/waveform_trace/dist/**
|
|
||||||
tools/waveform_trace/build.bat
|
|
||||||
tools/waveform_trace/build.sh
|
|
||||||
|
|
||||||
# 排除打包临时文件
|
|
||||||
**/__pycache__/**
|
|
||||||
**/*.pyc
|
|
||||||
**/*.pyo
|
|
||||||
**/*.spec
|
|
||||||
@ -31,28 +31,28 @@ export interface IccoderConfig {
|
|||||||
|
|
||||||
/** 环境配置 */
|
/** 环境配置 */
|
||||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||||
/** 本地开发环境 */
|
/** 本地开发环境 - 通过 Gateway 路由 */
|
||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:8080/iccoder",
|
||||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
backendUrlStrongeLoop: "http://localhost:8080",
|
||||||
loginUrl: "http://localhost/login",
|
loginUrl: "http://localhost/login",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max", // 默认使用 max
|
||||||
},
|
},
|
||||||
/** 测试服务器环境 */
|
/** 测试服务器环境 - 通过 Gateway 路由 */
|
||||||
test: {
|
test: {
|
||||||
backendUrl: "http://192.168.1.108:2233",
|
backendUrl: "http://192.168.1.108:2029/iccoder",
|
||||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
loginUrl: "http://192.168.1.108:2005/login",
|
loginUrl: "http://192.168.1.108:2005/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max",
|
serviceTier: "max",
|
||||||
},
|
},
|
||||||
/** 生产环境 */
|
/** 生产环境 - 通过 Gateway 路由 */
|
||||||
prod: {
|
prod: {
|
||||||
backendUrl: "https://api.iccoder.com",
|
backendUrl: "https://api.iccoder.com/iccoder",
|
||||||
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
backendUrlStrongeLoop: "https://api.iccoder.com",
|
||||||
loginUrl: "https://iccoder.com/login",
|
loginUrl: "https://iccoder.com/login",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
|
|||||||
@ -180,3 +180,8 @@ export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox=
|
|||||||
* 用户头像图标 SVG
|
* 用户头像图标 SVG
|
||||||
*/
|
*/
|
||||||
export const userAvatarIconSvg = `<svg t="1767947405083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4661" width="16" height="16"><path d="M515.541449 7.082899c-280.359429 0-508.458551 228.120391-508.458551 508.458551s228.120391 508.458551 508.458551 508.458551 508.458551-228.120391 508.458551-508.458551S795.900879 7.082899 515.541449 7.082899zM515.541449 981.864196c-257.132626 0-466.301477-209.190121-466.301477-466.322747 0-257.132626 209.168851-466.322747 466.301477-466.322747s466.301477 209.190121 466.301477 466.322747S772.674075 981.864196 515.541449 981.864196zM614.574414 524.177056 614.574414 524.177056c47.751075-31.96876 79.230625-86.398604 79.230625-148.187857 0-98.437405-79.804915-178.24232-178.24232-178.24232-98.437405 0-178.24232 79.804915-178.24232 178.24232 0 61.810523 31.479551 116.219097 79.251895 148.187857-100.266622 39.519598-171.244501 137.170014-171.244501 251.453545 0 0.23397 0 0.446669 0.02127 0.659369 0 0.04254-0.02127 0.10635-0.02127 0.14889 0 15.612155 12.65563 28.246516 28.267786 28.246516 15.590885 0 21.886796-12.63436 21.886796-28.246516 0-0.340319-0.08508-0.659369-0.10635-1.020958 0.10635-118.005774 102.159649-219.995264 220.207964-219.995264 118.112124 0 220.207964 102.095839 220.207964 220.207964 0 0.14889-1.467628 29.054774 21.971875 29.054774 15.505806 0 28.076356-12.57055 28.076356-28.055086 0-0.06381-0.02127-0.12762-0.02127-0.2127 0-0.25524 0.02127-0.510479 0.02127-0.786989C785.797645 661.34707 714.798496 563.696654 614.574414 524.177056zM515.541449 510.734437c-74.402343 0-134.723968-60.321625-134.723968-134.723968 0-74.423613 60.321625-134.723968 134.723968-134.723968 74.423613 0 134.723968 60.321625 134.723968 134.723968S589.943792 510.734437 515.541449 510.734437z" fill="currentColor" p-id="4662"></path></svg>`;
|
export const userAvatarIconSvg = `<svg t="1767947405083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4661" width="16" height="16"><path d="M515.541449 7.082899c-280.359429 0-508.458551 228.120391-508.458551 508.458551s228.120391 508.458551 508.458551 508.458551 508.458551-228.120391 508.458551-508.458551S795.900879 7.082899 515.541449 7.082899zM515.541449 981.864196c-257.132626 0-466.301477-209.190121-466.301477-466.322747 0-257.132626 209.168851-466.322747 466.301477-466.322747s466.301477 209.190121 466.301477 466.322747S772.674075 981.864196 515.541449 981.864196zM614.574414 524.177056 614.574414 524.177056c47.751075-31.96876 79.230625-86.398604 79.230625-148.187857 0-98.437405-79.804915-178.24232-178.24232-178.24232-98.437405 0-178.24232 79.804915-178.24232 178.24232 0 61.810523 31.479551 116.219097 79.251895 148.187857-100.266622 39.519598-171.244501 137.170014-171.244501 251.453545 0 0.23397 0 0.446669 0.02127 0.659369 0 0.04254-0.02127 0.10635-0.02127 0.14889 0 15.612155 12.65563 28.246516 28.267786 28.246516 15.590885 0 21.886796-12.63436 21.886796-28.246516 0-0.340319-0.08508-0.659369-0.10635-1.020958 0.10635-118.005774 102.159649-219.995264 220.207964-219.995264 118.112124 0 220.207964 102.095839 220.207964 220.207964 0 0.14889-1.467628 29.054774 21.971875 29.054774 15.505806 0 28.076356-12.57055 28.076356-28.055086 0-0.06381-0.02127-0.12762-0.02127-0.2127 0-0.25524 0.02127-0.510479 0.02127-0.786989C785.797645 661.34707 714.798496 563.696654 614.574414 524.177056zM515.541449 510.734437c-74.402343 0-134.723968-60.321625-134.723968-134.723968 0-74.423613 60.321625-134.723968 134.723968-134.723968 74.423613 0 134.723968 60.321625 134.723968 134.723968S589.943792 510.734437 515.541449 510.734437z" fill="currentColor" p-id="4662"></path></svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阶段图标 SVG
|
||||||
|
*/
|
||||||
|
export const updateStageIconSvg = `<svg t="1768188846282" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7848" width="14" height="14"><path d="M83.712 1024c-0.256 0-0.768 0-1.024-0.256-17.408-0.512-31.488-14.848-31.488-32.512V32.512C51.2 14.592 65.792 0 83.712 0h745.472c17.92 0 32.512 14.592 32.512 32.512v280.576c0 18.432-14.592 33.28-32.512 33.28-1.536 0-3.072 0-4.608-0.256-16.128-2.304-27.648-15.872-27.648-32V77.056c0-6.912-5.632-12.288-12.288-12.288H128.256c-6.912 0-12.288 5.632-12.288 12.288v869.632c0 6.912 5.632 12.288 12.288 12.288h238.08c9.728 0 18.944 4.096 25.344 11.52 6.144 7.168 8.96 16.384 7.68 25.6-2.304 16.128-15.872 27.648-32 27.648H83.712v0.256zM534.784 1024c-6.144 0-12.032-2.816-15.616-7.424-3.84-4.352-5.376-10.496-4.352-16.64l27.648-147.968c0-0.512 0.512-1.024 0.768-1.536L867.84 558.336c11.52-11.264 26.624-17.664 42.24-17.664 16.384 0 31.488 6.4 42.752 17.664l53.76 53.504c23.296 23.04 23.552 60.928 0.768 84.736l-0.768 0.768-323.584 294.912c-2.816 2.56-6.4 4.352-9.984 5.12l-134.656 26.368c-0.512 0-2.048 0.256-3.584 0.256z m95.488-182.528c-1.024 0-1.792 0.256-2.56 1.024L590.848 875.52c-0.512 0.512-1.024 1.28-1.28 2.048l-15.104 81.152c-0.256 1.792 0.768 3.072 0.768 3.072 0.768 0.768 1.792 1.28 2.816 1.28H578.816l73.984-14.336c0.768-0.256 1.28-0.512 1.792-0.768l38.4-34.816c0.768-0.768 1.024-1.536 1.024-2.56s-0.256-2.304-1.024-3.072l-60.416-64.768-0.256-0.256c0-0.512-1.024-1.024-2.048-1.024z m217.088-194.56c-1.024 0-1.792 0.256-2.56 1.024l-172.8 155.392c-0.768 0.768-1.28 1.536-1.28 2.816 0 1.024 0.256 2.048 1.024 2.56l60.16 64.768c0.768 0.768 1.792 1.28 2.816 1.28s1.792-0.256 2.56-1.024l173.568-157.952c0.768-0.768 1.024-1.536 1.024-2.56s-0.256-2.048-1.28-3.072L849.92 647.936c-0.512-0.256-1.536-1.024-2.56-1.024z m101.376 28.928c0.768 0.768 1.536 1.024 2.816 1.024 1.024 0 1.792-0.256 2.56-1.024l16.384-14.848 0.512-0.512c3.072-3.584 2.816-8.704-0.512-12.032L916.48 594.944c-1.536-1.536-4.096-2.56-6.144-2.56-2.56 0-4.864 1.024-6.4 2.56-0.256 0.256-0.512 0.512-0.768 0.512l-17.664 15.872 63.232 64.512z" p-id="7849" fill="#8a8a8a"></path><path d="M212.48 419.584h118.016v39.424H212.48v-39.424z m137.472-118.016h118.016v39.424h-118.016v-39.424z m137.728-118.016h196.608v39.424h-196.608V183.552z m0 0" fill="#8a8a8a" p-id="7850"></path><path d="M664.576 242.688h-157.184c-11.776 0-19.712 7.936-19.712 19.712v98.304h-118.016c-11.776 0-19.712 7.936-19.712 19.712V478.72h-117.76c-11.776 0-19.712 7.936-19.712 19.712v118.016c0 11.776 7.936 19.712 19.712 19.712h432.384c11.776 0 19.712-7.936 19.712-19.712V262.144c-0.256-11.776-7.936-19.456-19.712-19.456zM369.664 576.768h-117.76v-39.424h118.016v39.424z m137.728-118.016h-118.016v-39.424h118.016v39.424z m137.472-117.76h-118.016v-39.424h118.016v39.424z m0 0" fill="#8a8a8a" p-id="7851"></path></svg>`;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
|||||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||||
import { VCDFileServer } from "./services/vcdFileServer";
|
import { VCDFileServer } from "./services/vcdFileServer";
|
||||||
import { initUserService } from "./services/userService";
|
import { initUserService } from "./services/userService";
|
||||||
|
import { initCreditsService } from "./services/creditsService";
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("🎉 IC Coder 插件已激活!");
|
console.log("🎉 IC Coder 插件已激活!");
|
||||||
@ -13,6 +14,9 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
// 初始化用户服务
|
// 初始化用户服务
|
||||||
initUserService(context);
|
initUserService(context);
|
||||||
|
|
||||||
|
// 初始化 Credits 服务
|
||||||
|
initCreditsService(context);
|
||||||
|
|
||||||
// 初始化 VCD 文件服务器
|
// 初始化 VCD 文件服务器
|
||||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||||
vcdFileServer.start().then((port) => {
|
vcdFileServer.start().then((port) => {
|
||||||
@ -128,6 +132,17 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
|
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
|
||||||
|
try {
|
||||||
|
await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
clearSessionPreference: true,
|
||||||
|
createIfNone: false
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新 session
|
||||||
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
vscode.window.showErrorMessage(`登录失败: ${error}`);
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import {
|
|||||||
handleReplaceInFile,
|
handleReplaceInFile,
|
||||||
handleUserAnswer,
|
handleUserAnswer,
|
||||||
abortCurrentDialog,
|
abortCurrentDialog,
|
||||||
|
handleOptimizePrompt,
|
||||||
handlePlanAction,
|
handlePlanAction,
|
||||||
setPendingPlanExecution,
|
|
||||||
getCurrentTaskId,
|
getCurrentTaskId,
|
||||||
setLastTaskId,
|
setLastTaskId,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
@ -33,10 +33,10 @@ function getTierIconUri(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tierIconMap: Record<string, string> = {
|
const tierIconMap: Record<string, string> = {
|
||||||
BASIC: "free.png",
|
'BASIC': 'free.png',
|
||||||
TRIAL: "PRO-Try.png",
|
'TRIAL': 'PRO-Try.png',
|
||||||
ADVANCED: "PRO.png",
|
'ADVANCED': 'PRO.png',
|
||||||
PROFESSIONAL: "PRO+.png",
|
'PROFESSIONAL': 'PRO+.png'
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconFile = tierIconMap[tierCode];
|
const iconFile = tierIconMap[tierCode];
|
||||||
@ -45,13 +45,7 @@ function getTierIconUri(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iconUri = webview.asWebviewUri(
|
const iconUri = webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(
|
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile)
|
||||||
context.extensionUri,
|
|
||||||
"src",
|
|
||||||
"assets",
|
|
||||||
"titleIcon",
|
|
||||||
iconFile
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return iconUri.toString();
|
return iconUri.toString();
|
||||||
@ -100,7 +94,7 @@ export async function showICHelperPanel(
|
|||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [
|
localResourceRoots: [
|
||||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -125,40 +119,16 @@ export async function showICHelperPanel(
|
|||||||
|
|
||||||
// 获取模型图标URI
|
// 获取模型图标URI
|
||||||
const autoIconUri = panel.webview.asWebviewUri(
|
const autoIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||||
context.extensionUri,
|
|
||||||
"src",
|
|
||||||
"assets",
|
|
||||||
"model",
|
|
||||||
"Auto.png"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const liteIconUri = panel.webview.asWebviewUri(
|
const liteIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||||
context.extensionUri,
|
|
||||||
"src",
|
|
||||||
"assets",
|
|
||||||
"model",
|
|
||||||
"lite.png"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const syIconUri = panel.webview.asWebviewUri(
|
const syIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||||
context.extensionUri,
|
|
||||||
"src",
|
|
||||||
"assets",
|
|
||||||
"model",
|
|
||||||
"Sy.png"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const maxIconUri = panel.webview.asWebviewUri(
|
const maxIconUri = panel.webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(
|
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||||
context.extensionUri,
|
|
||||||
"src",
|
|
||||||
"assets",
|
|
||||||
"model",
|
|
||||||
"Max.png"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置HTML内容
|
// 设置HTML内容
|
||||||
@ -177,43 +147,40 @@ export async function showICHelperPanel(
|
|||||||
|
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
// 使用缓存的用户信息
|
// 使用缓存的用户信息
|
||||||
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
|
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo);
|
||||||
const tierIconUrl = getTierIconUri(
|
console.log('[ICHelperPanel] Credits 余额:', userInfo.credits);
|
||||||
panel.webview,
|
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
|
||||||
context,
|
const messageData = {
|
||||||
userInfo.membership?.tierCode
|
command: 'updateUserInfo',
|
||||||
);
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "updateUserInfo",
|
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userId: userInfo.userId,
|
userId: userInfo.userId,
|
||||||
nickname: userInfo.nickname,
|
nickname: userInfo.nickname,
|
||||||
username: userInfo.username,
|
username: userInfo.username,
|
||||||
|
credits: userInfo.credits
|
||||||
},
|
},
|
||||||
tierIconUrl: tierIconUrl,
|
tierIconUrl: tierIconUrl
|
||||||
});
|
};
|
||||||
|
console.log('[ICHelperPanel] 发送用户信息到前端:', messageData);
|
||||||
|
panel.webview.postMessage(messageData);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有缓存,从 session 中获取
|
// 如果没有缓存,从 session 中获取
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
createIfNone: false,
|
createIfNone: false,
|
||||||
});
|
});
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log(
|
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account);
|
||||||
"[ICHelperPanel] 从 session 获取用户信息, account:",
|
|
||||||
session.account
|
|
||||||
);
|
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateUserInfo",
|
command: 'updateUserInfo',
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userId: session.account.id,
|
userId: session.account.id,
|
||||||
nickname: session.account.label,
|
nickname: session.account.label,
|
||||||
username: session.account.label,
|
username: session.account.label
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ICHelperPanel] 获取用户信息失败:", error);
|
console.error('[ICHelperPanel] 获取用户信息失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
@ -250,14 +217,14 @@ export async function showICHelperPanel(
|
|||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: "showProgress" });
|
panel.webview.postMessage({ type: 'showProgress' });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
message.text,
|
message.text,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
message.mode,
|
message.mode,
|
||||||
message.model // 传递服务等级
|
message.model // 传递服务等级
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
@ -286,10 +253,7 @@ export async function showICHelperPanel(
|
|||||||
case "openWaveformViewer":
|
case "openWaveformViewer":
|
||||||
// 在新列中打开波形查看器
|
// 在新列中打开波形查看器
|
||||||
if (message.vcdFilePath) {
|
if (message.vcdFilePath) {
|
||||||
vscode.commands.executeCommand(
|
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
||||||
"ic-coder.openVCDViewer",
|
|
||||||
message.vcdFilePath
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getVCDInfo":
|
case "getVCDInfo":
|
||||||
@ -322,7 +286,7 @@ export async function showICHelperPanel(
|
|||||||
break;
|
break;
|
||||||
// 新增:处理用户回答
|
// 新增:处理用户回答
|
||||||
case "submitAnswer":
|
case "submitAnswer":
|
||||||
handleUserAnswer(
|
void handleUserAnswer(
|
||||||
message.askId,
|
message.askId,
|
||||||
message.selected,
|
message.selected,
|
||||||
message.customInput
|
message.customInput
|
||||||
@ -365,29 +329,34 @@ export async function showICHelperPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "optimizePrompt":
|
||||||
|
if (typeof message.prompt === "string") {
|
||||||
|
void handleOptimizePrompt(panel, message.prompt);
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "optimizeResult",
|
||||||
|
success: false,
|
||||||
|
error: "提示词为空或格式错误",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||||
case "planAction":
|
case "planAction":
|
||||||
if (message.action === "confirm") {
|
if (message.action === "confirm") {
|
||||||
// 确认执行:切换到 Agent 模式
|
// 确认执行:切换到 Agent 模式(UI 切换)
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "switchMode",
|
command: "switchMode",
|
||||||
mode: "agent",
|
mode: "agent",
|
||||||
});
|
});
|
||||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||||
const taskId = getCurrentTaskId();
|
} else if (message.action === "modify" || message.action === "cancel") {
|
||||||
if (taskId) {
|
void handlePlanAction(
|
||||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
panel,
|
||||||
setPendingPlanExecution(
|
message.action,
|
||||||
panel,
|
message.planTitle || "",
|
||||||
message.planTitle || "计划",
|
context.extensionPath,
|
||||||
context.extensionPath,
|
message.model
|
||||||
taskId
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// 添加文件上下文 - 显示工作区文件列表
|
// 添加文件上下文 - 显示工作区文件列表
|
||||||
@ -432,11 +401,7 @@ export async function showICHelperPanel(
|
|||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (
|
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
||||||
item.isDirectory() &&
|
|
||||||
item.name !== "node_modules" &&
|
|
||||||
!item.name.startsWith(".")
|
|
||||||
) {
|
|
||||||
const fullPath = path.join(dir, item.name);
|
const fullPath = path.join(dir, item.name);
|
||||||
const relativePath = path.relative(baseDir, fullPath);
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
folders.push({ path: fullPath, relativePath });
|
folders.push({ path: fullPath, relativePath });
|
||||||
@ -465,7 +430,7 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择图片",
|
openLabel: "选择图片",
|
||||||
filters: {
|
filters: {
|
||||||
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (imageUris && imageUris.length > 0) {
|
if (imageUris && imageUris.length > 0) {
|
||||||
@ -485,8 +450,8 @@ export async function showICHelperPanel(
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
openLabel: "选择文档",
|
openLabel: "选择文档",
|
||||||
filters: {
|
filters: {
|
||||||
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||||
所有文件: ["*"],
|
"所有文件": ["*"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (docUris && docUris.length > 0) {
|
if (docUris && docUris.length > 0) {
|
||||||
@ -522,29 +487,6 @@ export async function showICHelperPanel(
|
|||||||
hasWorkspace: hasWorkspace,
|
hasWorkspace: hasWorkspace,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
// 新增:处理面板宽度不足
|
|
||||||
case "panelWidthInsufficient":
|
|
||||||
// 关闭面板
|
|
||||||
panel.dispose();
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
"聊天面板宽度不足(最小 200px),已自动关闭"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
context.subscriptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听面板状态变化,检查宽度
|
|
||||||
panel.onDidChangeViewState(
|
|
||||||
(e) => {
|
|
||||||
if (e.webviewPanel.visible) {
|
|
||||||
// 请求前端检查宽度
|
|
||||||
panel.webview.postMessage({
|
|
||||||
command: "checkPanelWidth",
|
|
||||||
minWidth: 200,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
* API 客户端
|
* API 客户端
|
||||||
* 封装与后端的 HTTP 通信
|
* 封装与后端的 HTTP 通信
|
||||||
*/
|
*/
|
||||||
|
import * as vscode from 'vscode';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
@ -18,6 +19,18 @@ interface RequestOptions {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录的 Token
|
||||||
|
*/
|
||||||
|
async function getAuthToken(): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
|
return session?.accessToken;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送 HTTP 请求
|
* 发送 HTTP 请求
|
||||||
*/
|
*/
|
||||||
@ -25,6 +38,9 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
|||||||
const url = new URL(getApiUrl(path));
|
const url = new URL(getApiUrl(path));
|
||||||
const { timeout } = getConfig();
|
const { timeout } = getConfig();
|
||||||
|
|
||||||
|
// 自动获取 Token
|
||||||
|
const token = await getAuthToken();
|
||||||
|
|
||||||
const isHttps = url.protocol === 'https:';
|
const isHttps = url.protocol === 'https:';
|
||||||
const httpModule = isHttps ? https : http;
|
const httpModule = isHttps ? https : http;
|
||||||
|
|
||||||
@ -35,6 +51,7 @@ async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
|||||||
method: options.method,
|
method: options.method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
...options.headers
|
...options.headers
|
||||||
},
|
},
|
||||||
timeout: options.timeout || timeout
|
timeout: options.timeout || timeout
|
||||||
@ -224,3 +241,22 @@ export async function getUserInfo(): Promise<UserInfoResponse> {
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 余额查询响应 */
|
||||||
|
export interface CreditBalanceResponse {
|
||||||
|
success: boolean;
|
||||||
|
balance?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户资源点余额
|
||||||
|
* GET /api/dialog/balance?userId=xxx
|
||||||
|
*/
|
||||||
|
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
|
||||||
|
console.log('[API] 查询余额: userId=', userId);
|
||||||
|
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
255
src/services/creditsService.ts
Normal file
255
src/services/creditsService.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* 资源点余额管理服务
|
||||||
|
* 负责缓存余额、主动查询、发送前检测
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { getStrangeLoopApiUrl } from '../config/settings';
|
||||||
|
import { getCachedUserInfo } from './userService';
|
||||||
|
|
||||||
|
/** 低余额阈值 */
|
||||||
|
const LOW_CREDIT_THRESHOLD = 5;
|
||||||
|
|
||||||
|
/** 缓存的余额 */
|
||||||
|
let cachedBalance: number | null = null;
|
||||||
|
|
||||||
|
/** 最后更新时间 */
|
||||||
|
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<number>('icCoderCreditsBalance');
|
||||||
|
if (savedBalance !== undefined) {
|
||||||
|
cachedBalance = savedBalance;
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
console.log('[CreditsService] 从持久化存储加载余额:', savedBalance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存余额到持久化存储
|
||||||
|
*/
|
||||||
|
async function saveBalance(balance: number): Promise<void> {
|
||||||
|
if (extensionContext) {
|
||||||
|
await extensionContext.globalState.update('icCoderCreditsBalance', balance);
|
||||||
|
console.log('[CreditsService] 余额已保存到持久化存储:', balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新缓存的余额(从 SSE credit_update 事件调用)
|
||||||
|
*/
|
||||||
|
export function updateCachedBalance(balance: number): void {
|
||||||
|
cachedBalance = balance;
|
||||||
|
lastUpdateTime = Date.now();
|
||||||
|
console.log('[CreditsService] 余额已更新:', balance);
|
||||||
|
// 异步保存到持久化存储
|
||||||
|
saveBalance(balance).catch(err => {
|
||||||
|
console.error('[CreditsService] 保存余额失败:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的余额
|
||||||
|
*/
|
||||||
|
export function getCachedBalance(): number | null {
|
||||||
|
return cachedBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否有效
|
||||||
|
*/
|
||||||
|
function isCacheValid(): boolean {
|
||||||
|
if (cachedBalance === null) return false;
|
||||||
|
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StrangeLoop 余额响应类型
|
||||||
|
*/
|
||||||
|
interface StrangeLoopBalanceResponse {
|
||||||
|
userId?: number;
|
||||||
|
availableCredits?: number;
|
||||||
|
totalCredits?: number;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动查询余额(直接调用 StrangeLoop 接口)
|
||||||
|
*/
|
||||||
|
export async function fetchBalance(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
// 获取 JWT token
|
||||||
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetchBalanceWithToken(session.accessToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[CreditsService] 查询余额异常:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定 token 查询余额(登录过程中使用)
|
||||||
|
*/
|
||||||
|
export async function fetchBalanceWithToken(token: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||||
|
|
||||||
|
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||||
|
const response = await callStrangeLoopBalance(token);
|
||||||
|
|
||||||
|
if (response.availableCredits !== undefined) {
|
||||||
|
const balance = response.availableCredits;
|
||||||
|
updateCachedBalance(balance);
|
||||||
|
console.log('[CreditsService] 余额查询成功:', balance);
|
||||||
|
return balance;
|
||||||
|
} else {
|
||||||
|
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[CreditsService] 查询余额异常:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 StrangeLoop 余额接口
|
||||||
|
*/
|
||||||
|
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
|
||||||
|
const urlStr = getStrangeLoopApiUrl('/strangeloop/api/credit/balance');
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const httpModule = isHttps ? https : http;
|
||||||
|
|
||||||
|
// 余额查询使用固定短超时,避免阻塞发送前检查
|
||||||
|
const BALANCE_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
const requestOptions: http.RequestOptions = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
timeout: BALANCE_TIMEOUT_MS
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = httpModule.request(requestOptions, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log('[CreditsService] 响应状态码:', res.statusCode);
|
||||||
|
console.log('[CreditsService] 响应内容:', data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(json as StrangeLoopBalanceResponse);
|
||||||
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||||
|
// 登录过期或无权限
|
||||||
|
resolve({ error: '登录已过期,请重新登录' });
|
||||||
|
} else {
|
||||||
|
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ error: `解析响应失败: ${data}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||||
|
*/
|
||||||
|
export async function getBalance(): Promise<number | null> {
|
||||||
|
if (isCacheValid()) {
|
||||||
|
return cachedBalance;
|
||||||
|
}
|
||||||
|
return await fetchBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查余额是否足够发送消息
|
||||||
|
* @returns { allowed: boolean, balance: number | null, message?: string }
|
||||||
|
*/
|
||||||
|
export async function checkBalanceBeforeSend(): Promise<{
|
||||||
|
allowed: boolean;
|
||||||
|
balance: number | null;
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
|
const userInfo = getCachedUserInfo();
|
||||||
|
if (!userInfo) {
|
||||||
|
// 未登录,允许发送(后端会处理)
|
||||||
|
return { allowed: true, balance: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = await getBalance();
|
||||||
|
|
||||||
|
if (balance === null) {
|
||||||
|
// 无法获取余额,允许发送(后端会处理)
|
||||||
|
console.warn('[CreditsService] 无法获取余额,允许发送');
|
||||||
|
return { allowed: true, balance: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balance < LOW_CREDIT_THRESHOLD) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
balance,
|
||||||
|
message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true, balance };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存(登出时调用)
|
||||||
|
*/
|
||||||
|
export async function clearBalanceCache(): Promise<void> {
|
||||||
|
cachedBalance = null;
|
||||||
|
lastUpdateTime = 0;
|
||||||
|
if (extensionContext) {
|
||||||
|
await extensionContext.globalState.update('icCoderCreditsBalance', undefined);
|
||||||
|
}
|
||||||
|
console.log('[CreditsService] 余额缓存已清除');
|
||||||
|
}
|
||||||
@ -12,7 +12,8 @@ import { getConfig } from '../config/settings';
|
|||||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||||
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||||
import { getUserIdFromToken } from '../utils/jwtUtils';
|
import { getUserIdFromToken, isTokenExpired } from '../utils/jwtUtils';
|
||||||
|
import { updateCachedBalance } from './creditsService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息段落类型
|
* 消息段落类型
|
||||||
@ -95,6 +96,7 @@ export class DialogSession {
|
|||||||
private hasCompleted = false; // 标记是否已收到 complete 事件
|
private hasCompleted = false; // 标记是否已收到 complete 事件
|
||||||
private segments: MessageSegment[] = [];
|
private segments: MessageSegment[] = [];
|
||||||
private currentTextSegment: MessageSegment | null = null;
|
private currentTextSegment: MessageSegment | null = null;
|
||||||
|
private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发
|
||||||
|
|
||||||
constructor(extensionPath: string, existingTaskId?: string) {
|
constructor(extensionPath: string, existingTaskId?: string) {
|
||||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
@ -336,17 +338,33 @@ export class DialogSession {
|
|||||||
this.accumulatedText = '';
|
this.accumulatedText = '';
|
||||||
this.segments = [];
|
this.segments = [];
|
||||||
this.currentTextSegment = null;
|
this.currentTextSegment = null;
|
||||||
|
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
// 从登录 session 获取真实 userId
|
// 从登录 session 获取真实 userId 和 token
|
||||||
let userId = config.userId; // 默认值
|
let userId = config.userId; // 默认值
|
||||||
|
let token: string | undefined;
|
||||||
try {
|
try {
|
||||||
console.log('[DialogSession] 尝试获取登录 session...');
|
console.log('[DialogSession] 尝试获取登录 session...');
|
||||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
|
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
|
||||||
if (session?.accessToken) {
|
if (session?.accessToken) {
|
||||||
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
|
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
|
||||||
|
|
||||||
|
// 检测 token 是否过期
|
||||||
|
const expired = isTokenExpired(session.accessToken);
|
||||||
|
if (expired === true) {
|
||||||
|
console.error('[DialogSession] token 已过期,需要重新登录');
|
||||||
|
vscode.window.showErrorMessage('登录已过期,请重新登录', '重新登录').then(selection => {
|
||||||
|
if (selection === '重新登录') {
|
||||||
|
vscode.commands.executeCommand('iccoder.login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw new Error('登录已过期,请重新登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
token = session.accessToken; // 保存 token 用于扣费
|
||||||
const parsedUserId = getUserIdFromToken(session.accessToken);
|
const parsedUserId = getUserIdFromToken(session.accessToken);
|
||||||
console.log('[DialogSession] 解析的 userId:', parsedUserId);
|
console.log('[DialogSession] 解析的 userId:', parsedUserId);
|
||||||
if (parsedUserId) {
|
if (parsedUserId) {
|
||||||
@ -369,12 +387,15 @@ export class DialogSession {
|
|||||||
const knowledgeData = await this.loadKnowledgeData();
|
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);
|
||||||
|
|
||||||
const request: DialogRequest = {
|
const request: DialogRequest = {
|
||||||
taskId: this.taskId,
|
taskId: this.taskId,
|
||||||
message,
|
message,
|
||||||
userId,
|
userId,
|
||||||
mode: mode || 'agent',
|
mode: mode || 'agent',
|
||||||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||||||
|
token, // JWT token 用于扣费
|
||||||
compactedData: compactedData || undefined,
|
compactedData: compactedData || undefined,
|
||||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||||
knowledgeData: knowledgeData || undefined
|
knowledgeData: knowledgeData || undefined
|
||||||
@ -455,6 +476,8 @@ export class DialogSession {
|
|||||||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
// 追踪工具执行结果(用于后端重启后恢复)
|
||||||
|
historyManager.trackToolResult(data.tool_name, data.result);
|
||||||
},
|
},
|
||||||
|
|
||||||
onToolError: (data) => {
|
onToolError: (data) => {
|
||||||
@ -462,6 +485,8 @@ export class DialogSession {
|
|||||||
callbacks.onToolError?.(data.tool_name, data.error);
|
callbacks.onToolError?.(data.tool_name, data.error);
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
// 追踪工具执行错误(用于后端重启后恢复)
|
||||||
|
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||||
@ -702,6 +727,18 @@ export class DialogSession {
|
|||||||
|
|
||||||
onError: (data) => {
|
onError: (data) => {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
|
||||||
|
// 检测登录状态过期(只弹一次窗,不再传递错误)
|
||||||
|
if (data.message.includes('LOGIN_EXPIRED') || data.message.includes('登录状态已过期')) {
|
||||||
|
vscode.window.showErrorMessage('登录状态已过期,请重新登录', '重新登录').then(selection => {
|
||||||
|
if (selection === '重新登录') {
|
||||||
|
vscode.commands.executeCommand('ic-coder.login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 登录过期错误已处理,不再传递给外部
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
callbacks.onError?.(data.message);
|
callbacks.onError?.(data.message);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -789,6 +826,8 @@ export class DialogSession {
|
|||||||
|
|
||||||
onCreditUpdate: (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;
|
const LOW_CREDIT_THRESHOLD = 5;
|
||||||
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
||||||
@ -837,13 +876,25 @@ export class DialogSession {
|
|||||||
* 中止当前对话
|
* 中止当前对话
|
||||||
*/
|
*/
|
||||||
abort(): void {
|
abort(): void {
|
||||||
|
// 先标记完成,防止 onClose 重复触发
|
||||||
|
const wasActive = this.isActive;
|
||||||
|
this.hasCompleted = true;
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
if (this.sseController) {
|
if (this.sseController) {
|
||||||
this.sseController.abort();
|
this.sseController.abort();
|
||||||
this.sseController = null;
|
this.sseController = null;
|
||||||
}
|
}
|
||||||
this.isActive = false;
|
|
||||||
userInteractionManager.cancelAll();
|
userInteractionManager.cancelAll();
|
||||||
|
|
||||||
|
// 如果之前是活跃状态,触发完成回调以结束 Promise
|
||||||
|
if (wasActive && this.completeCallback) {
|
||||||
|
this.finalizeTextSegment();
|
||||||
|
console.log('[DialogSession] abort 触发完成回调');
|
||||||
|
this.completeCallback(this.segments);
|
||||||
|
this.completeCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 通知后端停止处理
|
// 通知后端停止处理
|
||||||
stopDialog(this.taskId).catch(err => {
|
stopDialog(this.taskId).catch(err => {
|
||||||
console.warn('[DialogSession] 停止对话请求失败:', err);
|
console.warn('[DialogSession] 停止对话请求失败:', err);
|
||||||
@ -872,7 +923,10 @@ export class DialogSession {
|
|||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await userInteractionManager.receiveAnswer(askId, selected, customInput);
|
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||||
|
// 如果 pendingQuestions 中有问题,走正常流程
|
||||||
|
// 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端
|
||||||
|
await userInteractionManager.receiveAnswer(askId, selected, customInput, this.taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -908,6 +962,7 @@ class DialogManager {
|
|||||||
*/
|
*/
|
||||||
abortCurrentSession(): void {
|
abortCurrentSession(): void {
|
||||||
this.currentSession?.abort();
|
this.currentSession?.abort();
|
||||||
|
this.currentSession = null; // 清空会话,确保下次创建新会话
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,8 +24,23 @@ export class ICCoderAuthenticationProvider
|
|||||||
private _sessions: vscode.AuthenticationSession[] = [];
|
private _sessions: vscode.AuthenticationSession[] = [];
|
||||||
|
|
||||||
constructor(private readonly context: vscode.ExtensionContext) {
|
constructor(private readonly context: vscode.ExtensionContext) {
|
||||||
// 从存储中恢复会话
|
// 从存储中恢复会话(同步执行)
|
||||||
this.loadSessions();
|
this.loadSessionsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储中加载会话(同步版本)
|
||||||
|
*/
|
||||||
|
private loadSessionsSync(): void {
|
||||||
|
const storedSessions = this.context.globalState.get<
|
||||||
|
vscode.AuthenticationSession[]
|
||||||
|
>("icCoderSessions", []);
|
||||||
|
this._sessions = storedSessions;
|
||||||
|
console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length);
|
||||||
|
if (this._sessions.length > 0) {
|
||||||
|
console.log("[AuthProvider] Session ID:", this._sessions[0].id);
|
||||||
|
console.log("[AuthProvider] Account:", this._sessions[0].account.label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +57,9 @@ export class ICCoderAuthenticationProvider
|
|||||||
* 保存会话到存储
|
* 保存会话到存储
|
||||||
*/
|
*/
|
||||||
private async saveSessions(): Promise<void> {
|
private async saveSessions(): Promise<void> {
|
||||||
|
console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length);
|
||||||
await this.context.globalState.update("icCoderSessions", this._sessions);
|
await this.context.globalState.update("icCoderSessions", this._sessions);
|
||||||
|
console.log("[AuthProvider] sessions 已保存到 globalState");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,6 +68,7 @@ export class ICCoderAuthenticationProvider
|
|||||||
async getSessions(
|
async getSessions(
|
||||||
scopes?: readonly string[]
|
scopes?: readonly string[]
|
||||||
): Promise<vscode.AuthenticationSession[]> {
|
): Promise<vscode.AuthenticationSession[]> {
|
||||||
|
console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length);
|
||||||
return [...this._sessions];
|
return [...this._sessions];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +79,20 @@ export class ICCoderAuthenticationProvider
|
|||||||
scopes: readonly string[]
|
scopes: readonly string[]
|
||||||
): Promise<vscode.AuthenticationSession> {
|
): Promise<vscode.AuthenticationSession> {
|
||||||
try {
|
try {
|
||||||
|
// 先删除旧的 session(静默删除,不弹窗、不重载窗口)
|
||||||
|
if (this._sessions.length > 0) {
|
||||||
|
const oldSession = this._sessions[0];
|
||||||
|
this._sessions = [];
|
||||||
|
await this.saveSessions();
|
||||||
|
await clearUserInfo();
|
||||||
|
this._onDidChangeSessions.fire({
|
||||||
|
added: [],
|
||||||
|
removed: [oldSession],
|
||||||
|
changed: [],
|
||||||
|
});
|
||||||
|
console.log("🔄 已清除旧的 session");
|
||||||
|
}
|
||||||
|
|
||||||
const token = await this.login();
|
const token = await this.login();
|
||||||
|
|
||||||
// 获取到 token 后立即调用用户信息接口
|
// 获取到 token 后立即调用用户信息接口
|
||||||
|
|||||||
103
src/services/promptOptimizeService.ts
Normal file
103
src/services/promptOptimizeService.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* 提示词优化服务
|
||||||
|
* 调用后端 API 优化用户输入的提示词
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { getApiUrl } from '../config/settings';
|
||||||
|
|
||||||
|
/** 优化响应类型 */
|
||||||
|
interface OptimizeResponse {
|
||||||
|
success: boolean;
|
||||||
|
optimizedPrompt?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化提示词
|
||||||
|
* @param prompt 原始提示词
|
||||||
|
* @returns 优化后的提示词
|
||||||
|
*/
|
||||||
|
export async function optimizePrompt(prompt: string): Promise<string> {
|
||||||
|
// 获取 JWT token
|
||||||
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
throw new Error('未登录,请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await callOptimizeApi(prompt, session.accessToken);
|
||||||
|
|
||||||
|
if (response.success && response.optimizedPrompt) {
|
||||||
|
return response.optimizedPrompt;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '优化失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用后端优化 API
|
||||||
|
*/
|
||||||
|
async function callOptimizeApi(prompt: string, token: string): Promise<OptimizeResponse> {
|
||||||
|
const urlStr = getApiUrl('/api/prompt/optimize');
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const httpModule = isHttps ? https : http;
|
||||||
|
|
||||||
|
const body = JSON.stringify({ prompt });
|
||||||
|
|
||||||
|
const requestOptions: http.RequestOptions = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = httpModule.request(requestOptions, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log('[PromptOptimize] 响应状态码:', res.statusCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(json as OptimizeResponse);
|
||||||
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||||
|
resolve({ success: false, error: '登录已过期,请重新登录' });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: json.error || json.message || `HTTP ${res.statusCode}` });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ success: false, error: `解析响应失败: ${data}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -173,7 +173,8 @@ export async function startStreamDialog(
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'text/event-stream',
|
'Accept': 'text/event-stream',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Content-Length': Buffer.byteLength(body)
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
...(request.token ? { 'Authorization': `Bearer ${request.token}` } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -183,9 +184,20 @@ export async function startStreamDialog(
|
|||||||
let errorBody = '';
|
let errorBody = '';
|
||||||
res.on('data', chunk => errorBody += chunk);
|
res.on('data', chunk => errorBody += chunk);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
// 检测是否是登录状态过期
|
||||||
callbacks.onError?.({ message: error.message });
|
const isLoginExpired = errorBody.includes('登录状态已过期') ||
|
||||||
reject(error);
|
errorBody.includes('token') && errorBody.includes('过期') ||
|
||||||
|
res.statusCode === 401;
|
||||||
|
|
||||||
|
if (isLoginExpired) {
|
||||||
|
const error = new Error('LOGIN_EXPIRED:登录状态已过期,请重新登录');
|
||||||
|
callbacks.onError?.({ message: error.message });
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||||
|
callbacks.onError?.({ message: error.message });
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -226,6 +238,25 @@ export async function startStreamDialog(
|
|||||||
res.on('data', (chunk: string) => {
|
res.on('data', (chunk: string) => {
|
||||||
if (!controller.aborted) {
|
if (!controller.aborted) {
|
||||||
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
|
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);
|
parser.feed(chunk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import type {
|
|||||||
FileDeleteArgs,
|
FileDeleteArgs,
|
||||||
FileListArgs,
|
FileListArgs,
|
||||||
SyntaxCheckArgs,
|
SyntaxCheckArgs,
|
||||||
|
IverilogArgs,
|
||||||
SimulationArgs,
|
SimulationArgs,
|
||||||
WaveformSummaryArgs,
|
WaveformSummaryArgs,
|
||||||
KnowledgeSaveArgs,
|
KnowledgeSaveArgs,
|
||||||
@ -75,6 +76,9 @@ export async function executeToolCall(
|
|||||||
case 'syntax_check':
|
case 'syntax_check':
|
||||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
||||||
break;
|
break;
|
||||||
|
case 'iverilog':
|
||||||
|
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
|
||||||
|
break;
|
||||||
case 'simulation':
|
case 'simulation':
|
||||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||||
break;
|
break;
|
||||||
@ -270,6 +274,71 @@ async function executeSyntaxCheck(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 iverilog 工具
|
||||||
|
* 直接执行 iverilog 命令
|
||||||
|
*/
|
||||||
|
async function executeIverilog(
|
||||||
|
args: IverilogArgs,
|
||||||
|
context: ToolExecutorContext
|
||||||
|
): Promise<string> {
|
||||||
|
// 检查 iverilog 是否可用
|
||||||
|
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||||
|
if (!iverilogCheck.available) {
|
||||||
|
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作目录
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('没有打开的工作区');
|
||||||
|
}
|
||||||
|
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||||
|
const workDir = args.workDir
|
||||||
|
? path.join(projectPath, args.workDir)
|
||||||
|
: projectPath;
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||||
|
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(iverilogPath, cmdArgs, {
|
||||||
|
cwd: workDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (data: Buffer) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (data: Buffer) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code: number) => {
|
||||||
|
const output = stderr || stdout || '(无输出)';
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(`执行成功\n${output}`);
|
||||||
|
} else {
|
||||||
|
resolve(`执行失败 (exit code: ${code})\n${output}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 simulation 工具
|
* 执行 simulation 工具
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -82,21 +82,28 @@ export class UserInteractionManager {
|
|||||||
* @param askId 问题ID
|
* @param askId 问题ID
|
||||||
* @param selected 选中的选项
|
* @param selected 选中的选项
|
||||||
* @param customInput 自定义输入
|
* @param customInput 自定义输入
|
||||||
|
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||||
*/
|
*/
|
||||||
async receiveAnswer(
|
async receiveAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string,
|
||||||
|
fallbackTaskId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pending = this.pendingQuestions.get(askId);
|
const pending = this.pendingQuestions.get(askId);
|
||||||
|
const answer = customInput || selected?.join(', ') || '';
|
||||||
|
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
|
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||||
|
if (fallbackTaskId) {
|
||||||
|
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||||
|
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
||||||
|
} else {
|
||||||
|
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建答案
|
|
||||||
const answer = customInput || selected?.join(', ') || '';
|
|
||||||
|
|
||||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||||
|
|
||||||
// 移除待处理问题
|
// 移除待处理问题
|
||||||
@ -173,6 +180,13 @@ export class UserInteractionManager {
|
|||||||
hasPendingQuestions(): boolean {
|
hasPendingQuestions(): boolean {
|
||||||
return this.pendingQuestions.size > 0;
|
return this.pendingQuestions.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查特定问题是否存在
|
||||||
|
*/
|
||||||
|
hasPendingQuestion(askId: string): boolean {
|
||||||
|
return this.pendingQuestions.has(askId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局实例
|
// 全局实例
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { URL } from 'url';
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||||
|
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 请求选项
|
* HTTP 请求选项
|
||||||
@ -114,6 +115,8 @@ export interface UserInfo {
|
|||||||
remainingDays?: number;
|
remainingDays?: number;
|
||||||
monthlyCredits?: number;
|
monthlyCredits?: number;
|
||||||
};
|
};
|
||||||
|
// Credits 余额
|
||||||
|
credits?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,12 +224,13 @@ function getHighestTierMembership(allMemberships?: MembershipItemVO[]): Membersh
|
|||||||
*/
|
*/
|
||||||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||||
try {
|
try {
|
||||||
console.log('[UserService] Token 已获取,正在获取用户信息和会员信息...');
|
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||||||
|
|
||||||
// 并行获取用户信息和会员信息
|
// 并行获取用户信息、会员信息和余额
|
||||||
const [userInfo, membershipInfo] = await Promise.all([
|
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||||||
getUserInfo(token),
|
getUserInfo(token),
|
||||||
getMembershipInfo(token)
|
getMembershipInfo(token),
|
||||||
|
fetchBalanceWithToken(token)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!userInfo) {
|
if (!userInfo) {
|
||||||
@ -234,6 +238,15 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
|||||||
return null;
|
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('='.repeat(60));
|
||||||
console.log('用户信息详情:');
|
console.log('用户信息详情:');
|
||||||
@ -286,6 +299,15 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打印 Credits 余额
|
||||||
|
console.log('');
|
||||||
|
console.log('资源点余额:');
|
||||||
|
if (userInfo.credits !== undefined) {
|
||||||
|
console.log(`当前余额: ${userInfo.credits} Credits`);
|
||||||
|
} else {
|
||||||
|
console.log('当前余额: 未获取到余额信息');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
|
|
||||||
// 保存到持久化存储
|
// 保存到持久化存储
|
||||||
@ -329,7 +351,18 @@ export function getCachedUserInfo(): UserInfo | null {
|
|||||||
console.warn('[UserService] ExtensionContext 未初始化');
|
console.warn('[UserService] ExtensionContext 未初始化');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
const userInfo = extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||||
|
|
||||||
|
// 从 creditsService 加载余额并合并到用户信息中
|
||||||
|
if (userInfo) {
|
||||||
|
const cachedCredits = getCachedBalance();
|
||||||
|
if (cachedCredits !== null) {
|
||||||
|
userInfo.credits = cachedCredits;
|
||||||
|
console.log('[UserService] 从 creditsService 加载余额:', cachedCredits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export interface DialogRequest {
|
|||||||
mode: RunMode;
|
mode: RunMode;
|
||||||
/** 服务等级 */
|
/** 服务等级 */
|
||||||
serviceTier?: ServiceTier;
|
serviceTier?: ServiceTier;
|
||||||
|
/** JWT Token(用于认证和扣费) */
|
||||||
|
token?: string;
|
||||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||||
compactedData?: CompactedMemory;
|
compactedData?: CompactedMemory;
|
||||||
/** 压缩后产生的新消息 */
|
/** 压缩后产生的新消息 */
|
||||||
@ -484,6 +486,7 @@ export type ToolName =
|
|||||||
| "file_delete"
|
| "file_delete"
|
||||||
| "file_list"
|
| "file_list"
|
||||||
| "syntax_check"
|
| "syntax_check"
|
||||||
|
| "iverilog"
|
||||||
| "simulation"
|
| "simulation"
|
||||||
| "waveform_summary"
|
| "waveform_summary"
|
||||||
| "waveform_trace"
|
| "waveform_trace"
|
||||||
@ -518,6 +521,12 @@ export interface SyntaxCheckArgs {
|
|||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** iverilog 工具参数 */
|
||||||
|
export interface IverilogArgs {
|
||||||
|
args: string;
|
||||||
|
workDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** simulation 工具参数 */
|
/** simulation 工具参数 */
|
||||||
export interface SimulationArgs {
|
export interface SimulationArgs {
|
||||||
rtlPath: string;
|
rtlPath: string;
|
||||||
@ -566,6 +575,7 @@ export type ToolArgs =
|
|||||||
| FileDeleteArgs
|
| FileDeleteArgs
|
||||||
| FileListArgs
|
| FileListArgs
|
||||||
| SyntaxCheckArgs
|
| SyntaxCheckArgs
|
||||||
|
| IverilogArgs
|
||||||
| SimulationArgs
|
| SimulationArgs
|
||||||
| WaveformSummaryArgs
|
| WaveformSummaryArgs
|
||||||
| WaveformTraceArgs
|
| WaveformTraceArgs
|
||||||
|
|||||||
@ -715,6 +715,10 @@ export class ChatHistoryManager {
|
|||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||||
|
// 通知用户压缩数据保存失败
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -731,6 +735,19 @@ export class ChatHistoryManager {
|
|||||||
// 文件不存在,使用空数组
|
// 文件不存在,使用空数组
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
|
||||||
|
let existingSummary: CompactionSummaryMessage | null = null;
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||||
|
existingSummary = messages[i] as CompactionSummaryMessage;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (existingSummary && existingSummary.version >= compacted.version) {
|
||||||
|
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建压缩摘要消息
|
// 创建压缩摘要消息
|
||||||
const summaryMessage: CompactionSummaryMessage = {
|
const summaryMessage: CompactionSummaryMessage = {
|
||||||
type: MessageType.COMPACTION_SUMMARY,
|
type: MessageType.COMPACTION_SUMMARY,
|
||||||
@ -893,4 +910,14 @@ export class ChatHistoryManager {
|
|||||||
content: text
|
content: text
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪新消息(工具执行结果)
|
||||||
|
*/
|
||||||
|
public trackToolResult(toolName: string, result: string): void {
|
||||||
|
this.newMessagesSinceCompaction.push({
|
||||||
|
type: 'TOOL_RESULT',
|
||||||
|
content: `[${toolName}] ${result}`
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,3 +71,31 @@ export function getUserIdFromToken(token: string): string | null {
|
|||||||
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
|
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 JWT token 是否已过期
|
||||||
|
* @param token JWT token
|
||||||
|
* @param bufferSeconds 提前多少秒判定为过期(默认60秒)
|
||||||
|
* @returns true 表示已过期,false 表示未过期,null 表示无法判断
|
||||||
|
*/
|
||||||
|
export function isTokenExpired(token: string, bufferSeconds: number = 60): boolean | null {
|
||||||
|
const payload = parseJwtPayload(token);
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.exp === undefined) {
|
||||||
|
console.warn('[JWT] payload 中没有 exp 字段,无法判断过期');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const expTime = payload.exp - bufferSeconds;
|
||||||
|
const isExpired = now >= expTime;
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
console.warn('[JWT] token 已过期,exp:', payload.exp, '当前:', now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExpired;
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,11 @@ import { ChatHistoryManager } from "./chatHistoryManager";
|
|||||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||||
import { userInteractionManager } from "../services/userInteraction";
|
import { userInteractionManager } from "../services/userInteraction";
|
||||||
import { healthCheck } from "../services/apiClient";
|
import { healthCheck } from "../services/apiClient";
|
||||||
|
import {
|
||||||
|
checkBalanceBeforeSend,
|
||||||
|
fetchBalance,
|
||||||
|
} from "../services/creditsService";
|
||||||
|
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||||
|
|
||||||
import type { RunMode, ServiceTier } from "../types/api";
|
import type { RunMode, ServiceTier } from "../types/api";
|
||||||
|
|
||||||
@ -30,27 +35,6 @@ let currentSession: DialogSession | null = null;
|
|||||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
let lastTaskId: string | null = null;
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
|
||||||
let pendingPlanExecution: {
|
|
||||||
panel: vscode.WebviewPanel;
|
|
||||||
planTitle: string;
|
|
||||||
extensionPath: string;
|
|
||||||
taskId: string; // 保存 taskId 以便复用
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
|
||||||
*/
|
|
||||||
export function setPendingPlanExecution(
|
|
||||||
panel: vscode.WebviewPanel,
|
|
||||||
planTitle: string,
|
|
||||||
extensionPath: string,
|
|
||||||
taskId: string
|
|
||||||
): void {
|
|
||||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
|
||||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户消息
|
* 处理用户消息
|
||||||
*/
|
*/
|
||||||
@ -59,7 +43,7 @@ export async function handleUserMessage(
|
|||||||
text: string,
|
text: string,
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
@ -88,10 +72,40 @@ export async function handleUserMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送前检测余额
|
||||||
|
const balanceCheck = await checkBalanceBeforeSend();
|
||||||
|
if (!balanceCheck.allowed) {
|
||||||
|
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
|
||||||
|
// 显示错误提示
|
||||||
|
const selection = await vscode.window.showWarningMessage(
|
||||||
|
balanceCheck.message || "资源点余额不足",
|
||||||
|
"去充值"
|
||||||
|
);
|
||||||
|
if (selection === "去充值") {
|
||||||
|
vscode.env.openExternal(
|
||||||
|
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 恢复输入状态
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateSegments",
|
||||||
|
segments: [],
|
||||||
|
isComplete: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试使用后端服务
|
// 尝试使用后端服务
|
||||||
if (useBackendService && extensionPath) {
|
if (useBackendService && extensionPath) {
|
||||||
try {
|
try {
|
||||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
|
await handleUserMessageWithBackend(
|
||||||
|
panel,
|
||||||
|
text,
|
||||||
|
extensionPath,
|
||||||
|
mode,
|
||||||
|
undefined,
|
||||||
|
serviceTier
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("后端服务不可用:", error);
|
console.error("后端服务不可用:", error);
|
||||||
@ -127,7 +141,7 @@ async function handleUserMessageWithBackend(
|
|||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
@ -135,13 +149,19 @@ async function handleUserMessageWithBackend(
|
|||||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||||
|
|
||||||
// 创建或复用会话
|
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||||
if (!currentSession || !currentSession.active) {
|
currentSession = dialogManager.createSession(
|
||||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
extensionPath,
|
||||||
// 保存 taskId 用于后续操作(如压缩)
|
taskIdToUse || undefined
|
||||||
lastTaskId = currentSession.getTaskId();
|
);
|
||||||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
// 保存 taskId 用于后续操作(如压缩)
|
||||||
}
|
lastTaskId = currentSession.getTaskId();
|
||||||
|
console.log(
|
||||||
|
"[MessageHandler] 创建会话: taskId=",
|
||||||
|
lastTaskId,
|
||||||
|
"来源=",
|
||||||
|
taskIdToUse ? "historyManager" : "新生成"
|
||||||
|
);
|
||||||
|
|
||||||
// 显示状态栏
|
// 显示状态栏
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@ -201,6 +221,17 @@ async function handleUserMessageWithBackend(
|
|||||||
// 最后一次发送完整的段落
|
// 最后一次发送完整的段落
|
||||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
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({
|
const result = await panel.webview.postMessage({
|
||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
segments: segments,
|
segments: segments,
|
||||||
@ -222,39 +253,6 @@ async function handleUserMessageWithBackend(
|
|||||||
console.warn("保存AI响应历史失败:", error);
|
console.warn("保存AI响应历史失败:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
|
||||||
if (pendingPlanExecution) {
|
|
||||||
const {
|
|
||||||
panel: execPanel,
|
|
||||||
planTitle,
|
|
||||||
extensionPath: execPath,
|
|
||||||
taskId: reuseTaskId,
|
|
||||||
} = pendingPlanExecution;
|
|
||||||
pendingPlanExecution = null;
|
|
||||||
console.log(
|
|
||||||
"[MessageHandler] 自动执行计划:",
|
|
||||||
planTitle,
|
|
||||||
"复用 taskId:",
|
|
||||||
reuseTaskId
|
|
||||||
);
|
|
||||||
|
|
||||||
// 延迟一小段时间确保当前对话完全结束
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
|
||||||
await handleUserMessageWithBackend(
|
|
||||||
execPanel,
|
|
||||||
`请按照刚才的计划执行:${planTitle}`,
|
|
||||||
execPath,
|
|
||||||
"agent",
|
|
||||||
reuseTaskId // 复用 Plan 模式的 taskId
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -320,7 +318,7 @@ async function handleUserMessageWithBackend(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mode,
|
mode,
|
||||||
serviceTier // 传递服务等级
|
serviceTier // 传递服务等级
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -400,9 +398,17 @@ export async function handlePlanAction(
|
|||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
action: string,
|
action: string,
|
||||||
planTitle: string,
|
planTitle: string,
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
|
serviceTier?: ServiceTier
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
console.log(
|
||||||
|
"[handlePlanAction] action:",
|
||||||
|
action,
|
||||||
|
"planTitle:",
|
||||||
|
planTitle,
|
||||||
|
"serviceTier:",
|
||||||
|
serviceTier
|
||||||
|
);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "confirm":
|
case "confirm":
|
||||||
@ -416,7 +422,8 @@ export async function handlePlanAction(
|
|||||||
panel,
|
panel,
|
||||||
`请按照刚才的计划执行:${planTitle}`,
|
`请按照刚才的计划执行:${planTitle}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
"agent"
|
"agent",
|
||||||
|
serviceTier
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -432,7 +439,8 @@ export async function handlePlanAction(
|
|||||||
panel,
|
panel,
|
||||||
`请根据以下建议修改计划:${modification}`,
|
`请根据以下建议修改计划:${modification}`,
|
||||||
extensionPath,
|
extensionPath,
|
||||||
"plan"
|
"plan",
|
||||||
|
serviceTier
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1024,3 +1032,35 @@ async function handleVCDGeneration(
|
|||||||
vscode.window.showErrorMessage(errorMsg);
|
vscode.window.showErrorMessage(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理提示词优化请求
|
||||||
|
*/
|
||||||
|
export async function handleOptimizePrompt(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
prompt: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
|
||||||
|
console.log("[MessageHandler] prompt:", prompt);
|
||||||
|
console.log("[MessageHandler] prompt 长度:", prompt?.length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("[MessageHandler] 开始调用 optimizePrompt...");
|
||||||
|
const optimized = await optimizePrompt(prompt);
|
||||||
|
console.log("[MessageHandler] 优化成功,结果:", optimized);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "optimizeResult",
|
||||||
|
success: true,
|
||||||
|
optimizedPrompt: optimized,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : "优化失败";
|
||||||
|
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "optimizeResult",
|
||||||
|
success: false,
|
||||||
|
error: errorMsg,
|
||||||
|
});
|
||||||
|
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { getWebviewContent } from "./webviewContent";
|
import { getWebviewContent } from "./webviewContent";
|
||||||
|
import { isTokenExpired } from "../utils/jwtUtils";
|
||||||
import {
|
import {
|
||||||
handleUserMessage,
|
handleUserMessage,
|
||||||
insertCodeToEditor,
|
insertCodeToEditor,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
handleReplaceInFile,
|
handleReplaceInFile,
|
||||||
handleUserAnswer,
|
handleUserAnswer,
|
||||||
abortCurrentDialog,
|
abortCurrentDialog,
|
||||||
|
handleOptimizePrompt,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,6 +71,9 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
// 处理消息
|
// 处理消息
|
||||||
panel.webview.onDidReceiveMessage(
|
panel.webview.onDidReceiveMessage(
|
||||||
(message) => {
|
(message) => {
|
||||||
|
console.log("[ICViewProvider] ====== 收到 WebView 消息 ======");
|
||||||
|
console.log("[ICViewProvider] command:", message.command);
|
||||||
|
console.log("[ICViewProvider] 完整消息:", JSON.stringify(message));
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case "sendMessage":
|
case "sendMessage":
|
||||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||||
@ -116,6 +121,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
case "abortDialog":
|
case "abortDialog":
|
||||||
void abortCurrentDialog();
|
void abortCurrentDialog();
|
||||||
break;
|
break;
|
||||||
|
// 新增:优化提示词
|
||||||
|
case "optimizePrompt":
|
||||||
|
handleOptimizePrompt(panel, message.prompt);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@ -127,10 +136,34 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
* 侧边栏视图提供者
|
* 侧边栏视图提供者
|
||||||
*/
|
*/
|
||||||
export class ICViewProvider implements vscode.WebviewViewProvider {
|
export class ICViewProvider implements vscode.WebviewViewProvider {
|
||||||
|
private _view?: vscode.WebviewView;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly extensionUri: vscode.Uri,
|
private readonly extensionUri: vscode.Uri,
|
||||||
private readonly context: vscode.ExtensionContext
|
private readonly context: vscode.ExtensionContext
|
||||||
) {}
|
) {
|
||||||
|
// 监听认证状态变化
|
||||||
|
this.context.subscriptions.push(
|
||||||
|
vscode.authentication.onDidChangeSessions((e) => {
|
||||||
|
if (e.provider.id === "iccoder") {
|
||||||
|
this.refreshLoginStatus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新登录状态并更新视图
|
||||||
|
*/
|
||||||
|
private async refreshLoginStatus(): Promise<void> {
|
||||||
|
if (this._view) {
|
||||||
|
const isLoggedIn = await this.checkLoginStatus();
|
||||||
|
this._view.webview.html = this.getWebviewContent(
|
||||||
|
this._view.webview,
|
||||||
|
isLoggedIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查登录状态(使用 Authentication API)
|
* 检查登录状态(使用 Authentication API)
|
||||||
@ -138,14 +171,29 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
private async checkLoginStatus(): Promise<boolean> {
|
private async checkLoginStatus(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||||
return !!session;
|
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
|
||||||
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 检查 token 是否过期
|
||||||
|
const expired = isTokenExpired(session.accessToken);
|
||||||
|
console.log("[ICViewProvider] token 过期检查结果:", expired);
|
||||||
|
// 只有明确过期才认为未登录,无法判断时认为已登录
|
||||||
|
if (expired === true) {
|
||||||
|
console.log("[ICViewProvider] Token 已过期");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("检查登录状态失败:", error);
|
console.log("[ICViewProvider] 检查登录状态失败:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveWebviewView(webviewView: vscode.WebviewView) {
|
resolveWebviewView(webviewView: vscode.WebviewView) {
|
||||||
|
// 保存引用以便后续刷新
|
||||||
|
this._view = webviewView;
|
||||||
|
|
||||||
webviewView.webview.options = {
|
webviewView.webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
|
||||||
|
|||||||
@ -14,9 +14,7 @@ export function getContextButtonContent(): string {
|
|||||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="add-context-label">添加上下文</span>
|
<span class="add-context-label">添加上下文</span>
|
||||||
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -303,6 +303,7 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
let totalHistory = 0;
|
let totalHistory = 0;
|
||||||
let hasMoreHistory = false;
|
let hasMoreHistory = false;
|
||||||
let isLoadingHistory = false;
|
let isLoadingHistory = false;
|
||||||
|
let currentLoadRequestId = 0; // 请求 ID,用于防止并发加载
|
||||||
const HISTORY_PAGE_SIZE = 10;
|
const HISTORY_PAGE_SIZE = 10;
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
@ -346,11 +347,15 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成新的请求 ID,用于防止并发加载
|
||||||
|
const requestId = ++currentLoadRequestId;
|
||||||
|
|
||||||
isLoadingHistory = true;
|
isLoadingHistory = true;
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
command: 'loadConversationHistory',
|
command: 'loadConversationHistory',
|
||||||
offset: currentOffset,
|
offset: currentOffset,
|
||||||
limit: HISTORY_PAGE_SIZE
|
limit: HISTORY_PAGE_SIZE,
|
||||||
|
requestId: requestId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,11 +367,19 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 追加新数据
|
// 追加新数据(去重)
|
||||||
conversationHistory = conversationHistory.concat(data.items);
|
const existingIds = new Set(conversationHistory.map(item => item.id));
|
||||||
|
const newItems = [];
|
||||||
|
for (const item of data.items) {
|
||||||
|
if (!existingIds.has(item.id)) {
|
||||||
|
existingIds.add(item.id);
|
||||||
|
newItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conversationHistory = conversationHistory.concat(newItems);
|
||||||
totalHistory = data.total;
|
totalHistory = data.total;
|
||||||
hasMoreHistory = data.hasMore;
|
hasMoreHistory = data.hasMore;
|
||||||
currentOffset += data.items.length;
|
currentOffset = conversationHistory.length;
|
||||||
|
|
||||||
const historyList = document.getElementById('historyList');
|
const historyList = document.getElementById('historyList');
|
||||||
if (!historyList) {
|
if (!historyList) {
|
||||||
@ -454,9 +467,10 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听下拉菜单滚动事件
|
// 监听下拉菜单滚动事件(防止重复注册)
|
||||||
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
||||||
if (historyDropdownMenu) {
|
if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) {
|
||||||
|
historyDropdownMenu._scrollListenerAdded = true;
|
||||||
historyDropdownMenu.addEventListener('scroll', () => {
|
historyDropdownMenu.addEventListener('scroll', () => {
|
||||||
const menu = historyDropdownMenu;
|
const menu = historyDropdownMenu;
|
||||||
const scrollTop = menu.scrollTop;
|
const scrollTop = menu.scrollTop;
|
||||||
|
|||||||
216
src/views/exampleShowcase.ts
Normal file
216
src/views/exampleShowcase.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* 获取展示区域的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getExampleShowcaseContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="example-showcase" id="exampleShowcase">
|
||||||
|
<div class="showcase-title">展示</div>
|
||||||
|
<div class="example-cards">
|
||||||
|
<div class="example-card" onclick="fillExample(0)">
|
||||||
|
<div class="example-icon">📝</div>
|
||||||
|
<div class="example-content">
|
||||||
|
<div class="example-title">代码生成</div>
|
||||||
|
<div class="example-desc">生成一个 8 位全加器的 Verilog 代码</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card" onclick="fillExample(1)">
|
||||||
|
<div class="example-icon">🔍</div>
|
||||||
|
<div class="example-content">
|
||||||
|
<div class="example-title">代码分析</div>
|
||||||
|
<div class="example-desc">分析当前项目中的时序逻辑设计</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="web-link">
|
||||||
|
<a href="https://iccoder.com" target="_blank" class="web-link-button">
|
||||||
|
<span class="link-icon">🌐</span>
|
||||||
|
<span>IC Coder Web端</span>
|
||||||
|
<span class="link-arrow">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取展示区域的样式
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -29,16 +29,21 @@ import {
|
|||||||
getOptimizeButtonStyles,
|
getOptimizeButtonStyles,
|
||||||
getOptimizeButtonScript,
|
getOptimizeButtonScript,
|
||||||
} from "./optimizeButton";
|
} from "./optimizeButton";
|
||||||
|
import {
|
||||||
|
getExampleShowcaseContent,
|
||||||
|
getExampleShowcaseStyles,
|
||||||
|
getExampleShowcaseScript,
|
||||||
|
} from "./exampleShowcase";
|
||||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取输入区域的 HTML 内容
|
* 获取输入区域的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getInputAreaContent(
|
export function getInputAreaContent(
|
||||||
autoIcon: string = '',
|
autoIcon: string = "",
|
||||||
liteIcon: string = '',
|
liteIcon: string = "",
|
||||||
syIcon: string = '',
|
syIcon: string = "",
|
||||||
maxIcon: string = ''
|
maxIcon: string = ""
|
||||||
): string {
|
): string {
|
||||||
return `
|
return `
|
||||||
<div class="input-area centered" id="inputArea">
|
<div class="input-area centered" id="inputArea">
|
||||||
@ -71,6 +76,8 @@ export function getInputAreaContent(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 展示区域:案例和 Web 端链接 -->
|
||||||
|
${getExampleShowcaseContent()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -86,6 +93,7 @@ export function getInputAreaStyles(): string {
|
|||||||
${getContextDisplayStyles()}
|
${getContextDisplayStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
|
${getExampleShowcaseStyles()}
|
||||||
.input-area {
|
.input-area {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@ -95,7 +103,7 @@ export function getInputAreaStyles(): string {
|
|||||||
/* 居中模式:未发起对话时 */
|
/* 居中模式:未发起对话时 */
|
||||||
.input-area.centered {
|
.input-area.centered {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 55%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
@ -292,6 +300,7 @@ export function getInputAreaScript(): string {
|
|||||||
${getContextDisplayScript()}
|
${getContextDisplayScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
${getExampleShowcaseScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
knowledgeLoadIconSvg,
|
knowledgeLoadIconSvg,
|
||||||
stateTransitionIconSvg,
|
stateTransitionIconSvg,
|
||||||
userQuestionIconSvg,
|
userQuestionIconSvg,
|
||||||
|
updateStageIconSvg,
|
||||||
} from "../constants/toolIcons";
|
} from "../constants/toolIcons";
|
||||||
import {
|
import {
|
||||||
getWaveformPreviewContent,
|
getWaveformPreviewContent,
|
||||||
@ -670,6 +671,7 @@ export function getMessageAreaScript(): string {
|
|||||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||||
|
const updateStageIconSvg = \`${updateStageIconSvg}\`;
|
||||||
|
|
||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
|
|
||||||
@ -724,6 +726,7 @@ export function getMessageAreaScript(): string {
|
|||||||
'updateNode': fileWriteIconSvg,
|
'updateNode': fileWriteIconSvg,
|
||||||
'addStateTransition': stateTransitionIconSvg,
|
'addStateTransition': stateTransitionIconSvg,
|
||||||
'askUser': userQuestionIconSvg,
|
'askUser': userQuestionIconSvg,
|
||||||
|
'updatePhase': updateStageIconSvg,
|
||||||
};
|
};
|
||||||
return iconMap[toolName] || '';
|
return iconMap[toolName] || '';
|
||||||
}
|
}
|
||||||
@ -756,6 +759,8 @@ export function getMessageAreaScript(): string {
|
|||||||
'spawnExplorer': '代码探索',
|
'spawnExplorer': '代码探索',
|
||||||
'spawnDebugger': '波形调试',
|
'spawnDebugger': '波形调试',
|
||||||
'askUser': '用户提问',
|
'askUser': '用户提问',
|
||||||
|
'updatePhase': '已更新阶段',
|
||||||
|
'iverilog': '已完成编译',
|
||||||
};
|
};
|
||||||
return toolNameMap[toolName] || toolName;
|
return toolNameMap[toolName] || toolName;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,35 +60,97 @@ export function getOptimizeButtonScript(): string {
|
|||||||
return `
|
return `
|
||||||
let isOptimized = false; // 标记是否已优化
|
let isOptimized = false; // 标记是否已优化
|
||||||
let originalText = ''; // 保存原始文本用于撤回
|
let originalText = ''; // 保存原始文本用于撤回
|
||||||
|
let isOptimizing = false; // 标记是否正在优化中
|
||||||
|
|
||||||
function handleOptimize() {
|
function handleOptimize() {
|
||||||
|
console.log('[Optimize] handleOptimize 被调用');
|
||||||
|
console.log('[Optimize] isOptimizing:', isOptimizing);
|
||||||
|
console.log('[Optimize] isOptimized:', isOptimized);
|
||||||
|
console.log('[Optimize] messageInput:', messageInput);
|
||||||
|
|
||||||
|
if (isOptimizing) {
|
||||||
|
console.log('[Optimize] 正在优化中,忽略点击');
|
||||||
|
return; // 正在优化中,忽略点击
|
||||||
|
}
|
||||||
|
|
||||||
if (isOptimized) {
|
if (isOptimized) {
|
||||||
// 撤回操作
|
// 撤回操作
|
||||||
|
console.log('[Optimize] 执行撤回操作');
|
||||||
messageInput.value = originalText;
|
messageInput.value = originalText;
|
||||||
resetOptimizeButton();
|
resetOptimizeButton();
|
||||||
} else {
|
} else {
|
||||||
// 优化操作
|
// 优化操作
|
||||||
|
const currentText = messageInput.value.trim();
|
||||||
|
console.log('[Optimize] 当前输入内容:', currentText);
|
||||||
|
console.log('[Optimize] 内容长度:', currentText.length);
|
||||||
|
|
||||||
|
if (!currentText) {
|
||||||
|
console.log('[Optimize] 输入框为空,不执行优化');
|
||||||
|
return; // 输入框为空,不执行优化
|
||||||
|
}
|
||||||
|
|
||||||
originalText = messageInput.value; // 保存原始文本
|
originalText = messageInput.value; // 保存原始文本
|
||||||
|
isOptimizing = true;
|
||||||
|
console.log('[Optimize] 开始优化,显示加载状态');
|
||||||
|
|
||||||
// 使用死数据替换输入框内容
|
// 显示加载状态
|
||||||
const optimizedTexts = [
|
showOptimizeLoading();
|
||||||
'请帮我优化这段代码,提高性能和可读性',
|
|
||||||
'请分析这个问题并给出最佳解决方案',
|
|
||||||
'请帮我重构这段代码,使其更加简洁高效',
|
|
||||||
'请检查代码中的潜在问题并提供改进建议'
|
|
||||||
];
|
|
||||||
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
|
|
||||||
messageInput.value = randomText;
|
|
||||||
|
|
||||||
// 切换到撤回状态
|
// 发送优化请求到扩展
|
||||||
isOptimized = true;
|
console.log('[Optimize] 发送 optimizePrompt 消息');
|
||||||
updateOptimizeButton();
|
vscode.postMessage({
|
||||||
|
command: 'optimizePrompt',
|
||||||
|
prompt: currentText
|
||||||
|
});
|
||||||
|
console.log('[Optimize] postMessage 已发送');
|
||||||
}
|
}
|
||||||
|
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
autoResizeTextarea();
|
autoResizeTextarea();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理优化结果
|
||||||
|
function handleOptimizeResult(success, optimizedPrompt, error) {
|
||||||
|
isOptimizing = false;
|
||||||
|
hideOptimizeLoading();
|
||||||
|
|
||||||
|
if (success && optimizedPrompt) {
|
||||||
|
messageInput.value = optimizedPrompt;
|
||||||
|
isOptimized = true;
|
||||||
|
updateOptimizeButton();
|
||||||
|
} else {
|
||||||
|
// 优化失败,恢复原始文本
|
||||||
|
messageInput.value = originalText;
|
||||||
|
console.error('优化失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageInput.focus();
|
||||||
|
autoResizeTextarea();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOptimizeLoading() {
|
||||||
|
const optimizeButton = document.getElementById('optimizeButton');
|
||||||
|
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||||
|
if (optimizeButton && optimizeIcon) {
|
||||||
|
optimizeButton.disabled = true;
|
||||||
|
optimizeButton.style.opacity = '0.5';
|
||||||
|
// 显示加载动画
|
||||||
|
optimizeIcon.innerHTML = '<circle cx="512" cy="512" r="400" fill="none" stroke="#409eff" stroke-width="60" stroke-dasharray="1200" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 512 512" to="360 512 512" dur="1s" repeatCount="indefinite"/></circle>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOptimizeLoading() {
|
||||||
|
const optimizeButton = document.getElementById('optimizeButton');
|
||||||
|
if (optimizeButton) {
|
||||||
|
optimizeButton.disabled = false;
|
||||||
|
optimizeButton.style.opacity = '1';
|
||||||
|
}
|
||||||
|
// 恢复图标会在 updateOptimizeButton 或 resetOptimizeButton 中处理
|
||||||
|
if (!isOptimized) {
|
||||||
|
resetOptimizeButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateOptimizeButton() {
|
function updateOptimizeButton() {
|
||||||
const optimizeIcon = document.getElementById('optimizeIcon');
|
const optimizeIcon = document.getElementById('optimizeIcon');
|
||||||
const optimizeTooltip = document.getElementById('optimizeTooltip');
|
const optimizeTooltip = document.getElementById('optimizeTooltip');
|
||||||
|
|||||||
@ -62,9 +62,9 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-summary p { margin: 8px 0; }
|
.plan-summary p { margin: 8px 0; }
|
||||||
.plan-summary ul, .plan-summary ol {
|
.plan-summary ul, .plan-summary ol {
|
||||||
margin: 8px 0;
|
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 {
|
.plan-summary code {
|
||||||
background: var(--vscode-textCodeBlock-background);
|
background: var(--vscode-textCodeBlock-background);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
@ -150,24 +150,50 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-actions {
|
.plan-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-top: 1px solid var(--vscode-input-border);
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
background: var(--vscode-sideBar-background);
|
background: var(--vscode-sideBar-background);
|
||||||
}
|
}
|
||||||
.plan-actions .question-options {
|
.plan-input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.plan-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.plan-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--vscode-focusBorder);
|
||||||
|
}
|
||||||
|
.plan-btn-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.plan-btn {
|
.plan-btn {
|
||||||
padding: 8px 18px;
|
padding: 8px 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.plan-btn-submit {
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.plan-btn-submit:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
.plan-btn-confirm {
|
.plan-btn-confirm {
|
||||||
background: var(--vscode-button-background);
|
background: var(--vscode-button-background);
|
||||||
color: var(--vscode-button-foreground);
|
color: var(--vscode-button-foreground);
|
||||||
@ -175,41 +201,26 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-btn-confirm:hover {
|
.plan-btn-confirm:hover {
|
||||||
background: var(--vscode-button-hoverBackground);
|
background: var(--vscode-button-hoverBackground);
|
||||||
}
|
}
|
||||||
.plan-btn-modify {
|
|
||||||
background: var(--vscode-input-background);
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
|
||||||
}
|
|
||||||
.plan-btn-cancel {
|
.plan-btn-cancel {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
}
|
|
||||||
.plan-actions .custom-input-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.plan-actions .custom-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--vscode-input-background);
|
|
||||||
color: var(--vscode-input-foreground);
|
|
||||||
border: 1px solid var(--vscode-input-border);
|
border: 1px solid var(--vscode-input-border);
|
||||||
border-radius: 4px;
|
}
|
||||||
|
.plan-btn-cancel:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.plan-answered {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.plan-actions .custom-submit {
|
.answered-label {
|
||||||
padding: 8px 18px;
|
color: var(--vscode-descriptionForeground);
|
||||||
background: var(--vscode-button-background);
|
|
||||||
color: var(--vscode-button-foreground);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
.plan-actions .custom-submit:hover {
|
.answered-value {
|
||||||
background: var(--vscode-button-hoverBackground);
|
color: var(--vscode-textLink-foreground);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 阶段进度条样式 */
|
/* 阶段进度条样式 */
|
||||||
@ -392,6 +403,12 @@ export function getPlanCardScript(): string {
|
|||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>');
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// 标题(必须在转义之后、其他处理之前)
|
||||||
|
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
||||||
|
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
// 代码块 (\`\`\`code\`\`\`)
|
// 代码块 (\`\`\`code\`\`\`)
|
||||||
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
|
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
|
||||||
|
|
||||||
@ -417,12 +434,6 @@ export function getPlanCardScript(): string {
|
|||||||
return table;
|
return table;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 标题
|
|
||||||
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
|
||||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
||||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
||||||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
||||||
|
|
||||||
// 粗体和斜体
|
// 粗体和斜体
|
||||||
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
||||||
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
||||||
@ -597,16 +608,17 @@ export function getPlanCardScript(): string {
|
|||||||
// 兼容旧格式:渲染步骤列表
|
// 兼容旧格式:渲染步骤列表
|
||||||
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
|
||||||
|
|
||||||
// 选项按钮
|
|
||||||
const options = ['确认执行', '修改计划', '取消'];
|
|
||||||
const optionsHtml = options.map(opt => {
|
|
||||||
const isSelected = isAnswered && opt === selectedAnswer;
|
|
||||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// 渲染 Markdown 格式的摘要
|
// 渲染 Markdown 格式的摘要
|
||||||
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
|
||||||
|
|
||||||
|
// 已回答时显示用户的选择
|
||||||
|
const answeredHtml = isAnswered ? \`
|
||||||
|
<div class="plan-answered">
|
||||||
|
<span class="answered-label">已回复:</span>
|
||||||
|
<span class="answered-value">\${selectedAnswer}</span>
|
||||||
|
</div>
|
||||||
|
\` : '';
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="plan-card">
|
<div class="plan-card">
|
||||||
<div class="plan-header">
|
<div class="plan-header">
|
||||||
@ -618,58 +630,72 @@ export function getPlanCardScript(): string {
|
|||||||
<div class="plan-summary">\${summaryHtml}</div>
|
<div class="plan-summary">\${summaryHtml}</div>
|
||||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-actions">
|
<div class="plan-actions" data-ask-id="\${segment.askId}" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||||
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
<div class="plan-input-row">
|
||||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||||
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||||
<button class="custom-submit">提交</button>
|
</div>
|
||||||
|
<div class="plan-btn-row">
|
||||||
|
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||||
|
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
\${answeredHtml}
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
// 只在未回答时添加事件监听
|
// 只在未回答时添加事件监听
|
||||||
if (!isAnswered) {
|
if (!isAnswered) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||||
optionButtons.forEach(btn => {
|
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||||
btn.addEventListener('click', function() {
|
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||||
const option = this.getAttribute('data-option');
|
const planInput = segmentDiv.querySelector('.plan-input');
|
||||||
// 发送答案到后端
|
|
||||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
|
||||||
// 同时发送 planAction 用于模式切换
|
|
||||||
const actionMap = {
|
|
||||||
'确认执行': 'confirm',
|
|
||||||
'修改计划': 'modify',
|
|
||||||
'取消': 'cancel'
|
|
||||||
};
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'planAction',
|
|
||||||
action: actionMap[option] || option,
|
|
||||||
planTitle: segment.planTitle
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
// 提交修改按钮
|
||||||
const customInput = segmentDiv.querySelector('.custom-input');
|
if (submitBtn && planInput) {
|
||||||
if (submitBtn && customInput) {
|
|
||||||
submitBtn.addEventListener('click', function() {
|
submitBtn.addEventListener('click', function() {
|
||||||
const customValue = customInput.value.trim();
|
const inputValue = planInput.value.trim();
|
||||||
if (customValue) {
|
if (inputValue) {
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
customInput.addEventListener('keypress', function(e) {
|
// 回车键提交修改
|
||||||
|
planInput.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const customValue = customInput.value.trim();
|
const inputValue = planInput.value.trim();
|
||||||
if (customValue) {
|
if (inputValue) {
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
handleQuestionAnswerInSegment(segment.askId, inputValue, segmentDiv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确认执行按钮
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', function() {
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, '确认执行', segmentDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消按钮 - 直接中止对话,不发送给智能体
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', function() {
|
||||||
|
// 标记问题已回答
|
||||||
|
answeredQuestions.set(segment.askId, '取消');
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
|
||||||
|
// 隐藏操作按钮
|
||||||
|
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||||
|
if (actionsDiv) {
|
||||||
|
actionsDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送中止对话命令
|
||||||
|
vscode.postMessage({ command: 'abortDialog' });
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -702,29 +728,65 @@ export function getPlanCardScript(): string {
|
|||||||
<div class="plan-summary">\${summaryHtml}</div>
|
<div class="plan-summary">\${summaryHtml}</div>
|
||||||
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`}
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-actions">
|
<div class="plan-actions" data-ask-id="\${segment.askId}">
|
||||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
<div class="plan-input-row">
|
||||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
<input type="text" class="plan-input" placeholder="输入修改建议..." />
|
||||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
<button class="plan-btn plan-btn-submit">提交修改</button>
|
||||||
|
</div>
|
||||||
|
<div class="plan-btn-row">
|
||||||
|
<button class="plan-btn plan-btn-confirm">确认执行</button>
|
||||||
|
<button class="plan-btn plan-btn-cancel">取消</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
|
|
||||||
// 绑定按钮事件
|
// 绑定按钮事件(静态渲染时也需要能响应)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const planCard = segmentDiv.querySelector('.plan-card');
|
const submitBtn = segmentDiv.querySelector('.plan-btn-submit');
|
||||||
if (planCard) {
|
const confirmBtn = segmentDiv.querySelector('.plan-btn-confirm');
|
||||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
const cancelBtn = segmentDiv.querySelector('.plan-btn-cancel');
|
||||||
btn.addEventListener('click', (e) => {
|
const planInput = segmentDiv.querySelector('.plan-input');
|
||||||
const action = e.currentTarget?.dataset?.action;
|
|
||||||
|
// 提交修改按钮
|
||||||
|
if (submitBtn && planInput) {
|
||||||
|
submitBtn.addEventListener('click', function() {
|
||||||
|
const inputValue = planInput.value.trim();
|
||||||
|
if (inputValue) {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
command: 'planAction',
|
command: 'submitAnswer',
|
||||||
action: action,
|
askId: segment.askId,
|
||||||
planTitle: segment.planTitle
|
selected: [inputValue],
|
||||||
|
customInput: inputValue
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认执行按钮
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', function() {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'submitAnswer',
|
||||||
|
askId: segment.askId,
|
||||||
|
selected: ['确认执行'],
|
||||||
|
customInput: '确认执行'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消按钮 - 直接中止对话
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', function() {
|
||||||
|
// 隐藏操作按钮
|
||||||
|
const actionsDiv = segmentDiv.querySelector('.plan-actions');
|
||||||
|
if (actionsDiv) {
|
||||||
|
actionsDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
// 发送中止对话命令
|
||||||
|
vscode.postMessage({ command: 'abortDialog' });
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -250,14 +250,27 @@ export function getUserInfoComponentScript(): string {
|
|||||||
|
|
||||||
// 更新剩余 Credits
|
// 更新剩余 Credits
|
||||||
const creditsDetail = document.getElementById('creditsDetail');
|
const creditsDetail = document.getElementById('creditsDetail');
|
||||||
|
console.log('[UserInfoComponent] 更新 Credits 显示');
|
||||||
|
console.log('[UserInfoComponent] currentUserInfo.credits:', currentUserInfo.credits);
|
||||||
|
console.log('[UserInfoComponent] creditsDetail 元素:', creditsDetail);
|
||||||
if (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) {
|
function updateUserInfoDisplay(userInfo) {
|
||||||
currentUserInfo = userInfo;
|
currentUserInfo = userInfo;
|
||||||
|
console.log('[UserInfoComponent] 更新用户信息:', userInfo);
|
||||||
|
// 如果下拉面板已打开,立即更新显示
|
||||||
|
const dropdown = document.getElementById('userDetailDropdown');
|
||||||
|
if (dropdown && dropdown.classList.contains('active')) {
|
||||||
|
updateUserDetailModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定下拉面板事件
|
// 绑定下拉面板事件
|
||||||
|
|||||||
@ -174,7 +174,7 @@ export function getWaveformPreviewScript(): string {
|
|||||||
const content = document.createElement('div');
|
const content = document.createElement('div');
|
||||||
content.className = 'waveform-preview-content';
|
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');
|
const miniViewer = document.createElement('div');
|
||||||
miniViewer.id = miniViewerId;
|
miniViewer.id = miniViewerId;
|
||||||
miniViewer.className = 'waveform-mini-viewer';
|
miniViewer.className = 'waveform-mini-viewer';
|
||||||
|
|||||||
@ -428,6 +428,7 @@ export function getWebviewContent(
|
|||||||
<script>
|
<script>
|
||||||
console.log('[WebView] 脚本开始执行');
|
console.log('[WebView] 脚本开始执行');
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
|
window.vscode = vscode; // 确保全局可访问
|
||||||
console.log('[WebView] vscode API 已获取');
|
console.log('[WebView] vscode API 已获取');
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const modeSelect = document.getElementById('modeSelect');
|
const modeSelect = document.getElementById('modeSelect');
|
||||||
@ -588,20 +589,25 @@ export function getWebviewContent(
|
|||||||
case 'updateUserInfo':
|
case 'updateUserInfo':
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
console.log('[WebView] 收到用户信息:', message.userInfo);
|
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||||
|
console.log('[WebView] Credits 字段值:', message.userInfo?.credits);
|
||||||
if (message.userInfo) {
|
if (message.userInfo) {
|
||||||
const userInfoData = {
|
const userInfoData = {
|
||||||
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||||
userId: message.userInfo.userId || message.userInfo.id,
|
userId: message.userInfo.userId || message.userInfo.id,
|
||||||
tierName: message.userInfo.tierName,
|
tierName: message.userInfo.tierName,
|
||||||
tierIconUrl: message.tierIconUrl,
|
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);
|
||||||
|
console.log('[WebView] userInfoData.credits:', userInfoData.credits);
|
||||||
|
|
||||||
// 调用更新用户头像图标按钮的函数
|
// 调用更新用户头像图标按钮的函数
|
||||||
if (typeof updateUserAvatarIconButton === 'function') {
|
if (typeof updateUserAvatarIconButton === 'function') {
|
||||||
updateUserAvatarIconButton(userInfoData);
|
updateUserAvatarIconButton(userInfoData);
|
||||||
|
} else {
|
||||||
|
console.warn('[WebView] updateUserAvatarIconButton 函数不存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -632,21 +638,6 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'checkPanelWidth':
|
|
||||||
// 检查面板宽度
|
|
||||||
const minWidth = message.minWidth || 200;
|
|
||||||
const currentWidth = document.body.clientWidth;
|
|
||||||
console.log('[WebView] 检查面板宽度:', currentWidth, '最小宽度:', minWidth);
|
|
||||||
if (currentWidth < minWidth) {
|
|
||||||
// 宽度不足,通知后端关闭面板
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'panelWidthInsufficient',
|
|
||||||
currentWidth: currentWidth,
|
|
||||||
minWidth: minWidth
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vcdInfo':
|
case 'vcdInfo':
|
||||||
// 渲染迷你波形预览信息
|
// 渲染迷你波形预览信息
|
||||||
try {
|
try {
|
||||||
@ -752,40 +743,18 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'optimizeResult':
|
||||||
|
// 处理提示词优化结果
|
||||||
|
if (typeof handleOptimizeResult === 'function') {
|
||||||
|
handleOptimizeResult(message.success, message.optimizedPrompt, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听窗口大小变化,检查面板宽度
|
|
||||||
let resizeTimer;
|
|
||||||
const MIN_PANEL_WIDTH = 500;
|
|
||||||
|
|
||||||
function checkPanelWidth() {
|
|
||||||
const currentWidth = document.body.clientWidth;
|
|
||||||
if (currentWidth < MIN_PANEL_WIDTH) {
|
|
||||||
console.log('[WebView] 面板宽度不足:', currentWidth, 'px,最小要求:', MIN_PANEL_WIDTH, 'px');
|
|
||||||
vscode.postMessage({
|
|
||||||
command: 'panelWidthInsufficient',
|
|
||||||
currentWidth: currentWidth,
|
|
||||||
minWidth: MIN_PANEL_WIDTH
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
// 使用防抖,避免频繁检查
|
|
||||||
clearTimeout(resizeTimer);
|
|
||||||
resizeTimer = setTimeout(() => {
|
|
||||||
checkPanelWidth();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始加载时也检查一次
|
|
||||||
setTimeout(() => {
|
|
||||||
checkPanelWidth();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
${getMessageAreaScript()}
|
${getMessageAreaScript()}
|
||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
|
|||||||
Reference in New Issue
Block a user