validpilot-oss 1.1.0
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/CHANGELOG.md +111 -0
- package/README.md +196 -0
- package/bin/validpilot.js +173 -0
- package/brain/error_aggregator.js +203 -0
- package/core/artifacts.js +44 -0
- package/core/config.js +37 -0
- package/core/redaction.js +39 -0
- package/core/report.js +42 -0
- package/core/result.js +29 -0
- package/core/security.js +57 -0
- package/engines/chrome_mcp_adapter.js +319 -0
- package/engines/playwright_adapter.js +421 -0
- package/examples/demo/README.md +58 -0
- package/examples/demo/diagnostic-error-flow.json +22 -0
- package/examples/demo/diagnostic-error.html +29 -0
- package/examples/demo/flow.json +27 -0
- package/examples/demo/index.html +29 -0
- package/hands/browser_operator.js +67 -0
- package/hands/evidence_collector.js +97 -0
- package/package.json +55 -0
- package/rules/suggested-rules.json +237 -0
- package/server.js +5376 -0
- package/standalone-start.js +43 -0
- package/start-http.js +45 -0
- package/tools/ai_debug_investigate.json +30 -0
- package/tools/benchmark_run.json +37 -0
- package/tools/browser_a11y_check.json +21 -0
- package/tools/browser_artifacts.json +8 -0
- package/tools/browser_artifacts_clear.json +11 -0
- package/tools/browser_assert.json +16 -0
- package/tools/browser_batch.json +61 -0
- package/tools/browser_click.json +11 -0
- package/tools/browser_console.json +26 -0
- package/tools/browser_cookies.json +38 -0
- package/tools/browser_debug_report.json +11 -0
- package/tools/browser_diagnose.json +23 -0
- package/tools/browser_dom.json +11 -0
- package/tools/browser_element_status.json +26 -0
- package/tools/browser_errors.json +17 -0
- package/tools/browser_errors_aggregate.json +12 -0
- package/tools/browser_errors_clear.json +8 -0
- package/tools/browser_eval.json +11 -0
- package/tools/browser_events.json +15 -0
- package/tools/browser_events_clear.json +8 -0
- package/tools/browser_find_element.json +30 -0
- package/tools/browser_find_page.json +22 -0
- package/tools/browser_flow.json +38 -0
- package/tools/browser_har_export.json +17 -0
- package/tools/browser_highlight.json +18 -0
- package/tools/browser_hover.json +14 -0
- package/tools/browser_instrument.json +10 -0
- package/tools/browser_links.json +21 -0
- package/tools/browser_locator_suggest.json +16 -0
- package/tools/browser_locator_validate.json +12 -0
- package/tools/browser_network.json +16 -0
- package/tools/browser_network_detail.json +17 -0
- package/tools/browser_open.json +12 -0
- package/tools/browser_performance_check.json +25 -0
- package/tools/browser_press_key.json +18 -0
- package/tools/browser_quick_fix.json +29 -0
- package/tools/browser_screenshot.json +15 -0
- package/tools/browser_scroll.json +31 -0
- package/tools/browser_select.json +26 -0
- package/tools/browser_session_close.json +12 -0
- package/tools/browser_session_create.json +17 -0
- package/tools/browser_session_switch.json +12 -0
- package/tools/browser_sessions.json +8 -0
- package/tools/browser_snapshot.json +8 -0
- package/tools/browser_step.json +18 -0
- package/tools/browser_storage.json +10 -0
- package/tools/browser_trace_start.json +14 -0
- package/tools/browser_trace_stop.json +10 -0
- package/tools/browser_traverse_menu.json +25 -0
- package/tools/browser_type.json +12 -0
- package/tools/browser_verify_fix.json +39 -0
- package/tools/browser_visual_baseline.json +19 -0
- package/tools/browser_visual_compare.json +20 -0
- package/tools/browser_visual_report.json +8 -0
- package/tools/browser_wait.json +18 -0
- package/tools/debug_investigate.json +17 -0
- package/tools/error_fix_suggestion.json +13 -0
- package/tools/error_summary_md.json +11 -0
- package/tools/fix_verify.json +13 -0
- package/tools/mcp_health_check.json +8 -0
- package/tools/mcp_self_test.json +12 -0
- package/tools/screenshot_diff.json +16 -0
- package/tools/validation_check.json +20 -0
- package/tools/validation_decision.json +24 -0
- package/tools/validation_element.json +13 -0
- package/tools/validation_flow.json +12 -0
- package/tools/validation_matrix.json +27 -0
- package/tools/validation_quick_run.json +13 -0
- package/tools/validation_report.json +10 -0
- package/tools/validation_report_export.json +8 -0
- package/tools/validation_run.json +35 -0
- package/tools/validation_start.json +12 -0
- package/tools/validation_suite_run.json +17 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { PNG } = require('pngjs');
|
|
6
|
+
const pixelmatch = require('pixelmatch');
|
|
7
|
+
const { defaultAdapter, ensureDir, truncate } = require('./../engines/playwright_adapter');
|
|
8
|
+
|
|
9
|
+
const DIFF_DIR = path.join(__dirname, '..', 'artifacts', 'phase1');
|
|
10
|
+
|
|
11
|
+
async function collectEvidence(args = {}) {
|
|
12
|
+
const evidence = await defaultAdapter.collectEvidenceSummary(args);
|
|
13
|
+
const artifactPath = defaultAdapter.writeArtifact('evidence-summary', evidence);
|
|
14
|
+
|
|
15
|
+
const summary = {
|
|
16
|
+
consoleCount: evidence.console.count,
|
|
17
|
+
networkErrorCount: evidence.network.count,
|
|
18
|
+
pageErrorCount: evidence.pageerror.count,
|
|
19
|
+
title: evidence.dom?.title || '',
|
|
20
|
+
url: evidence.dom?.url || ''
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const fullEvidence = {
|
|
24
|
+
console: evidence.console,
|
|
25
|
+
network: evidence.network,
|
|
26
|
+
pageerror: evidence.pageerror,
|
|
27
|
+
dom: evidence.dom ? {
|
|
28
|
+
url: evidence.dom.url,
|
|
29
|
+
title: evidence.dom.title,
|
|
30
|
+
readyState: evidence.dom.readyState,
|
|
31
|
+
textSummary: truncate(evidence.dom.textSummary, 600),
|
|
32
|
+
controls: (evidence.dom.controls || []).slice(0, 20),
|
|
33
|
+
alerts: (evidence.dom.alerts || []).slice(0, 10)
|
|
34
|
+
} : null
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
summary,
|
|
40
|
+
evidence: fullEvidence,
|
|
41
|
+
artifactPath
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function screenshotDiff(args = {}) {
|
|
46
|
+
ensureDir(DIFF_DIR);
|
|
47
|
+
const baseline = args.baselinePath || args.beforePath;
|
|
48
|
+
let actual = args.actualPath || args.afterPath;
|
|
49
|
+
|
|
50
|
+
if (!actual) {
|
|
51
|
+
const shot = await defaultAdapter.screenshot({ name: args.name || 'diff-actual', fullPage: args.fullPage });
|
|
52
|
+
actual = shot.artifactPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = {
|
|
56
|
+
ok: true,
|
|
57
|
+
baselinePath: baseline || null,
|
|
58
|
+
actualPath: actual,
|
|
59
|
+
artifactPath: null,
|
|
60
|
+
summary: 'screenshot captured; baseline not provided so pixel diff skipped',
|
|
61
|
+
diffPixels: 0,
|
|
62
|
+
diffRatio: 0
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!baseline || !fs.existsSync(baseline) || !actual || !fs.existsSync(actual)) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const before = PNG.sync.read(fs.readFileSync(baseline));
|
|
70
|
+
const after = PNG.sync.read(fs.readFileSync(actual));
|
|
71
|
+
const width = Math.min(before.width, after.width);
|
|
72
|
+
const height = Math.min(before.height, after.height);
|
|
73
|
+
const beforeCrop = new PNG({ width, height });
|
|
74
|
+
const afterCrop = new PNG({ width, height });
|
|
75
|
+
PNG.bitblt(before, beforeCrop, 0, 0, width, height, 0, 0);
|
|
76
|
+
PNG.bitblt(after, afterCrop, 0, 0, width, height, 0, 0);
|
|
77
|
+
const diff = new PNG({ width, height });
|
|
78
|
+
const diffPixels = pixelmatch(beforeCrop.data, afterCrop.data, diff.data, width, height, { threshold: args.threshold || 0.1 });
|
|
79
|
+
const diffPath = path.join(DIFF_DIR, `screenshot-diff-${Date.now()}.png`);
|
|
80
|
+
fs.writeFileSync(diffPath, PNG.sync.write(diff));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: diffPixels === 0,
|
|
84
|
+
baselinePath: baseline,
|
|
85
|
+
actualPath: actual,
|
|
86
|
+
artifactPath: diffPath,
|
|
87
|
+
summary: diffPixels === 0 ? 'no visual pixel difference detected' : `${diffPixels} pixels differ (${((diffPixels / (width * height)) * 100).toFixed(2)}%)`,
|
|
88
|
+
diffPixels,
|
|
89
|
+
diffRatio: Number((diffPixels / (width * height)).toFixed(4)),
|
|
90
|
+
dimensions: { width, height }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
collectEvidence,
|
|
96
|
+
screenshotDiff
|
|
97
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "validpilot-oss",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "ValidPilot Open Source - Browser automation and validation framework with MCP protocol",
|
|
5
|
+
"bin": {
|
|
6
|
+
"validpilot": "bin/validpilot.js",
|
|
7
|
+
"bv-cli": "bin/validpilot.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"engines/",
|
|
12
|
+
"hands/",
|
|
13
|
+
"brain/error_aggregator.js",
|
|
14
|
+
"core/",
|
|
15
|
+
"tools/",
|
|
16
|
+
"rules/",
|
|
17
|
+
"examples/demo/",
|
|
18
|
+
"server.js",
|
|
19
|
+
"start-http.js",
|
|
20
|
+
"standalone-start.js",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"validpilot",
|
|
26
|
+
"mcp",
|
|
27
|
+
"ai-programming",
|
|
28
|
+
"ai-validation",
|
|
29
|
+
"browser-automation",
|
|
30
|
+
"debugging",
|
|
31
|
+
"ai-code-verification",
|
|
32
|
+
"visual-regression",
|
|
33
|
+
"natural-language-testing"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/yourorg/validpilot-oss"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"start": "node server.js",
|
|
41
|
+
"http": "node start-http.js"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"axe-core": "^4.12.1",
|
|
50
|
+
"dotenv": "^17.4.2",
|
|
51
|
+
"pixelmatch": "^5.3.0",
|
|
52
|
+
"playwright": "^1.61.0",
|
|
53
|
+
"pngjs": "^7.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"generated_at": "2026-06-26T12:47:00+08:00",
|
|
5
|
+
"source": "stage7-debug.md 根因分析",
|
|
6
|
+
"total_errors": 275,
|
|
7
|
+
"error_breakdown": {
|
|
8
|
+
"consoleError": 139,
|
|
9
|
+
"pageError": 66,
|
|
10
|
+
"networkError": 70
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"rules": [
|
|
14
|
+
{
|
|
15
|
+
"id": "RULE-001",
|
|
16
|
+
"type": "pre_check",
|
|
17
|
+
"trigger": "before_polling_api",
|
|
18
|
+
"action": "检测 API 状态码,500 时停止轮询并应用指数退避机制",
|
|
19
|
+
"source": "RC-1 后端 chat 轮询风暴",
|
|
20
|
+
"confidence": 0.95,
|
|
21
|
+
"priority": "P1",
|
|
22
|
+
"details": {
|
|
23
|
+
"api_endpoint": "/api/v1/chat/contacts",
|
|
24
|
+
"error_pattern": "internal_error (500)",
|
|
25
|
+
"impact": "30分钟内产生约60个pageError累积",
|
|
26
|
+
"recommendation": "前端给fetch加指数退避,长轮询未登入时暂停"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "RULE-002",
|
|
31
|
+
"type": "pre_check",
|
|
32
|
+
"trigger": "before_api_call",
|
|
33
|
+
"action": "验证 API 路由是否已注册(检查是否返回 404 Not Found)",
|
|
34
|
+
"source": "RC-2 API /api/v1/identity/me 完全未注册",
|
|
35
|
+
"confidence": 0.90,
|
|
36
|
+
"priority": "P0",
|
|
37
|
+
"details": {
|
|
38
|
+
"api_endpoint": "/api/v1/identity/me",
|
|
39
|
+
"error_pattern": "404 Not Found",
|
|
40
|
+
"impact": "导致 Tenant init failed",
|
|
41
|
+
"recommendation": "在 gateway routes 中注册 identity.py 路由"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "RULE-003",
|
|
46
|
+
"type": "pre_check",
|
|
47
|
+
"trigger": "before_function_call",
|
|
48
|
+
"action": "检查函数是否已定义,避免 ReferenceError",
|
|
49
|
+
"source": "RC-3 ensureAcquisitionWorkbench 未定义",
|
|
50
|
+
"confidence": 0.85,
|
|
51
|
+
"priority": "P2",
|
|
52
|
+
"details": {
|
|
53
|
+
"file": "bootstrap-tools.js:160",
|
|
54
|
+
"error_pattern": "ReferenceError: function is not defined",
|
|
55
|
+
"impact": "boot链终止,按钮点击无响应",
|
|
56
|
+
"recommendation": "确保函数已正确导出和导入"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "RULE-004",
|
|
61
|
+
"type": "recommended",
|
|
62
|
+
"trigger": "after_page_load",
|
|
63
|
+
"action": "验证页面必需的无障碍元素(h1、aria-label、对比度等)",
|
|
64
|
+
"source": "RC-4 全局 a11y 与标题缺陷",
|
|
65
|
+
"confidence": 0.75,
|
|
66
|
+
"priority": "P3",
|
|
67
|
+
"details": {
|
|
68
|
+
"issues": [
|
|
69
|
+
"缺少 <h1> 标题",
|
|
70
|
+
"#roleSelect 无 aria-label",
|
|
71
|
+
"#leadStatusFilter 无 aria-label",
|
|
72
|
+
".version 颜色对比度 4.39:1(要求 ≥4.5:1)"
|
|
73
|
+
],
|
|
74
|
+
"recommendation": "添加 aria-label、调整颜色对比度、添加页面标题"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "RULE-005",
|
|
79
|
+
"type": "pre_check",
|
|
80
|
+
"trigger": "before_business_api_call",
|
|
81
|
+
"action": "检查业务 API 状态码,500 时停止点击业务按钮",
|
|
82
|
+
"source": "RC-5 业务 API internal_error 集中爆发",
|
|
83
|
+
"confidence": 0.90,
|
|
84
|
+
"priority": "P0",
|
|
85
|
+
"details": {
|
|
86
|
+
"affected_apis": [
|
|
87
|
+
"/api/v1/intelligence/high-value-customers",
|
|
88
|
+
"/api/v1/tenants",
|
|
89
|
+
"/api/v1/reports/overview",
|
|
90
|
+
"/api/v1/reports/channel-roi",
|
|
91
|
+
"/api/v1/chat/contacts"
|
|
92
|
+
],
|
|
93
|
+
"error_pattern": "500 internal_error",
|
|
94
|
+
"impact": "业务功能完全不可用",
|
|
95
|
+
"recommendation": "检查 gateway 和下游服务的健康状态,运行数据库迁移脚本"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "RULE-006",
|
|
100
|
+
"type": "recommended",
|
|
101
|
+
"trigger": "after_page_load",
|
|
102
|
+
"action": "等待 3s 后再检查 DOM 元素稳定性",
|
|
103
|
+
"source": "F001-FILE01 成功用例经验",
|
|
104
|
+
"confidence": 0.80,
|
|
105
|
+
"priority": "low",
|
|
106
|
+
"details": {
|
|
107
|
+
"rationale": "页面初始化可能需要异步加载,等待可提高元素检查的准确性"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "RULE-007",
|
|
112
|
+
"type": "monitoring",
|
|
113
|
+
"trigger": "on_continuous_api_failure",
|
|
114
|
+
"action": "当同一 API 连续失败 3 次时,暂停后续请求并记录告警",
|
|
115
|
+
"source": "RC-1 chat 轮询风暴",
|
|
116
|
+
"confidence": 0.88,
|
|
117
|
+
"priority": "P1",
|
|
118
|
+
"details": {
|
|
119
|
+
"threshold": 3,
|
|
120
|
+
"backoff_strategy": "exponential",
|
|
121
|
+
"max_retry_interval": "300s"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"forbidden_actions": [
|
|
126
|
+
{
|
|
127
|
+
"id": "FORBID-001",
|
|
128
|
+
"trigger": "api_500_detected",
|
|
129
|
+
"action": "不要继续点击业务按钮",
|
|
130
|
+
"reason": "会导致无效请求堆积,增加服务器负担",
|
|
131
|
+
"source": "RC-1 chat 轮询风暴",
|
|
132
|
+
"priority": "P0"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "FORBID-002",
|
|
136
|
+
"trigger": "api_404_detected",
|
|
137
|
+
"action": "不要假设 API 路由已正确注册",
|
|
138
|
+
"reason": "可能调用到不存在的端点,导致初始化失败",
|
|
139
|
+
"source": "RC-2 API 未注册",
|
|
140
|
+
"priority": "P0"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "FORBID-003",
|
|
144
|
+
"trigger": "function_undefined",
|
|
145
|
+
"action": "不要调用未定义的函数",
|
|
146
|
+
"reason": "会导致 ReferenceError 并终止执行链",
|
|
147
|
+
"source": "RC-3 模块未定义",
|
|
148
|
+
"priority": "P2"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "FORBID-004",
|
|
152
|
+
"trigger": "before_seed_data_check",
|
|
153
|
+
"action": "不要假设数据库已包含种子数据",
|
|
154
|
+
"reason": "未运行迁移和种子脚本会导致业务 API 返回 500",
|
|
155
|
+
"source": "RC-5 业务 API 错误",
|
|
156
|
+
"priority": "P0"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"id": "FORBID-005",
|
|
160
|
+
"trigger": "during_polling",
|
|
161
|
+
"action": "不要在未登入状态下持续轮询用户相关 API",
|
|
162
|
+
"reason": "会产生大量无效错误日志",
|
|
163
|
+
"source": "RC-1 chat 轮询风暴",
|
|
164
|
+
"priority": "P1"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"health_score_factors": {
|
|
168
|
+
"static_resources": {
|
|
169
|
+
"score": 20,
|
|
170
|
+
"status": "pass",
|
|
171
|
+
"note": "静态资源全部 200"
|
|
172
|
+
},
|
|
173
|
+
"dom_rendering": {
|
|
174
|
+
"score": 15,
|
|
175
|
+
"status": "pass",
|
|
176
|
+
"note": "DOM 渲染完整"
|
|
177
|
+
},
|
|
178
|
+
"performance_lcp": {
|
|
179
|
+
"score": 10,
|
|
180
|
+
"status": "pass",
|
|
181
|
+
"note": "LCP 164ms"
|
|
182
|
+
},
|
|
183
|
+
"business_apis": {
|
|
184
|
+
"score": 0,
|
|
185
|
+
"status": "fail",
|
|
186
|
+
"note": "业务 API 仅 0/5 成功"
|
|
187
|
+
},
|
|
188
|
+
"accessibility": {
|
|
189
|
+
"score": 0,
|
|
190
|
+
"status": "fail",
|
|
191
|
+
"note": "a11y 仅 5/5 FAIL"
|
|
192
|
+
},
|
|
193
|
+
"page_errors": {
|
|
194
|
+
"score": -10,
|
|
195
|
+
"status": "fail",
|
|
196
|
+
"note": "pageError 持续累积"
|
|
197
|
+
},
|
|
198
|
+
"total_score": 35,
|
|
199
|
+
"max_score": 100,
|
|
200
|
+
"status": "blocked"
|
|
201
|
+
},
|
|
202
|
+
"priority_fixes": [
|
|
203
|
+
{
|
|
204
|
+
"priority": "P0",
|
|
205
|
+
"description": "立即修复",
|
|
206
|
+
"items": [
|
|
207
|
+
"RC-2 identity/me 注册",
|
|
208
|
+
"RC-5 seed/服务组装"
|
|
209
|
+
],
|
|
210
|
+
"expected_benefit": "让 boot 不再抛 5 类 500,工作台可见列表"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"priority": "P1",
|
|
214
|
+
"description": "24小时内修复",
|
|
215
|
+
"items": [
|
|
216
|
+
"RC-1 chat 轮询退避 + 频次拉长"
|
|
217
|
+
],
|
|
218
|
+
"expected_benefit": "减少约 60 个 pageError / 30 分钟,提高调试效率"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"priority": "P2",
|
|
222
|
+
"description": "次要修复",
|
|
223
|
+
"items": [
|
|
224
|
+
"RC-3 bootstrap 模块修补"
|
|
225
|
+
],
|
|
226
|
+
"expected_benefit": "runApiDemoBtn '试点链路' 工作"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"priority": "P3",
|
|
230
|
+
"description": "优化级",
|
|
231
|
+
"items": [
|
|
232
|
+
"RC-4 a11y 改进"
|
|
233
|
+
],
|
|
234
|
+
"expected_benefit": "WCAG AA 合规"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|