@szc-ft/mcp-szcd-client 0.34.0 → 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/opencode-extension/skills/local-browser-test/SKILL.md +266 -111
- package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +612 -15
- package/opencode-extension/skills/local-browser-test/local-browser-executor.js +200 -2
- package/package.json +1 -1
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +266 -111
- package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +612 -15
- package/qwen-extension/skills/local-browser-test/local-browser-executor.js +200 -2
- package/scripts/lib/codex.js +105 -1
- package/standard-skill/local-browser-test/SKILL.md +266 -111
- package/standard-skill/local-browser-test/lib/browser-engine.js +612 -15
- package/standard-skill/local-browser-test/local-browser-executor.js +200 -2
- package/standard-skill/local-browser-test/local-browser-executor.cjs +0 -742
|
@@ -238,6 +238,24 @@ function parseArgs() {
|
|
|
238
238
|
case "--fail-on-falsy":
|
|
239
239
|
parsed.failOnFalsy = true;
|
|
240
240
|
break;
|
|
241
|
+
case "--filename":
|
|
242
|
+
parsed.filename = args[++i];
|
|
243
|
+
break;
|
|
244
|
+
case "--full-page":
|
|
245
|
+
parsed.fullPage = true;
|
|
246
|
+
break;
|
|
247
|
+
case "--output-dir":
|
|
248
|
+
parsed.outputDir = args[++i];
|
|
249
|
+
break;
|
|
250
|
+
case "--interval":
|
|
251
|
+
parsed.interval = Number.parseInt(args[++i], 10);
|
|
252
|
+
break;
|
|
253
|
+
case "--max-rounds":
|
|
254
|
+
parsed.maxRounds = Number.parseInt(args[++i], 10);
|
|
255
|
+
break;
|
|
256
|
+
case "--no-reload":
|
|
257
|
+
parsed.noReload = true;
|
|
258
|
+
break;
|
|
241
259
|
case "--help":
|
|
242
260
|
case "-h":
|
|
243
261
|
printHelp();
|
|
@@ -264,7 +282,7 @@ function printHelp() {
|
|
|
264
282
|
node local-browser-executor.js --plan-file <path> [options]
|
|
265
283
|
|
|
266
284
|
选项:
|
|
267
|
-
--action <name> 直接执行动作,当前支持 observe/act/assert/evaluate/api-capture/assert-api/run-task/test-case/plan
|
|
285
|
+
--action <name> 直接执行动作,当前支持 observe/act/assert/evaluate/api-capture/assert-api/run-task/test-case/plan/list-pages/diagnose/perf/watch
|
|
268
286
|
--plan <json> 测试计划 JSON 字符串
|
|
269
287
|
--plan-file <path> 测试计划 JSON 文件路径
|
|
270
288
|
--output <path> 结果输出文件路径(默认 stdout)
|
|
@@ -324,6 +342,12 @@ function printHelp() {
|
|
|
324
342
|
--snapshot-each run-task 每条菜单成功后做一次轻量 observe
|
|
325
343
|
--screenshot-each run-task 失败时截图(依赖 outputDir,默认关闭)
|
|
326
344
|
--no-continue-on-fail run-task 第一条失败即终止(默认遇错继续)
|
|
345
|
+
--filename <name> screenshot / diagnose 截图文件名
|
|
346
|
+
--full-page screenshot 全页截图
|
|
347
|
+
--output-dir <dir> screenshot / diagnose / watch 产物目录
|
|
348
|
+
--interval <ms> watch 监控间隔(默认 5000)
|
|
349
|
+
--max-rounds <n> watch 最大轮数(默认 60)
|
|
350
|
+
--no-reload diagnose 不刷新页面(默认刷新)
|
|
327
351
|
-h, --help 显示帮助
|
|
328
352
|
`);
|
|
329
353
|
}
|
|
@@ -494,6 +518,11 @@ async function executeAction(args) {
|
|
|
494
518
|
frameUrlContains: args.frameUrlContains,
|
|
495
519
|
frameContentContains: args.frameContentContains,
|
|
496
520
|
frameIndex: args.frameIndex,
|
|
521
|
+
drawerTriggerSelector: args.drawerTriggerSelector,
|
|
522
|
+
drawerOpenSelector: args.drawerOpenSelector,
|
|
523
|
+
menuScrollContainer: args.menuScrollContainer,
|
|
524
|
+
useNativeHover: args.useNativeHover,
|
|
525
|
+
maxRetryPerStep: args.maxRetryPerStep,
|
|
497
526
|
});
|
|
498
527
|
if (!args.observeAfter) {
|
|
499
528
|
return { type: "act", ...actResult, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
@@ -671,11 +700,180 @@ async function executeAction(args) {
|
|
|
671
700
|
|
|
672
701
|
return { type: "testCase", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
673
702
|
}
|
|
703
|
+
case "list-pages":
|
|
704
|
+
case "listPages": {
|
|
705
|
+
const pages = await engine._safeGetPages();
|
|
706
|
+
const result = [];
|
|
707
|
+
for (let i = 0; i < pages.length; i++) {
|
|
708
|
+
let title = "";
|
|
709
|
+
try { title = await pages[i].title(); } catch {}
|
|
710
|
+
result.push({ index: i, url: pages[i].url(), title });
|
|
711
|
+
}
|
|
712
|
+
return { type: "list-pages", pages: result, total: result.length, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
713
|
+
}
|
|
714
|
+
case "screenshot": {
|
|
715
|
+
const filename = args.filename || "screenshot.png";
|
|
716
|
+
const outputDir = args.outputDir || "./";
|
|
717
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
718
|
+
const filepath = path.join(outputDir, filename);
|
|
719
|
+
await engine.page.screenshot({ path: filepath, fullPage: args.fullPage });
|
|
720
|
+
return { type: "screenshot", filepath, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
721
|
+
}
|
|
722
|
+
case "diagnose": {
|
|
723
|
+
const outputDir = args.outputDir || "./";
|
|
724
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
725
|
+
|
|
726
|
+
const pageErrors = [];
|
|
727
|
+
const consoleMessages = [];
|
|
728
|
+
const failedRequests = [];
|
|
729
|
+
const apiRequests = [];
|
|
730
|
+
const resourceRequests = [];
|
|
731
|
+
|
|
732
|
+
engine.page.on("pageerror", (err) => {
|
|
733
|
+
pageErrors.push({ message: err.message, stack: err.stack?.split("\n").slice(0, 5).join("\n") });
|
|
734
|
+
});
|
|
735
|
+
engine.page.on("console", (msg) => {
|
|
736
|
+
consoleMessages.push({ type: msg.type(), text: msg.text().substring(0, 500) });
|
|
737
|
+
});
|
|
738
|
+
engine.page.on("response", (resp) => {
|
|
739
|
+
const url = resp.url();
|
|
740
|
+
const status = resp.status();
|
|
741
|
+
const type = resp.request().resourceType();
|
|
742
|
+
if (type === "xhr" || type === "fetch") {
|
|
743
|
+
apiRequests.push({ status, url, method: resp.request().method() });
|
|
744
|
+
}
|
|
745
|
+
if (type === "stylesheet" || type === "script") {
|
|
746
|
+
resourceRequests.push({ status, url, type });
|
|
747
|
+
}
|
|
748
|
+
if (status >= 400) {
|
|
749
|
+
failedRequests.push({ status, url, type });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
engine.page.on("requestfailed", (req) => {
|
|
753
|
+
failedRequests.push({ status: "ABORTED", url: req.url(), error: req.failure()?.errorText });
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
if (!args.noReload) {
|
|
757
|
+
try { await engine.page.reload({ waitUntil: "load", timeout: 30000 }); } catch {}
|
|
758
|
+
}
|
|
759
|
+
await new Promise((r) => setTimeout(r, args.wait || 5000));
|
|
760
|
+
|
|
761
|
+
const screenshotPath = path.join(outputDir, args.filename || "diagnose-screenshot.png");
|
|
762
|
+
await engine.page.screenshot({ path: screenshotPath, fullPage: false });
|
|
763
|
+
|
|
764
|
+
const frames = engine.page.frames();
|
|
765
|
+
const iframeInfo = [];
|
|
766
|
+
for (const frame of frames) {
|
|
767
|
+
const url = frame.url();
|
|
768
|
+
if (url === engine.page.url() || url === "about:blank") continue;
|
|
769
|
+
try {
|
|
770
|
+
const info = await frame.evaluate(() => ({
|
|
771
|
+
bodyText: document.body?.innerText?.substring(0, 200) || "",
|
|
772
|
+
antdElements: document.querySelectorAll("[class*='ant-']").length,
|
|
773
|
+
}));
|
|
774
|
+
iframeInfo.push({ url, ...info });
|
|
775
|
+
} catch (e) {
|
|
776
|
+
iframeInfo.push({ url, error: e.message.substring(0, 100) });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const errors = consoleMessages.filter((m) => m.type === "error");
|
|
781
|
+
const warnings = consoleMessages.filter((m) => m.type === "warning");
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
type: "diagnose",
|
|
785
|
+
url: engine.page.url(),
|
|
786
|
+
timestamp: new Date().toISOString(),
|
|
787
|
+
screenshot: screenshotPath,
|
|
788
|
+
pageErrors,
|
|
789
|
+
consoleErrors: errors,
|
|
790
|
+
consoleWarnings: warnings,
|
|
791
|
+
consoleTotal: consoleMessages.length,
|
|
792
|
+
failedRequests,
|
|
793
|
+
apiRequests,
|
|
794
|
+
resourceStatus: resourceRequests.map((r) => ({ status: r.status, type: r.type, name: r.url.split("/").pop() })),
|
|
795
|
+
iframes: iframeInfo,
|
|
796
|
+
summary: {
|
|
797
|
+
jsErrors: pageErrors.length,
|
|
798
|
+
consoleErrors: errors.length,
|
|
799
|
+
failedNetwork: failedRequests.length,
|
|
800
|
+
totalAPI: apiRequests.length,
|
|
801
|
+
failedAPI: apiRequests.filter((r) => r.status >= 400).length,
|
|
802
|
+
totalResources: resourceRequests.length,
|
|
803
|
+
failedResources: resourceRequests.filter((r) => r.status >= 400).length,
|
|
804
|
+
},
|
|
805
|
+
browserInfo: initResult,
|
|
806
|
+
startTime: startedAt,
|
|
807
|
+
endTime: new Date().toISOString(),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
case "perf": {
|
|
811
|
+
const perfMetrics = await engine.page.evaluate(() => {
|
|
812
|
+
const perf = performance.getEntriesByType("navigation")[0];
|
|
813
|
+
const paint = performance.getEntriesByType("paint");
|
|
814
|
+
const fcp = paint.find((p) => p.name === "first-contentful-paint");
|
|
815
|
+
const resources = performance.getEntriesByType("resource");
|
|
816
|
+
return {
|
|
817
|
+
dns: perf?.domainEnd - perf?.domainStart || 0,
|
|
818
|
+
tcp: perf?.connectEnd - perf?.connectStart || 0,
|
|
819
|
+
ttfb: perf?.responseStart - perf?.requestStart || 0,
|
|
820
|
+
domReady: perf?.domContentLoadedEventEnd - perf?.startTime || 0,
|
|
821
|
+
loadComplete: perf?.loadEventEnd - perf?.startTime || 0,
|
|
822
|
+
fcp: fcp?.startTime || null,
|
|
823
|
+
totalResources: resources.length,
|
|
824
|
+
totalTransferSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
|
|
825
|
+
slowResources: resources.filter((r) => r.duration > 1000).map((r) => ({
|
|
826
|
+
name: r.name.split("/").pop(), duration: Math.round(r.duration), size: r.transferSize,
|
|
827
|
+
})),
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
let webVitals = {};
|
|
832
|
+
try {
|
|
833
|
+
const client = await engine.page.target().createCDPSession();
|
|
834
|
+
await client.send("Performance.enable");
|
|
835
|
+
const metrics = await client.send("Performance.getMetrics");
|
|
836
|
+
const m = {};
|
|
837
|
+
metrics.metrics.forEach((item) => { m[item.name] = item.value; });
|
|
838
|
+
webVitals = {
|
|
839
|
+
layoutCount: m.LayoutCount || 0,
|
|
840
|
+
recalcCount: m.RecalcLayoutCount || 0,
|
|
841
|
+
jsHeapUsed: Math.round((m.JSHeapUsedSize || 0) / 1024 / 1024) + "MB",
|
|
842
|
+
jsHeapTotal: Math.round((m.JSHeapTotalSize || 0) / 1024 / 1024) + "MB",
|
|
843
|
+
taskDuration: (m.TaskDuration || 0).toFixed(2) + "s",
|
|
844
|
+
};
|
|
845
|
+
await client.send("Performance.disable");
|
|
846
|
+
await client.detach();
|
|
847
|
+
} catch {}
|
|
848
|
+
|
|
849
|
+
return { type: "perf", perfMetrics, webVitals, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
850
|
+
}
|
|
851
|
+
case "watch": {
|
|
852
|
+
const interval = args.interval || 5000;
|
|
853
|
+
const maxRounds = args.maxRounds || 60;
|
|
854
|
+
const outputDir = args.outputDir || "./";
|
|
855
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
856
|
+
|
|
857
|
+
const watchErrors = [];
|
|
858
|
+
engine.page.on("pageerror", (err) => watchErrors.push({ time: new Date().toISOString(), error: err.message }));
|
|
859
|
+
engine.page.on("console", (msg) => {
|
|
860
|
+
if (msg.type() === "error") watchErrors.push({ time: new Date().toISOString(), type: "console", text: msg.text() });
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
for (let i = 1; i <= maxRounds; i++) {
|
|
864
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
865
|
+
const fp = path.join(outputDir, `watch-${String(i).padStart(3, "0")}-${ts}.png`);
|
|
866
|
+
await engine.page.screenshot({ path: fp });
|
|
867
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return { type: "watch", rounds: maxRounds, errors: watchErrors, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
871
|
+
}
|
|
674
872
|
default:
|
|
675
873
|
return {
|
|
676
874
|
error: `未知 action: ${args.action}`,
|
|
677
875
|
errorCode: "UNKNOWN_ACTION",
|
|
678
|
-
supportedActions: ["observe", "act", "assert", "evaluate", "api-capture", "assert-api", "run-task", "test-case", "plan"],
|
|
876
|
+
supportedActions: ["observe", "act", "assert", "evaluate", "api-capture", "assert-api", "run-task", "test-case", "plan", "list-pages", "screenshot", "diagnose", "perf", "watch"],
|
|
679
877
|
startTime: startedAt,
|
|
680
878
|
endTime: new Date().toISOString(),
|
|
681
879
|
};
|
package/scripts/lib/codex.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* 1. MCP:优先 spawn `codex mcp remove` 再 `codex mcp add`(CLI 自己做 TOML 合并 + 幂等)
|
|
24
24
|
* 2. MCP fallback:找不到 `codex` 时直接写 config.toml(扁平 TOML,与 CLI 序列化一致)
|
|
25
25
|
* 3. Skill:调用 deps.copySkill(...) 把 standard-skill/* 整目录拷到 ~/.agents/skills/
|
|
26
|
+
* 4. Command:Codex 无自定义命令目录,把 commands/*.md 转换为 Open Agent Skills:
|
|
27
|
+
* ~/.agents/skills/<command-name>/SKILL.md
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
import fs from "node:fs";
|
|
@@ -201,6 +203,105 @@ function copySkillsToCodex(deps) {
|
|
|
201
203
|
return { ok: results.every(r => r.ok), results, skillsRoot };
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
// ==================== Command → Skill 转换 ====================
|
|
207
|
+
|
|
208
|
+
function getCommandsSourceDirectory(deps) {
|
|
209
|
+
return path.join(deps.PACKAGE_ROOT, "commands");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function copyCommandsAsSkillsToCodex(deps) {
|
|
213
|
+
const commandsSourceDir = getCommandsSourceDirectory(deps);
|
|
214
|
+
if (!fs.existsSync(commandsSourceDir)) {
|
|
215
|
+
console.log("⏭️ Skipping Codex command-to-skill install: commands source directory not found");
|
|
216
|
+
return { ok: true, skipped: true, results: [] };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const skillsRoot = getCodexSkillsDirectory();
|
|
220
|
+
const commandFiles = fs.readdirSync(commandsSourceDir).filter((f) => f.endsWith(".md"));
|
|
221
|
+
const results = [];
|
|
222
|
+
|
|
223
|
+
for (const commandFile of commandFiles) {
|
|
224
|
+
const sourcePath = path.join(commandsSourceDir, commandFile);
|
|
225
|
+
const commandName = path.basename(commandFile, ".md");
|
|
226
|
+
const skillDir = path.join(skillsRoot, commandName);
|
|
227
|
+
const destPath = path.join(skillDir, "SKILL.md");
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const sourceContent = fs.readFileSync(sourcePath, "utf8");
|
|
231
|
+
const skillContent = renderCommandAsSkill(commandName, sourceContent);
|
|
232
|
+
deps.ensureDirectory?.(skillDir) ?? fs.mkdirSync(skillDir, { recursive: true });
|
|
233
|
+
|
|
234
|
+
if (fs.existsSync(destPath)) {
|
|
235
|
+
const existingContent = fs.readFileSync(destPath, "utf8").trim();
|
|
236
|
+
if (existingContent !== skillContent.trim()) {
|
|
237
|
+
console.log(`⏭️ Skipping Codex command skill "${commandName}": already exists with custom content`);
|
|
238
|
+
results.push({ name: commandName, ok: false, skipped: true, path: skillDir });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.writeFileSync(destPath, skillContent);
|
|
244
|
+
console.log(`✓ Converted command "${commandName}" to Codex skill (${skillDir})`);
|
|
245
|
+
results.push({ name: commandName, ok: true, path: skillDir });
|
|
246
|
+
} catch (e) {
|
|
247
|
+
console.warn(`⚠️ Failed to convert command "${commandName}" to Codex skill: ${e.message}`);
|
|
248
|
+
results.push({ name: commandName, ok: false, error: e.message });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { ok: results.every(r => r.ok || r.skipped), results, skillsRoot };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderCommandAsSkill(commandName, sourceContent) {
|
|
256
|
+
const { metadata, body } = splitFrontmatter(sourceContent);
|
|
257
|
+
const description = metadata.description || `Codex skill converted from /${commandName}`;
|
|
258
|
+
const argumentHint = metadata["argument-hint"];
|
|
259
|
+
const lines = [
|
|
260
|
+
"---",
|
|
261
|
+
`name: ${yamlScalar(commandName)}`,
|
|
262
|
+
`description: ${yamlScalar(description)}`,
|
|
263
|
+
"---",
|
|
264
|
+
"",
|
|
265
|
+
`# /${commandName}`,
|
|
266
|
+
"",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
if (argumentHint) {
|
|
270
|
+
lines.push(`参数提示:\`${argumentHint}\``, "");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
lines.push(body.trimStart());
|
|
274
|
+
return lines.join("\n").replace(/\n*$/, "\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function splitFrontmatter(content) {
|
|
278
|
+
const match = String(content).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
279
|
+
if (!match) return { metadata: {}, body: String(content) };
|
|
280
|
+
return { metadata: parseSimpleYaml(match[1]), body: match[2] };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseSimpleYaml(yaml) {
|
|
284
|
+
const metadata = {};
|
|
285
|
+
for (const line of yaml.split(/\r?\n/)) {
|
|
286
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
287
|
+
if (!match) continue;
|
|
288
|
+
metadata[match[1]] = stripYamlScalar(match[2]);
|
|
289
|
+
}
|
|
290
|
+
return metadata;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function stripYamlScalar(value) {
|
|
294
|
+
const trimmed = String(value).trim();
|
|
295
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
296
|
+
return trimmed.slice(1, -1);
|
|
297
|
+
}
|
|
298
|
+
return trimmed;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function yamlScalar(value) {
|
|
302
|
+
return JSON.stringify(String(value));
|
|
303
|
+
}
|
|
304
|
+
|
|
204
305
|
// ==================== 主入口 ====================
|
|
205
306
|
|
|
206
307
|
export function setupCodex(deps) {
|
|
@@ -223,7 +324,10 @@ export function setupCodex(deps) {
|
|
|
223
324
|
// 3) Skill 复制(独立于 MCP 注册成败,文件操作本身基本不会失败)
|
|
224
325
|
const skillResult = copySkillsToCodex(deps);
|
|
225
326
|
|
|
226
|
-
|
|
327
|
+
// 4) Command 转 Skill(Codex 无自定义命令目录,使用 Open Agent Skills 承载入口)
|
|
328
|
+
const commandSkillResult = copyCommandsAsSkillsToCodex(deps);
|
|
329
|
+
|
|
330
|
+
return { mcp: mcpResult, skill: skillResult, commandSkill: commandSkillResult };
|
|
227
331
|
}
|
|
228
332
|
|
|
229
333
|
function registerMcpServers(ourServers) {
|