@autobest-ui/agent 1.0.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/README.md +182 -0
- package/bin/sync-assets.mjs +126 -0
- package/bin/sync-assets.test.mjs +64 -0
- package/mcp/azurepr-mcp-bridge/README.md +37 -0
- package/mcp/azurepr-mcp-bridge/azure-devops.js +327 -0
- package/mcp/azurepr-mcp-bridge/config.toml.example +7 -0
- package/mcp/azurepr-mcp-bridge/index.js +65 -0
- package/mcp/azurepr-mcp-bridge/index.test.js +116 -0
- package/mcp/azurepr-mcp-bridge/package.json +22 -0
- package/mcp/rag-mcp-bridge/README.md +42 -0
- package/mcp/rag-mcp-bridge/codex-system-prompt.md +20 -0
- package/mcp/rag-mcp-bridge/config.toml.example +12 -0
- package/mcp/rag-mcp-bridge/index.js +361 -0
- package/mcp/rag-mcp-bridge/index.test.js +56 -0
- package/mcp/rag-mcp-bridge/package.json +21 -0
- package/package.json +44 -0
- package/plugins/autobest-delivery/.codex-plugin/plugin.json +25 -0
- package/plugins/autobest-delivery/.mcp.json +11 -0
- package/plugins/autobest-delivery/README.md +164 -0
- package/plugins/autobest-delivery/assets/delivery-report-template.xlsx +0 -0
- package/plugins/autobest-delivery/mcp-server/npm-shrinkwrap.json +3511 -0
- package/plugins/autobest-delivery/mcp-server/package.json +23 -0
- package/plugins/autobest-delivery/mcp-server/src/paths.mjs +43 -0
- package/plugins/autobest-delivery/mcp-server/src/report.mjs +605 -0
- package/plugins/autobest-delivery/mcp-server/src/runner.mjs +489 -0
- package/plugins/autobest-delivery/mcp-server/src/server.mjs +199 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixture-server.mjs +36 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixtures/basic.feature.mjs +68 -0
- package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +83 -0
- package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +254 -0
- package/plugins/autobest-delivery/mcp-server/tests/runner.test.mjs +354 -0
- package/plugins/autobest-delivery/scripts/export-delivery-report.mjs +41 -0
- package/plugins/autobest-delivery/scripts/setup.mjs +295 -0
- package/plugins/autobest-delivery/scripts/setup.test.mjs +145 -0
- package/plugins/autobest-delivery/scripts/start-mcp.mjs +7 -0
- package/plugins/autobest-delivery/skills/code-audit/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/code-audit/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/code-craft/SKILL.md +27 -0
- package/plugins/autobest-delivery/skills/code-craft/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/SKILL.md +43 -0
- package/plugins/autobest-delivery/skills/delivery-loop/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +235 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/SKILL.md +35 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/SKILL.md +30 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/export-report/SKILL.md +66 -0
- package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +8 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/agents/openai.yaml +7 -0
- package/skills/README.md +38 -0
- package/skills/common/figma-ui-capture/SKILL.md +197 -0
- package/skills/common/figma-ui-capture/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/SKILL.md +67 -0
- package/skills/common/ui-prd-scope/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/references/scope-schema.md +158 -0
- package/skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs +302 -0
- package/skills/react/react-code-standards/SKILL.md +78 -0
- package/skills/react/react-code-standards/agents/openai.yaml +4 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(process.argv[2] || '.scratch/page-scope');
|
|
7
|
+
const errors = [];
|
|
8
|
+
const warnings = [];
|
|
9
|
+
const coverageAreas = [
|
|
10
|
+
'businessRules',
|
|
11
|
+
'statesTransitions',
|
|
12
|
+
'emptyErrors',
|
|
13
|
+
'permissions',
|
|
14
|
+
'crossPage'
|
|
15
|
+
];
|
|
16
|
+
const coverageStatuses = new Set([
|
|
17
|
+
'hit',
|
|
18
|
+
'no-hit',
|
|
19
|
+
'not-applicable',
|
|
20
|
+
'deferred-by-user'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const fail = message => errors.push(message);
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
26
|
+
console.error(`Scope 根目录不存在:${root}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const readmePath = path.join(root, 'README.md');
|
|
31
|
+
const manifestPath = path.join(root, 'scope-manifest.json');
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(readmePath)) fail('缺少根目录 README.md 索引');
|
|
34
|
+
if (!fs.existsSync(manifestPath)) fail('缺少 scope-manifest.json');
|
|
35
|
+
|
|
36
|
+
let manifest = null;
|
|
37
|
+
if (fs.existsSync(manifestPath)) {
|
|
38
|
+
try {
|
|
39
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
fail(`scope-manifest.json 无效:${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (manifest) {
|
|
46
|
+
for (const field of [
|
|
47
|
+
'schemaVersion',
|
|
48
|
+
'prId',
|
|
49
|
+
'prUrl',
|
|
50
|
+
'requestedPath',
|
|
51
|
+
'ragPlatform',
|
|
52
|
+
'changedSources',
|
|
53
|
+
'scopes',
|
|
54
|
+
'unmappedSources',
|
|
55
|
+
'figmaSources'
|
|
56
|
+
]) {
|
|
57
|
+
if (!(field in manifest)) fail(`Manifest 缺少字段 ${field}`);
|
|
58
|
+
}
|
|
59
|
+
if (!/^https:\/\//.test(manifest.prUrl || '')) fail('Manifest prUrl 必须是 HTTPS 链接');
|
|
60
|
+
if (!Array.isArray(manifest.scopes)) fail('Manifest scopes 必须是数组');
|
|
61
|
+
if (!Array.isArray(manifest.changedSources) || manifest.changedSources.length === 0) {
|
|
62
|
+
fail('Manifest changedSources 必须是非空数组');
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(manifest.unmappedSources)) {
|
|
65
|
+
fail('Manifest unmappedSources 必须是数组');
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(manifest.figmaSources)) {
|
|
68
|
+
fail('Manifest figmaSources 必须是数组');
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(manifest.changedSources)) {
|
|
71
|
+
const uniqueSources = new Set(manifest.changedSources);
|
|
72
|
+
if (uniqueSources.size !== manifest.changedSources.length) {
|
|
73
|
+
fail('Manifest changedSources 包含重复项');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(manifest.unmappedSources)) {
|
|
77
|
+
for (const item of manifest.unmappedSources) {
|
|
78
|
+
if (!item || typeof item.path !== 'string' || !item.reason) {
|
|
79
|
+
fail('每个未映射来源都必须包含 path 和 reason');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const scopeDirs = fs
|
|
86
|
+
.readdirSync(root, { withFileTypes: true })
|
|
87
|
+
.filter(entry => entry.isDirectory())
|
|
88
|
+
.map(entry => entry.name)
|
|
89
|
+
.filter(name => fs.existsSync(path.join(root, name, 'scope.md')))
|
|
90
|
+
.sort();
|
|
91
|
+
|
|
92
|
+
if (scopeDirs.length === 0) fail('没有找到包含 scope.md 的页面目录');
|
|
93
|
+
|
|
94
|
+
const manifestScopes = new Map();
|
|
95
|
+
if (manifest && Array.isArray(manifest.scopes)) {
|
|
96
|
+
for (const item of manifest.scopes) {
|
|
97
|
+
if (!item || typeof item.directory !== 'string') {
|
|
98
|
+
fail('Manifest scope 缺少 directory');
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (manifestScopes.has(item.directory)) {
|
|
102
|
+
fail(`Manifest 包含重复 scope:${item.directory}`);
|
|
103
|
+
}
|
|
104
|
+
manifestScopes.set(item.directory, item);
|
|
105
|
+
if (!item.pageFamily) fail(`Manifest ${item.directory} 缺少 pageFamily`);
|
|
106
|
+
if (!item.decision) fail(`Manifest ${item.directory} 缺少 decision`);
|
|
107
|
+
if (!Array.isArray(item.sourceFiles) || item.sourceFiles.length === 0) {
|
|
108
|
+
fail(`Manifest ${item.directory} 缺少 sourceFiles`);
|
|
109
|
+
}
|
|
110
|
+
if (!item.ragCoverage || typeof item.ragCoverage !== 'object') {
|
|
111
|
+
fail(`Manifest ${item.directory} 缺少 ragCoverage`);
|
|
112
|
+
} else {
|
|
113
|
+
for (const area of coverageAreas) {
|
|
114
|
+
const status = item.ragCoverage[area];
|
|
115
|
+
if (!coverageStatuses.has(status)) {
|
|
116
|
+
fail(`Manifest ${item.directory} 的 ${area} 覆盖状态无效:${status}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
Object.values(item.ragCoverage).includes('deferred-by-user') &&
|
|
121
|
+
!item.ragDeferralReason
|
|
122
|
+
) {
|
|
123
|
+
fail(`Manifest ${item.directory} 必须说明 RAG 暂缓原因`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const dirName of scopeDirs) {
|
|
129
|
+
if (!manifestScopes.has(dirName)) fail(`Manifest 未索引 ${dirName}`);
|
|
130
|
+
}
|
|
131
|
+
for (const dirName of manifestScopes.keys()) {
|
|
132
|
+
if (!scopeDirs.includes(dirName)) fail(`Manifest 索引了不存在的 scope:${dirName}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(manifest.changedSources)) {
|
|
136
|
+
const mappedSources = new Set(
|
|
137
|
+
manifest.scopes.flatMap(item =>
|
|
138
|
+
Array.isArray(item.sourceFiles) ? item.sourceFiles : []
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
const unmappedSources = new Set(
|
|
142
|
+
Array.isArray(manifest.unmappedSources)
|
|
143
|
+
? manifest.unmappedSources.map(item =>
|
|
144
|
+
typeof item === 'string' ? item : item && item.path
|
|
145
|
+
)
|
|
146
|
+
: []
|
|
147
|
+
);
|
|
148
|
+
for (const source of manifest.changedSources) {
|
|
149
|
+
if (!mappedSources.has(source) && !unmappedSources.has(source)) {
|
|
150
|
+
fail(`变更来源既未映射也未明确标记为未映射:${source}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (Array.isArray(manifest.figmaSources)) {
|
|
156
|
+
for (const item of manifest.figmaSources) {
|
|
157
|
+
if (!item || !manifestScopes.has(item.scope)) {
|
|
158
|
+
fail(`Figma 溯源引用了未知 scope:${item && item.scope}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!item.source || !item.destination || !item.operation || !item.sourceStatus) {
|
|
162
|
+
fail(`${item.scope} 的 Figma 溯源信息不完整`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const source = path.resolve(process.cwd(), item.source);
|
|
166
|
+
const destination = path.resolve(process.cwd(), item.destination);
|
|
167
|
+
if (!fs.existsSync(destination)) {
|
|
168
|
+
fail(`${item.scope} 的 Figma 目标目录不存在:${item.destination}`);
|
|
169
|
+
}
|
|
170
|
+
if (item.operation === 'copy' && !fs.existsSync(source)) {
|
|
171
|
+
fail(`${item.scope} 的 Figma 复制源不存在:${item.source}`);
|
|
172
|
+
} else if (item.operation === 'legacy-move') {
|
|
173
|
+
if (item.sourceStatus !== 'not-present-after-move') {
|
|
174
|
+
fail(`${item.scope} 的历史 Figma 移动记录包含无效 sourceStatus`);
|
|
175
|
+
}
|
|
176
|
+
} else if (!['copy', 'move'].includes(item.operation)) {
|
|
177
|
+
fail(`${item.scope} 的 Figma 溯源 operation 无效:${item.operation}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const readme = fs.existsSync(readmePath)
|
|
184
|
+
? fs.readFileSync(readmePath, 'utf8')
|
|
185
|
+
: '';
|
|
186
|
+
|
|
187
|
+
for (const match of readme.matchAll(/\]\(\.\/([^/]+)\/scope\.md\)/g)) {
|
|
188
|
+
if (!scopeDirs.includes(match[1])) {
|
|
189
|
+
fail(`README 链接指向缺失或失效的 scope:${match[1]}/scope.md`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const dirName of scopeDirs) {
|
|
194
|
+
const dir = path.join(root, dirName);
|
|
195
|
+
const scopePath = path.join(dir, 'scope.md');
|
|
196
|
+
const content = fs.readFileSync(scopePath, 'utf8');
|
|
197
|
+
const isGlobal = dirName === '00-global';
|
|
198
|
+
|
|
199
|
+
if (!readme.includes(`./${dirName}/scope.md`)) {
|
|
200
|
+
fail(`README 未链接 ${dirName}/scope.md`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!/^# .+(?:Scope|范围)$/m.test(content)) fail(`${dirName}:缺少 H1 Scope/范围标题`);
|
|
204
|
+
if (!/^## 项目运行与访问$/m.test(content)) {
|
|
205
|
+
fail(`${dirName}:缺少“项目运行与访问”`);
|
|
206
|
+
}
|
|
207
|
+
if (!/^## RAG 补充/m.test(content)) fail(`${dirName}:缺少“RAG 补充”`);
|
|
208
|
+
if (!/^## 待质询$/m.test(content)) fail(`${dirName}:缺少“待质询”`);
|
|
209
|
+
|
|
210
|
+
if (!isGlobal) {
|
|
211
|
+
for (const heading of ['需求来源', '页面族', '改动范围', '范围外', 'Figma 输入']) {
|
|
212
|
+
if (!new RegExp(`^## ${heading}$`, 'm').test(content)) {
|
|
213
|
+
fail(`${dirName}:缺少“${heading}”`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!content.includes('- Azure PR: https://')) {
|
|
217
|
+
fail(`${dirName}:缺少 Azure PR 链接`);
|
|
218
|
+
}
|
|
219
|
+
if (manifest && !content.includes(`- Azure PR: ${manifest.prUrl}`)) {
|
|
220
|
+
fail(`${dirName}:Azure PR 链接与 manifest 不一致`);
|
|
221
|
+
}
|
|
222
|
+
if (!content.includes('- Evidence type: `') && !content.includes('- 证据类型:`')) {
|
|
223
|
+
fail(`${dirName}:缺少证据类型`);
|
|
224
|
+
}
|
|
225
|
+
if (!content.includes('合并依据:')) {
|
|
226
|
+
fail(`${dirName}:缺少页面族合并依据`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const fields of [['Workspace', '工作区'], ['页面模块'], ['启动'], ['权限'], ['数据前置']]) {
|
|
231
|
+
if (!fields.some(field => content.includes(`| ${field} |`))) {
|
|
232
|
+
fail(`${dirName}:运行信息表缺少 ${fields.join('/')}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!/^\| .*URL \|/m.test(content) && !content.includes('http://localhost:')) {
|
|
236
|
+
fail(`${dirName}:运行信息表缺少 URL 上下文`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
!content.includes('| Query focus | Filters | Result |') &&
|
|
241
|
+
!content.includes('| 查询重点 | 过滤条件 | 结果 |')
|
|
242
|
+
) {
|
|
243
|
+
fail(`${dirName}:缺少 RAG 查询台账`);
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
!content.includes('| Coverage area | Status |') &&
|
|
247
|
+
!content.includes('| 覆盖领域 | 状态 |')
|
|
248
|
+
) {
|
|
249
|
+
fail(`${dirName}:缺少 RAG 覆盖表`);
|
|
250
|
+
}
|
|
251
|
+
const manifestScope = manifestScopes.get(dirName);
|
|
252
|
+
if (manifestScope && manifestScope.ragCoverage) {
|
|
253
|
+
for (const area of coverageAreas) {
|
|
254
|
+
const status = manifestScope.ragCoverage[area];
|
|
255
|
+
if (!content.includes(`| ${area} | \`${status}\` |`)) {
|
|
256
|
+
fail(`${dirName}:RAG 覆盖表中的 ${area} 与 manifest 不一致`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!content.includes('platform=')) fail(`${dirName}:RAG 查询台账缺少 platform`);
|
|
261
|
+
if (!/doc_id|no hit|未命中/i.test(content)) {
|
|
262
|
+
fail(`${dirName}:RAG 证据缺少 doc_id 或未命中结果`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const match of content.matchAll(/\]\(\.\/([^\)]+)\)/g)) {
|
|
266
|
+
const target = path.resolve(dir, match[1]);
|
|
267
|
+
if (!fs.existsSync(target)) fail(`${dirName}:本地链接无效 ${match[1]}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const files = fs.readdirSync(dir);
|
|
271
|
+
const assetFiles = files.filter(name => /\.(json|png)$/i.test(name));
|
|
272
|
+
const pngFiles = new Set(files.filter(name => name.endsWith('.png')));
|
|
273
|
+
|
|
274
|
+
for (const assetName of assetFiles) {
|
|
275
|
+
if (!content.includes(`./${assetName}`)) {
|
|
276
|
+
fail(`${dirName}:资产未在 scope.md 中声明:${assetName}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const jsonName of assetFiles.filter(name => name.endsWith('.json'))) {
|
|
281
|
+
try {
|
|
282
|
+
JSON.parse(fs.readFileSync(path.join(dir, jsonName), 'utf8'));
|
|
283
|
+
} catch (error) {
|
|
284
|
+
fail(`${dirName}:JSON 无效 ${jsonName}:${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
const expectedPng = jsonName.replace(/\.json$/, '.png');
|
|
287
|
+
if (!pngFiles.has(expectedPng)) {
|
|
288
|
+
warnings.push(`${dirName}:JSON 缺少同名 PNG:${jsonName}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isGlobal && assetFiles.length === 0) {
|
|
293
|
+
warnings.push(`${dirName}:没有同目录 Figma 资产;scope 必须明确缺失采集物的负责人`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const warning of warnings) console.warn(`警告 ${warning}`);
|
|
298
|
+
for (const error of errors) console.error(`错误 ${error}`);
|
|
299
|
+
console.log(
|
|
300
|
+
`已检查 ${scopeDirs.length} 个 scope 目录:${errors.length} 个错误,${warnings.length} 个警告`
|
|
301
|
+
);
|
|
302
|
+
process.exit(errors.length === 0 ? 0 : 1);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-code-standards
|
|
3
|
+
description: 实现或修改 React 业务代码时使用的仓库感知编码标准,约束技术栈识别、组件范式、TypeScript 类型、SCSS 组织、核心路径注释和组件库复用。适用于新增功能、缺陷修复、重构及 UI 实现;仓库真实配置和局部规范优先于本 skill 的默认技术栈。
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# React 业务代码标准
|
|
9
|
+
|
|
10
|
+
## 执行顺序
|
|
11
|
+
|
|
12
|
+
1. 实现前读取适用范围内的 `AGENTS.md`,并检查目标包的 `package.json`、`tsconfig.json`、现有依赖、相邻组件和样式组织。以仓库真实环境、既有公共 API 和局部惯例为准;它们覆盖本文件的默认值。
|
|
13
|
+
2. 识别可复用的 `@autobest-ui/*` 组件和工具,以及仓库已有的业务组件、类型、状态管理和样式模式。现有能力满足需求时直接复用;只有确实无法满足时才新增自定义实现。
|
|
14
|
+
3. 按下列标准完成最小范围的业务代码改动,并执行仓库规定的 lint、类型检查或构建验证。
|
|
15
|
+
4. 完成前检查所有新增或修改的核心业务路径均有必要注释,类型覆盖实际边界,逻辑与样式保持分离,并在最终响应中说明验证结果和未覆盖风险。
|
|
16
|
+
|
|
17
|
+
## 技术栈
|
|
18
|
+
|
|
19
|
+
以下是仓库未明确指定时的默认值,不用于覆盖真实配置:
|
|
20
|
+
|
|
21
|
+
- React 16.8 或更高版本;只使用当前仓库 React 版本支持的 API。
|
|
22
|
+
- TypeScript 严格类型约束。
|
|
23
|
+
- SCSS。
|
|
24
|
+
- Redux。
|
|
25
|
+
- 组件与工具库:`@autobest-ui/components`、`@autobest-ui/ajax`、`@autobest-ui/utils`。
|
|
26
|
+
|
|
27
|
+
不得为套用默认栈而升级依赖、切换框架或引入新的状态管理、请求、样式或组件方案。
|
|
28
|
+
|
|
29
|
+
## 组件范式
|
|
30
|
+
|
|
31
|
+
- 业务逻辑复杂、状态较多、依赖生命周期或包含多个副作用时,优先使用 Class 类组件,并沿用仓库现有生命周期和状态组织方式。
|
|
32
|
+
- 组件仅负责简单展示或逻辑极简,且没有复杂状态与生命周期时,可以使用函数组件和当前 React 版本支持的 Hooks。
|
|
33
|
+
- 修改现有组件时优先保持其既有范式;只有当前范式明显无法承载需求且变更范围已获授权时才转换组件类型。
|
|
34
|
+
- 拆分组件应形成清晰的业务或复用边界,不为缩短文件机械拆分,也不把跨组件业务状态藏入展示组件。
|
|
35
|
+
|
|
36
|
+
## TypeScript
|
|
37
|
+
|
|
38
|
+
- 为 Props、函数入参、业务数据结构和对外返回值声明显式 `interface`;既有项目明确使用 `type` 表达联合、映射等结构时沿用该惯例。
|
|
39
|
+
- 类型应覆盖可选字段、可空值、业务状态和边界枚举,使异常或缺失数据在类型与实现中都有对应处理。
|
|
40
|
+
- 优先通过类型收窄、泛型或领域类型表达不确定性。未知外部数据先使用 `unknown` 并在边界处校验;仅在第三方声明缺失且无法合理建模时局部使用 `any`,同时说明原因。
|
|
41
|
+
- 对网络响应、路由参数、存储数据和跨模块输入进行边界校验,不把类型断言当作运行时校验。
|
|
42
|
+
- 遵循仓库实际 `tsconfig` 和既有编译目标;默认按严格模式编写,即使目标包尚未开启全部严格选项,也不主动扩大弱类型区域。
|
|
43
|
+
|
|
44
|
+
## 代码与样式组织
|
|
45
|
+
|
|
46
|
+
- 业务逻辑、状态处理和数据转换放在 `.tsx` / `.ts` 文件;样式放在独立 SCSS 文件,并沿用当前包的 CSS Modules、命名和导入方式。
|
|
47
|
+
- 优先使用样式类表达稳定视觉规则。只有值由运行时数据计算且现有样式机制无法表达时才使用局部内联样式。
|
|
48
|
+
- 类型、业务逻辑和样式按职责组织。共享或复杂业务类型应放在相邻的类型模块中;组件私有且短小的 Props 可以与组件入口共置。
|
|
49
|
+
- 保持现有目录、导出、Redux 和请求层边界,不在局部实现中建立平行架构。
|
|
50
|
+
|
|
51
|
+
## 注释与代码输出
|
|
52
|
+
|
|
53
|
+
交付可维护的业务实现,不只提供脱离仓库的裸代码。注释应解释意图、数据流和约束,避免逐行复述语法。
|
|
54
|
+
|
|
55
|
+
每个新增模块或组件入口应有简短用途说明。核心业务路径的注释必须覆盖其中实际存在的内容:
|
|
56
|
+
|
|
57
|
+
- 入参边界校验和异常兜底为何这样处理;
|
|
58
|
+
- 关键数据从请求、Props 或 Redux 到状态及 UI 的流向;
|
|
59
|
+
- 容易误判的边界条件和异常场景;
|
|
60
|
+
- 复杂业务中的关键假设、外部约束和潜在风险。
|
|
61
|
+
|
|
62
|
+
简单赋值、显然的 JSX 和类型声明无需叙述性注释。实现变化后同步更新相关注释,确保注释描述当前行为。
|
|
63
|
+
|
|
64
|
+
## 组件与工具复用
|
|
65
|
+
|
|
66
|
+
- 搜索并优先使用 `@autobest-ui/components` 的现成组件及仓库已有业务组件,保持其 Props、交互和样式约定。
|
|
67
|
+
- 请求优先使用 `@autobest-ui/ajax` 或仓库对它的既有封装;通用处理优先使用 `@autobest-ui/utils` 或仓库已有工具。
|
|
68
|
+
- 新建自定义组件或工具前,确认现有实现无法通过受支持的组合或扩展点满足需求。新增实现应保持单一职责,并避免复制已有组件的核心能力。
|
|
69
|
+
- 不猜测组件库 API。通过仓库调用点、类型声明或可用文档确认用法后再实现。
|
|
70
|
+
|
|
71
|
+
## 完成标准
|
|
72
|
+
|
|
73
|
+
- 仓库配置、目标模块惯例与本标准之间的优先级已正确处理。
|
|
74
|
+
- 组件范式与业务复杂度匹配,且兼容仓库当前 React 版本。
|
|
75
|
+
- Props、边界输入、业务数据和返回值具有明确类型,外部数据经过必要校验。
|
|
76
|
+
- 逻辑与 SCSS 分离,复用了可用的 `@autobest-ui/*` 或仓库现有能力。
|
|
77
|
+
- 核心业务路径具有准确、必要的注释,复杂假设和风险已被说明。
|
|
78
|
+
- 已按仓库要求完成与改动范围相称的验证,并如实报告结果。
|