ai-project-manage-cli 4.0.8 → 4.0.10
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 +6 -2
- package/dist/index.js +219 -29
- package/package.json +1 -1
- package/template/skills/apm-refine/SKILL.md +39 -46
- package/template/skills/apm-refine/apm-refine-template.md +51 -17
- package/template/skills/apm-review/SKILL.md +75 -31
- package/template/skills/apm-review/output-template.md +52 -31
package/README.md
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
`npm install ai-project-manage-cli@latest`
|
|
7
|
+
`npm install -g ai-project-manage-cli@latest`
|
|
8
|
+
|
|
9
|
+
## 更新到最新版
|
|
10
|
+
|
|
11
|
+
`apm update`(等价于 `npm install -g ai-project-manage-cli@latest`)
|
|
8
12
|
|
|
9
13
|
## 登录
|
|
10
14
|
|
|
@@ -16,4 +20,4 @@
|
|
|
16
20
|
|
|
17
21
|
## 连接服务器
|
|
18
22
|
|
|
19
|
-
`apm connect`
|
|
23
|
+
`apm connect`
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync10 } from "fs";
|
|
5
|
-
import { dirname as dirname2, join as join10 } from "path";
|
|
6
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
4
|
import { Command } from "commander";
|
|
8
5
|
|
|
9
6
|
// src/config.ts
|
|
@@ -103,6 +100,10 @@ var requestConfig = {
|
|
|
103
100
|
method: "POST",
|
|
104
101
|
path: "/cli/requirements/comment"
|
|
105
102
|
}),
|
|
103
|
+
commentStructured: defineEndpoint({
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/cli/requirements/comment-structured"
|
|
106
|
+
}),
|
|
106
107
|
refine: defineEndpoint({
|
|
107
108
|
method: "POST",
|
|
108
109
|
path: "/cli/requirements/refine"
|
|
@@ -240,15 +241,120 @@ function resolveCwdPath(file) {
|
|
|
240
241
|
return resolve(process.cwd(), file);
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// src/structured-review-yaml.ts
|
|
245
|
+
import { parse } from "yaml";
|
|
246
|
+
var KIND_MAP = {
|
|
247
|
+
clarify: "CLARIFY",
|
|
248
|
+
difficulty: "DIFFICULTY",
|
|
249
|
+
business: "BUSINESS",
|
|
250
|
+
coordination: "COORDINATION",
|
|
251
|
+
CLARIFY: "CLARIFY",
|
|
252
|
+
DIFFICULTY: "DIFFICULTY",
|
|
253
|
+
BUSINESS: "BUSINESS",
|
|
254
|
+
COORDINATION: "COORDINATION"
|
|
255
|
+
};
|
|
256
|
+
var STANCE_VALUES = /* @__PURE__ */ new Set(["frontend", "backend", "fullstack"]);
|
|
257
|
+
function asRecord(value) {
|
|
258
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
throw new Error("YAML \u7ED3\u6784\u65E0\u6548\uFF1A\u9700\u8981\u5BF9\u8C61\u6839\u8282\u70B9");
|
|
262
|
+
}
|
|
263
|
+
function parseAnchor(anchor, index) {
|
|
264
|
+
const rec = asRecord(anchor);
|
|
265
|
+
const start = Number(rec.start ?? rec.startLine);
|
|
266
|
+
const end = Number(rec.end ?? rec.endLine);
|
|
267
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) {
|
|
268
|
+
throw new Error(`items[${index}].anchor \u884C\u53F7\u65E0\u6548\uFF08\u9700 1 \u2264 start \u2264 end\uFF09`);
|
|
269
|
+
}
|
|
270
|
+
return { start, end };
|
|
271
|
+
}
|
|
272
|
+
function parseKind(raw, index) {
|
|
273
|
+
const key = String(raw ?? "").trim();
|
|
274
|
+
const kind = KIND_MAP[key] ?? KIND_MAP[key.toLowerCase()];
|
|
275
|
+
if (!kind) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`items[${index}].kind \u65E0\u6548\uFF0C\u5E94\u4E3A clarify | difficulty | business | coordination`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return kind;
|
|
281
|
+
}
|
|
282
|
+
function parseStructuredReviewYaml(raw, cliModel) {
|
|
283
|
+
let doc;
|
|
284
|
+
try {
|
|
285
|
+
doc = parse(raw);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
288
|
+
throw new Error(`YAML \u89E3\u6790\u5931\u8D25\uFF1A${msg}`);
|
|
289
|
+
}
|
|
290
|
+
const root = asRecord(doc);
|
|
291
|
+
const reviewer = asRecord(root.reviewer ?? {});
|
|
292
|
+
const stance = String(reviewer.stance ?? root.stance ?? "").trim();
|
|
293
|
+
if (!STANCE_VALUES.has(stance)) {
|
|
294
|
+
throw new Error("reviewer.stance \u5FC5\u987B\u4E3A frontend\u3001backend \u6216 fullstack");
|
|
295
|
+
}
|
|
296
|
+
const modelFromYaml = reviewer.model != null ? String(reviewer.model).trim() : "";
|
|
297
|
+
const model = cliModel?.trim() || modelFromYaml || null;
|
|
298
|
+
const itemsRaw = root.items;
|
|
299
|
+
if (!Array.isArray(itemsRaw) || itemsRaw.length === 0) {
|
|
300
|
+
throw new Error("items \u4E0D\u80FD\u4E3A\u7A7A");
|
|
301
|
+
}
|
|
302
|
+
const items = itemsRaw.map((entry, index) => {
|
|
303
|
+
const item = asRecord(entry);
|
|
304
|
+
const anchor = parseAnchor(item.anchor ?? item, index);
|
|
305
|
+
const kind = parseKind(item.kind, index);
|
|
306
|
+
const body = typeof item.body === "string" ? item.body.trim() : String(item.body ?? "").trim();
|
|
307
|
+
if (!body) {
|
|
308
|
+
throw new Error(`items[${index}].body \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
309
|
+
}
|
|
310
|
+
if (kind === "COORDINATION" && item.reply != null && String(item.reply).trim()) {
|
|
311
|
+
throw new Error(`items[${index}] \u8054\u8C03\u4F9D\u8D56\uFF08coordination\uFF09\u4E0D\u5141\u8BB8 reply`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
startLine: anchor.start,
|
|
315
|
+
endLine: anchor.end,
|
|
316
|
+
kind,
|
|
317
|
+
body
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return { stance, model, items };
|
|
321
|
+
}
|
|
322
|
+
function inferCommentFormat(filePath, explicit) {
|
|
323
|
+
if (explicit === "structured" || explicit === "legacy") {
|
|
324
|
+
return explicit;
|
|
325
|
+
}
|
|
326
|
+
const lower = filePath.toLowerCase();
|
|
327
|
+
if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
|
|
328
|
+
return "structured";
|
|
329
|
+
}
|
|
330
|
+
return "legacy";
|
|
331
|
+
}
|
|
332
|
+
|
|
243
333
|
// src/commands/comment.ts
|
|
244
|
-
async function runComment(requirementId, file,
|
|
334
|
+
async function runComment(requirementId, file, options) {
|
|
245
335
|
const cfg = await ensureLoggedConfig();
|
|
246
|
-
const
|
|
336
|
+
const resolvedPath = resolveCwdPath(file);
|
|
337
|
+
const raw = readFileSync2(resolvedPath, "utf8");
|
|
338
|
+
const format = inferCommentFormat(resolvedPath, options?.format);
|
|
247
339
|
const api = createApmApiClient(cfg);
|
|
340
|
+
if (format === "structured") {
|
|
341
|
+
const parsed = parseStructuredReviewYaml(raw, options?.model);
|
|
342
|
+
const data2 = await api.cliRequirements.commentStructured({
|
|
343
|
+
requirementId,
|
|
344
|
+
stance: parsed.stance,
|
|
345
|
+
model: parsed.model,
|
|
346
|
+
items: parsed.items
|
|
347
|
+
});
|
|
348
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!raw.trim()) {
|
|
352
|
+
throw new Error("\u8BC4\u8BBA\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A");
|
|
353
|
+
}
|
|
248
354
|
const data = await api.cliRequirements.comment({
|
|
249
355
|
requirementId,
|
|
250
|
-
content,
|
|
251
|
-
model
|
|
356
|
+
content: raw,
|
|
357
|
+
model: options?.model
|
|
252
358
|
});
|
|
253
359
|
console.log(JSON.stringify(data, null, 2));
|
|
254
360
|
}
|
|
@@ -1006,6 +1112,84 @@ async function runRefine(requirementId) {
|
|
|
1006
1112
|
console.log(JSON.stringify(data, null, 2));
|
|
1007
1113
|
}
|
|
1008
1114
|
|
|
1115
|
+
// src/commands/update.ts
|
|
1116
|
+
import { spawnSync } from "child_process";
|
|
1117
|
+
|
|
1118
|
+
// src/version.ts
|
|
1119
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1120
|
+
import { dirname as dirname2, join as join9 } from "path";
|
|
1121
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1122
|
+
var CLI_PACKAGE_NAME = "ai-project-manage-cli";
|
|
1123
|
+
function readCliVersion() {
|
|
1124
|
+
try {
|
|
1125
|
+
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
1126
|
+
const pkgPath = join9(dir, "..", "package.json");
|
|
1127
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
|
|
1128
|
+
return pkg.version ?? "0.0.0";
|
|
1129
|
+
} catch {
|
|
1130
|
+
return "0.0.0";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/commands/update.ts
|
|
1135
|
+
function registryBaseUrl() {
|
|
1136
|
+
const fromEnv = process.env.npm_config_registry?.trim() || process.env.NPM_CONFIG_REGISTRY?.trim();
|
|
1137
|
+
return (fromEnv || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1138
|
+
}
|
|
1139
|
+
async function fetchLatestPublishedVersion() {
|
|
1140
|
+
const url = `${registryBaseUrl()}/${CLI_PACKAGE_NAME}/latest`;
|
|
1141
|
+
try {
|
|
1142
|
+
const res = await fetch(url);
|
|
1143
|
+
if (!res.ok) return null;
|
|
1144
|
+
const data = await res.json();
|
|
1145
|
+
return data.version?.trim() || null;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function npmAvailable() {
|
|
1151
|
+
const r = spawnSync("npm", ["--version"], { encoding: "utf8" });
|
|
1152
|
+
return !r.error && r.status === 0;
|
|
1153
|
+
}
|
|
1154
|
+
async function runUpdate() {
|
|
1155
|
+
const current = readCliVersion();
|
|
1156
|
+
const latest = await fetchLatestPublishedVersion();
|
|
1157
|
+
if (latest && current === latest) {
|
|
1158
|
+
console.log(`[apm] \u5DF2\u662F\u6700\u65B0\u7248\u672C ${current}`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (!npmAvailable()) {
|
|
1162
|
+
console.error(
|
|
1163
|
+
`[apm] \u672A\u627E\u5230 npm\u3002\u8BF7\u5B89\u88C5 Node.js \u540E\u6267\u884C\uFF1Anpm install -g ${CLI_PACKAGE_NAME}@latest`
|
|
1164
|
+
);
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
const targetLabel = latest ?? "latest";
|
|
1168
|
+
console.error(
|
|
1169
|
+
`[apm] \u5F53\u524D\u7248\u672C ${current}\uFF0C\u6B63\u5728\u5B89\u88C5 ${CLI_PACKAGE_NAME}@${targetLabel} \u2026`
|
|
1170
|
+
);
|
|
1171
|
+
const install = spawnSync(
|
|
1172
|
+
"npm",
|
|
1173
|
+
["install", "-g", `${CLI_PACKAGE_NAME}@latest`],
|
|
1174
|
+
{ stdio: "inherit", shell: process.platform === "win32" }
|
|
1175
|
+
);
|
|
1176
|
+
if (install.error) {
|
|
1177
|
+
console.error("[apm] \u66F4\u65B0\u5931\u8D25:", install.error.message);
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
if (install.status !== 0) {
|
|
1181
|
+
process.exit(install.status ?? 1);
|
|
1182
|
+
}
|
|
1183
|
+
const after = readCliVersion();
|
|
1184
|
+
if (latest && after === latest) {
|
|
1185
|
+
console.log(`[apm] \u5DF2\u66F4\u65B0\u5230 ${after}`);
|
|
1186
|
+
} else {
|
|
1187
|
+
console.log(
|
|
1188
|
+
`[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`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1009
1193
|
// src/commands/update-dev-status.ts
|
|
1010
1194
|
async function runUpdateDevStatus(requirementId, status) {
|
|
1011
1195
|
const cfg = await ensureLoggedConfig();
|
|
@@ -1032,7 +1216,7 @@ async function runUpdateStatus(requirementId, status) {
|
|
|
1032
1216
|
import path5 from "node:path";
|
|
1033
1217
|
|
|
1034
1218
|
// src/commands/deploy/internal/apm-config.ts
|
|
1035
|
-
import { existsSync as existsSync4, readFileSync as
|
|
1219
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
|
|
1036
1220
|
import { resolve as resolve4 } from "node:path";
|
|
1037
1221
|
function loadApmConfig(options) {
|
|
1038
1222
|
const p = resolve4(
|
|
@@ -1044,7 +1228,7 @@ function loadApmConfig(options) {
|
|
|
1044
1228
|
process.exit(1);
|
|
1045
1229
|
}
|
|
1046
1230
|
try {
|
|
1047
|
-
const raw =
|
|
1231
|
+
const raw = readFileSync7(p, "utf8");
|
|
1048
1232
|
return JSON.parse(raw);
|
|
1049
1233
|
} catch (e) {
|
|
1050
1234
|
console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
|
|
@@ -1143,7 +1327,7 @@ import path4 from "node:path";
|
|
|
1143
1327
|
import Docker from "dockerode";
|
|
1144
1328
|
|
|
1145
1329
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
|
|
1146
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1330
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
1147
1331
|
import path from "node:path";
|
|
1148
1332
|
function asOptionalTlsBuffer(value) {
|
|
1149
1333
|
if (typeof value !== "string") {
|
|
@@ -1156,7 +1340,7 @@ function asOptionalTlsBuffer(value) {
|
|
|
1156
1340
|
return void 0;
|
|
1157
1341
|
}
|
|
1158
1342
|
if (existsSync5(normalized)) {
|
|
1159
|
-
return
|
|
1343
|
+
return readFileSync8(normalized);
|
|
1160
1344
|
}
|
|
1161
1345
|
const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
|
|
1162
1346
|
if (looksLikePath) {
|
|
@@ -1366,7 +1550,7 @@ var DockerodeClient = class {
|
|
|
1366
1550
|
var createDockerodeClient = (config) => new DockerodeClient(config);
|
|
1367
1551
|
|
|
1368
1552
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
|
|
1369
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1553
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, statSync as statSync3 } from "node:fs";
|
|
1370
1554
|
import path2 from "node:path";
|
|
1371
1555
|
function stripSurroundingQuotes(value) {
|
|
1372
1556
|
const t = value.trim();
|
|
@@ -1386,7 +1570,7 @@ function loadEnvFromFile(envFilePath) {
|
|
|
1386
1570
|
if (!existsSync6(targetPath) || !statSync3(targetPath).isFile()) {
|
|
1387
1571
|
return {};
|
|
1388
1572
|
}
|
|
1389
|
-
const raw =
|
|
1573
|
+
const raw = readFileSync9(targetPath, "utf-8");
|
|
1390
1574
|
const result = {};
|
|
1391
1575
|
for (const line of raw.split(/\r?\n/)) {
|
|
1392
1576
|
const normalized = line.trim();
|
|
@@ -1691,16 +1875,16 @@ import { copyFile, readdir as readdir2, stat } from "node:fs/promises";
|
|
|
1691
1875
|
import path7 from "node:path";
|
|
1692
1876
|
|
|
1693
1877
|
// src/commands/deploy/internal/load-apm-dotenv.ts
|
|
1694
|
-
import { existsSync as existsSync8, readFileSync as
|
|
1695
|
-
import { join as
|
|
1878
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
1879
|
+
import { join as join10 } from "node:path";
|
|
1696
1880
|
function loadApmDotEnvIfPresent() {
|
|
1697
|
-
const p =
|
|
1881
|
+
const p = join10(WORKSPACE_APM_DIR, ".env");
|
|
1698
1882
|
if (!existsSync8(p)) {
|
|
1699
1883
|
return;
|
|
1700
1884
|
}
|
|
1701
1885
|
let text;
|
|
1702
1886
|
try {
|
|
1703
|
-
text =
|
|
1887
|
+
text = readFileSync10(p, "utf8");
|
|
1704
1888
|
} catch {
|
|
1705
1889
|
return;
|
|
1706
1890
|
}
|
|
@@ -2056,16 +2240,6 @@ function registerDeployCommands(program) {
|
|
|
2056
2240
|
}
|
|
2057
2241
|
|
|
2058
2242
|
// src/index.ts
|
|
2059
|
-
function readCliVersion() {
|
|
2060
|
-
try {
|
|
2061
|
-
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
2062
|
-
const pkgPath = join10(dir, "..", "package.json");
|
|
2063
|
-
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
2064
|
-
return pkg.version ?? "0.0.0";
|
|
2065
|
-
} catch {
|
|
2066
|
-
return "0.0.0";
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
2243
|
function buildProgram() {
|
|
2070
2244
|
const program = new Command();
|
|
2071
2245
|
program.name("apm").description(
|
|
@@ -2085,6 +2259,11 @@ function buildProgram() {
|
|
|
2085
2259
|
program.command("init").description("\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u521D\u59CB\u5316 .apm \u6A21\u677F\uFF08\u82E5 .apm \u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\u5219\u62A5\u9519\uFF09").option("--name <name>", "\u5DE5\u4F5C\u76EE\u5F55\u540D\u79F0").action(async (opts) => {
|
|
2086
2260
|
await runInit(opts.name);
|
|
2087
2261
|
});
|
|
2262
|
+
program.command("update").description(
|
|
2263
|
+
`\u901A\u8FC7 npm \u5168\u5C40\u5B89\u88C5 ${CLI_PACKAGE_NAME}@latest\uFF0C\u5C06 apm \u66F4\u65B0\u5230 registry \u6700\u65B0\u7248`
|
|
2264
|
+
).action(async () => {
|
|
2265
|
+
await runUpdate();
|
|
2266
|
+
});
|
|
2088
2267
|
program.command("pull").description(
|
|
2089
2268
|
"GET /api/cli/requirements/pull\uFF0C\u540C\u6B65\u6570\u636E\u4E0E\u9644\u4EF6\u5230 .apm/workitems/<\u9700\u6C42ID>"
|
|
2090
2269
|
).argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
|
|
@@ -2103,9 +2282,20 @@ function buildProgram() {
|
|
|
2103
2282
|
).action(async (requirementId, opts) => {
|
|
2104
2283
|
await runBranch(requirementId, { message: opts.message });
|
|
2105
2284
|
});
|
|
2106
|
-
program.command("comment").description(
|
|
2285
|
+
program.command("comment").description(
|
|
2286
|
+
"\u63D0\u4EA4\u9700\u6C42\u8BC4\u5BA1\uFF1Alegacy \u4E3A\u6574\u7BC7 Markdown\uFF08POST \u2026/comment\uFF09\uFF1Bstructured \u4E3A\u884C\u7EA7 YAML\uFF08POST \u2026/comment-structured\uFF09"
|
|
2287
|
+
).argument("<requirementId>", "\u9700\u6C42 ID").requiredOption(
|
|
2288
|
+
"--file <path>",
|
|
2289
|
+
"\u8BC4\u8BBA\u6587\u4EF6\u8DEF\u5F84\uFF08.yaml/.yml \u9ED8\u8BA4 structured\uFF09"
|
|
2290
|
+
).option(
|
|
2291
|
+
"--format <format>",
|
|
2292
|
+
"structured | legacy\uFF08\u9ED8\u8BA4 legacy\uFF1B.yaml/.yml \u672A\u6307\u5B9A\u65F6\u89C6\u4E3A structured\uFF09"
|
|
2293
|
+
).option("--model <model>", "\u8BC4\u8BBA\u6A21\u578B\uFF08\u8986\u76D6 YAML reviewer.model\uFF09").action(
|
|
2107
2294
|
async (requirementId, options) => {
|
|
2108
|
-
await runComment(requirementId, options.file,
|
|
2295
|
+
await runComment(requirementId, options.file, {
|
|
2296
|
+
model: options.model,
|
|
2297
|
+
format: options.format
|
|
2298
|
+
});
|
|
2109
2299
|
}
|
|
2110
2300
|
);
|
|
2111
2301
|
program.command("refine").description("POST /api/cli/requirements/refine\uFF08\u6B63\u6587\u6765\u81EA\u6587\u4EF6\uFF09").argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
|
package/package.json
CHANGED
|
@@ -1,75 +1,68 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apm-refine
|
|
3
|
-
description: 根据需求 ID
|
|
3
|
+
description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、可验收的业务需求文档并回写,再执行 apm refine 同步平台;当用户 @ 本技能或提出需求润色时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# APM 需求润色
|
|
7
7
|
|
|
8
8
|
用户仅提供 **需求 ID**(workitem id)。**缺 ID 时索要,不猜测。**
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
1. **正文依据 = 需求原文 + 补充信息**:只把用户在补充信息中**明确**要体现的内容(补充、修改、删除、拍板口径)合并进修订稿;用户没说到的,**保持原文或不写**,**不自作主张**补需求、不替用户「落实」评审建议。
|
|
13
|
-
2. **未回应的评审**:不列入「待确认」、不改成待办、不推断「仍有问题」;视为用户未要求在本次修订中处理(可能无此问题、暂不改、或评审误解)。
|
|
14
|
-
3. **评审的定位**:仅辅助理解补充信息在回应什么;补充信息与评审不一致时,**以补充信息为准**。
|
|
15
|
-
4. **消除重复**:同一议题在原文与补充信息中多处出现时,合并为**一处**表述。
|
|
16
|
-
5. **可追溯(轻量)**:可选「修订说明」概括相对原文的变化,且**只写补充信息实际带来的变化**,不罗列未采纳的评审。
|
|
17
|
-
6. **与仓库一致**:对照 `AGENTS.md`、`.apm/product-capability-inventory` 等,避免修订稿与用户已确认表述冲突;**禁止**用代码路径当需求论据。
|
|
18
|
-
7. **图片引用保持原样**:若原文含图片(如 `<img ...>`),在修订稿中直接保留对应 `img` 标签原文;不要改写成“图片见原始文档”等占位说明。
|
|
19
|
-
|
|
20
|
-
## 强制执行顺序(四步)
|
|
21
|
-
|
|
22
|
-
### 步骤 1:读取 `prd.md` 与 `reviews.xml`
|
|
10
|
+
## 产出品质
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
2. 当PRD涉及到图片链接时,自动下载对应的图片,存放在 `.apm/workitems/<需求ID>/images/<对应图片名称>.png`,并理解图片内容
|
|
26
|
-
3. 使用 **Read** 读取 **`.apm/workitems/<需求ID>/reviews.xml`**。
|
|
12
|
+
润色后的 `prd.md` 应做到:
|
|
27
13
|
|
|
28
|
-
|
|
14
|
+
- **可验收**:产品、测试、开发不读代码也能写用例;每条规则能对应一个测试点
|
|
15
|
+
- **简短**:合并后删繁就简,两三分钟可通读
|
|
16
|
+
- **业务化**:写界面、按钮、选项、场景与系统表现;材料中的技术名译为用户能懂的说法(原文要求保留的名称除外)
|
|
17
|
+
- **分场景**:新增 / 编辑 / 审核等分开写;**暂存**与**提交**的行为、校验时机分别写清
|
|
18
|
+
- **边界清楚**:范围写明包含与不包含;需求点内用 **「不考虑」** 标明明确排除项
|
|
19
|
+
- **术语统一**:同一字段、按钮全文同一叫法;改名时写明界面、提示、校验文案一并调整
|
|
20
|
+
- **结构平**:每个需求点一层 bullet(2 ~ 5 条),关键用语 **加粗**
|
|
29
21
|
|
|
30
|
-
-
|
|
31
|
-
- 若 **`reviews.xml` 无法读取**(不存在等):**不终止**;视为**无评审意见**,仅基于当前 `prd.md` 做标准化与润色,不杜撰评审内容。
|
|
22
|
+
正文骨架、需求点写法、示例与落盘自检见 **[apm-refine-template.md](./apm-refine-template.md)**。
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
在**不向用户追问补充信息**的前提下完成(**禁止**为润色向用户发起对话式提问;材料中已写的 `reply` 与评审意见视为**补充信息**可吸收。有歧义、缺信息、与 reviews 冲突等,**对材料未载明之事项**一律记入 **「问题与局限」** 或按条件写入 **「待确认」**,见 **[apm-refine-template.md](./apm-refine-template.md)** 与小节 3)。
|
|
24
|
+
## 合并原则
|
|
36
25
|
|
|
37
|
-
1.
|
|
38
|
-
|
|
26
|
+
1. **正文 = 需求原文 + 已拍板补充**:合并 `prd.md` 与 `reviews.xml` 里 `reply` 的明确口径;未说到的保持原文或不写,不自行扩需求。
|
|
27
|
+
2. **评审辅助理解 `reply`**;与补充冲突时以补充为准;未回应的评审本次不处理。
|
|
28
|
+
3. **同一议题只写一处**;合并后篇幅短于或接近原文。
|
|
29
|
+
4. **图片**:原文图片语法原样保留;理解图片后把规则写入对应需求点的文字描述。
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
使用 **Read** 读取与 `SKILL.md` 同目录的 **[apm-refine-template.md](./apm-refine-template.md)**,按其中**修订稿骨架**与章节说明组织正文(默认全文润色形态)。执行本技能时**须**读取该文件,不以记忆代替。
|
|
31
|
+
## 执行步骤
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
### 步骤 1:读取材料
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
1. **Read** `.apm/workitems/<需求ID>/prd.md`
|
|
36
|
+
2. **Read** `.apm/workitems/<需求ID>/reviews.xml`(不存在则视为无评审)
|
|
37
|
+
3. **理解 PRD 中的图片**(有则执行):
|
|
38
|
+
- 图片为标准 Markdown:`` — 圆括号内为展示 URL,**引号内**为相对路径(如 `attachments/图1.png`)
|
|
39
|
+
- **优先读本地附件**:`.apm/workitems/<需求ID>/<相对路径>`;存在则用该文件理解图片内容
|
|
40
|
+
- **本地不存在时**:再用圆括号内的展示 URL 下载图片后理解
|
|
41
|
+
- 将理解到的界面/字段/交互规则写入对应需求点;回写时保留原 `` 语法不变
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
若本轮实质上只需替换若干段落:可在 `prd.md` 中保留未改章节,仅重写涉及段落为**修改后的完整段落**,并在 **修订说明**(或 **相对原文的变更摘要**)中用简短 bullet 说明相对原文改了什么。**对用户对话仍以步骤 5 表格为唯一输出**,不把局部稿粘贴到对话里。
|
|
43
|
+
`prd.md` 不可读 → 终止,步骤 5 注明原因。
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
触发条件见 **[apm-refine-template.md](./apm-refine-template.md)** 文末两节;凡触发「问题与局限」的情形须按该节撰写,**禁止**因上述情况向用户发起追问。
|
|
45
|
+
### 步骤 2:润色
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
**不向用户追问。** `reply` 视为补充信息。
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
1. 将 `reply` 中已拍板内容并入正文对应需求点。
|
|
50
|
+
2. **Read** **[apm-refine-template.md](./apm-refine-template.md)**,按骨架、写法表与示例组织全文。
|
|
51
|
+
3. 写完后按模板 **「落盘前自检」** 核对,不满足则再改一版。
|
|
52
|
+
4. **待确认**:仅当补充信息原文含「待定」「再议」等时追加该章,每条一行归纳用户原意。
|
|
53
|
+
5. **局部替换**:只改涉及段落时,保留其余章节,重写为完整段落。
|
|
57
54
|
|
|
58
|
-
### 步骤
|
|
55
|
+
### 步骤 3:回写
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
**Write** 覆盖 `.apm/workitems/<需求ID>/prd.md`。
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
apm refine <需求ID>
|
|
64
|
-
```
|
|
59
|
+
### 步骤 4:同步
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
仓库根目录执行:`apm refine <需求ID>`
|
|
67
62
|
|
|
68
63
|
### 步骤 5:回复用户
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
建议表头(每行「状态」仅填 **成功** / **失败** / **跳过**;失败时可加简短原因,如 `失败:文件不存在`):
|
|
65
|
+
**仅**输出一张状态表,表格外无其他文字:
|
|
73
66
|
|
|
74
67
|
| 步骤 | 状态 |
|
|
75
68
|
| --------------------------------- | ---- |
|
|
@@ -78,4 +71,4 @@ apm refine <需求ID>
|
|
|
78
71
|
| 3. 回写 `prd.md` | |
|
|
79
72
|
| 4. 执行 `apm refine <需求ID>` | |
|
|
80
73
|
|
|
81
|
-
|
|
74
|
+
状态:**成功** / **失败** / **跳过**(失败可加简短原因)。
|
|
@@ -1,39 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 需求文档模板
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
执行润色时 **Read 本文件**,按下列骨架与写法组织 `prd.md`。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- 本次合并的要点:1~3 条 bullet
|
|
5
|
+
## 正文骨架
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
```markdown
|
|
8
|
+
# [需求标题]
|
|
9
9
|
|
|
10
10
|
## 背景与目标
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
(1 ~ 3 句:业务背景、要解决的问题、预期效果)
|
|
13
13
|
|
|
14
14
|
## 范围
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- **包含**:…
|
|
17
|
+
- **不包含**:…
|
|
18
18
|
|
|
19
19
|
## 需求说明
|
|
20
20
|
|
|
21
|
-
### 需求点 1:[
|
|
21
|
+
### 需求点 1:[简短名称]
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
- …
|
|
24
|
+
- …
|
|
24
25
|
|
|
25
26
|
### 需求点 2:…
|
|
27
|
+
```
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
按需追加(材料中确有相关内容时):
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
- **非功能与约束** — 性能、权限、兼容、埋点等
|
|
32
|
+
- **待确认** — 补充信息里用户原文写明的待定项,每条 `[ ]` 一行
|
|
30
33
|
|
|
31
|
-
##
|
|
34
|
+
## 需求点怎么写
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
每条 bullet 写清一个可验收点,优先覆盖:
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
| 类型 | 写法要点 |
|
|
39
|
+
| --------- | ------------------------------------------------------ |
|
|
40
|
+
| 展示/选项 | 展示或隐藏;保留/去掉哪些选项;新建默认值 |
|
|
41
|
+
| 文案 | 原名称 → 新名称;界面、提示、校验文案一并统一 |
|
|
42
|
+
| 按钮 | 场景 + 按钮名 + 行为(对照哪个既有按钮、是否仅改文案) |
|
|
43
|
+
| 校验 | 场景 + 字段展示条件 + 必填时机(暂存 / 提交分别怎样) |
|
|
44
|
+
| 边界 | **不考虑** …(历史数据、迁移、导出等明确排除项) |
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
材料中的技术名可译为业务表述,例如:`audit1` → 一级审批;保存不推进流程 → **暂存**;办理并推进流程 → **提交**。
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
## 示例
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
### 需求点 2:新增/编辑弹窗底部操作
|
|
52
|
+
|
|
53
|
+
- 在**新增、编辑**场景下,弹窗底部新增 **「暂存」** 按钮。
|
|
54
|
+
- **「暂存」**:沿用原 **「确定」** 按钮的业务逻辑(含校验与保存/流程行为),**仅将按钮展示文案改为「暂存」**。
|
|
55
|
+
|
|
56
|
+
### 需求点 3:「科室医德考评小组人员名单」字段
|
|
57
|
+
|
|
58
|
+
- 原字段展示名 **「科室医德考评人员名单」** 统一改为 **「科室医德考评小组人员名单」**;凡界面、提示、校验文案等涉及该名称处**一并修改**。
|
|
59
|
+
- 当流程处于 **一级审批** 且该字段**展示**时,须校验为**必填**(在展示场景下触发,而非所有保存入口一律校验)。
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 落盘前自检
|
|
63
|
+
|
|
64
|
+
- [ ] 含 **背景与目标、范围、需求说明** 三章,叙述完整
|
|
65
|
+
- [ ] 2 分钟内可通读;每个需求点 2 ~ 5 条 bullet,一层列表
|
|
66
|
+
- [ ] 场景、按钮行为、校验时机表述清楚,前后一致
|
|
67
|
+
- [ ] 「范围」与需求点中的 **「不考虑」** 口径一致
|
|
68
|
+
- [ ] 全文使用业务语言,术语统一
|
|
69
|
+
- [ ] 仅材料明确涉及时才有「非功能」「待确认」
|
|
70
|
+
|
|
71
|
+
## 局部替换
|
|
72
|
+
|
|
73
|
+
只改部分段落时:保留未改章节,替换涉及段落为完整段落;结构仍符合上文骨架。
|
|
@@ -1,57 +1,101 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apm-review
|
|
3
|
-
description: 结合本仓库上下文对需求做结构化评审,只有当用户主动提及该技能时才可被使用,该技能调用依赖需求ID。
|
|
3
|
+
description: 结合本仓库上下文对需求做结构化评审,只有当用户主动提及该技能时才可被使用,该技能调用依赖需求 ID。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 需求评审
|
|
7
7
|
|
|
8
8
|
用户仅提供 **需求 ID**(workitem id)。缺 ID 时索要,不猜测。
|
|
9
9
|
|
|
10
|
+
## 评审目标
|
|
11
|
+
|
|
12
|
+
帮助产品经理**梳理需求边界、发现文档中未写清的口径**,并在实现难度较高时给出提示。**不是**为了凑问题而提问——PRD 已足够清晰且无高难度改造时,可只输出少量锚定条目,不必强行列出待澄清项。
|
|
13
|
+
|
|
14
|
+
## 评审立场
|
|
15
|
+
|
|
16
|
+
每轮评审从 **前端 / 后端 / 全栈** 中择定一种立场,按该视角输出 **YAML 行级评审**,并执行一次 `apm comment --format=structured`。
|
|
17
|
+
|
|
18
|
+
### 如何确定立场
|
|
19
|
+
|
|
20
|
+
1. **用户明示**:用户说明「前端 / 后端 / 全栈 / 仅 UI / 仅接口」等,以用户为准。
|
|
21
|
+
2. **用户未说明**:阅读仓库中与该需求相关的现有代码,推断评审者更贴近哪一端;仍无法判断时,**默认前端**,并在 `reviewer.stance` 使用 `frontend`。
|
|
22
|
+
3. **全栈**:在同一轮评审中同时覆盖 UI 与流程/数据关注点;同一业务点若需澄清,合并为**一条** `clarify`(不要拆成两句重复问题)。
|
|
23
|
+
|
|
24
|
+
### 各端关注点(互斥优先,减少并行评审重复)
|
|
25
|
+
|
|
26
|
+
| 维度 | 前端 | 后端 |
|
|
27
|
+
| -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
28
|
+
| **核心关注** | 页面、弹窗、Tab、按钮、文案、显隐、交互顺序、校验在界面上的反馈 | 流程节点、业务规则、数据落库、字段口径、汇总/计算逻辑、权限与校验时机 |
|
|
29
|
+
| **典型待澄清** | 哪些页面要改、按钮在什么场景出现、暂存/提交时界面如何提示、只读/可编辑范围 | 默认值与枚举、必填校验在暂存/提交下是否不同、主子表关系、汇总取哪一级数据 |
|
|
30
|
+
| **尽量不提** | 表结构、API 契约、payload 字段名(留给技术评审) | 组件名、具体控件选型、Tab 顺序(除非 PRD 已涉及且影响校验规则) |
|
|
31
|
+
|
|
32
|
+
**去重规则**(按立场选用):
|
|
33
|
+
|
|
34
|
+
- **前端立场**:只写界面与交互侧待澄清,不写落库/API 契约类问题(联调诉求用 `kind: coordination`)。
|
|
35
|
+
- **后端立场**:只写流程、规则与数据侧待澄清,不写控件选型、Tab 布局等纯 UI 问题。
|
|
36
|
+
- **全栈立场**:同一业务点合并为一条 `clarify`,**不要**拆成两句意思重复的问题。
|
|
37
|
+
|
|
38
|
+
## 表述规范(产品经理可读)
|
|
39
|
+
|
|
40
|
+
1. **产品语言优先**:用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体(行号锚定除外)。
|
|
41
|
+
2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
|
|
42
|
+
3. **锚定 PRD**:每条 `items[]` 必须写 **`anchor: { start, end }`**,与 Read 读到的 `prd.md` 行号一致;**不**在 `body` 里复述该段需求原文。
|
|
43
|
+
4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点。已写清楚的规则不要重复质疑。
|
|
44
|
+
|
|
45
|
+
## 不在「待澄清」里写的内容
|
|
46
|
+
|
|
47
|
+
以下不得使用 `kind: clarify`,若需记录则用 **`kind: coordination`**(且必须有行号锚定):
|
|
48
|
+
|
|
49
|
+
- 接口字段名、请求体结构、子表编码
|
|
50
|
+
- 前后端谁传哪个参数、现有 edit 接口是否够用
|
|
51
|
+
- 与现网代码实现对齐的改造方案
|
|
52
|
+
|
|
53
|
+
`coordination` **不用产品回复**,仅作记录,待技术评审阶段处理。若同一行区间既要澄清又要联调,**拆成两条** `items`。
|
|
54
|
+
|
|
10
55
|
## 评审原则
|
|
11
56
|
|
|
12
|
-
1.
|
|
13
|
-
2. **与仓库对齐(能力边界)**:阅读 `AGENTS.md
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
|
|
17
|
-
- 若已足以判断「可以实现」(非关键细节不阻塞落地),写 `- **可行性:**`(成本低/中/高 + 精简说明);影响面大时再追加 `- **风险点:**`。
|
|
18
|
-
5. **澄清优先但不泛问**:不阻塞落地则不提问;不问可推断的琐碎问题。
|
|
19
|
-
6. **隐性知识沉淀可执行**:对用户单独输出的每条沉淀建议应能回答「在仓库里补什么、能消除哪类隐性歧义」;若本次未发现值得沉淀的差异点,仍须给出一句结论(例如「未发现与通用假设显著偏离、需单独成文的隐性规则」)。
|
|
57
|
+
1. **产品视角优先**:用户侧影响、业务闭环、边界与风险。
|
|
58
|
+
2. **与仓库对齐(能力边界)**:阅读 `AGENTS.md`、`.apm/product-capability-inventory` 及与需求相关的代码,判断改造面。**禁止**用代码证明 PRD 对错;口径不清归入 `clarify`。
|
|
59
|
+
3. **`kind: business`**:仅当方案与业务目标明显冲突或明显非较优解时使用。
|
|
60
|
+
4. **`kind: difficulty`**:仅当改造面大或业务逻辑影响大时使用。
|
|
61
|
+
5. **澄清优先但不泛问**:不阻塞理解则不提问。
|
|
20
62
|
|
|
21
63
|
## 执行步骤
|
|
22
64
|
|
|
23
65
|
### 步骤 1:读取 `prd.md`
|
|
24
66
|
|
|
25
67
|
1. 使用 **Read** 读取 `.apm/workitems/<需求ID>/prd.md`。
|
|
26
|
-
2. 当 PRD 涉及到图片链接时,自动下载对应的图片,存放在 `.apm/workitems/<需求ID>/images/<对应图片名称>.png
|
|
27
|
-
3. 若 Read
|
|
68
|
+
2. 当 PRD 涉及到图片链接时,自动下载对应的图片,存放在 `.apm/workitems/<需求ID>/images/<对应图片名称>.png`,并理解图片内容。
|
|
69
|
+
3. 若 Read **失败**:本步骤记为失败,**终止**,不执行后续步骤。
|
|
28
70
|
|
|
29
71
|
### 步骤 2:读代码
|
|
30
72
|
|
|
31
|
-
阅读仓库源码以及 `.apm/product-capability-inventory`
|
|
32
|
-
|
|
33
|
-
### 步骤 3:评审
|
|
34
|
-
|
|
35
|
-
1. **拆条**:多条诉求时拆成 `### 需求点 1..N`,每条独立走完「业务合理性(可选)→ 问题 或 可行性(+风险)」逻辑。
|
|
36
|
-
|
|
37
|
-
2. **成文(仅评审模板)**:按 `output-template.md` 拼出写入 comment 的 Markdown(顶格可加 `### 评审人` + 当前模型名);**不含**「隐性知识沉淀」段落;
|
|
38
|
-
|
|
39
|
-
3. 使用 **Write** 将正文写入**临时文件**,路径建议使用**绝对路径**,例如 `/tmp/apm-review-<需求ID>.md`。**Markdown 评审正文**:**只写确实存在的问题**;用「需求背景 / 需求范围 / 交互与功能要求第 X 节 / 非目标」等文档自有结构**点名条款**(半句锚定即可),不先单独铺一节「对用户意图的理解」,**不写与 `prd-review` 类似的整条「需求描述」复述**。撰写前须完成代码检索并锁定本轮**评审立场**(见 reference 中「评审立场」),正文内容与措辞须与该立场一致,**不得超越可见范围下断定**。
|
|
73
|
+
阅读仓库源码以及 `.apm/product-capability-inventory` 中与需求相关的条目,用于**理解现网能力与边界、判断改造面**。
|
|
40
74
|
|
|
41
|
-
|
|
75
|
+
### 步骤 3:评审与提交
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
1. **锁定立场**,填入 YAML `reviewer.stance`(`frontend` / `backend` / `fullstack`)。
|
|
78
|
+
2. **拆条**:每条对应 `prd.md` 连续行号;按需设置 `kind`(`clarify` / `difficulty` / `business` / `coordination`)与 `body`。
|
|
79
|
+
3. **成文**:按 **[output-template.md](./output-template.md)** 写 YAML。
|
|
80
|
+
4. 使用 **Write** 写入临时文件(建议 `/tmp/apm-review-<需求ID>.yaml`)。
|
|
81
|
+
5. **自检**:
|
|
82
|
+
- 每条均有 `anchor`,且无全篇评审;
|
|
83
|
+
- `stance` 与立场一致;
|
|
84
|
+
- `clarify` 均为产品可回答的业务问题;
|
|
85
|
+
- `coordination` 无 `reply` 字段;
|
|
86
|
+
- 无重复语义的条目。
|
|
87
|
+
6. 在项目根目录执行:
|
|
44
88
|
|
|
45
|
-
|
|
89
|
+
`apm comment <需求ID> --file=<临时文件绝对路径> --format=structured --model=<模型名>`
|
|
46
90
|
|
|
47
|
-
|
|
91
|
+
7. 命令结束后 **删除临时文件**(失败时若需排错可保留,默认仍删除)。
|
|
48
92
|
|
|
49
|
-
|
|
93
|
+
### 步骤 4:回复用户
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
对用户可见回复 **仅限一张 Markdown 表格**,汇总各步**结果**。**不得**在表格外输出其他内容。
|
|
52
96
|
|
|
53
|
-
| 步骤 | 结果 | 说明
|
|
54
|
-
| ----------------- | ----------- |
|
|
55
|
-
| 1. 读取 prd.md | 成功 / 失败 |
|
|
56
|
-
| 2. 评审与 comment | 成功 / 失败 |
|
|
57
|
-
| 3. 清理临时文件 | 成功 / 失败 | —
|
|
97
|
+
| 步骤 | 结果 | 说明 |
|
|
98
|
+
| ----------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
99
|
+
| 1. 读取 prd.md | 成功 / 失败 | 实际路径;失败时写原因 |
|
|
100
|
+
| 2. 评审与 comment | 成功 / 失败 | 临时文件;`apm comment` 情况;**注明 stance**(frontend / backend / fullstack) |
|
|
101
|
+
| 3. 清理临时文件 | 成功 / 失败 | — |
|
|
@@ -1,43 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
# 评审输出模板(YAML · structured)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- (关键点 1)
|
|
5
|
-
- (关键点 2)
|
|
3
|
+
使用 **YAML** 文件提交行级评审;每条必须锚定 `prd.md` 行号。全文使用**产品经理可读**的表述;**不**在 `body` 里复述需求原文。
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
## 文件示例
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
```yaml
|
|
8
|
+
reviewer:
|
|
9
|
+
model: Auto
|
|
10
|
+
stance: frontend # frontend | backend | fullstack
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
items:
|
|
13
|
+
- anchor: { start: 44, end: 55 }
|
|
14
|
+
kind: clarify
|
|
15
|
+
body: |
|
|
16
|
+
- 「类型」隐藏后,统计分析页的「分类」筛选是否一并去掉,导出是否统一走临床模板
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
- anchor: { start: 59, end: 67 }
|
|
19
|
+
kind: clarify
|
|
20
|
+
body: |
|
|
21
|
+
- (PRD 已写清且无缺口时可省略该条,不要写「仅锚定」空条目)
|
|
17
22
|
|
|
18
|
-
-
|
|
23
|
+
- anchor: { start: 83, end: 106 }
|
|
24
|
+
kind: business
|
|
25
|
+
body: |
|
|
26
|
+
- 方案与业务目标冲突的原因;可给替代思路
|
|
19
27
|
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
- anchor: { start: 108, end: 132 }
|
|
29
|
+
kind: difficulty
|
|
30
|
+
body: |
|
|
31
|
+
- 用产品语言说明改造面:涉及哪些页面/流程/规则,为何面大
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
- anchor: { start: 44, end: 55 }
|
|
34
|
+
kind: coordination
|
|
35
|
+
body: |
|
|
36
|
+
- 需与后端对齐的接口/字段诉求(不用产品回复)
|
|
37
|
+
```
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
- **可行性:**
|
|
28
|
-
- 成本:中/高
|
|
29
|
-
- (在不引入猜测的前提下,说明成本对应的业务/口径/环节调整)
|
|
30
|
-
- **风险点:**
|
|
31
|
-
- (风险 1:可能影响的不止是展示文案,例如统计口径/汇总逻辑/跨模块联动等)
|
|
32
|
-
- (风险 2)
|
|
39
|
+
## kind 与技能表述
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
| `kind`(YAML 小写) | 含义 | 说明 |
|
|
42
|
+
| ------------------- | ---------- | ---------------------------- |
|
|
43
|
+
| `clarify` | 待澄清 | 文档未写清、影响本端理解边界 |
|
|
44
|
+
| `difficulty` | 实现难度高 | 仅改造面大或业务逻辑影响大时 |
|
|
45
|
+
| `business` | 业务合理性 | 仅方案明显不合理时 |
|
|
46
|
+
| `coordination` | 联调依赖 | **禁止** `reply`;待技术评审 |
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
- **业务合理性:**
|
|
38
|
-
- (为何不合理/与用户目标或现有业务冲突/用户方案非当下较优;可写更可取的替代思路或需产品先拍板的点)
|
|
39
|
-
- **可行性:**
|
|
40
|
-
- 成本:低/中/高
|
|
41
|
-
- (仍可在指出业务问题的同时评估落地成本;若信息仍不足以落地,则改用「需求点 2」结构:业务合理性 + **问题**,不出现可行性/风险点)
|
|
48
|
+
## 字段规则
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
| 字段 | 规则 |
|
|
51
|
+
| ----------------- | ---------------------------------------------------------------- |
|
|
52
|
+
| `reviewer.stance` | 必填:`frontend` / `backend` / `fullstack` |
|
|
53
|
+
| `reviewer.model` | 建议填写;可被 CLI `--model` 覆盖 |
|
|
54
|
+
| `anchor` | 必填 `{ start, end }`,1-based 闭区间,与 Read `prd.md` 行号一致 |
|
|
55
|
+
| `body` | 必填;产品语言;**禁止**全篇评审(必须有行号) |
|
|
56
|
+
| 同区间多类型 | 拆成多条 `items`(例如同时 `clarify` 与 `coordination` 各一条) |
|
|
57
|
+
|
|
58
|
+
## 提交命令
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
apm comment <需求ID> --file=/tmp/apm-review-<需求ID>.yaml --format=structured --model=<模型名>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`.yaml` / `.yml` 扩展名在未指定 `--format` 时默认按 structured 解析。
|