ai-project-manage-cli 6.0.54 → 6.0.56

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/dist/index.js CHANGED
@@ -999,6 +999,19 @@ function reasonForCleanup(sessionId, sessionStatusById) {
999
999
  }
1000
1000
  return "\u4FDD\u7559";
1001
1001
  }
1002
+ async function isBranchMergedIntoDefault(cwd, branch, defaultBranch) {
1003
+ const ref = await localBranchExists2(cwd, branch) ? branch : `origin/${branch}`;
1004
+ try {
1005
+ await execGit3(
1006
+ cwd,
1007
+ ["merge-base", "--is-ancestor", ref, `origin/${defaultBranch}`],
1008
+ true
1009
+ );
1010
+ return true;
1011
+ } catch {
1012
+ return false;
1013
+ }
1014
+ }
1002
1015
  async function runCleanBranches(options = {}) {
1003
1016
  const cwd = options.cwd ?? process.cwd();
1004
1017
  const dryRun = options.dryRun ?? false;
@@ -1035,11 +1048,20 @@ async function runCleanBranches(options = {}) {
1035
1048
  }
1036
1049
  let currentBranch = await getCurrentBranch2(cwd);
1037
1050
  let defaultBranch = null;
1051
+ if (dryRun) {
1052
+ defaultBranch = await resolveDefaultBranch(cwd);
1053
+ }
1038
1054
  for (const item of toDelete) {
1039
1055
  const { branch, sessionId, reason } = item;
1040
1056
  const label = `${branch} (${sessionId}: ${reason})`;
1041
1057
  if (dryRun) {
1042
- console.log(`[apm] [dry-run] \u5C06\u5220\u9664 ${label}`);
1058
+ const merged = await isBranchMergedIntoDefault(
1059
+ cwd,
1060
+ branch,
1061
+ defaultBranch
1062
+ );
1063
+ const mergeTag = merged ? "\u5DF2\u5408\u5E76" : "\u672A\u5408\u5E76";
1064
+ console.log(`[apm] [dry-run] \u5C06\u5220\u9664 ${label} [${mergeTag}]`);
1043
1065
  continue;
1044
1066
  }
1045
1067
  if (currentBranch === branch) {
@@ -1482,7 +1504,7 @@ async function runUpdate() {
1482
1504
  const latest = await fetchLatestPublishedVersion();
1483
1505
  if (latest && current === latest) {
1484
1506
  console.log(`[apm] \u5DF2\u662F\u6700\u65B0\u7248\u672C ${current}`);
1485
- return;
1507
+ return { didUpdate: false };
1486
1508
  }
1487
1509
  if (!npmAvailable()) {
1488
1510
  console.error(
@@ -1512,22 +1534,22 @@ async function runUpdate() {
1512
1534
  `[apm] \u66F4\u65B0\u5B8C\u6210\u3002\u82E5\u7248\u672C\u53F7\u672A\u53D8\u5316\uFF0C\u8BF7\u5728\u65B0\u7EC8\u7AEF\u6267\u884C apm -V \u786E\u8BA4\uFF08\u5168\u5C40\u5B89\u88C5\u8DEF\u5F84\u53EF\u80FD\u672A\u5237\u65B0\uFF09`
1513
1535
  );
1514
1536
  }
1537
+ return { didUpdate: true };
1515
1538
  }
1516
1539
 
1517
1540
  // src/commands/update-skills.ts
1518
1541
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
1519
1542
  import { join as join10 } from "path";
1520
- async function runUpdateSkills() {
1521
- const apmDir = workspaceApmDir();
1522
- if (!existsSync6(apmDir)) {
1523
- console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1524
- process.exit(1);
1543
+ async function syncWorkspaceSkills(cfg, workdir) {
1544
+ const apmDir = workspaceApmDir(workdir);
1545
+ const fsApmDir = toFsPath(apmDir);
1546
+ if (!existsSync6(fsApmDir)) {
1547
+ throw new Error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1525
1548
  }
1526
- const apmStat = statSync3(apmDir);
1549
+ const apmStat = statSync3(fsApmDir);
1527
1550
  if (!apmStat.isDirectory()) {
1528
1551
  throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${apmDir}`);
1529
1552
  }
1530
- const cfg = await ensureLoggedConfig();
1531
1553
  const api = createApmApiClient(cfg);
1532
1554
  const { list } = await api.cli.listSkills({});
1533
1555
  if (syncAgentsGuide(apmDir)) {
@@ -1539,7 +1561,7 @@ async function runUpdateSkills() {
1539
1561
  console.log(`[apm] \u5DF2\u540C\u6B65\u57FA\u7840\u89C4\u5219: rules/${name}`);
1540
1562
  }
1541
1563
  const skillsDir = join10(apmDir, "skills");
1542
- mkdirSync4(skillsDir, { recursive: true });
1564
+ mkdirSync4(toFsPath(skillsDir), { recursive: true });
1543
1565
  const baseNames = syncBaseSkills(skillsDir);
1544
1566
  for (const name of baseNames) {
1545
1567
  console.log(`[apm] \u5DF2\u540C\u6B65\u57FA\u7840\u6280\u80FD: skills/${name}/`);
@@ -1563,6 +1585,19 @@ async function runUpdateSkills() {
1563
1585
  `[apm] \u540C\u6B65\u5B8C\u6210\uFF1A${ruleNames.length} \u4E2A\u57FA\u7840\u89C4\u5219\uFF0C${baseNames.length} \u4E2A\u57FA\u7840\u6280\u80FD\uFF0C${written.length} \u4E2A\u8865\u5145\u6280\u80FD`
1564
1586
  );
1565
1587
  }
1588
+ async function runUpdateSkills() {
1589
+ const apmDir = workspaceApmDir();
1590
+ if (!existsSync6(apmDir)) {
1591
+ console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
1592
+ process.exit(1);
1593
+ }
1594
+ const apmStat = statSync3(apmDir);
1595
+ if (!apmStat.isDirectory()) {
1596
+ throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${apmDir}`);
1597
+ }
1598
+ const cfg = await ensureLoggedConfig();
1599
+ await syncWorkspaceSkills(cfg, resolveWorkdirPath());
1600
+ }
1566
1601
 
1567
1602
  // src/commands/sync-deploy-config.ts
1568
1603
  import { existsSync as existsSync7, statSync as statSync4 } from "fs";
@@ -1741,6 +1776,7 @@ async function runUpdateMessageStatus(options) {
1741
1776
  }
1742
1777
 
1743
1778
  // src/commands/connect.ts
1779
+ import { spawnSync as spawnSync2 } from "child_process";
1744
1780
  import WebSocket from "ws";
1745
1781
 
1746
1782
  // src/ws/protocol.ts
@@ -2395,6 +2431,56 @@ async function runCursorAgent(cfg, ctx, options) {
2395
2431
  }
2396
2432
  }
2397
2433
 
2434
+ // src/commands/connect/cli-version-sync.ts
2435
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
2436
+ import { join as join12 } from "path";
2437
+ var CLI_VERSION_FILE = ".cli-version.json";
2438
+ function manifestPath(apmDir) {
2439
+ return join12(apmDir, CLI_VERSION_FILE);
2440
+ }
2441
+ function loadManifest3(apmDir) {
2442
+ const path10 = toFsPath(manifestPath(apmDir));
2443
+ if (!existsSync11(path10)) {
2444
+ return null;
2445
+ }
2446
+ try {
2447
+ const parsed = JSON.parse(
2448
+ readFileSync9(path10, "utf8")
2449
+ );
2450
+ if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
2451
+ return parsed;
2452
+ }
2453
+ } catch {
2454
+ }
2455
+ return null;
2456
+ }
2457
+ function saveManifest3(apmDir, cliVersion) {
2458
+ const manifest = { version: 1, cliVersion };
2459
+ writeFileSync10(
2460
+ toFsPath(manifestPath(apmDir)),
2461
+ `${JSON.stringify(manifest, null, 2)}
2462
+ `,
2463
+ "utf8"
2464
+ );
2465
+ }
2466
+ var syncedInSession = /* @__PURE__ */ new Map();
2467
+ function shouldSyncSkillsForCliVersion(workdir, currentVersion) {
2468
+ const cached = syncedInSession.get(workdir);
2469
+ if (cached === currentVersion) {
2470
+ return false;
2471
+ }
2472
+ const stored = loadManifest3(workspaceApmDir(workdir));
2473
+ if (stored?.cliVersion === currentVersion) {
2474
+ syncedInSession.set(workdir, currentVersion);
2475
+ return false;
2476
+ }
2477
+ return true;
2478
+ }
2479
+ function markSkillsSyncedForCliVersion(workdir, cliVersion) {
2480
+ saveManifest3(workspaceApmDir(workdir), cliVersion);
2481
+ syncedInSession.set(workdir, cliVersion);
2482
+ }
2483
+
2398
2484
  // src/commands/connect/pre-step-cache.ts
2399
2485
  var PULL_TTL_MS = 3e4;
2400
2486
  function sessionWorkdirKey(sessionId, workdir) {
@@ -2513,6 +2599,19 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
2513
2599
  } else {
2514
2600
  console.log(`[apm] step=commit-pull skipped sessionId=${msg.sessionId}`);
2515
2601
  }
2602
+ const cliVersion = readCliVersion();
2603
+ if (shouldSyncSkillsForCliVersion(workdir, cliVersion)) {
2604
+ if (signal.aborted) return;
2605
+ console.log(
2606
+ `[apm] CLI \u7248\u672C ${cliVersion} \u4E0E\u5DE5\u4F5C\u533A\u8BB0\u5F55\u4E0D\u4E00\u81F4\uFF0C\u6267\u884C update-skills`
2607
+ );
2608
+ await runStep("update-skills", async () => {
2609
+ await syncWorkspaceSkills(cfg, workdir);
2610
+ markSkillsSyncedForCliVersion(workdir, cliVersion);
2611
+ });
2612
+ } else {
2613
+ console.log(`[apm] step=update-skills skipped workdir=${workdir}`);
2614
+ }
2516
2615
  if (signal.aborted) return;
2517
2616
  await runStep(
2518
2617
  "cursor-agent",
@@ -2584,7 +2683,24 @@ function startHeartbeat(ws, clientMachineId) {
2584
2683
  const timer = setInterval(send, HEARTBEAT_MS);
2585
2684
  return () => clearInterval(timer);
2586
2685
  }
2686
+ function reexecConnect(options) {
2687
+ const args = [process.argv[1], "connect"];
2688
+ const server = options.server?.trim();
2689
+ if (server) {
2690
+ args.push("--server", server);
2691
+ }
2692
+ const result = spawnSync2(process.execPath, args, { stdio: "inherit" });
2693
+ if (result.error) {
2694
+ console.error("[apm] \u91CD\u542F connect \u5931\u8D25:", result.error.message);
2695
+ process.exit(1);
2696
+ }
2697
+ process.exit(result.status ?? 0);
2698
+ }
2587
2699
  async function runConnect(options) {
2700
+ const { didUpdate } = await runUpdate();
2701
+ if (didUpdate) {
2702
+ reexecConnect(options);
2703
+ }
2588
2704
  const cfg = await ensureLoggedConfig();
2589
2705
  if (options.server?.trim()) {
2590
2706
  cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
@@ -2736,19 +2852,19 @@ async function runCreatePr(options) {
2736
2852
  import path5 from "node:path";
2737
2853
 
2738
2854
  // src/commands/deploy/internal/apm-config.ts
2739
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
2855
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
2740
2856
  import { resolve as resolve4 } from "node:path";
2741
2857
  function loadApmConfig(options) {
2742
2858
  const p = resolve4(
2743
2859
  process.cwd(),
2744
2860
  options?.configPath ?? resolve4(workspaceApmDir(), "apm.config.json")
2745
2861
  );
2746
- if (!existsSync11(p)) {
2862
+ if (!existsSync12(p)) {
2747
2863
  console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
2748
2864
  process.exit(1);
2749
2865
  }
2750
2866
  try {
2751
- const raw = readFileSync9(p, "utf8");
2867
+ const raw = readFileSync10(p, "utf8");
2752
2868
  return JSON.parse(raw);
2753
2869
  } catch (e) {
2754
2870
  console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
@@ -2870,7 +2986,7 @@ import path4 from "node:path";
2870
2986
  import Docker from "dockerode";
2871
2987
 
2872
2988
  // src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
2873
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
2989
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
2874
2990
  import path from "node:path";
2875
2991
  function asOptionalTlsBuffer(value) {
2876
2992
  if (typeof value !== "string") {
@@ -2882,8 +2998,8 @@ function asOptionalTlsBuffer(value) {
2882
2998
  if (normalized === "") {
2883
2999
  return void 0;
2884
3000
  }
2885
- if (existsSync12(normalized)) {
2886
- return readFileSync10(normalized);
3001
+ if (existsSync13(normalized)) {
3002
+ return readFileSync11(normalized);
2887
3003
  }
2888
3004
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
2889
3005
  if (looksLikePath) {
@@ -3093,7 +3209,7 @@ var DockerodeClient = class {
3093
3209
  var createDockerodeClient = (config) => new DockerodeClient(config);
3094
3210
 
3095
3211
  // src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
3096
- import { existsSync as existsSync13, readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
3212
+ import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync5 } from "node:fs";
3097
3213
  import path2 from "node:path";
3098
3214
  function stripSurroundingQuotes(value) {
3099
3215
  const t = value.trim();
@@ -3110,10 +3226,10 @@ function loadEnvFromFile(envFilePath) {
3110
3226
  return {};
3111
3227
  }
3112
3228
  const targetPath = path2.resolve(envFilePath);
3113
- if (!existsSync13(targetPath) || !statSync5(targetPath).isFile()) {
3229
+ if (!existsSync14(targetPath) || !statSync5(targetPath).isFile()) {
3114
3230
  return {};
3115
3231
  }
3116
- const raw = readFileSync11(targetPath, "utf-8");
3232
+ const raw = readFileSync12(targetPath, "utf-8");
3117
3233
  const result = {};
3118
3234
  for (const line of raw.split(/\r?\n/)) {
3119
3235
  const normalized = line.trim();
@@ -3284,12 +3400,12 @@ function dockerPushImage(params, cwd) {
3284
3400
  }
3285
3401
 
3286
3402
  // src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
3287
- import { existsSync as existsSync14 } from "node:fs";
3403
+ import { existsSync as existsSync15 } from "node:fs";
3288
3404
  import path3 from "node:path";
3289
3405
  function resolveDockerBuildPaths(cwd) {
3290
3406
  const dockerfilePath = path3.join(cwd, "Dockerfile");
3291
3407
  Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
3292
- if (!existsSync14(dockerfilePath)) {
3408
+ if (!existsSync15(dockerfilePath)) {
3293
3409
  throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
3294
3410
  }
3295
3411
  Logger.info("\u2713 Dockerfile \u5B58\u5728");
@@ -3939,7 +4055,7 @@ function buildProgram() {
3939
4055
  await runUpdateMessageStatus(opts);
3940
4056
  });
3941
4057
  program.command("connect").description(
3942
- "\u8FDE\u63A5\u5E73\u53F0 WebSocket\uFF08/ws/agent\uFF09\uFF0C\u7EF4\u6301\u5FC3\u8DF3\u5E76\u5904\u7406\u4E0B\u884C message\uFF08TYPING \u2192 Cursor \u2192 SUCCESS/FAILED\uFF09"
4058
+ "\u8FDE\u63A5\u5E73\u53F0 WebSocket\uFF08/ws/agent\uFF09\uFF0C\u7EF4\u6301\u5FC3\u8DF3\u5E76\u5904\u7406\u4E0B\u884C message\uFF08TYPING \u2192 Cursor \u2192 SUCCESS/FAILED\uFF09\uFF1B\u542F\u52A8\u524D\u81EA\u52A8 apm update \u5230\u6700\u65B0\u7248"
3943
4059
  ).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").action(async (opts) => {
3944
4060
  await runConnect(opts);
3945
4061
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.54",
3
+ "version": "6.0.56",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,7 +21,10 @@
21
21
  固定文件名: `SQL.md`
22
22
  保存位置: `.apm/sessions/<会话 ID>/docs/SQL.md`
23
23
  适用场景: 后端开发涉及 SQL 改动(DDL/DML、表结构、Mapper/XML 中 SQL 等)时必须产出或追加更新,详见 `.apm/skills/apm-dev/SKILL.md` 中「后端 SQL 变更文档」章节。
24
- 格式要求: 待执行的 SQL 语句须放在 ` ```sql ` 代码块中,按执行顺序逐条列出。
24
+ 格式要求:
25
+
26
+ - 须标注**目标数据库**(库名/实例名、类型如 MySQL;同一变更涉及多库时分别标注)
27
+ - 待执行的 SQL 语句须放在 ` ```sql ` 代码块中,按执行顺序逐条列出
25
28
 
26
29
  ## 文档同步
27
30
 
@@ -5,7 +5,7 @@
5
5
  ### 步骤 1: 获取实现计划与协作内容
6
6
 
7
7
  1. 用 **Read** 工具阅读本端计划:前端读 `.apm/sessions/<会话ID>/docs/FRONTEND-PLAN.md`,后端读 `docs/BACKEND-PLAN.md`;计划不存在则退出流程并回复说明(兼容旧流程:若存在 `PRD.md` + `FRONTEND.md` / `BACKEND.md` + `API.md`,按旧文档执行)。
8
- 2. 前端涉及接口对接时,以后端计划中的「API 契约」章节为准,**不等后端部署完成**;契约没写清的字段 `@后端` 确认,禁止自行猜测。
8
+ 2. 前端涉及接口对接时,以 `docs/API.md` 为唯一契约来源,**不等后端部署完成**;`API.md` 不存在或字段没写清时 `@后端` 补充,禁止自行猜测或在计划中重复编写接口定义。
9
9
  3. **假设门禁(开发前必须检查)**:查看计划「依据与假设」章节——
10
10
  - 「假设」仍有未确认项:**Read** `.apm/sessions/<会话ID>/messages.xml`,查找项目经理是否已回复确认;
11
11
  - 项目经理已回复:先按 `.apm/skills/apm-write-plan/SKILL.md` 步骤 5 把确认结果**回填进计划文档并同步**(确认的假设移入「依据」,否定的修订实现步骤与白名单),然后再开发;
@@ -50,6 +50,7 @@
50
50
 
51
51
  若本次改动涉及 SQL(DDL/DML、表结构、索引、数据修复、Mapper/XML 中新增或修改 SQL 语句等),开发阶段必须 **Write** `.apm/sessions/<会话ID>/docs/SQL.md`,内容包括:
52
52
 
53
+ - **目标数据库**(库名/实例名、类型如 MySQL;同一变更涉及多库时分别标注)
53
54
  - 变更摘要(改了什么表/数据、为什么)
54
55
  - 完整 SQL 语句(按执行顺序排列;**每条须放在 ` ```sql ` 代码块中**,便于复制执行)
55
56
  - 执行环境说明(测试/生产是否一致、是否需人工执行)
@@ -58,6 +59,10 @@
58
59
  示例:
59
60
 
60
61
  ````markdown
62
+ ## 目标数据库
63
+
64
+ `oxc_platform`(MySQL 8.x)
65
+
61
66
  ## 变更摘要
62
67
 
63
68
  为 inspection_class 表新增 is_project_add 字段。
@@ -84,6 +89,6 @@ ALTER TABLE inspection_class ADD COLUMN is_project_add VARCHAR(1) DEFAULT '0';
84
89
  2. **发布测试环境**:**Read** `.apm/skills/apm-deploy/SKILL.md` 并按其流程部署;**后端**涉及 SQL 变更时,须在回复中引用 `docs/SQL.md`,并写明待执行的 SQL 文件名或执行顺序。
85
90
  3. **白名单对账**:在回复中逐文件列出本次改动与计划白名单的对应关系。
86
91
 
87
- **注意:不做联调。** 前后端各自按 API 契约交付,接口对不上属于契约或实现问题,由 diff 评审与人工验收暴露后打回修复;禁止自行发起「联调」「接口实测」类的开放式动作。
92
+ **注意:不做联调。** 前后端各自按 `API.md` 交付,接口对不上属于契约或实现问题,由 diff 评审与人工验收暴露后打回修复;禁止自行发起「联调」「接口实测」类的开放式动作。
88
93
 
89
94
  完成后用 `append_message` 回复:改动概述 + 白名单对账 + 构建结果 + 测试环境地址,并 `@` 评审角色进行 diff 评审。
@@ -18,12 +18,12 @@
18
18
 
19
19
  ### 步骤 2:四项检查
20
20
 
21
- | 检查项 | 判定 |
22
- | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
23
- | **白名单对账** | diff 中出现白名单之外的文件,且计划未更新说明 → **不通过** |
24
- | **需求相关性** | 存在与本需求无关的改动(顺手重构、改格式、动了无关逻辑)→ **不通过** |
25
- | **计划落实** | 计划「实现步骤」中的关键点在 diff 中找不到对应实现 → **不通过** |
26
- | **SQL 文档** | **仅后端**:diff 涉及 SQL 改动(DDL/DML、表结构、Mapper/XML 中 SQL 等),但 `docs/SQL.md` 缺失或与改动不一致 → **不通过** |
21
+ | 检查项 | 判定 |
22
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | **白名单对账** | diff 中出现白名单之外的文件,且计划未更新说明 → **不通过** |
24
+ | **需求相关性** | 存在与本需求无关的改动(顺手重构、改格式、动了无关逻辑)→ **不通过** |
25
+ | **计划落实** | 计划「实现步骤」中的关键点在 diff 中找不到对应实现 → **不通过** |
26
+ | **SQL 文档** | **仅后端**:diff 涉及 SQL 改动(DDL/DML、表结构、Mapper/XML 中 SQL 等),但 `docs/SQL.md` 缺失、未标注目标数据库、或与改动不一致 → **不通过** |
27
27
 
28
28
  注意事项:
29
29
 
@@ -6,10 +6,12 @@
6
6
 
7
7
  本技能合并了原 `apm-write-prd`、`apm-review`、`apm-write-frontend-plan`、`apm-write-backend-api` 四个技能的职能:评审(判断是否参与、发现口径缺口)和方案(怎么改、改哪些文件)一步完成。
8
8
 
9
- | 角色 | 产出文档 | 路径 |
10
- | ---- | --------------------------------------- | ----------------------- |
11
- | 后端 | `BACKEND-PLAN.md`(含「API 契约」章节) | `docs/BACKEND-PLAN.md` |
12
- | 前端 | `FRONTEND-PLAN.md` | `docs/FRONTEND-PLAN.md` |
9
+ | 角色 | 产出文档 | 路径 |
10
+ | ---- | ------------------------------------------------------------- | ------------------------------------- |
11
+ | 后端 | `BACKEND-PLAN.md`(实现计划)+ `API.md`(联调契约,单独产出) | `docs/BACKEND-PLAN.md`、`docs/API.md` |
12
+ | 前端 | `FRONTEND-PLAN.md` | `docs/FRONTEND-PLAN.md` |
13
+
14
+ **两份后端文档禁止合并**:`BACKEND-PLAN.md` 不写完整参数表 / JSON 示例;`API.md` 不写 Service / SQL 等实现细节。
13
15
 
14
16
  ---
15
17
 
@@ -28,14 +30,17 @@
28
30
  2. **再调研代码**:只看与需求直接相关的页面 / 接口 / 表。**调研预算:最多读 15 个代码文件**,禁止全库考古、禁止顺藤摸瓜阅读无关模块。
29
31
  3. **口径不清不要自己拍板**:调研中发现需求没写清的业务口径(例如字段含义、互斥规则、历史数据兼容),一律记入计划的「依据与假设」章节,禁止编造业务规则。
30
32
 
33
+ **前端额外要求**:若本次涉及接口对接,**Read** `docs/API.md`;不存在则先 `@后端` 产出 `API.md`,**禁止在计划中自行编写或抄写接口定义**。
34
+
31
35
  ### 步骤 3:按模板写计划
32
36
 
33
- **Read** `.apm/skills/apm-write-plan/plan-template.md`,按模板 **Write** 对应的计划文档。三个章节为硬性要求,缺一不可:
37
+ **Read** `.apm/skills/apm-write-plan/plan-template.md`,按模板 **Write** 对应的计划文档。硬性要求,缺一不可:
34
38
 
35
39
  1. **依据与假设**:每条关键口径标注来源(需求原文第几条 / 现有代码行为 / 已有文档);标不出来源的就是「假设」,单独列出。
36
40
  2. **改动文件白名单**:本次允许改动的文件完整列表。后续开发与 diff 评审都以此为准,**开发时改了白名单之外的文件会被打回**。
37
- 3. **后端专属——API 契约**:给前端看的接口定义(Path、参数、响应示例、错误码)。前端开发以契约为准,不等后端部署完成。
41
+ 3. **后端专属——同步产出 API.md**(涉及接口变更时):**Read** `.apm/skills/apm-write-plan/api-template.md`,按模板 **Write** `docs/API.md`。这是前端联调的唯一契约来源,前端直接阅读本文档,禁止各写一份。
38
42
  4. **后端专属——SQL 变更声明**(仅当涉及表结构/数据变更时):在「实现步骤」中注明开发须产出 `docs/SQL.md`;计划本身不写大段 SQL,完整语句由后端开发阶段写入该文档。
43
+ 5. **前端专属——接口对接要点**:只写「何时调、关键点」,引用 `API.md` 章节,**禁止复制参数表 / JSON 示例**。
39
44
 
40
45
  ### 步骤 4:回复
41
46
 
@@ -43,7 +48,8 @@
43
48
 
44
49
  - **「假设」章节非空**:把假设逐条列进回复内容,`@项目经理` 请其确认;明确说「以上假设确认前不开始开发」。
45
50
  - **无假设**:回复计划已就绪,可进入测试要点编写。
46
- - 前端计划依赖的接口后端契约还没出:在计划「期望接口」小节写出前端期望的接口形态,回复时 `@后端` 对齐,不要空等。
51
+ - **后端**:涉及接口变更时,回复中 `@前端` 阅读 `API.md` 并编写 `FRONTEND-PLAN.md`。
52
+ - **前端**:`API.md` 尚未就绪时,回复 `@后端` 先产出 `API.md`,**禁止自行编写接口契约**。
47
53
 
48
54
  ### 步骤 5:假设回填(项目经理确认后必须执行)
49
55
 
@@ -54,10 +60,11 @@
54
60
  - 被确认的假设 → **移入「依据」表格**,来源写「项目经理确认(第 N 轮)」;
55
61
  - 被否定或修正的假设 → 按项目经理给出的口径**修订「实现步骤」与「改动文件白名单」**;
56
62
  - 项目经理没有回应的假设 → 保留在「假设」中,再次 `@项目经理` 追问。
57
- 3. 重新执行 `apm sync-document` 同步计划。
58
- 4. 回复消息:逐条说明每个假设的处理结果(确认采纳 / 按口径修订了什么),全部解决则声明「假设已清零,可进入测试要点编写」。
63
+ 3. **后端**:假设修订涉及接口口径时,同步更新 `docs/API.md`。
64
+ 4. 重新执行 `apm sync-document` 同步计划与 API 文档。
65
+ 5. 回复消息:逐条说明每个假设的处理结果(确认采纳 / 按口径修订了什么),全部解决则声明「假设已清零,可进入测试要点编写」。
59
66
 
60
- **禁止**跳过回填直接开发:澄清结论必须落进计划文档,后续开发与 diff 评审都只认计划文档,不认聊天记录。
67
+ **禁止**跳过回填直接开发:澄清结论必须落进计划文档(接口口径落进 `API.md`),后续开发与 diff 评审都只认文档,不认聊天记录。
61
68
 
62
69
  ---
63
70
 
@@ -65,8 +72,8 @@
65
72
 
66
73
  - 篇幅 **40 ~ 100 行**,宁可少写;不要伪代码、不要大段 SQL(完整语句写入 `docs/SQL.md`,由后端开发阶段产出)。
67
74
  - 用产品语言描述行为,文件路径只出现在「改动文件白名单」。
68
- - 前后端可同轮并行编写计划,不互相阻塞。
75
+ - 前后端可同轮并行编写计划;前端接口部分依赖 `API.md`,后端须先或同步产出。
69
76
 
70
77
  ## 何时使用
71
78
 
72
- 协作流程阶段 1(实现计划);或用户要求写 `BACKEND-PLAN.md` / `FRONTEND-PLAN.md`。
79
+ 协作流程阶段 1(实现计划);或用户要求写 `BACKEND-PLAN.md` / `FRONTEND-PLAN.md` / `API.md`。
@@ -0,0 +1,35 @@
1
+ # API.md 模板
2
+
3
+ 给前端联调用。实现步骤见 `BACKEND-PLAN.md`;本文只写契约,篇幅尽量克制。
4
+
5
+ ```markdown
6
+ # <功能名称> — 接口
7
+
8
+ ## 背景
9
+
10
+ <1 ~ 2 句:本次新增/变更/复用哪些接口>
11
+
12
+ ## <接口名称>(新增)
13
+
14
+ - **GET** `/实际/path`
15
+ - **用途**:…
16
+ - **参数**:`pageNo`、`pageSize`(可选,默认 …);`xxx`(何时传、何时不传)
17
+ - **成功**:`result` 结构简述 + 简短 JSON 示例
18
+ - **失败**:格式非法 → `message` 示例;无数据 → 空列表算成功
19
+
20
+ ## <接口名称>(现网,不变)
21
+
22
+ - **GET** `/实际/path/{id}`
23
+ - **用途**:…
24
+ - **前端关注字段**:`fieldA` → 模板 xxx
25
+
26
+ ## 联调说明
27
+
28
+ 1. 首屏:只传 …
29
+ 2. 筛选:…
30
+ 3. 选中后:必须先调详情,禁止 …
31
+ ```
32
+
33
+ ## 不要写
34
+
35
+ SQL、Java 类路径、Service 实现、后端验收清单。
@@ -1,10 +1,12 @@
1
1
  # 计划文档模板
2
2
 
3
- > 后端写 `docs/BACKEND-PLAN.md`,前端写 `docs/FRONTEND-PLAN.md`。
4
- > API 契约」章节仅后端需要;「期望接口」小节仅前端在后端契约未就绪时需要。
3
+ > 后端写 `docs/BACKEND-PLAN.md` + `docs/API.md`(两份文档禁止合并)。
4
+ > 前端写 `docs/FRONTEND-PLAN.md`;接口细节只读 `docs/API.md`,禁止在计划中重复抄写。
5
+
6
+ ## 后端计划(BACKEND-PLAN.md)
5
7
 
6
8
  ```markdown
7
- # <需求名> · <前端|后端>实现计划
9
+ # <需求名> · 后端实现计划
8
10
 
9
11
  ## 1. 需求理解
10
12
 
@@ -33,35 +35,61 @@
33
35
  2. 第二步做什么
34
36
  3. ...(通常 3 ~ 6 步,不写代码细节)
35
37
 
36
- > **后端**:若涉及表结构或数据变更,在步骤中注明「开发须产出 `docs/SQL.md`」,SQL 语句不写在本计划内。
38
+ > 若涉及表结构或数据变更,在步骤中注明「开发须产出 `docs/SQL.md`」,SQL 语句不写在本计划内。
39
+ > 若涉及接口变更,在步骤中注明「须同步产出 `docs/API.md`」,接口参数表不写在本计划内。
37
40
 
38
41
  ## 4. 改动文件白名单
39
42
 
40
43
  > 开发只允许改这些文件;diff 评审会逐一对账,越界会被打回。
41
44
  > 开发中确需新增,先更新本清单并在群里说明原因。
42
45
 
43
- - `src/views/inspection/InspectionProjectForm.vue` — 新增「是否加分」单选与互斥逻辑
44
- - `src/api/inspection.ts` — 新增字段透传
46
+ - `src/.../InspectionProjectController.java` — 新增保存入参校验
47
+ - `src/.../InspectionProjectService.java` — 互斥逻辑
45
48
  - ...
49
+ ```
50
+
51
+ ## 前端计划(FRONTEND-PLAN.md)
52
+
53
+ > 写计划前必须先 **Read** `docs/API.md`;不存在则退出并 `@后端`,禁止自行编造接口或写「期望接口」。
54
+
55
+ ```markdown
56
+ # <需求名> · 前端实现计划
46
57
 
47
- ## 5. API 契约(仅后端;前端开发以此为准)
58
+ ## 1. 需求理解
48
59
 
49
- ### 5.1 <接口名>
60
+ 3 ~ 5 行复述本端要做什么(产品语言),不复制需求原文。
50
61
 
51
- - Path:`POST /inspection/project/save`
52
- - 新增入参:
62
+ ## 2. 依据与假设
53
63
 
54
- | 字段 | 类型 | 必填 | 说明 |
55
- | ------------ | ------ | ---- | -------------------------------- |
56
- | isProjectAdd | string | 否 | "1"=加分项;与 isProjectDed 互斥 |
64
+ ### 依据(口径 + 来源)
57
65
 
58
- - 响应示例:
66
+ | # | 口径 | 来源 |
67
+ | --- | ------------------------------ | --------------- |
68
+ | 1 | 「是否加分」与「是否扣分」互斥 | 需求原文第 1 条 |
69
+ | 2 | 加分项不汇总进分类分值 | 需求原文第 2 条 |
59
70
 
60
- { "success": true, "result": { "id": "xxx" } }
71
+ ### 假设(待项目经理确认,确认前不开发)
61
72
 
62
- - 错误码:互斥冲突返回 `success=false, message="加分与扣分不可同时勾选"`
73
+ - [ ] A1:xxx(为什么需要确认)
74
+
75
+ > 无假设时写「无,口径均有依据」。
76
+
77
+ ## 3. 实现步骤
78
+
79
+ 1. 第一步做什么(对应哪条口径)
80
+ 2. 第二步做什么
81
+ 3. 对接 `API.md` 中的某某接口(只写调用时机,不复制参数表)
82
+ 4. ...
83
+
84
+ ## 4. 改动文件白名单
85
+
86
+ - `src/views/inspection/InspectionProjectForm.vue` — 新增「是否加分」单选与互斥逻辑
87
+ - `src/api/inspection.ts` — 新增字段透传
88
+ - ...
63
89
 
64
- ## 6. 期望接口(仅前端,后端契约未就绪时填写)
90
+ ## 5. 接口对接要点(引用 API.md,禁止复制)
65
91
 
66
- - 期望 `查询分类详情` 返回 `classScore` 字段,由后端确认。
92
+ - 列表:<何时请求、首屏传什么>(见 `API.md` § xxx)
93
+ - 详情:<选中后怎么走>(见 `API.md` § xxx)
94
+ - 注意:<禁止用列表数据代替详情等>
67
95
  ```