@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,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RAG MCP bridge for the local PRD knowledge Express API.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities are deliberately limited to:
|
|
7
|
+
* 1. Expanding file, file-array, and directory inputs into Markdown paths.
|
|
8
|
+
* 2. Forwarding MCP tool calls to the local HTTP API.
|
|
9
|
+
*
|
|
10
|
+
* Database access, Markdown parsing, embeddings, and RAG stay in Express.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readdir, stat } from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
21
|
+
function apiBaseUrlFromEnvironment() {
|
|
22
|
+
const configuredUrl = process.env.RAG_API_BASE_URL?.trim();
|
|
23
|
+
if (!configuredUrl) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"RAG_API_BASE_URL is required; configure it in the MCP server environment",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let url;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(configuredUrl);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("RAG_API_BASE_URL must be a valid URL");
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
36
|
+
throw new Error("RAG_API_BASE_URL must use http or https");
|
|
37
|
+
}
|
|
38
|
+
return url.toString().replace(/\/$/, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function httpTimeoutFromEnvironment() {
|
|
42
|
+
const configuredTimeout =
|
|
43
|
+
process.env.RAG_MCP_HTTP_TIMEOUT_MS ?? String(DEFAULT_HTTP_TIMEOUT_MS);
|
|
44
|
+
const timeout = Number(configuredTimeout);
|
|
45
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
46
|
+
throw new Error("RAG_MCP_HTTP_TIMEOUT_MS must be a positive integer");
|
|
47
|
+
}
|
|
48
|
+
return timeout;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const API_BASE_URL = apiBaseUrlFromEnvironment();
|
|
52
|
+
const HTTP_TIMEOUT_MS = httpTimeoutFromEnvironment();
|
|
53
|
+
|
|
54
|
+
const pathsSchema = z
|
|
55
|
+
.union([
|
|
56
|
+
z.string().min(1),
|
|
57
|
+
z.array(z.string().min(1)).min(1),
|
|
58
|
+
])
|
|
59
|
+
.describe("单个Markdown文件、Markdown文件数组或文件夹的绝对路径");
|
|
60
|
+
const platformSchema = z
|
|
61
|
+
.enum(["app", "california-web", "shared"])
|
|
62
|
+
.describe("知识所属平台;必须明确指定,防止APP与加州网站需求串库");
|
|
63
|
+
const moduleSchema = z
|
|
64
|
+
.string()
|
|
65
|
+
.min(1)
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("可选业务模块标识,例如vehicle-selector");
|
|
68
|
+
const pageSchema = z
|
|
69
|
+
.string()
|
|
70
|
+
.min(1)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(
|
|
73
|
+
"业务页面键,例如home、pl、pn、pd、name-search;录入多个文件时不要传,由后端按文件名分别推断",
|
|
74
|
+
);
|
|
75
|
+
const docKeySchema = z
|
|
76
|
+
.string()
|
|
77
|
+
.min(1)
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("需要限定到单一文档时传knowledge_doc.doc_key绝对路径");
|
|
80
|
+
|
|
81
|
+
/** Recursively collects Markdown files from a directory in stable order. */
|
|
82
|
+
async function scanMarkdownDirectory(directoryPath) {
|
|
83
|
+
const markdownFiles = [];
|
|
84
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
85
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
markdownFiles.push(...(await scanMarkdownDirectory(entryPath)));
|
|
91
|
+
} else if (entry.isFile() && path.extname(entry.name).toLowerCase() === ".md") {
|
|
92
|
+
markdownFiles.push(path.resolve(entryPath));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return markdownFiles;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Normalizes all supported path inputs to a unique array of absolute .md files. */
|
|
99
|
+
async function expandMarkdownPaths(pathsInput) {
|
|
100
|
+
const inputs = Array.isArray(pathsInput) ? pathsInput : [pathsInput];
|
|
101
|
+
const markdownFiles = [];
|
|
102
|
+
|
|
103
|
+
for (const input of inputs) {
|
|
104
|
+
if (typeof input !== "string" || !input.trim()) {
|
|
105
|
+
throw new Error("paths中的每一项都必须是非空字符串");
|
|
106
|
+
}
|
|
107
|
+
if (!path.isAbsolute(input.trim())) {
|
|
108
|
+
throw new Error(`必须使用绝对路径: ${input}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const absolutePath = path.resolve(input.trim());
|
|
112
|
+
const pathStat = await stat(absolutePath);
|
|
113
|
+
if (pathStat.isDirectory()) {
|
|
114
|
+
markdownFiles.push(...(await scanMarkdownDirectory(absolutePath)));
|
|
115
|
+
} else if (
|
|
116
|
+
pathStat.isFile() &&
|
|
117
|
+
path.extname(absolutePath).toLowerCase() === ".md"
|
|
118
|
+
) {
|
|
119
|
+
markdownFiles.push(absolutePath);
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`路径不是Markdown文件或文件夹: ${absolutePath}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const uniquePaths = [...new Set(markdownFiles)].sort((left, right) =>
|
|
126
|
+
left.localeCompare(right),
|
|
127
|
+
);
|
|
128
|
+
if (uniquePaths.length === 0) {
|
|
129
|
+
throw new Error("指定路径中没有找到任何.md文件");
|
|
130
|
+
}
|
|
131
|
+
return uniquePaths;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Posts JSON to Express and returns its parsed response body. */
|
|
135
|
+
async function postJson(endpoint, body) {
|
|
136
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
141
|
+
});
|
|
142
|
+
const responseText = await response.text();
|
|
143
|
+
let responseBody;
|
|
144
|
+
try {
|
|
145
|
+
responseBody = responseText ? JSON.parse(responseText) : null;
|
|
146
|
+
} catch {
|
|
147
|
+
responseBody = responseText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const details =
|
|
152
|
+
typeof responseBody === "string"
|
|
153
|
+
? responseBody
|
|
154
|
+
: JSON.stringify(responseBody);
|
|
155
|
+
throw new Error(`Express后端返回HTTP ${response.status}: ${details}`);
|
|
156
|
+
}
|
|
157
|
+
return responseBody;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Sends a GET request to Express and returns its parsed JSON response. */
|
|
161
|
+
async function getJson(endpoint, query) {
|
|
162
|
+
const url = new URL(`${API_BASE_URL}${endpoint}`);
|
|
163
|
+
for (const [name, value] of Object.entries(query)) {
|
|
164
|
+
if (value === undefined || value === null || value === "") continue;
|
|
165
|
+
url.searchParams.set(name, value);
|
|
166
|
+
}
|
|
167
|
+
const response = await fetch(url, {
|
|
168
|
+
method: "GET",
|
|
169
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
170
|
+
});
|
|
171
|
+
const responseText = await response.text();
|
|
172
|
+
let responseBody;
|
|
173
|
+
try {
|
|
174
|
+
responseBody = responseText ? JSON.parse(responseText) : null;
|
|
175
|
+
} catch {
|
|
176
|
+
responseBody = responseText;
|
|
177
|
+
}
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const details =
|
|
180
|
+
typeof responseBody === "string"
|
|
181
|
+
? responseBody
|
|
182
|
+
: JSON.stringify(responseBody);
|
|
183
|
+
throw new Error(`Express后端返回HTTP ${response.status}: ${details}`);
|
|
184
|
+
}
|
|
185
|
+
return responseBody;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toolSuccess(payload) {
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: JSON.stringify(payload, null, 2),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toolError(error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
return {
|
|
202
|
+
isError: true,
|
|
203
|
+
content: [{ type: "text", text: message }],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const server = new McpServer(
|
|
208
|
+
{ name: "rag-mcp-bridge", version: "1.0.0" },
|
|
209
|
+
{
|
|
210
|
+
instructions:
|
|
211
|
+
"Use retrieve_knowledge before implementing requirement-sensitive code. " +
|
|
212
|
+
"Every tool call requires an explicit platform: app, california-web, or shared. " +
|
|
213
|
+
"add_prd_file and update_spec_file accept one file, an array of files, or a directory. " +
|
|
214
|
+
"Only call update_spec_file after the PR is merged and leader review confirms the spec is effective.",
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
server.registerTool(
|
|
219
|
+
"add_prd_file",
|
|
220
|
+
{
|
|
221
|
+
title: "录入或更新PRD基线",
|
|
222
|
+
description:
|
|
223
|
+
"录入/更新PRD基线。paths支持单个md绝对路径、md绝对路径数组或文件夹绝对路径;文件夹会递归扫描全部.md文件。后端负责解析截图并将旧文档标记为作废。",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
paths: pathsSchema,
|
|
226
|
+
platform: platformSchema,
|
|
227
|
+
module: moduleSchema,
|
|
228
|
+
page: pageSchema,
|
|
229
|
+
},
|
|
230
|
+
annotations: {
|
|
231
|
+
readOnlyHint: false,
|
|
232
|
+
destructiveHint: false,
|
|
233
|
+
idempotentHint: false,
|
|
234
|
+
openWorldHint: false,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
async ({ paths, platform, module, page }) => {
|
|
238
|
+
try {
|
|
239
|
+
const expandedPaths = await expandMarkdownPaths(paths);
|
|
240
|
+
return toolSuccess(
|
|
241
|
+
await postJson("/add-prd", {
|
|
242
|
+
paths: expandedPaths,
|
|
243
|
+
platform,
|
|
244
|
+
module,
|
|
245
|
+
page,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return toolError(error);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
server.registerTool(
|
|
255
|
+
"update_spec_file",
|
|
256
|
+
{
|
|
257
|
+
title: "录入迭代变更Spec",
|
|
258
|
+
description:
|
|
259
|
+
"仅在PR合并且leader review确认生效后调用。paths支持单个spec.md绝对路径、路径数组或文件夹;只需传改动spec。sprintVersion必须递增,数值越大需求越新。",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
paths: pathsSchema,
|
|
262
|
+
sprintVersion: z.number().int().positive(),
|
|
263
|
+
iterationTag: z.string().min(1),
|
|
264
|
+
platform: platformSchema,
|
|
265
|
+
module: moduleSchema,
|
|
266
|
+
page: pageSchema,
|
|
267
|
+
},
|
|
268
|
+
annotations: {
|
|
269
|
+
readOnlyHint: false,
|
|
270
|
+
destructiveHint: false,
|
|
271
|
+
idempotentHint: false,
|
|
272
|
+
openWorldHint: false,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
async ({ paths, sprintVersion, iterationTag, platform, module, page }) => {
|
|
276
|
+
try {
|
|
277
|
+
const expandedPaths = await expandMarkdownPaths(paths);
|
|
278
|
+
return toolSuccess(
|
|
279
|
+
await postJson("/add-spec", {
|
|
280
|
+
paths: expandedPaths,
|
|
281
|
+
sprintVersion,
|
|
282
|
+
iterationTag,
|
|
283
|
+
platform,
|
|
284
|
+
module,
|
|
285
|
+
page,
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return toolError(error);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
server.registerTool(
|
|
295
|
+
"retrieve_knowledge",
|
|
296
|
+
{
|
|
297
|
+
title: "检索PRD与迭代Spec",
|
|
298
|
+
description:
|
|
299
|
+
"Codex编写需求相关代码前调用。后端执行向量+BM25-RRF混合检索并按sprint_version业务排序。enableStruct=true时才生成结构化PRD;includeImages=true时才保留图片并替换为数据库图片URL,两者默认均为false。",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
query: z.string().min(1),
|
|
302
|
+
platform: platformSchema,
|
|
303
|
+
module: moduleSchema.describe(
|
|
304
|
+
"可选业务模块硬过滤,例如vehicle-selector",
|
|
305
|
+
),
|
|
306
|
+
page: pageSchema.describe(
|
|
307
|
+
"可选页面硬过滤,例如首页传home;不传时后端会从明确的查询词自动推断",
|
|
308
|
+
),
|
|
309
|
+
docKey: docKeySchema,
|
|
310
|
+
enableStruct: z
|
|
311
|
+
.boolean()
|
|
312
|
+
.default(false)
|
|
313
|
+
.describe("是否调用LLM生成structuredPrd,默认false"),
|
|
314
|
+
includeImages: z
|
|
315
|
+
.boolean()
|
|
316
|
+
.default(false)
|
|
317
|
+
.describe("是否返回可供UI显示的数据库图片URL,默认false"),
|
|
318
|
+
},
|
|
319
|
+
annotations: {
|
|
320
|
+
readOnlyHint: true,
|
|
321
|
+
destructiveHint: false,
|
|
322
|
+
idempotentHint: true,
|
|
323
|
+
openWorldHint: false,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
async ({
|
|
327
|
+
query,
|
|
328
|
+
platform,
|
|
329
|
+
module,
|
|
330
|
+
page,
|
|
331
|
+
docKey,
|
|
332
|
+
enableStruct = false,
|
|
333
|
+
includeImages = false,
|
|
334
|
+
}) => {
|
|
335
|
+
try {
|
|
336
|
+
return toolSuccess(
|
|
337
|
+
await getJson("/search", {
|
|
338
|
+
query: query.trim(),
|
|
339
|
+
platform,
|
|
340
|
+
module,
|
|
341
|
+
page,
|
|
342
|
+
docKey,
|
|
343
|
+
enableStruct: String(enableStruct === true),
|
|
344
|
+
includeImages: String(includeImages === true),
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return toolError(error);
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
async function main() {
|
|
354
|
+
const transport = new StdioServerTransport();
|
|
355
|
+
await server.connect(transport);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
main().catch((error) => {
|
|
359
|
+
console.error("Failed to start rag-mcp-bridge:", error);
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
|
|
9
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
test("initializes and exposes the RAG tools over stdio", async () => {
|
|
12
|
+
const client = new Client({ name: "rag-mcp-bridge-test", version: "1.0.0" });
|
|
13
|
+
const transport = new StdioClientTransport({
|
|
14
|
+
command: process.execPath,
|
|
15
|
+
args: [path.join(directory, "index.js")],
|
|
16
|
+
cwd: path.resolve(directory, "../.."),
|
|
17
|
+
env: {
|
|
18
|
+
RAG_API_BASE_URL: "http://127.0.0.1:3000/api/knowledge",
|
|
19
|
+
},
|
|
20
|
+
stderr: "pipe",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await client.connect(transport);
|
|
25
|
+
|
|
26
|
+
assert.equal(client.getServerVersion()?.name, "rag-mcp-bridge");
|
|
27
|
+
assert.match(client.getInstructions() ?? "", /retrieve_knowledge/);
|
|
28
|
+
|
|
29
|
+
const { tools } = await client.listTools();
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
tools.map(({ name }) => name),
|
|
32
|
+
["add_prd_file", "update_spec_file", "retrieve_knowledge"],
|
|
33
|
+
);
|
|
34
|
+
assert.equal(
|
|
35
|
+
tools.find(({ name }) => name === "retrieve_knowledge")?.annotations
|
|
36
|
+
?.readOnlyHint,
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
} finally {
|
|
40
|
+
await client.close();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("fails to start when RAG_API_BASE_URL is not configured", () => {
|
|
45
|
+
const environment = { ...process.env };
|
|
46
|
+
delete environment.RAG_API_BASE_URL;
|
|
47
|
+
|
|
48
|
+
const result = spawnSync(process.execPath, [path.join(directory, "index.js")], {
|
|
49
|
+
cwd: path.resolve(directory, "../.."),
|
|
50
|
+
env: environment,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.notEqual(result.status, 0);
|
|
55
|
+
assert.match(result.stderr, /RAG_API_BASE_URL is required/);
|
|
56
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autobest/rag-mcp-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "MCP bridge for the PRD knowledge RAG API",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node index.js",
|
|
13
|
+
"lint": "node --check index.js && node --check index.test.js",
|
|
14
|
+
"test": "node --test index.test.js",
|
|
15
|
+
"verify": "npm run lint && npm test"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
19
|
+
"zod": "4.4.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autobest-ui/agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Autobest Agent skills/plugins/mcp assets + sync cli",
|
|
6
|
+
"files": [
|
|
7
|
+
"skills",
|
|
8
|
+
"plugins",
|
|
9
|
+
"!plugins/**/node_modules",
|
|
10
|
+
"!plugins/**/.runtime",
|
|
11
|
+
"mcp",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"autobest-agent-sync": "./bin/sync-assets.mjs",
|
|
20
|
+
"autobest-azurepr-mcp": "./mcp/azurepr-mcp-bridge/index.js",
|
|
21
|
+
"autobest-delivery-setup": "./plugins/autobest-delivery/scripts/setup.mjs",
|
|
22
|
+
"autobest-rag-mcp": "./mcp/rag-mcp-bridge/index.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"mcp:azurepr": "node ./mcp/azurepr-mcp-bridge/index.js",
|
|
26
|
+
"mcp:rag": "node ./mcp/rag-mcp-bridge/index.js",
|
|
27
|
+
"test": "npm run test:skills && npm run test:mcp && npm run test:plugin",
|
|
28
|
+
"test:skills": "node --test ./bin/sync-assets.test.mjs",
|
|
29
|
+
"test:mcp:azurepr": "node --test ./mcp/azurepr-mcp-bridge/index.test.js",
|
|
30
|
+
"test:mcp:rag": "node --test ./mcp/rag-mcp-bridge/index.test.js",
|
|
31
|
+
"test:mcp": "npm run test:mcp:azurepr && npm run test:mcp:rag",
|
|
32
|
+
"test:plugin": "node --test ./plugins/autobest-delivery/scripts/setup.test.mjs"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
36
|
+
"diff": "9.0.0",
|
|
37
|
+
"fs-extra": "11.3.0",
|
|
38
|
+
"zod": "4.4.3"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"registry": "https://registry.npmjs.org/",
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autobest-delivery",
|
|
3
|
+
"version": "1.0.0+codex.npm",
|
|
4
|
+
"description": "\u5728\u4e0d\u5411\u4e1a\u52a1\u4ed3\u5e93\u6dfb\u52a0\u6d4b\u8bd5\u4f9d\u8d56\u7684\u524d\u63d0\u4e0b\uff0c\u8fd0\u884c\u9694\u79bb\u4e14\u7531\u89c4\u683c\u9a71\u52a8\u7684 UI \u4ea4\u4ed8\u95ed\u73af\u3002",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Autobest \u524d\u7aef\u56e2\u961f"
|
|
7
|
+
},
|
|
8
|
+
"skills": "./skills/",
|
|
9
|
+
"interface": {
|
|
10
|
+
"displayName": "Autobest \u4ea4\u4ed8\u95ed\u73af",
|
|
11
|
+
"shortDescription": "\u9694\u79bb\u8fd0\u884c Playwright UI \u4ea4\u4ed8\u95ed\u73af",
|
|
12
|
+
"longDescription": "\u5c06\u4ea4\u4ed8\u95ed\u73af Skill \u4e0e\u9694\u79bb\u7684 Playwright MCP \u8fd0\u884c\u5668\u6253\u5305\uff0c\u4f7f\u8001\u4e1a\u52a1\u4ed3\u5e93\u65e0\u9700\u5b89\u88c5\u6d4b\u8bd5\u4f9d\u8d56\u3002",
|
|
13
|
+
"developerName": "Autobest \u524d\u7aef\u56e2\u961f",
|
|
14
|
+
"category": "Productivity",
|
|
15
|
+
"capabilities": [
|
|
16
|
+
"Browser Automation",
|
|
17
|
+
"Write"
|
|
18
|
+
],
|
|
19
|
+
"defaultPrompt": [
|
|
20
|
+
"\u9488\u5bf9\u5f53\u524d\u529f\u80fd\u89c4\u683c\u8fd0\u884c\u5b8c\u6574\u4ea4\u4ed8\u95ed\u73af\u3002",
|
|
21
|
+
"\u68c0\u67e5\u9694\u79bb E2E \u8fd0\u884c\u5668\u662f\u5426\u5df2\u7ecf\u5c31\u7eea\u3002"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"mcpServers": "./.mcp.json"
|
|
25
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Autobest 交付插件
|
|
2
|
+
|
|
3
|
+
`autobest-delivery` 将 Autobest 交付闭环 Skill 与插件自带的 Playwright MCP 运行器打包在一起。目标业务仓库只需提供应用源码、功能规格和启动命令,不需要安装 Playwright 依赖。
|
|
4
|
+
|
|
5
|
+
## 隔离机制
|
|
6
|
+
|
|
7
|
+
运行器的 Node 依赖位于 `mcp-server/node_modules/`,Chromium 位于 `mcp-server/.runtime/ms-playwright/`,这两个路径均被插件忽略。工具只接受绝对路径形式的目标工作区,并且仅允许读取冻结场景、向指定功能输出目录写入证据。
|
|
8
|
+
|
|
9
|
+
运行器不会在目标工作区调用包管理器,也不会修改目标仓库的清单、锁文件、`node_modules`、配置或源码。每次执行前后都会校验冻结场景的 SHA-256。
|
|
10
|
+
|
|
11
|
+
## 使用 npx 安装
|
|
12
|
+
|
|
13
|
+
前置条件:
|
|
14
|
+
|
|
15
|
+
- Node.js 20 或更高版本;
|
|
16
|
+
- 支持 `codex plugin` 命令的 Codex CLI;
|
|
17
|
+
- 能访问 `@autobest-ui/agent` 所在的 npm registry。
|
|
18
|
+
|
|
19
|
+
安装或更新插件:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
安装器会在临时目录中执行 `npm ci` 并下载固定版本的 Chromium。运行时完整就绪后,插件才会替换到以下持久化目录:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
~/.autobest-agent/marketplaces/autobest-team/
|
|
29
|
+
├── .agents/plugins/marketplace.json
|
|
30
|
+
└── plugins/autobest-delivery/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
随后安装器会注册 `autobest-team` 本地市场,并执行等效于 `codex plugin add autobest-delivery@autobest-team` 的安装操作。原来从源码仓库注册的同名市场会自动迁移到该持久化目录;重复执行同一命令即为更新,并复用已下载的同版本浏览器运行时。插件版本与 npm 包版本保持一致。安装或更新后需要重启 Codex,并在新会话中使用。
|
|
34
|
+
|
|
35
|
+
生产环境建议固定 npm 包版本,例如:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx --yes --package=@autobest-ui/agent@1.0.0 autobest-delivery-setup
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
卸载插件及 Autobest 专用本地市场:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup uninstall
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
卸载命令只会删除 `~/.autobest-agent/marketplaces/autobest-team` 这一精确路径。查看命令帮助可执行:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## MCP 工具
|
|
54
|
+
|
|
55
|
+
### `check_environment`
|
|
56
|
+
|
|
57
|
+
启动插件自带的 Chromium,返回 Node、运行器、Playwright、浏览器及其存储目录版本。交付闭环会在第一次实现者回合前调用此工具。
|
|
58
|
+
|
|
59
|
+
### `run_feature_e2e`
|
|
60
|
+
|
|
61
|
+
必填参数:
|
|
62
|
+
|
|
63
|
+
- `workspaceRoot`:目标工作区的绝对路径。
|
|
64
|
+
- `scenarioPath`:工作区内冻结的 `.mjs` 场景路径。
|
|
65
|
+
- `baseUrl`:可访问的应用 URL。
|
|
66
|
+
- `outputDir`:工作区内的证据输出目录。
|
|
67
|
+
|
|
68
|
+
可选参数包括 `iteration`、`timeoutMs`、`headed`、`keepBrowserOpenOnBlock` 和 `blockedSessionTtlMs`。交付验收使用 headed 模式;发生 Blocked 时,工具保留可见浏览器并返回 session ID、过期时间、结构化检查、组件截图、Trace、控制台错误、网络失败和 SHA-256。
|
|
69
|
+
|
|
70
|
+
新版场景通过 `scene(id, callback)` 隔离页面和执行错误。一个 scene 的定位器、弹窗或准备步骤阻塞时,运行器会记录该 blocker 并继续执行后续独立 scene,全部 scene 尝试结束后再返回 `blocked`。旧版直接使用顶层 `page` 的场景仍可运行,但无法在未捕获异常后继续。
|
|
71
|
+
|
|
72
|
+
### `resolve_blocked_run`
|
|
73
|
+
|
|
74
|
+
用户查看 Blocked 浏览器后,由创建该会话的验收代理调用此工具。工具将选择写入 `human-decision.json` 并关闭浏览器;`accept` 或 `skip` 会形成 waiver,而不是伪装成完整通过。
|
|
75
|
+
|
|
76
|
+
| 决策 | 后续效果 | 代表什么 | 建议使用场景 |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `retry` | 使用未修改的冻结场景重新验收 | 本轮证据无效,原样再跑一次 | 服务刚启动、网络抖动、偶发加载超时、Blocked 会话已过期;只有相信同样输入再次执行可能成功时使用 |
|
|
79
|
+
| `accept` | 记录人工确认 waiver,继续代码审计 | 自动证据缺失,但用户已在保留的可见浏览器中亲自确认阻断点符合预期 | 页面实际状态清晰可见且可由人工可靠判断;不适合无法观察的请求、状态污染或后续行为 |
|
|
80
|
+
| `skip` | 记录风险 waiver,继续代码审计 | 不判断阻断点是否正确,接受该 scene 的证据缺失 | 阻断覆盖低风险、非发布关键路径,且用户愿意接受未验证风险;它不会从抛异常的语句下一行恢复执行,后续覆盖来自已隔离并继续执行的其他 scene |
|
|
81
|
+
| `implementation_defect` | 进入新的 Maker 修复回合 | 测试可信,业务页面或接口行为不符合 spec | 页面内容、交互、导航、布局或业务请求确实错误,并且可由业务代码修复 |
|
|
82
|
+
| `test_defect` | 进入新的测试编写者修订回合并重新冻结 | spec 与实现可能正确,冻结场景的定位器、fixture、弹窗处理或等待逻辑错误 | Trace/DOM 表明测试假设不成立,例如把选项错误地定位为 `listitem`、错误等待原生 alert、fixture 未提供所需数据 |
|
|
83
|
+
| `stop` | 终止闭环并写为 `blocked` | 不再继续实现、验收或审计 | 需求取消、环境短期无法恢复、需要外部决策,或继续执行的成本已不合理 |
|
|
84
|
+
|
|
85
|
+
选择时先判断“同样输入重跑是否可能恢复”:可能则用 `retry`。稳定复现后判断测试本身是否可信:测试机械问题用 `test_defect`,真实业务问题用 `implementation_defect`。只有愿意承担缺失证据时才使用 `accept` 或 `skip`;其中 `accept` 表示已经人工确认,`skip` 表示未确认。`stop` 是唯一终止选项。
|
|
86
|
+
|
|
87
|
+
## 场景契约
|
|
88
|
+
|
|
89
|
+
测试编写者创建 `featureDir/e2e/e2e.feature.mjs`。该文件不导入任何包,所需能力全部由运行器注入:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
export const metadata = {
|
|
93
|
+
scenes: ['initial'],
|
|
94
|
+
viewports: [{ name: 'desktop', width: 1400, height: 1000 }],
|
|
95
|
+
visualMappings: []
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default async function run({
|
|
99
|
+
scene
|
|
100
|
+
}) {
|
|
101
|
+
await scene('initial', async ({
|
|
102
|
+
page,
|
|
103
|
+
expect,
|
|
104
|
+
check,
|
|
105
|
+
route,
|
|
106
|
+
capture,
|
|
107
|
+
artifact,
|
|
108
|
+
baseUrl,
|
|
109
|
+
parseUrl
|
|
110
|
+
}) => {});
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
每个 `metadata.scenes` 项必须由 `scene()` 恰好执行一次,并自行完成导航、API fixture 和状态准备。测试编写者在冻结 SHA-256 前使用同一运行器进行 preflight;定位器、弹窗、fixture 和等待条件等测试机械问题应在草稿阶段修正。preflight 只证明测试代码能够完整执行,不替代独立验收和视觉审查。
|
|
115
|
+
|
|
116
|
+
视觉审查使用 `1400px` 桌面视口和与 Figma 一致的 `375px` 移动视口。截图必须定位到与基准相同的语义节点;比较组件结构、布局和视觉样式,不比较业务文字的字面值、长度或业务图片主体。文案及数据内容由独立功能检查验证。
|
|
117
|
+
|
|
118
|
+
角色所有权、结果结构、视觉映射规则和状态流转详见 `skills/delivery-loop/references/delivery-contract.md`。
|
|
119
|
+
|
|
120
|
+
## 人读报告
|
|
121
|
+
|
|
122
|
+
Checker 和代码审计生成的 Markdown 使用简体中文,包括标题、摘要、表头、检查说明、期望、实际结果和 finding。JSON 字段名、机器状态、ID、路径及原始工具错误保持协议格式;英文原始错误旁提供中文解释。
|
|
123
|
+
|
|
124
|
+
手动调用 `$export-report` 时,Excel 只生成一个 `自测报告` 工作表。每行代表一个真实 UI 组件或完整业务功能;同一组件或功能的文案、样式、布局、响应式行为和相关检查合并到同一行,桌面端与移动端代表截图也放在该行。报告不展示 story ID、scene 名或原子检查明细。新证据通过 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod` 和 `reportDevice` 提供通用分组;旧证据必须由导出 Skill 分析 `spec.md` 后生成经过哈希和完整覆盖校验的语义分组清单,禁止按 E2E scene 猜测。
|
|
125
|
+
|
|
126
|
+
## 交付流程
|
|
127
|
+
|
|
128
|
+
1. `$delivery-loop` 调用 `check_environment` 并记录固定审查点。
|
|
129
|
+
2. `$code-craft` 实现规格。
|
|
130
|
+
3. `$e2e-gen-spec` 创建无依赖场景,运行冻结前 preflight,确认所有 scene 和检查可达后冻结。
|
|
131
|
+
4. `$e2e-ui-checker` 启动声明的应用服务,调用 `run_feature_e2e`,并审查已映射的组件截图。
|
|
132
|
+
5. Blocked 时进入人工决策;只有用户选择 `stop` 才终止。
|
|
133
|
+
6. 验收通过或用户接受 waiver 后,`$code-audit` 审查代码规范和规格符合度。
|
|
134
|
+
|
|
135
|
+
运行器或工具故障不消耗实现者修复回合。隔离 scene 会先继续执行其他场景,再进入 `awaiting-decision`;用户可查看可见浏览器和完整的其余场景证据后重试、确认、跳过、分类为实现/测试缺陷或停止。
|
|
136
|
+
|
|
137
|
+
## 源码开发与验证
|
|
138
|
+
|
|
139
|
+
在 `packages/agents` 目录直接运行源码安装器时,也会使用上述用户级持久化目录:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node plugins/autobest-delivery/scripts/setup.mjs
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
如需只验证插件运行器而不安装插件:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
cd plugins/autobest-delivery/mcp-server
|
|
149
|
+
npm run verify
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
测试套件会启动插件自带的真实浏览器,调用 stdio MCP 工具,验证通过、失败和阻断结果,检查超时清理与路径越界保护,确保冻结场景哈希不变,并确认目标仓库的哨兵清单和依赖没有被修改。
|
|
153
|
+
|
|
154
|
+
CI 可按固定 Playwright 版本缓存 `mcp-server/.runtime/ms-playwright`,然后在 `mcp-server/` 中执行 `npm run verify`。
|
|
155
|
+
|
|
156
|
+
## 状态含义
|
|
157
|
+
|
|
158
|
+
- `passed`:所有运行检查均成功完成。
|
|
159
|
+
- `failed`:执行完整结束,但至少一个业务断言失败。
|
|
160
|
+
- `blocked`:运行器证据不完整,等待人工决策,不直接终止编排。
|
|
161
|
+
- `awaiting-decision`:可见浏览器和证据已保留,等待用户选择下一步。
|
|
162
|
+
- `passed-with-waivers`:代码审计通过,但用户明确接受或跳过了不完整验收证据。
|
|
163
|
+
|
|
164
|
+
通用 Playwright MCP 回放不作为降级方案。冻结场景始终通过本插件的确定性运行器执行。
|
|
Binary file
|