Skip to content

Commit d6f8b0b

Browse files
committed
v10.3 插件修复+改造:修复HTML缺失按钮根因+项目选择器+平台路由+蓝本分组+空项目引导+引擎连接自动扫描
1 parent 09e9a3d commit d6f8b0b

3 files changed

Lines changed: 120 additions & 95 deletions

File tree

extension/src/sidebarProvider.ts

Lines changed: 71 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,9 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
304304
}
305305

306306
private async _handleScanBlueprints(): Promise<void> {
307-
// 按项目文件夹分组扫描蓝本
308-
type BlueprintEntry = { path: string; mtime: number; appName: string; description: string; platform: string; scenarioCount: number; stepCount: number };
309-
type ProjectGroup = { projectDir: string; projectName: string; platform: string; blueprints: BlueprintEntry[] };
307+
type BpEntry = { path: string; mtime: number; appName: string; description: string; platform: string; scenarioCount: number; stepCount: number };
308+
type ProjectGroup = { projectDir: string; projectName: string; platform: string; blueprints: BpEntry[] };
310309
const projectMap = new Map<string, ProjectGroup>();
311-
312310
const folders = vscode.workspace.workspaceFolders;
313311
if (folders) {
314312
for (const folder of folders) {
@@ -339,43 +337,26 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
339337
scenarioCount = (json.pages || []).reduce((n: number, p: any) => n + (p.scenarios || []).length, 0);
340338
stepCount = (json.pages || []).reduce((n: number, p: any) => n + (p.scenarios || []).reduce((m: number, s: any) => m + (s.steps || []).length, 0), 0);
341339
} catch { /* ignore parse errors */ }
342-
343-
// 确定项目目录:蓝本文件的父目录(如果在testpilot/子目录里则取上一级)
344-
const filePath = f.fsPath.replace(/\\/g, "/");
345-
const parts = filePath.split("/");
346-
const fileName = parts.pop() || "";
347-
let dir = parts.join("/");
348-
// 如果蓝本在 testpilot/ 子目录里,项目目录取上一级
349-
if (parts.length > 0 && parts[parts.length - 1] === "testpilot") {
350-
parts.pop();
351-
dir = parts.join("/");
352-
}
353-
354-
const entry: BlueprintEntry = { path: f.fsPath, mtime, appName, description, platform, scenarioCount, stepCount };
355-
340+
// 确定项目目录(testpilot/子目录取上一级)
341+
const fPath = f.fsPath.replace(/\\/g, "/");
342+
const parts = fPath.split("/");
343+
parts.pop(); // 移除文件名
344+
if (parts.length > 0 && parts[parts.length - 1] === "testpilot") { parts.pop(); }
345+
const dir = parts.join("/");
346+
const entry: BpEntry = { path: f.fsPath, mtime, appName, description, platform, scenarioCount, stepCount };
356347
if (projectMap.has(dir)) {
357348
projectMap.get(dir)!.blueprints.push(entry);
358349
} else {
359-
// 项目名优先用蓝本的app_name,兜底用目录名
360350
const dirName = dir.split("/").pop() || dir;
361-
projectMap.set(dir, {
362-
projectDir: dir,
363-
projectName: appName || dirName,
364-
platform: platform,
365-
blueprints: [entry],
366-
});
351+
projectMap.set(dir, { projectDir: dir, projectName: appName || dirName, platform, blueprints: [entry] });
367352
}
368353
}
369354
}
370355
}
371356
}
372-
373-
// 转为数组,按蓝本总数降序排列
374357
const projects = Array.from(projectMap.values());
375358
projects.sort((a, b) => b.blueprints.length - a.blueprints.length);
376-
// 每个项目内蓝本按修改时间降序
377359
projects.forEach(p => p.blueprints.sort((a, b) => b.mtime - a.mtime));
378-
379360
this._postMessage({ command: "blueprintList", data: projects });
380361
}
381362

@@ -593,12 +574,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
593574
<select id="projectSelect" style="flex:1;font-size:12px;padding:4px 6px" disabled>
594575
<option value="">暂无测试项目</option>
595576
</select>
596-
<button id="btnRefreshProjects" class="btn-secondary" style="width:32px;min-width:32px;margin:0;padding:4px;font-size:14px" title="刷新项目列表">🔄</button>
597-
</div>
598-
<div class="btn-row">
599-
<button class="btn-secondary" id="btnCheckEngine" style="flex:1">检查连接</button>
600-
<button id="btnLaunchEngine" style="flex:1;background:#22c55e">🚀 启动引擎</button>
577+
<button id="btnRefreshProjects" class="btn-secondary" style="width:28px;min-width:28px;margin:0;padding:3px;font-size:13px" title="刷新项目列表">🔄</button>
601578
</div>
579+
<button class="btn-secondary" id="btnCheckEngine">检查连接</button>
580+
<button id="btnLaunchEngine" class="hidden" style="background:#22c55e;margin-top:6px">🚀 一键启动引擎</button>
602581
<button id="btnStopEngine" class="hidden" style="background:#ef4444;margin-top:6px">⏹ 断开引擎</button>
603582
</div>
604583
@@ -615,6 +594,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
615594
<span style="font-size:11px;color:var(--muted);margin-left:4px">(勾选要测试的蓝本)</span>
616595
</label>
617596
<div id="blueprintList" style="max-height:150px;overflow-y:auto;border:1px solid var(--border);border-radius:3px;padding:4px;font-size:11px;margin-bottom:4px"></div>
597+
<div class="btn-row" style="margin-top:4px">
598+
<button class="btn-secondary" id="btnScanBp" style="flex:1;font-size:11px;padding:4px">🔍 扫描蓝本</button>
599+
<button class="btn-secondary" id="btnBrowseBp" style="flex:1;font-size:11px;padding:4px">📂 浏览文件</button>
600+
</div>
618601
<input type="text" id="inputBlueprintPath" placeholder="或手动输入路径..." style="font-size:11px;margin-top:4px" />
619602
620603
<label>覆盖 base_url(可选)</label>
@@ -748,14 +731,14 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
748731
const blueprintListEl = document.getElementById("blueprintList");
749732
const inputBlueprintPath = document.getElementById("inputBlueprintPath");
750733
const projectSelect = document.getElementById("projectSelect");
751-
let allProjects = []; // 存储分组后的项目数据
752-
let currentProjectIdx = 0; // 当前选中项目索引
734+
let allProjects = [];
735+
let currentProjectIdx = 0;
753736
let blueprintEntries = []; // 当前项目的蓝本列表
754737
755-
// 获取当前项目的平台
738+
// 获取当前项目平台
756739
function getCurrentPlatform() {
757740
if (allProjects.length === 0) { return "web"; }
758-
return allProjects[currentProjectIdx]?.platform || "web";
741+
return allProjects[currentProjectIdx] ? allProjects[currentProjectIdx].platform : "web";
759742
}
760743
761744
// 获取所有选中的蓝本路径
@@ -772,17 +755,17 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
772755
projectSelect.addEventListener("change", () => {
773756
currentProjectIdx = projectSelect.selectedIndex;
774757
if (allProjects.length > 0 && allProjects[currentProjectIdx]) {
775-
blueprintEntries = allProjects[currentProjectIdx].blueprints;
758+
blueprintEntries = allProjects[currentProjectIdx].blueprints || [];
776759
renderBlueprintList(blueprintEntries);
777760
}
778761
});
779762
780-
// 刷新按钮
763+
// 刷新项目按钮
781764
document.getElementById("btnRefreshProjects").addEventListener("click", () => {
782765
vscode.postMessage({ command: "scanBlueprints" });
783766
});
784767
785-
// 扫描按钮(蓝本区域内的)
768+
// 扫描按钮
786769
document.getElementById("btnScanBp").addEventListener("click", () => {
787770
vscode.postMessage({ command: "scanBlueprints" });
788771
});
@@ -975,37 +958,30 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
975958
976959
function onBlueprintList(projects) {
977960
allProjects = projects || [];
978-
979961
// 更新项目下拉框
980962
projectSelect.innerHTML = "";
981963
if (allProjects.length === 0) {
982964
projectSelect.innerHTML = '<option value="">暂无测试项目</option>';
983965
projectSelect.disabled = true;
984-
blueprintListEl.innerHTML = '<div style="color:var(--muted);padding:12px;text-align:center;font-size:12px;line-height:1.6">暂无测试项目<br><br>请在项目中创建蓝本文件,<br>或点击下方「📋 复制蓝本生成提示词」<br>让编程AI自动生成。</div>';
985966
blueprintEntries = [];
967+
blueprintListEl.innerHTML = '<div style="color:var(--muted);padding:12px;text-align:center;font-size:12px;line-height:1.6">暂无测试项目<br><br>请在项目中创建蓝本文件,<br>或点击下方「📋 复制蓝本生成提示词」<br>让编程AI自动生成。</div>';
986968
return;
987969
}
988-
989-
const platformBadges = {web:"🌐",android:"📱",ios:"📱",miniprogram:"💬",desktop:"🖥️"};
990-
allProjects.forEach((proj, i) => {
991-
const opt = document.createElement("option");
992-
opt.value = i;
993-
const badge = platformBadges[proj.platform] || "📄";
994-
const bpCount = proj.blueprints ? proj.blueprints.length : 0;
970+
var platformBadges = {web:"🌐",android:"📱",ios:"📱",miniprogram:"💬",desktop:"🖥️"};
971+
allProjects.forEach(function(proj, i) {
972+
var opt = document.createElement("option");
973+
opt.value = String(i);
974+
var badge = platformBadges[proj.platform] || "📄";
975+
var bpCount = proj.blueprints ? proj.blueprints.length : 0;
995976
opt.textContent = badge + " " + proj.projectName + " (" + bpCount + ")";
996977
projectSelect.appendChild(opt);
997978
});
998-
999-
// 单项目时锁定不可选
1000979
projectSelect.disabled = allProjects.length <= 1;
1001980
currentProjectIdx = 0;
1002981
projectSelect.selectedIndex = 0;
1003-
1004-
// 渲染第一个项目的蓝本
1005982
blueprintEntries = allProjects[0].blueprints || [];
1006983
renderBlueprintList(blueprintEntries);
1007-
1008-
const totalBp = allProjects.reduce((n, p) => n + (p.blueprints ? p.blueprints.length : 0), 0);
984+
var totalBp = allProjects.reduce(function(n, p) { return n + (p.blueprints ? p.blueprints.length : 0); }, 0);
1009985
addLog("找到 " + allProjects.length + " 个项目,共 " + totalBp + " 个蓝本", "info");
1010986
}
1011987
@@ -1017,63 +993,63 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
1017993
}
1018994
1019995
// 全局蓝本选项(固定在顶部)
1020-
const globalItem = document.createElement("label");
996+
var globalItem = document.createElement("label");
1021997
globalItem.style.cssText = "display:flex;align-items:center;gap:4px;padding:4px 4px;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600;background:var(--bg-secondary,#1e1e1e);border:1px solid var(--border);margin-bottom:4px";
1022998
globalItem.title = "勾选后将测试当前项目所有蓝本";
1023999
1024-
const globalCb = document.createElement("input");
1000+
var globalCb = document.createElement("input");
10251001
globalCb.type = "checkbox";
10261002
globalCb.id = "cbGlobalBlueprint";
10271003
globalCb.checked = true;
10281004
globalCb.style.cssText = "flex-shrink:0;width:14px;height:14px";
10291005
1030-
const globalLabel = document.createElement("span");
1006+
var globalLabel = document.createElement("span");
10311007
globalLabel.textContent = "🌍 全局蓝本(测试所有)";
10321008
globalLabel.style.cssText = "flex:1;color:var(--fg)";
10331009
10341010
globalItem.appendChild(globalCb);
10351011
globalItem.appendChild(globalLabel);
10361012
blueprintListEl.appendChild(globalItem);
10371013
1038-
const divider = document.createElement("div");
1014+
var divider = document.createElement("div");
10391015
divider.style.cssText = "height:1px;background:var(--border);margin:4px 0";
10401016
blueprintListEl.appendChild(divider);
10411017
1042-
const platformBadges = {web:"🌐",android:"📱",ios:"📱",miniprogram:"💬",desktop:"🖥️"};
1018+
var platformBadges = {web:"🌐",android:"📱",ios:"📱",miniprogram:"💬",desktop:"🖥️"};
10431019
1044-
entries.forEach((entry, i) => {
1045-
const path = typeof entry === "string" ? entry : entry.path;
1046-
const appName = entry.appName || "";
1047-
const desc = entry.description || "";
1048-
const platform = entry.platform || "web";
1049-
const scenarios = entry.scenarioCount || 0;
1050-
const steps = entry.stepCount || 0;
1020+
entries.forEach(function(entry, i) {
1021+
var path = typeof entry === "string" ? entry : entry.path;
1022+
var appName = entry.appName || "";
1023+
var desc = entry.description || "";
1024+
var platform = entry.platform || "web";
1025+
var scenarios = entry.scenarioCount || 0;
1026+
var steps = entry.stepCount || 0;
10511027
1052-
const parts = path.replace(/\\\\/g, "/").split("/");
1053-
const shortPath = parts.slice(-3).join("/");
1054-
const displayName = appName || shortPath;
1028+
var parts = path.replace(/\\\\/g, "/").split("/");
1029+
var shortPath = parts.slice(-3).join("/");
1030+
var displayName = appName || shortPath;
10551031
1056-
const platformBadge = platformBadges[platform] || "📄";
1057-
const tooltipText = desc ? (desc + "\\n场景:" + scenarios + " 步骤:" + steps + "\\n" + path) : ("场景:" + scenarios + " 步骤:" + steps + "\\n" + path);
1032+
var platformBadge = platformBadges[platform] || "📄";
1033+
var tooltipText = desc ? (desc + "\\n场景:" + scenarios + " 步骤:" + steps + "\\n" + path) : ("场景:" + scenarios + " 步骤:" + steps + "\\n" + path);
10581034
1059-
const item = document.createElement("label");
1035+
var item = document.createElement("label");
10601036
item.className = "local-blueprint-item";
10611037
item.style.cssText = "display:flex;align-items:flex-start;gap:4px;padding:3px 4px;border-radius:4px;cursor:pointer;font-size:12px;line-height:1.4;overflow:hidden;width:100%;box-sizing:border-box";
10621038
item.title = tooltipText;
1063-
item.addEventListener("mouseenter", () => { item.style.background = "var(--hover-bg,#2a2d2e)"; });
1064-
item.addEventListener("mouseleave", () => { item.style.background = "transparent"; });
1039+
item.addEventListener("mouseenter", function() { item.style.background = "var(--hover-bg,#2a2d2e)"; });
1040+
item.addEventListener("mouseleave", function() { item.style.background = "transparent"; });
10651041
1066-
const cb = document.createElement("input");
1042+
var cb = document.createElement("input");
10671043
cb.type = "checkbox";
10681044
cb.className = "local-blueprint-cb";
10691045
cb.value = path;
10701046
cb.checked = false;
10711047
cb.disabled = true;
10721048
cb.style.cssText = "margin-top:2px;flex-shrink:0;width:14px;height:14px";
10731049
1074-
const fileName = path.replace(/\\\\/g, "/").split("/").pop() || path;
1050+
var fileName = path.replace(/\\\\/g, "/").split("/").pop() || path;
10751051
1076-
const info = document.createElement("div");
1052+
var info = document.createElement("div");
10771053
info.style.cssText = "flex:1;min-width:0;overflow:hidden";
10781054
info.innerHTML = '<div style="font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + platformBadge + ' ' + displayName + '</div>'
10791055
+ (desc ? '<div style="color:var(--muted);font-size:11px;margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + desc + '</div>' : '')
@@ -1085,31 +1061,32 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
10851061
});
10861062
10871063
// 全局蓝本checkbox交互逻辑
1088-
globalCb.addEventListener("change", () => {
1089-
const localItems = blueprintListEl.querySelectorAll(".local-blueprint-item");
1090-
const localCbs = blueprintListEl.querySelectorAll(".local-blueprint-cb");
1064+
globalCb.addEventListener("change", function() {
1065+
var localItems = blueprintListEl.querySelectorAll(".local-blueprint-item");
1066+
var localCbs = blueprintListEl.querySelectorAll(".local-blueprint-cb");
10911067
if (globalCb.checked) {
1092-
localCbs.forEach(cb => { cb.disabled = true; cb.checked = false; });
1093-
localItems.forEach(item => {
1068+
localCbs.forEach(function(cb) { cb.disabled = true; cb.checked = false; });
1069+
localItems.forEach(function(item) {
10941070
item.style.opacity = "0.5";
10951071
item.title = "已勾选全局蓝本,无需勾选局部。如需局部测试,请取消全局勾选。";
10961072
});
10971073
} else {
1098-
localCbs.forEach(cb => { cb.disabled = false; });
1099-
localItems.forEach((item, i) => {
1074+
localCbs.forEach(function(cb) { cb.disabled = false; });
1075+
localItems.forEach(function(item, i) {
11001076
item.style.opacity = "1";
1101-
const entry = entries[i];
1102-
const desc = entry.description || "";
1103-
const scenarios = entry.scenarioCount || 0;
1104-
const steps = entry.stepCount || 0;
1105-
const path = entry.path;
1077+
var entry = entries[i];
1078+
if (!entry) return;
1079+
var desc = entry.description || "";
1080+
var scenarios = entry.scenarioCount || 0;
1081+
var steps = entry.stepCount || 0;
1082+
var path = entry.path;
11061083
item.title = desc ? (desc + "\\n场景:" + scenarios + " 步骤:" + steps + "\\n" + path) : ("场景:" + scenarios + " 步骤:" + steps + "\\n" + path);
11071084
});
11081085
}
11091086
});
11101087
11111088
// 填入第一个路径到手动输入框
1112-
const firstPath = typeof entries[0] === "string" ? entries[0] : entries[0].path;
1089+
var firstPath = typeof entries[0] === "string" ? entries[0] : entries[0].path;
11131090
inputBlueprintPath.value = firstPath;
11141091
}
11151092
@@ -1142,7 +1119,6 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
11421119
statusDot.className = "status-dot connected";
11431120
engineStatus.textContent = "v" + (data.version || "?");
11441121
btnLaunchEngine.classList.add("hidden");
1145-
document.getElementById("btnCheckEngine").classList.add("hidden");
11461122
btnStopEngine.classList.remove("hidden");
11471123
btnStopEngine.textContent = "⏹ 断开引擎";
11481124
btnStopEngine.disabled = false;
@@ -1153,11 +1129,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
11531129
statusDot.className = "status-dot disconnected";
11541130
engineStatus.textContent = isStarting ? "启动中..." : "未连接";
11551131
if (!isStarting) {
1132+
// 只有不在启动中时才恢复启动按钮,避免闪烁
11561133
btnLaunchEngine.classList.remove("hidden");
1157-
document.getElementById("btnCheckEngine").classList.remove("hidden");
11581134
btnStopEngine.classList.add("hidden");
11591135
}
1160-
addLog(isStarting ? "引擎尚未就绪,继续等待..." : "引擎未连接", isStarting ? "warn" : "error");
1136+
addLog(isStarting ? "引擎尚未就绪,继续等待..." : "引擎未连接,点击「一键启动引擎」按钮启动", isStarting ? "warn" : "error");
11611137
}
11621138
}
11631139

开发备忘录.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,3 +851,20 @@ MCP配置文件位置:`~/.codeium/windsurf/mcp_config.json`(即 `C:\Users\
851851

852852
实现6步:扫描重构→下拉框UI→列表联动→平台路由→小程序启动→空项目引导
853853

854+
**v10.3 实施记录(2026-03-07)**
855+
856+
**根因发现**:插件UI从v10.2起就有隐藏Bug——HTML中缺少`btnScanBp``btnBrowseBp`两个按钮元素,但JS代码引用了它们。`getElementById("btnScanBp").addEventListener(...)` 抛null异常后,**后续所有JS代码不执行**(包括引擎检查、蓝本扫描、按钮显示逻辑)。用户一直没重装vsix所以没发现。
857+
858+
**修复+改造(最小改动策略)**
859+
1. ✅ 修复根因:HTML补上`btnScanBp`+`btnBrowseBp`按钮
860+
2. ✅ 后端扫描重构:`_handleScanBlueprints`按项目文件夹分组返回`{projectDir, projectName, platform, blueprints[]}`
861+
3. ✅ 项目下拉框UI:引擎状态下方新增`<select>`+🔄刷新按钮
862+
4. ✅ 蓝本列表联动:`onBlueprintList`处理分组数据,`renderBlueprintList`独立函数,切换项目自动过滤
863+
5. ✅ 平台自动路由:蓝本测试按钮传递`platform`参数(web/miniprogram/android/desktop)
864+
6. ✅ 引擎连接自动扫描:连接成功时自动`postMessage({ command: "scanBlueprints" })`
865+
7. ✅ 空项目引导:无蓝本时显示引导文字
866+
867+
**经验教训**
868+
- 插件HTML里的DOM元素必须和JS的`getElementById`严格对应,缺一个就导致后续全崩
869+
- 改造插件时要用最小改动策略,不要大规模重写
870+
- 回滚只影响`sidebarProvider.ts`一个文件,核心引擎和测试成果完全不受影响

0 commit comments

Comments
 (0)