sillyspec 3.7.3 → 3.7.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "type": "module",
6
6
  "bin": {
package/src/setup.js CHANGED
@@ -1,10 +1,22 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { execSync } from 'child_process';
4
4
  import chalk from 'chalk';
5
5
  import ora from 'ora';
6
6
  import { checkbox, confirm, input, select } from '@inquirer/prompts';
7
7
 
8
+ // ── Skill 定义 ──
9
+
10
+ const SKILLS = [
11
+ {
12
+ id: 'playwright-e2e',
13
+ name: 'Playwright E2E 测试参考',
14
+ description: 'E2E 测试编写最佳实践,AI 执行测试任务时自动读取',
15
+ source: join(__dirname, '..', 'templates', 'skills', 'playwright-e2e'),
16
+ target: '.sillyspec/skills/playwright-e2e',
17
+ },
18
+ ];
19
+
8
20
  // ── MCP 工具定义 ──
9
21
 
10
22
  const MCP_TOOLS = [
@@ -266,6 +278,12 @@ export async function cmdSetup(dir, options = {}) {
266
278
  ...dbChoices,
267
279
  ...globalChoices.length > 0 ? [{ name: chalk.bold('── 全局工具 ──'), value: '_global_header', disabled: true }] : [],
268
280
  ...globalChoices,
281
+ ...[{ name: chalk.bold('── AI Skills(编写参考)──'), value: '_skill_header', disabled: true }],
282
+ ...SKILLS.filter(s => !existsSync(join(dir, s.target, 'SKILL.md'))).map(s => ({
283
+ name: `${s.name} — ${s.description}`,
284
+ value: `skill:${s.id}`,
285
+ checked: false,
286
+ })),
269
287
  ];
270
288
 
271
289
  if (allChoices.length === 0) {
@@ -355,6 +373,24 @@ export async function cmdSetup(dir, options = {}) {
355
373
  }
356
374
  }
357
375
 
376
+ // ── 安装 Skills ──
377
+
378
+ const selectedSkills = SKILLS.filter(s => selected.includes(`skill:${s.id}`));
379
+
380
+ if (selectedSkills.length > 0) {
381
+ console.log('');
382
+ for (const skill of selectedSkills) {
383
+ const spinner = ora(`安装 ${skill.name}...`).start();
384
+ try {
385
+ const targetDir = join(dir, skill.target);
386
+ cpSync(skill.source, targetDir, { recursive: true });
387
+ spinner.succeed(`${skill.name} 安装完成 → ${skill.target}/SKILL.md`);
388
+ } catch (err) {
389
+ spinner.fail(`${skill.name} 安装失败: ${err.message}`);
390
+ }
391
+ }
392
+ }
393
+
358
394
  // ── 总结 ──
359
395
 
360
396
  console.log('');
@@ -374,6 +410,10 @@ export async function cmdSetup(dir, options = {}) {
374
410
  console.log(` 🛠️ ${chalk.cyan(g.name)} — ${g.description}`);
375
411
  console.log(chalk.dim(` ${g.url}`));
376
412
  }
413
+ for (const s of selectedSkills) {
414
+ console.log(` 📚 ${chalk.cyan(s.name)} — ${s.description}`);
415
+ console.log(chalk.dim(` → ${s.target}/SKILL.md`));
416
+ }
377
417
  console.log('');
378
418
  if (selectedMcp.length > 0) {
379
419
  console.log(chalk.dim(' 重启你的 AI 工具以使 MCP 配置生效。'));
@@ -96,12 +96,55 @@ cat package.json 2>/dev/null | grep -A5 '"lint\|"format\|"typecheck\|"type-check
96
96
  将此摘要注入到每个子代理 prompt 的「项目约定」段之后,并追加一条铁律:
97
97
  > **10. 遵守编码规范:** 以上「编码规范约束」中的所有规则必须严格遵守。如规则与任务描述冲突,优先遵守规范约束并报告。
98
98
 
99
- **MCP 能力检测(主代理执行):**
99
+ **测试模式扫描(主代理执行):**
100
+ 对包含 E2E/测试任务时,扫描项目已有的测试文件,提取测试风格注入子代理 prompt 的「测试模式参考」段。
101
+
100
102
  ```bash
101
- for cfg in .claude/mcp.json .cursor/mcp.json; do
102
- [ -f "$cfg" ] && echo "=== $cfg ===" && cat "$cfg"
103
- done
103
+ # 检测测试框架
104
+ cat package.json 2>/dev/null | grep -E "playwright|cypress|jest|vitest|mocha"
105
+
106
+ # 查找已有测试文件
107
+ find . -name "*.spec.ts" -o -name "*.test.ts" -o -name "*.spec.tsx" -o -name "*.spec.js" \
108
+ -o -name "playwright.config.*" -o -name "vitest.config.*" -o -name "jest.config.*" \
109
+ 2>/dev/null | grep -v node_modules | head -10
110
+
111
+ # 读取 1-2 个已有测试文件作为风格参考
112
+ # 优先读 E2E 测试,其次读通用测试
113
+ find tests/e2e e2e cypress/e2e __tests__ src -name "*.spec.ts" -o -name "*.test.ts" \
114
+ 2>/dev/null | grep -v node_modules | head -3 | xargs cat 2>/dev/null
115
+ ```
116
+
117
+ 扫描后生成**测试模式摘要**:
118
+
119
+ ```
120
+ ## 测试模式参考(自动扫描)
121
+
122
+ ### 测试框架
123
+ {检测到的框架及版本,如:Playwright 1.42、Vitest 1.2}
124
+
125
+ ### 断言风格
126
+ {从已有测试提取的断言模式,如:expect(page).toHaveText()、toEqual、toBeTruthy 等}
127
+
128
+ ### Fixtures / Helper
129
+ {项目自定义的 test fixtures、page objects、helper 函数}
130
+
131
+ ### 文件组织
132
+ {测试文件目录结构、命名约定、文件内组织方式}
133
+
134
+ ### 配置要点
135
+ {playwright.config.ts 中的 baseURL、timeout、retries 等关键配置}
104
136
  ```
137
+
138
+ 将此摘要注入到每个 E2E/测试子代理 prompt 的「任务描述」段之后,并追加一条铁律:
139
+ > **11. 参照已有测试风格:** 编写新测试时必须参照以上「测试模式参考」中的风格,包括断言方式、fixtures 使用、文件组织。不要凭记忆写测试。
140
+ > **12. 参考已有实现:** 写新功能前,先 grep 项目中类似功能的已有代码(`grep -r "关键词" src/`),照着项目现有的模式、风格和封装方式写,不要凭记忆编造。
141
+
142
+ **MCP 能力检测(主代理执行):**
143
+ 检查当前可用工具列表中是否存在以下类型的 MCP 工具(不要只依赖配置文件路径,不同客户端配置位置不同):
144
+ - Context7 / 文档查询工具
145
+ - 数据库工具(postgres/sqlite/mysql/redis)
146
+ - 浏览器工具(browser/chrome/playwright/devtools)
147
+ - 搜索工具(search/web_search)
105
148
  根据检测结果,在子代理 prompt 的「文档查询指引」段动态注入:
106
149
  - 有 `context7` → `遇到不熟悉的库/API,使用 Context7 MCP(resolve-library-id → query-docs)查询最新文档`
107
150
  - 无 `context7` → `遇到不熟悉的库/API,使用 web search 查询最新官方文档`
@@ -148,6 +191,9 @@ done
148
191
  ## 编码规范约束(自动扫描)
149
192
  {主代理扫描项目配置文件后生成的规范摘要,见上方「编码规范扫描」步骤}
150
193
 
194
+ ## 测试模式参考(自动扫描,仅 E2E/测试任务注入)
195
+ {主代理扫描项目已有测试文件后生成的测试风格摘要,见上方「测试模式扫描」步骤。非 E2E/测试任务省略此段}
196
+
151
197
  ## 项目架构
152
198
  {ARCHITECTURE.md 全文}
153
199
 
@@ -174,7 +220,12 @@ done
174
220
  4. **不自行补全:** 发现缺失接口/方法,不自己写,报告 BLOCKED
175
221
  5. **TDD 不跳步:** 按任务步骤逐步执行,每步必须运行测试命令并确认结果
176
222
  6. **测试直接通过 = 测了已有行为,重写测试**
177
- 7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端",先 cat 相关功能代码和页面组件,理解交互逻辑。有测试框架则编写测试文件,无框架则编写 `.sillyspec/changes/<变更名>/e2e-steps.md` 结构化测试步骤(每条包含操作和断言)
223
+ 7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端"
224
+ - 先 cat 相关功能代码和页面组件,理解交互逻辑
225
+ - 参考 prompt 中「测试模式参考」段的已有测试风格
226
+ - **查阅 Playwright 用法:** 优先使用已安装的 playwright skill(SKILL.md),不要凭记忆写 API。未安装则通过 Context7 MCP 或 web search 查最新文档
227
+ - 有测试框架则编写测试文件,无框架则编写 `.sillyspec/changes/<变更名>/e2e-steps.md` 结构化测试步骤
228
+ - **写完必须立即跑一遍确认通过**,失败则修复后重跑,不要"写了就算完成"
178
229
  8. **暂存:** 完成后在工作目录执行 git add -A(不要 commit,由用户通过 /sillyspec:commit 统一提交)
179
230
  9. **不修改计划外的文件**,如必须修改则在报告中说明
180
231
  10. **遵守编码规范:** prompt 中「编码规范约束」段的所有规则必须严格遵守。如规范与任务描述冲突,优先遵守规范并报告
@@ -37,9 +37,7 @@ cat .sillyspec/knowledge/INDEX.md 2>/dev/null
37
37
 
38
38
  ## MCP 能力(按需使用)
39
39
 
40
- ```bash
41
- cat .claude/mcp.json .cursor/mcp.json 2>/dev/null
42
- ```
40
+ 检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),不依赖配置文件路径。
43
41
 
44
42
  - 有 Context7 → 探索时查询最新文档,验证技术方案的可行性
45
43
  - 有浏览器 MCP → 可浏览相关网站、查竞品实现
@@ -27,7 +27,7 @@ $ARGUMENTS
27
27
  cat .sillyspec/knowledge/INDEX.md 2>/dev/null
28
28
  ```
29
29
  根据当前任务描述中的关键词匹配 INDEX.md 条目,命中时 `cat` 对应知识文件,将内容纳入后续开发考量。未命中则跳过。
30
- **MCP 检测:** `cat .claude/mcp.json .cursor/mcp.json 2>/dev/null`,根据检测结果动态利用:
30
+ **MCP 检测:** 检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),根据检测结果动态利用:
31
31
  - 有 Context7 → 查询不熟悉库/API 的最新文档
32
32
  - 有浏览器 MCP → 验证页面改动效果
33
33
  - 有数据库 MCP → 查询表结构和数据验证(只读)
@@ -0,0 +1,340 @@
1
+ ---
2
+ name: playwright-e2e
3
+ description: Playwright E2E 测试编写参考。AI 执行 E2E 任务时自动读取,不要凭记忆编写 Playwright API。
4
+ ---
5
+
6
+ # Playwright E2E 测试编写参考
7
+
8
+ > ⚠️ 执行 E2E/测试任务时必须先读取此文件,不要凭训练数据记忆编写 Playwright API。
9
+
10
+ ## 测试基本结构
11
+
12
+ ```typescript
13
+ import { test, expect } from '@playwright/test';
14
+
15
+ test.describe('功能名称', () => {
16
+ test.beforeEach(async ({ page }) => {
17
+ await page.goto('/');
18
+ });
19
+
20
+ test('具体场景', async ({ page }) => {
21
+ // Arrange
22
+ // Act
23
+ // Assert
24
+ });
25
+ });
26
+ ```
27
+
28
+ ## 选择器优先级
29
+
30
+ ```typescript
31
+ // ✅ 最推荐:data-testid(最稳定)
32
+ await page.locator('[data-testid="submit-btn"]').click();
33
+
34
+ // ✅ 推荐:role-based(语义化)
35
+ await page.getByRole('button', { name: 'Submit' }).click();
36
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
37
+
38
+ // ✅ 推荐:文本匹配
39
+ await page.getByText('Sign in').click();
40
+ await page.getByText(/welcome back/i).click();
41
+
42
+ // ✅ 可用:语义 HTML
43
+ await page.locator('button[type="submit"]').click();
44
+ await page.locator('input[name="email"]').fill('test@test.com');
45
+
46
+ // ❌ 避免:CSS 类名和 ID(重构易变)
47
+ // await page.locator('.btn-primary').click();
48
+ // await page.locator('#submit').click();
49
+ ```
50
+
51
+ ## 断言
52
+
53
+ ```typescript
54
+ // URL
55
+ await expect(page).toHaveURL('/dashboard');
56
+ await expect(page).toHaveURL(/.*dashboard/);
57
+
58
+ // 文本
59
+ await expect(page.locator('h1')).toHaveText('Welcome');
60
+ await expect(page.locator('.message')).toContainText('success');
61
+ await expect(page.locator('.items')).toHaveText(['Item 1', 'Item 2']);
62
+
63
+ // 可见性
64
+ await expect(page.locator('.modal')).toBeVisible();
65
+ await expect(page.locator('.spinner')).toBeHidden();
66
+ await expect(page.locator('button')).toBeEnabled();
67
+ await expect(page.locator('input')).toBeDisabled();
68
+
69
+ // 数量
70
+ await expect(page.locator('.item')).toHaveCount(3);
71
+
72
+ // 输入值
73
+ await expect(page.locator('input')).toHaveValue('test@example.com');
74
+ ```
75
+
76
+ ## 表单操作
77
+
78
+ ```typescript
79
+ // 文本输入
80
+ await page.getByLabel('Email').fill('user@example.com');
81
+ await page.getByPlaceholder('Enter your name').fill('John Doe');
82
+
83
+ // 清空再输入
84
+ await page.locator('#username').clear();
85
+ await page.locator('#username').type('newuser', { delay: 100 });
86
+
87
+ // 复选框
88
+ await page.getByLabel('I agree').check();
89
+ await page.getByLabel('I agree').uncheck();
90
+
91
+ // 单选按钮
92
+ await page.getByLabel('Option 2').check();
93
+
94
+ // 下拉选择
95
+ await page.selectOption('select#country', 'usa');
96
+ await page.selectOption('select#country', { label: 'United States' });
97
+
98
+ // 多选下拉
99
+ await page.selectOption('select#colors', ['red', 'blue']);
100
+
101
+ // 文件上传
102
+ await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
103
+ await page.setInputFiles('input[type="file"]', ['file1.pdf', 'file2.pdf']);
104
+ ```
105
+
106
+ ## 鼠标与键盘
107
+
108
+ ```typescript
109
+ // 点击
110
+ await page.click('button');
111
+ await page.click('button', { button: 'right' }); // 右键
112
+ await page.dblclick('button'); // 双击
113
+
114
+ // 悬停
115
+ await page.hover('.menu-item');
116
+
117
+ // 拖拽
118
+ await page.dragAndDrop('#source', '#target');
119
+
120
+ // 键盘
121
+ await page.keyboard.type('Hello', { delay: 100 });
122
+ await page.keyboard.press('Control+A');
123
+ await page.keyboard.press('Enter');
124
+ await page.keyboard.press('Tab');
125
+ ```
126
+
127
+ ## 测试数据准备
128
+
129
+ ```typescript
130
+ // ⚠️ E2E 测试不能因为"没有数据"就跳过!必须确保测试数据存在。
131
+
132
+ // 方案 1:通过 API 创建前置数据(推荐)
133
+ test.beforeEach(async ({ request }) => {
134
+ // 调用项目 API 创建测试所需数据
135
+ const resp = await request.post('/api/test/setup', {
136
+ data: { createUser: true, createOrder: true }
137
+ });
138
+ expect(resp.ok()).toBeTruthy();
139
+ });
140
+
141
+ // 方案 2:使用 storageState 复用认证状态
142
+ // 先登录一次保存状态,后续测试直接复用
143
+ test.use({ storageState: 'tests/auth/user.json' });
144
+
145
+ // 生成认证状态的脚本:
146
+ // npx playwright codegen --save-storage=tests/auth/user.json http://localhost:3000/login
147
+
148
+ // 方案 3:通过数据库 MCP 直接插入数据(有数据库 MCP 时)
149
+ // 在 beforeEach 中调用数据库 MCP 执行 INSERT 语句准备数据
150
+
151
+ // 方案 4:API Mock(不需要真实数据)
152
+ test('显示用户列表', async ({ page }) => {
153
+ await page.route('**/api/users', route => {
154
+ route.fulfill({
155
+ status: 200,
156
+ contentType: 'application/json',
157
+ body: JSON.stringify([{ id: 1, name: 'Test User' }])
158
+ });
159
+ });
160
+ await page.goto('/users');
161
+ await expect(page.locator('.user-name')).toHaveText('Test User');
162
+ });
163
+
164
+ // 清理:测试后清理数据,避免影响其他测试
165
+ test.afterEach(async ({ request }) => {
166
+ await request.post('/api/test/cleanup');
167
+ });
168
+ ```
169
+
170
+ **铁律:E2E 测试禁止因"没有数据"跳过。** 必须通过以上任一方案准备数据,确保测试可独立运行。
171
+
172
+ ## 弹窗与 iframe
173
+
174
+ ```typescript
175
+ // 新窗口/弹窗
176
+ const [popup] = await Promise.all([
177
+ page.waitForEvent('popup'),
178
+ page.click('button.open-popup'),
179
+ ]);
180
+ await popup.waitForLoadState();
181
+
182
+ // iframe
183
+ const frame = page.frameLocator('#my-iframe');
184
+ await frame.locator('button').click();
185
+ ```
186
+
187
+ ## 截图
188
+
189
+ ```typescript
190
+ // 全页截图
191
+ await page.screenshot({ path: 'screenshot.png', fullPage: true });
192
+
193
+ // 元素截图
194
+ await page.locator('.chart').screenshot({ path: 'chart.png' });
195
+
196
+ // 视觉回归对比(首次生成基线,后续对比)
197
+ await expect(page).toHaveScreenshot('homepage.png');
198
+
199
+ // ⚠️ playwright.config.ts 中配置失败自动截图:
200
+ // screenshot: 'only-on-failure'(已包含在上方 config 模板中)
201
+ ```
202
+
203
+ ## 等待策略
204
+
205
+ ```typescript
206
+ // ✅ 推荐:自动等待(Playwright 内置)
207
+ await expect(page.locator('.loaded')).toBeVisible();
208
+
209
+ // ✅ 推荐:等待 URL 变化
210
+ await page.waitForURL('**/dashboard');
211
+
212
+ // ✅ 推荐:等待网络响应
213
+ const responsePromise = page.waitForResponse('**/api/users');
214
+ await page.click('button#load-users');
215
+ const response = await responsePromise;
216
+
217
+ // ❌ 避免:硬编码 sleep
218
+ // await page.waitForTimeout(3000);
219
+ ```
220
+
221
+ ## Page Object Model
222
+
223
+ ```typescript
224
+ // pages/LoginPage.ts
225
+ export class LoginPage {
226
+ constructor(private page: Page) {}
227
+
228
+ async goto() {
229
+ await this.page.goto('/login');
230
+ }
231
+
232
+ async login(email: string, password: string) {
233
+ await this.page.getByLabel('Email').fill(email);
234
+ await this.page.getByLabel('Password').fill(password);
235
+ await this.page.getByRole('button', { name: 'Submit' }).click();
236
+ }
237
+ }
238
+
239
+ // tests/login.spec.ts
240
+ import { test, expect } from '@playwright/test';
241
+ import { LoginPage } from '../pages/LoginPage';
242
+
243
+ test('登录成功', async ({ page }) => {
244
+ const loginPage = new LoginPage(page);
245
+ await loginPage.goto();
246
+ await loginPage.login('test@example.com', 'password123');
247
+ await expect(page).toHaveURL('/dashboard');
248
+ });
249
+ ```
250
+
251
+ ## API Mock(隔离后端)
252
+
253
+ ```typescript
254
+ test('显示用户列表', async ({ page }) => {
255
+ await page.route('**/api/users', route => {
256
+ route.fulfill({
257
+ status: 200,
258
+ contentType: 'application/json',
259
+ body: JSON.stringify([{ id: 1, name: 'Test User' }])
260
+ });
261
+ });
262
+
263
+ await page.goto('/users');
264
+ await expect(page.locator('.user-name')).toHaveText('Test User');
265
+ });
266
+ ```
267
+
268
+ ## 常见错误
269
+
270
+ ### ❌ 用 CSS 类名选择元素
271
+ ```typescript
272
+ await page.click('.btn-primary'); // 脆弱!重构就坏
273
+ ```
274
+
275
+ ### ✅ 用 data-testid
276
+ ```typescript
277
+ await page.click('[data-testid="submit-btn"]'); // 稳定
278
+ ```
279
+
280
+ ### ❌ 测试之间有依赖
281
+ ```typescript
282
+ test('步骤1:登录', async ({ page }) => { /* ... */ });
283
+ test('步骤2:需要先登录', async ({ page }) => { /* 依赖步骤1,脆弱 */ });
284
+ ```
285
+
286
+ ### ✅ 每个测试独立
287
+ ```typescript
288
+ test.beforeEach(async ({ page }) => {
289
+ await page.goto('/login');
290
+ await page.getByLabel('Email').fill('test@example.com');
291
+ await page.getByLabel('Password').fill('password');
292
+ await page.getByRole('button', { name: 'Submit' }).click();
293
+ await expect(page).toHaveURL('/dashboard');
294
+ });
295
+ ```
296
+
297
+ ### ❌ 不处理异步状态
298
+ ```typescript
299
+ await page.click('[data-testid="submit"]');
300
+ await expect(page.locator('.result')).toBeVisible(); // 可能还没加载完
301
+ ```
302
+
303
+ ### ✅ 等待操作完成
304
+ ```typescript
305
+ await page.click('[data-testid="submit"]');
306
+ await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });
307
+ ```
308
+
309
+ ## playwright.config.ts 模板
310
+
311
+ ```typescript
312
+ import { defineConfig } from '@playwright/test';
313
+
314
+ export default defineConfig({
315
+ testDir: './tests/e2e',
316
+ fullyParallel: true,
317
+ retries: process.env.CI ? 2 : 0,
318
+ timeout: 30000,
319
+ use: {
320
+ baseURL: 'http://localhost:3000', // ⚠️ 根据项目实际端口调整
321
+ trace: 'on-first-retry',
322
+ screenshot: 'only-on-failure',
323
+ },
324
+ webServer: {
325
+ command: 'npm run dev', // ⚠️ 根据项目实际命令调整
326
+ port: 3000,
327
+ reuseExistingServer: !process.env.CI,
328
+ },
329
+ });
330
+ ```
331
+
332
+ ## 调试命令
333
+
334
+ ```bash
335
+ npx playwright test --headed # 有头模式
336
+ npx playwright test --debug # 调试模式(带 inspector)
337
+ npx playwright test --slowmo=1000 # 慢放
338
+ npx playwright show-report # 查看报告
339
+ npx playwright codegen URL # 录制生成代码
340
+ ```
@@ -167,9 +167,14 @@ grep -r "TODO\|FIXME\|HACK\|XXX" src/ lib/ app/ --include="*.ts" --include="*.ts
167
167
  检测已配置的 MCP 服务,利用它们做实际验证(不只查文档):
168
168
 
169
169
  **MCP 能力检测:**
170
- ```bash
171
- cat .claude/mcp.json .cursor/mcp.json 2>/dev/null
172
- ```
170
+
171
+ 不要只检查配置文件路径(不同客户端配置位置不同),直接检查当前可用工具列表中是否存在以下工具:
172
+
173
+ - 数据库相关工具(包含 postgres/sqlite/mysql/redis 关键词)
174
+ - 浏览器相关工具(包含 browser/chrome/puppeteer/playwright/devtools 关键词)
175
+ - 搜索相关工具(包含 search/web_search 关键词)
176
+
177
+ **判断方式:** 尝试调用或列出当前可用的 MCP 工具,有就用来验证,没有就跳过。
173
178
 
174
179
  **按检测结果执行对应验证:**
175
180