@autobest-ui/agent 1.0.10 → 1.0.11

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.
Files changed (26) hide show
  1. package/bin/sync-assets.test.mjs +2 -0
  2. package/mcp/code-mcp-pr/README.md +3 -2
  3. package/mcp/code-mcp-pr/azure-devops.js +81 -2
  4. package/mcp/code-mcp-pr/index.js +34 -1
  5. package/mcp/code-mcp-pr/index.test.js +161 -11
  6. package/package.json +1 -1
  7. package/plugins/autobest-delivery/.codex-plugin/plugin.json +1 -0
  8. package/plugins/autobest-delivery/README.md +25 -4
  9. package/plugins/autobest-delivery/mcp-server/package.json +1 -1
  10. package/plugins/autobest-delivery/mcp-server/src/fast-runner.mjs +470 -0
  11. package/plugins/autobest-delivery/mcp-server/src/paths.mjs +12 -7
  12. package/plugins/autobest-delivery/mcp-server/src/report.mjs +57 -12
  13. package/plugins/autobest-delivery/mcp-server/src/server.mjs +167 -2
  14. package/plugins/autobest-delivery/mcp-server/tests/fast-runner.test.mjs +232 -0
  15. package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +71 -0
  16. package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +69 -1
  17. package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -3
  18. package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +11 -0
  19. package/plugins/autobest-delivery/skills/delivery-loop-fast/SKILL.md +62 -0
  20. package/plugins/autobest-delivery/skills/delivery-loop-fast/agents/openai.yaml +7 -0
  21. package/plugins/autobest-delivery/skills/export-report/SKILL.md +3 -3
  22. package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +1 -2
  23. package/skills/README.md +1 -0
  24. package/skills/common/code-pr-submit/SKILL.md +25 -4
  25. package/skills/common/repo-setup/SKILL.md +91 -0
  26. package/skills/common/repo-setup/agents/openai.yaml +4 -0
@@ -23,6 +23,7 @@ test("installs common skills into the user-level skills directory", async () =>
23
23
  "code-pr-submit",
24
24
  "figma-ui-capture",
25
25
  "make-spec",
26
+ "repo-setup",
26
27
  "review-from-docs",
27
28
  "ui-prd-scope",
28
29
  ]);
@@ -33,6 +34,7 @@ test("installs common skills into the user-level skills directory", async () =>
33
34
  await access(
34
35
  path.join(result.targetRoot, "make-spec", "scripts", "validate-spec-traceability.mjs")
35
36
  );
37
+ await access(path.join(result.targetRoot, "repo-setup", "SKILL.md"));
36
38
  await access(
37
39
  path.join(result.targetRoot, "review-from-docs", "references", "review-result-schema.md")
38
40
  );
@@ -9,7 +9,7 @@ export AZURE_DEVOPS_PAT="your-personal-access-token"
9
9
  export DINGDING_ALERT_TOKEN="your-dingtalk-robot-access-token"
10
10
  ```
11
11
 
12
- `AZURE_DEVOPS_PAT` 是所有工具访问 Azure DevOps 的必需配置。`DINGDING_ALERT_TOKEN` 仅在 `code-mcp-pr` 同时收到钉钉 markdown 标题和正文时使用。两个 token 都不会作为 MCP 工具参数接收。
12
+ `AZURE_DEVOPS_PAT` 是访问 Azure DevOps 的工具所需配置。`DINGDING_ALERT_TOKEN` 在 `code-mcp-pr` 创建新 PR 后发送通知,或 `send_pr_dingtalk` 为已有 PR 补发通知时使用。两个 token 都不会作为 MCP 工具参数接收。
13
13
 
14
14
  ## 启动
15
15
 
@@ -30,7 +30,8 @@ Codex 配置示例见 [config.toml.example](config.toml.example)。
30
30
 
31
31
  ## 工具
32
32
 
33
- - `code-mcp-pr`:`businessTargetBranch` 作为 ADO `sourceRefName`,`businessSourceBranch` 作为 `targetRefName`。仅在 PR 创建成功且两个 markdown 字段都有值时发送钉钉通知。
33
+ - `code-mcp-pr`:`businessTargetBranch` 作为 ADO `sourceRefName`,`businessSourceBranch` 作为 `targetRefName`。目标为 `main/master` 时要求显式传入 `targetBranchConfirmed=true`。创建前检查相同方向的 active PR;已存在时返回 `alreadyExists=true` 和 `prWebUrl`,不重复发送钉钉。仅在新 PR 创建成功且两个 markdown 字段都有值时发送钉钉通知。
34
+ - `send_pr_dingtalk`:在用户确认后,为一个已存在的 PR 链接补发钉钉 markdown 通知;不会创建或修改 PR。
34
35
  - `get_repo_default_branch`:读取仓库默认分支。
35
36
  - `guess_parent_branch`:以默认分支和 ADO merge-base 信息进行启发式推测。
36
37
  - `get_branch_diff`:读取 source 相对 target 的文件列表和受限 diff 片段。
@@ -28,6 +28,7 @@ const DIFF_FILE_CONCURRENCY = 5;
28
28
  * @property {string} prTitle
29
29
  * @property {string=} prDescription
30
30
  * @property {string[]=} reviewers
31
+ * @property {boolean=} targetBranchConfirmed
31
32
  * @property {string=} dingtalkMarkdownTitle
32
33
  * @property {string=} dingtalkMarkdownContent
33
34
  */
@@ -35,6 +36,9 @@ const DIFF_FILE_CONCURRENCY = 5;
35
36
  /**
36
37
  * @typedef {object} CreatePullRequestResult
37
38
  * @property {boolean} success
39
+ * @property {boolean} created
40
+ * @property {boolean} alreadyExists
41
+ * @property {boolean} confirmationRequired
38
42
  * @property {string|null} prId
39
43
  * @property {string|null} prWebUrl
40
44
  * @property {JsonObject} adoApiPayload
@@ -43,6 +47,13 @@ const DIFF_FILE_CONCURRENCY = 5;
43
47
  * @property {string|null} errorMsg
44
48
  */
45
49
 
50
+ /**
51
+ * @typedef {object} SendPullRequestDingtalkInput
52
+ * @property {string} prWebUrl
53
+ * @property {string} dingtalkMarkdownTitle
54
+ * @property {string} dingtalkMarkdownContent
55
+ */
56
+
46
57
  /**
47
58
  * @typedef {object} DefaultBranchResult
48
59
  * @property {boolean} success
@@ -203,6 +214,37 @@ function pullRequestWebUrl(input, responseBody, prId) {
203
214
  return `https://dev.azure.com/${encodeURIComponent(input.orgName)}/${encodeURIComponent(input.projectName)}/_git/${encodeURIComponent(repositoryName)}/pullrequest/${encodeURIComponent(prId)}`;
204
215
  }
205
216
 
217
+ function activePullRequestsUrl(input, sourceRefName, targetRefName) {
218
+ const query = new URLSearchParams({
219
+ 'searchCriteria.status': 'active',
220
+ 'searchCriteria.sourceRefName': sourceRefName,
221
+ 'searchCriteria.targetRefName': targetRefName,
222
+ '$top': '1'
223
+ });
224
+ return `${repositoryRoot(input)}/pullrequests?${query.toString()}`;
225
+ }
226
+
227
+ async function findActivePullRequest(input, sourceRefName, targetRefName, pat) {
228
+ const result = await adoRequest(activePullRequestsUrl(input, sourceRefName, targetRefName), pat);
229
+ if (!result.ok) return { pullRequest: null, errorMsg: result.errorMsg };
230
+ const body = asObject(result.data);
231
+ const pullRequests = Array.isArray(body.value) ? body.value : [];
232
+ return { pullRequest: asObject(pullRequests[0]), errorMsg: null };
233
+ }
234
+
235
+ function existingPullRequestResult(baseResult, input, pullRequest) {
236
+ const rawPrId = pullRequest.pullRequestId;
237
+ if (rawPrId === undefined || rawPrId === null) return null;
238
+ const prId = String(rawPrId);
239
+ return {
240
+ ...baseResult,
241
+ success: true,
242
+ alreadyExists: true,
243
+ prId,
244
+ prWebUrl: pullRequestWebUrl(input, pullRequest, prId)
245
+ };
246
+ }
247
+
206
248
  async function sendDingtalkMarkdown(title, content) {
207
249
  const token = process.env.DINGDING_ALERT_TOKEN?.trim();
208
250
  if (!token) {
@@ -239,6 +281,9 @@ export async function createPullRequest(input) {
239
281
  };
240
282
  const baseResult = {
241
283
  success: false,
284
+ created: false,
285
+ alreadyExists: false,
286
+ confirmationRequired: false,
242
287
  prId: null,
243
288
  prWebUrl: null,
244
289
  adoApiPayload,
@@ -251,14 +296,35 @@ export async function createPullRequest(input) {
251
296
  return { ...baseResult, errorMsg: 'businessTargetBranch 不能等于 businessSourceBranch' };
252
297
  }
253
298
 
299
+ const targetBranch = branchVersion(targetRefName).toLowerCase();
300
+ if ((targetBranch === 'main' || targetBranch === 'master') && input.targetBranchConfirmed !== true) {
301
+ return {
302
+ ...baseResult,
303
+ confirmationRequired: true,
304
+ errorMsg: `目标分支 ${targetBranch} 是主分支,需要用户明确确认后才能创建PR`
305
+ };
306
+ }
307
+
254
308
  const pat = requirePat();
255
309
  if (!pat) return { ...baseResult, errorMsg: missingPatError() };
256
310
 
311
+ const existingLookup = await findActivePullRequest(input, sourceRefName, targetRefName, pat);
312
+ if (existingLookup.errorMsg) return { ...baseResult, errorMsg: existingLookup.errorMsg };
313
+ const existingResult = existingPullRequestResult(baseResult, input, existingLookup.pullRequest);
314
+ if (existingResult) return existingResult;
315
+
257
316
  const createResult = await adoRequest(`${repositoryRoot(input)}/pullrequests`, pat, {
258
317
  method: 'POST',
259
318
  body: JSON.stringify(adoApiPayload)
260
319
  });
261
- if (!createResult.ok) return { ...baseResult, errorMsg: createResult.errorMsg };
320
+ if (!createResult.ok) {
321
+ if (createResult.status === 409) {
322
+ const raceLookup = await findActivePullRequest(input, sourceRefName, targetRefName, pat);
323
+ const racedExistingResult = existingPullRequestResult(baseResult, input, raceLookup.pullRequest);
324
+ if (racedExistingResult) return racedExistingResult;
325
+ }
326
+ return { ...baseResult, errorMsg: createResult.errorMsg };
327
+ }
262
328
 
263
329
  const response = asObject(createResult.data);
264
330
  const rawPrId = response.pullRequestId;
@@ -268,7 +334,7 @@ export async function createPullRequest(input) {
268
334
 
269
335
  const prId = String(rawPrId);
270
336
  const prWebUrl = pullRequestWebUrl(input, response, prId);
271
- const successResult = { ...baseResult, success: true, prId, prWebUrl };
337
+ const successResult = { ...baseResult, success: true, created: true, prId, prWebUrl };
272
338
  const title = input.dingtalkMarkdownTitle;
273
339
  const content = input.dingtalkMarkdownContent;
274
340
  if (!title?.trim() || !content?.trim()) return successResult;
@@ -282,6 +348,19 @@ export async function createPullRequest(input) {
282
348
  };
283
349
  }
284
350
 
351
+ /** @param {SendPullRequestDingtalkInput} input */
352
+ export async function sendPullRequestDingtalk(input) {
353
+ const renderedContent = input.dingtalkMarkdownContent.replaceAll('{{PR_WEB_URL}}', input.prWebUrl);
354
+ const dingtalk = await sendDingtalkMarkdown(input.dingtalkMarkdownTitle, renderedContent);
355
+ return {
356
+ success: dingtalk.success,
357
+ prWebUrl: input.prWebUrl,
358
+ dingtalkSendSuccess: dingtalk.success,
359
+ dingtalkResponse: dingtalk.response,
360
+ errorMsg: dingtalk.success ? null : boundedJson(dingtalk.response) || '钉钉通知发送失败'
361
+ };
362
+ }
363
+
285
364
  /** @param {RepositoryInput} input @returns {Promise<DefaultBranchResult>} */
286
365
  export async function getRepoDefaultBranch(input) {
287
366
  const pat = requirePat();
@@ -8,7 +8,8 @@ import {
8
8
  getBranchDiff,
9
9
  getRepoDefaultBranch,
10
10
  guessParentBranch,
11
- readFileAtRef
11
+ readFileAtRef,
12
+ sendPullRequestDingtalk
12
13
  } from './azure-devops.js';
13
14
 
14
15
  const server = new McpServer(
@@ -56,6 +57,10 @@ registerJsonTool(
56
57
  prTitle: z.string().min(1).describe('PR标题'),
57
58
  prDescription: z.string().optional().describe('PR描述,可选,条目化变更详情'),
58
59
  reviewers: z.array(z.string().min(1)).optional().describe('评审人ID列表,可选'),
60
+ targetBranchConfirmed: z
61
+ .boolean()
62
+ .optional()
63
+ .describe('目标分支为main或master时,必须在用户明确二次确认后传true'),
59
64
  dingtalkMarkdownTitle: z.string().optional().describe('钉钉通知markdown标题,可选;不传则不发送钉钉'),
60
65
  dingtalkMarkdownContent: z
61
66
  .string()
@@ -67,6 +72,9 @@ registerJsonTool(
67
72
  createPullRequest,
68
73
  errorMsg => ({
69
74
  success: false,
75
+ created: false,
76
+ alreadyExists: false,
77
+ confirmationRequired: false,
70
78
  prId: null,
71
79
  prWebUrl: null,
72
80
  adoApiPayload: {},
@@ -76,6 +84,31 @@ registerJsonTool(
76
84
  })
77
85
  );
78
86
 
87
+ registerJsonTool(
88
+ 'send_pr_dingtalk',
89
+ {
90
+ title: '发送已有 PR 钉钉通知',
91
+ description: '为已确认的 PR 链接发送钉钉 markdown 通知;不会创建或修改 PR。',
92
+ inputSchema: {
93
+ prWebUrl: z.string().url().describe('已存在 PR 的可访问 Web 链接'),
94
+ dingtalkMarkdownTitle: z.string().min(1).describe('钉钉通知 markdown 标题'),
95
+ dingtalkMarkdownContent: z
96
+ .string()
97
+ .min(1)
98
+ .describe('钉钉通知 markdown 正文;内部会替换{{PR_WEB_URL}}占位符为 PR 链接')
99
+ },
100
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
101
+ },
102
+ sendPullRequestDingtalk,
103
+ errorMsg => ({
104
+ success: false,
105
+ prWebUrl: null,
106
+ dingtalkSendSuccess: false,
107
+ dingtalkResponse: null,
108
+ errorMsg
109
+ })
110
+ );
111
+
79
112
  registerJsonTool(
80
113
  'get_repo_default_branch',
81
114
  {
@@ -12,7 +12,8 @@ import {
12
12
  getRepoDefaultBranch,
13
13
  guessParentBranch,
14
14
  normalizeBranchRef,
15
- readFileAtRef
15
+ readFileAtRef,
16
+ sendPullRequestDingtalk
16
17
  } from './azure-devops.js';
17
18
 
18
19
  const directory = path.dirname(fileURLToPath(import.meta.url));
@@ -57,6 +58,7 @@ test('creates the reverse PR with the required branch direction and sends render
57
58
  globalThis.fetch = async (url, options) => {
58
59
  calls.push({ url: String(url), options });
59
60
  if (String(url).startsWith('https://dev.azure.com/')) {
61
+ if (!options?.method || options.method === 'GET') return jsonResponse({ value: [] });
60
62
  return jsonResponse({
61
63
  pullRequestId: 42,
62
64
  repository: { name: 'web' },
@@ -75,12 +77,15 @@ test('creates the reverse PR with the required branch direction and sends render
75
77
  businessTargetBranch: 'release/with-change',
76
78
  businessSourceBranch: 'main',
77
79
  prTitle: 'Reverse PR',
80
+ targetBranchConfirmed: true,
78
81
  reviewers: ['reviewer-id'],
79
82
  dingtalkMarkdownTitle: 'PR created',
80
83
  dingtalkMarkdownContent: '[Open PR]({{PR_WEB_URL}})'
81
84
  });
82
85
 
83
86
  assert.equal(result.success, true);
87
+ assert.equal(result.created, true);
88
+ assert.equal(result.alreadyExists, false);
84
89
  assert.equal(result.dingtalkSendSuccess, true);
85
90
  assert.deepEqual(result.adoApiPayload, {
86
91
  sourceRefName: 'refs/heads/release/with-change',
@@ -88,14 +93,14 @@ test('creates the reverse PR with the required branch direction and sends render
88
93
  title: 'Reverse PR',
89
94
  reviewers: [{ id: 'reviewer-id' }]
90
95
  });
91
- assert.equal(calls.length, 2);
92
- assert.match(calls[0].url, new RegExp(`api-version=${API_VERSION.replaceAll('.', '\\.')}`));
93
- assert.deepEqual(JSON.parse(calls[0].options.body), result.adoApiPayload);
96
+ assert.equal(calls.length, 3);
97
+ assert.match(calls[1].url, new RegExp(`api-version=${API_VERSION.replaceAll('.', '\\.')}`));
98
+ assert.deepEqual(JSON.parse(calls[1].options.body), result.adoApiPayload);
94
99
  assert.equal(
95
- JSON.parse(calls[1].options.body).markdown.text,
100
+ JSON.parse(calls[2].options.body).markdown.text,
96
101
  '[Open PR](https://dev.azure.com/org/project/_git/web/pullrequest/42)'
97
102
  );
98
- assert.doesNotMatch(calls[0].options.headers.Authorization, /secret-pat/);
103
+ assert.doesNotMatch(calls[1].options.headers.Authorization, /secret-pat/);
99
104
  });
100
105
  } finally {
101
106
  globalThis.fetch = originalFetch;
@@ -105,8 +110,9 @@ test('creates the reverse PR with the required branch direction and sends render
105
110
  test('does not call DingTalk after an Azure DevOps create failure', async () => {
106
111
  let callCount = 0;
107
112
  const originalFetch = globalThis.fetch;
108
- globalThis.fetch = async () => {
113
+ globalThis.fetch = async (_url, options) => {
109
114
  callCount += 1;
115
+ if (!options?.method || options.method === 'GET') return jsonResponse({ value: [] });
110
116
  return jsonResponse({ message: 'denied' }, 401);
111
117
  };
112
118
 
@@ -119,12 +125,13 @@ test('does not call DingTalk after an Azure DevOps create failure', async () =>
119
125
  businessTargetBranch: 'feature',
120
126
  businessSourceBranch: 'main',
121
127
  prTitle: 'PR',
128
+ targetBranchConfirmed: true,
122
129
  dingtalkMarkdownTitle: 'title',
123
130
  dingtalkMarkdownContent: 'content'
124
131
  });
125
132
  assert.equal(result.success, false);
126
133
  assert.equal(result.dingtalkSendSuccess, null);
127
- assert.equal(callCount, 1);
134
+ assert.equal(callCount, 2);
128
135
  });
129
136
  } finally {
130
137
  globalThis.fetch = originalFetch;
@@ -133,7 +140,10 @@ test('does not call DingTalk after an Azure DevOps create failure', async () =>
133
140
 
134
141
  test('keeps PR success when DingTalk token is missing', async () => {
135
142
  const originalFetch = globalThis.fetch;
136
- globalThis.fetch = async () => jsonResponse({ pullRequestId: 7 });
143
+ globalThis.fetch = async (_url, options) =>
144
+ !options?.method || options.method === 'GET'
145
+ ? jsonResponse({ value: [] })
146
+ : jsonResponse({ pullRequestId: 7 });
137
147
  try {
138
148
  await withEnvironment({ AZURE_DEVOPS_PAT: 'pat', DINGDING_ALERT_TOKEN: undefined }, async () => {
139
149
  const result = await createPullRequest({
@@ -143,6 +153,7 @@ test('keeps PR success when DingTalk token is missing', async () => {
143
153
  businessTargetBranch: 'feature',
144
154
  businessSourceBranch: 'main',
145
155
  prTitle: 'PR',
156
+ targetBranchConfirmed: true,
146
157
  dingtalkMarkdownTitle: 'title',
147
158
  dingtalkMarkdownContent: 'content'
148
159
  });
@@ -155,6 +166,138 @@ test('keeps PR success when DingTalk token is missing', async () => {
155
166
  }
156
167
  });
157
168
 
169
+ test('requires explicit confirmation before targeting main or master', async () => {
170
+ const originalFetch = globalThis.fetch;
171
+ globalThis.fetch = async () => assert.fail('fetch must not be called before confirmation');
172
+ try {
173
+ for (const targetBranch of ['main', 'refs/heads/master', 'MAIN']) {
174
+ const result = await createPullRequest({
175
+ orgName: 'org',
176
+ projectName: 'project',
177
+ repoId: 'repo',
178
+ businessTargetBranch: 'feature',
179
+ businessSourceBranch: targetBranch,
180
+ prTitle: 'PR'
181
+ });
182
+ assert.equal(result.success, false);
183
+ assert.equal(result.confirmationRequired, true);
184
+ assert.match(result.errorMsg, /明确确认/);
185
+ }
186
+ } finally {
187
+ globalThis.fetch = originalFetch;
188
+ }
189
+ });
190
+
191
+ test('returns the existing active PR link without sending DingTalk again', async () => {
192
+ const calls = [];
193
+ const originalFetch = globalThis.fetch;
194
+ globalThis.fetch = async (url, options) => {
195
+ calls.push({ url: String(url), options });
196
+ return jsonResponse({
197
+ value: [
198
+ {
199
+ pullRequestId: 88,
200
+ repository: { name: 'web' },
201
+ _links: { web: { href: 'https://dev.azure.com/org/project/_git/web/pullrequest/88' } }
202
+ }
203
+ ]
204
+ });
205
+ };
206
+
207
+ try {
208
+ await withEnvironment({ AZURE_DEVOPS_PAT: 'pat', DINGDING_ALERT_TOKEN: 'ding-token' }, async () => {
209
+ const result = await createPullRequest({
210
+ orgName: 'org',
211
+ projectName: 'project',
212
+ repoId: 'repo',
213
+ businessTargetBranch: 'feature',
214
+ businessSourceBranch: 'develop',
215
+ prTitle: 'PR',
216
+ dingtalkMarkdownTitle: 'title',
217
+ dingtalkMarkdownContent: '[PR]({{PR_WEB_URL}})'
218
+ });
219
+
220
+ assert.equal(result.success, true);
221
+ assert.equal(result.created, false);
222
+ assert.equal(result.alreadyExists, true);
223
+ assert.equal(result.prId, '88');
224
+ assert.equal(result.prWebUrl, 'https://dev.azure.com/org/project/_git/web/pullrequest/88');
225
+ assert.equal(result.dingtalkSendSuccess, null);
226
+ assert.equal(calls.length, 1);
227
+ const query = new URL(calls[0].url).searchParams;
228
+ assert.equal(query.get('searchCriteria.status'), 'active');
229
+ assert.equal(query.get('searchCriteria.sourceRefName'), 'refs/heads/feature');
230
+ assert.equal(query.get('searchCriteria.targetRefName'), 'refs/heads/develop');
231
+ assert.equal(calls[0].options?.method, undefined);
232
+ });
233
+ } finally {
234
+ globalThis.fetch = originalFetch;
235
+ }
236
+ });
237
+
238
+ test('returns the existing PR link when creation races with another submitter', async () => {
239
+ let adoCall = 0;
240
+ const originalFetch = globalThis.fetch;
241
+ globalThis.fetch = async () => {
242
+ adoCall += 1;
243
+ if (adoCall === 1) return jsonResponse({ value: [] });
244
+ if (adoCall === 2) return jsonResponse({ message: 'active pull request already exists' }, 409);
245
+ return jsonResponse({
246
+ value: [
247
+ {
248
+ pullRequestId: 99,
249
+ repository: { name: 'web' },
250
+ _links: { web: { href: 'https://dev.azure.com/org/project/_git/web/pullrequest/99' } }
251
+ }
252
+ ]
253
+ });
254
+ };
255
+
256
+ try {
257
+ await withEnvironment({ AZURE_DEVOPS_PAT: 'pat' }, async () => {
258
+ const result = await createPullRequest({
259
+ orgName: 'org',
260
+ projectName: 'project',
261
+ repoId: 'repo',
262
+ businessTargetBranch: 'feature',
263
+ businessSourceBranch: 'develop',
264
+ prTitle: 'PR'
265
+ });
266
+ assert.equal(result.success, true);
267
+ assert.equal(result.created, false);
268
+ assert.equal(result.alreadyExists, true);
269
+ assert.equal(result.prWebUrl, 'https://dev.azure.com/org/project/_git/web/pullrequest/99');
270
+ assert.equal(adoCall, 3);
271
+ });
272
+ } finally {
273
+ globalThis.fetch = originalFetch;
274
+ }
275
+ });
276
+
277
+ test('sends a confirmed DingTalk notification for an existing PR link', async () => {
278
+ const originalFetch = globalThis.fetch;
279
+ let markdownText;
280
+ globalThis.fetch = async (_url, options) => {
281
+ markdownText = JSON.parse(options.body).markdown.text;
282
+ return jsonResponse({ errcode: 0, errmsg: 'ok' });
283
+ };
284
+
285
+ try {
286
+ await withEnvironment({ DINGDING_ALERT_TOKEN: 'ding-token' }, async () => {
287
+ const result = await sendPullRequestDingtalk({
288
+ prWebUrl: 'https://dev.azure.com/org/project/_git/web/pullrequest/88',
289
+ dingtalkMarkdownTitle: 'Existing PR',
290
+ dingtalkMarkdownContent: '[Open]({{PR_WEB_URL}})'
291
+ });
292
+ assert.equal(result.success, true);
293
+ assert.equal(result.dingtalkSendSuccess, true);
294
+ assert.equal(markdownText, '[Open](https://dev.azure.com/org/project/_git/web/pullrequest/88)');
295
+ });
296
+ } finally {
297
+ globalThis.fetch = originalFetch;
298
+ }
299
+ });
300
+
158
301
  test('rejects identical normalized PR branches without making an HTTP request', async () => {
159
302
  const originalFetch = globalThis.fetch;
160
303
  globalThis.fetch = async () => assert.fail('fetch must not be called');
@@ -266,7 +409,7 @@ test('returns a structured missing-PAT error', async () => {
266
409
  });
267
410
  });
268
411
 
269
- test('exposes all five tools over stdio', async () => {
412
+ test('exposes all six tools over stdio', async () => {
270
413
  const client = new Client({ name: 'code-mcp-pr-test', version: '1.0.0' });
271
414
  const transport = new StdioClientTransport({
272
415
  command: process.execPath,
@@ -281,7 +424,14 @@ test('exposes all five tools over stdio', async () => {
281
424
  const { tools } = await client.listTools();
282
425
  assert.deepEqual(
283
426
  tools.map(tool => tool.name),
284
- ['code-mcp-pr', 'get_repo_default_branch', 'guess_parent_branch', 'get_branch_diff', 'read_file_at_ref']
427
+ [
428
+ 'code-mcp-pr',
429
+ 'send_pr_dingtalk',
430
+ 'get_repo_default_branch',
431
+ 'guess_parent_branch',
432
+ 'get_branch_diff',
433
+ 'read_file_at_ref'
434
+ ]
285
435
  );
286
436
  } finally {
287
437
  await client.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobest-ui/agent",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "private": false,
5
5
  "description": "Autobest Agent skills/plugins/mcp assets + sync cli",
6
6
  "files": [
@@ -18,6 +18,7 @@
18
18
  ],
19
19
  "defaultPrompt": [
20
20
  "\u9488\u5bf9\u5f53\u524d\u529f\u80fd\u89c4\u683c\u8fd0\u884c\u5b8c\u6574\u4ea4\u4ed8\u95ed\u73af\u3002",
21
+ "\u4f7f\u7528\u5feb\u901f\u4ea4\u4ed8\u95ed\u73af\u5b9e\u73b0\u5f53\u524d\u529f\u80fd\uff0c\u5b8c\u6210\u4ea4\u4e92\u5f0f Playwright \u81ea\u6d4b\u3001\u622a\u56fe\u548c\u72ec\u7acb\u4ee3\u7801\u5ba1\u8ba1\u3002",
21
22
  "\u68c0\u67e5\u9694\u79bb E2E \u8fd0\u884c\u5668\u662f\u5426\u5df2\u7ecf\u5c31\u7eea\u3002"
22
23
  ]
23
24
  },
@@ -1,12 +1,12 @@
1
1
  # Autobest 交付插件
2
2
 
3
- `autobest-delivery` 将 Autobest 交付闭环 Skill 与插件自带的 Playwright MCP 运行器打包在一起。目标业务仓库只需提供应用源码、功能规格和启动命令,不需要安装 Playwright 依赖。
3
+ `autobest-delivery` 将 Autobest 完整及快速交付闭环 Skill 与插件自带的 Playwright MCP 运行器打包在一起。目标业务仓库只需提供应用源码、功能规格和启动命令,不需要安装 Playwright 依赖。
4
4
 
5
5
  ## 隔离机制
6
6
 
7
- 运行器的 Node 依赖位于 `mcp-server/node_modules/`,Chromium 位于 `mcp-server/.runtime/ms-playwright/`,这两个路径均被插件忽略。工具只接受绝对路径形式的目标工作区,并且仅允许读取冻结场景、向指定功能输出目录写入证据。
7
+ 运行器的 Node 依赖位于 `mcp-server/node_modules/`,Chromium 位于 `mcp-server/.runtime/ms-playwright/`,这两个路径均被插件忽略。工具只接受绝对路径形式的目标工作区,并且仅允许读取所需输入、向工作区内指定功能输出目录写入证据。标准运行器会额外校验冻结场景在执行前后的 SHA-256。
8
8
 
9
- 运行器不会在目标工作区调用包管理器,也不会修改目标仓库的清单、锁文件、`node_modules`、配置或源码。每次执行前后都会校验冻结场景的 SHA-256。
9
+ 运行器不会在目标工作区调用包管理器,也不会修改目标仓库的清单、锁文件、`node_modules`、配置或源码。标准 `run_feature_e2e` 每次执行前后都会校验冻结场景的 SHA-256。
10
10
 
11
11
  ## 使用 npx 安装
12
12
 
@@ -71,6 +71,21 @@ npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup --help
71
71
 
72
72
  新版场景通过 `scene(id, callback)` 隔离页面和执行错误。一个 scene 的定位器、弹窗或准备步骤阻塞时,运行器会记录该 blocker 并继续执行后续独立 scene,全部 scene 尝试结束后再返回 `blocked`。旧版直接使用顶层 `page` 的场景仍可运行,但无法在未捕获异常后继续。
73
73
 
74
+ ### 快速交互式 Playwright 工具
75
+
76
+ `$delivery-loop-fast` 使用以下工具直接操作浏览器,不创建或执行 `e2e/e2e.feature.mjs`:
77
+
78
+ - `start_fast_check`:启动会话并导航到应用;API 模式支持 `real`、`mock` 和无需业务 API 证明的 `none`。
79
+ - `fast_browser_snapshot`:读取当前页面的可访问性快照。
80
+ - `fast_browser_action`:执行导航、点击、填写、选择、按键、等待和视口切换。
81
+ - `mock_fast_check_route`:仅在 `mock` 模式安装受控 JSON 响应。
82
+ - `fast_browser_check`:执行并记录结构化页面检查。
83
+ - `capture_fast_check`:保存按报告功能组映射的桌面端或移动端截图。
84
+ - `finish_fast_check`:写入结果 JSON、中文 Markdown、Trace 和截图索引后关闭会话。
85
+ - `abort_fast_check`:放弃未完成的会话并关闭浏览器。
86
+
87
+ 每轮产物位于 `fast-check/runs/iteration-NN/`,并以 `evidenceSource: interactive-playwright` 明确标识为实现者交互式自测,不属于独立 Checker 证据。后续 `$export-report` 可直接读取最新快速结果和截图。
88
+
74
89
  ### `resolve_blocked_run`
75
90
 
76
91
  用户查看 Blocked 浏览器后,由创建该会话的验收代理调用此工具。工具将选择写入 `human-decision.json` 并关闭浏览器;`accept` 或 `skip` 会形成 waiver,而不是伪装成完整通过。
@@ -144,7 +159,7 @@ export default async function run({
144
159
 
145
160
  Checker 和代码审计生成的 Markdown 使用简体中文,包括标题、摘要、表头、检查说明、期望、实际结果和 finding。JSON 字段名、机器状态、ID、路径及原始工具错误保持协议格式;英文原始错误旁提供中文解释。
146
161
 
147
- 手动调用 `$export-report` 时,Excel 只生成一个 `自测报告` 工作表。每行代表一个真实 UI 组件或完整业务功能;同一组件或功能的文案、样式、布局、响应式行为和相关检查合并到同一行,桌面端与移动端代表截图也放在该行。报告不展示 REQ/story ID、scene 名或原子检查明细。新证据通过 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod` 和 `reportDevice` 提供通用分组,并通过 `reportCaptures` 留下运行时 UI 截图。`visualMappings` 只负责有 Figma 基准时的视觉一致性验收;普通报告截图不表示视觉比对通过。旧证据必须由导出 Skill 分析 `spec.md` 后生成经过哈希和完整覆盖校验的语义分组清单,禁止按 E2E scene 猜测。
162
+ 手动调用 `$export-report` 时,Excel 可读取标准 Runner/Checker 证据或快速交互式自测证据,并且只生成一个 `自测报告` 工作表。每行代表一个真实 UI 组件或完整业务功能;同一组件或功能的文案、样式、布局、响应式行为和相关检查合并到同一行,桌面端与移动端代表截图也放在该行。报告不展示 REQ/story ID、scene 名或原子检查明细。新证据通过 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod` 和 `reportDevice` 提供通用分组,并通过 `reportCaptures` 留下运行时 UI 截图。`visualMappings` 只负责有 Figma 基准时的视觉一致性验收;普通报告截图不表示视觉比对通过。旧证据必须由导出 Skill 分析 `spec.md` 后生成经过哈希和完整覆盖校验的语义分组清单,禁止按 E2E scene 猜测。
148
163
 
149
164
  ## 交付流程
150
165
 
@@ -157,6 +172,12 @@ Checker 和代码审计生成的 Markdown 使用简体中文,包括标题、
157
172
  5. Blocked 时进入人工决策;只有用户选择 `stop` 才终止。
158
173
  6. 验收通过或用户接受 waiver 后,`$code-audit` 审查代码规范和规格符合度。
159
174
 
175
+ ### 快速流程 `$delivery-loop-fast`
176
+
177
+ 快速流程由用户显式选择,不进行风险评分,也不会自动切换为完整流程。当前 Agent 根据 spec 实现代码、执行仓库定向检查,并使用上述交互式 Playwright 工具自测、截图和自查自纠;业务代码或测试步骤变化后开启新的完整自测 iteration。最新快速自测通过后,再由全新的独立 `$code-audit` 代理审查固定范围。审计发现阻断问题时,当前 Agent 修复后必须重新完成仓库检查、快速自测和独立审计。
178
+
179
+ 快速流程不生成持久化 E2E 场景、测试编写者结果或 Checker 结果。完成条件仍包括:最新交互式自测通过、每个报告功能组具备截图、独立代码审计通过,并且证据与状态一致。现有 `$delivery-loop` 的角色隔离、冻结 E2E 和可见验收流程保持不变。
180
+
160
181
  运行器或工具故障不消耗实现者修复回合。隔离 scene 会先继续执行其他场景,再进入 `awaiting-decision`;用户可查看可见浏览器和完整的其余场景证据后重试、确认、跳过、分类为实现/测试缺陷或停止。
161
182
 
162
183
  ## 源码开发与验证
@@ -7,7 +7,7 @@
7
7
  "node": ">=20"
8
8
  },
9
9
  "scripts": {
10
- "lint": "node --check ../scripts/setup.mjs && node --check ../scripts/start-mcp.mjs && node --check ../scripts/export-delivery-report.mjs && node --check src/paths.mjs && node --check src/report.mjs && node --check src/runner.mjs && node --check src/server.mjs && node --check tests/fixture-server.mjs && node --check tests/report.test.mjs && node --check tests/runner.test.mjs && node --check tests/mcp-smoke.test.mjs && node --check tests/fixtures/basic.feature.mjs",
10
+ "lint": "node --check ../scripts/setup.mjs && node --check ../scripts/start-mcp.mjs && node --check ../scripts/export-delivery-report.mjs && node --check src/paths.mjs && node --check src/report.mjs && node --check src/runner.mjs && node --check src/fast-runner.mjs && node --check src/server.mjs && node --check tests/fixture-server.mjs && node --check tests/report.test.mjs && node --check tests/runner.test.mjs && node --check tests/fast-runner.test.mjs && node --check tests/mcp-smoke.test.mjs && node --check tests/fixtures/basic.feature.mjs",
11
11
  "test": "node --test --test-concurrency=1 tests/*.test.mjs",
12
12
  "verify": "npm run lint && npm test"
13
13
  },