ai-project-manage-cli 6.0.40 → 6.0.42
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 +186 -44
- package/package.json +1 -1
- package/template/AGENTS.md +3 -3
- package/template/rules/reply.md +2 -2
- package/template/rules/write_doc.md +2 -6
- package/template/skills/apm-dev/SKILL.md +27 -11
- package/template/skills/apm-diff-review/SKILL.md +50 -0
- package/template/skills/apm-write-backend-api/SKILL.md +5 -5
- package/template/skills/apm-write-checklist/SKILL.md +42 -0
- package/template/skills/apm-write-checklist/checklist-template.md +34 -0
- package/template/skills/apm-write-frontend-plan/SKILL.md +1 -1
- package/template/skills/apm-write-plan/SKILL.md +57 -0
- package/template/skills/apm-write-plan/plan-template.md +65 -0
- package/template/skills/apm-write-prd/SKILL.md +0 -4
package/dist/index.js
CHANGED
|
@@ -1211,6 +1211,17 @@ async function runSyncDocument(sessionId, options) {
|
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
1213
|
// src/commands/append-message.ts
|
|
1214
|
+
async function appendMessageContent(cfg, messageId, content) {
|
|
1215
|
+
const trimmedId = messageId.trim();
|
|
1216
|
+
if (!trimmedId) {
|
|
1217
|
+
throw new Error("messageId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1218
|
+
}
|
|
1219
|
+
if (!content) {
|
|
1220
|
+
throw new Error("content \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1221
|
+
}
|
|
1222
|
+
const api = createApmApiClient(cfg);
|
|
1223
|
+
await api.cli.appendMessageContent({ id: trimmedId, content });
|
|
1224
|
+
}
|
|
1214
1225
|
async function runAppendMessage(options) {
|
|
1215
1226
|
const messageId = options.id?.trim();
|
|
1216
1227
|
if (!messageId) {
|
|
@@ -1223,8 +1234,7 @@ async function runAppendMessage(options) {
|
|
|
1223
1234
|
process.exit(1);
|
|
1224
1235
|
}
|
|
1225
1236
|
const cfg = await ensureLoggedConfig();
|
|
1226
|
-
|
|
1227
|
-
await api.cli.appendMessageContent({ id: messageId, content });
|
|
1237
|
+
await appendMessageContent(cfg, messageId, content);
|
|
1228
1238
|
console.log(`[apm] \u5DF2\u8FFD\u52A0\u6D88\u606F\u5185\u5BB9: ${messageId}`);
|
|
1229
1239
|
}
|
|
1230
1240
|
|
|
@@ -1415,9 +1425,12 @@ ${stack}`
|
|
|
1415
1425
|
}
|
|
1416
1426
|
|
|
1417
1427
|
// src/commands/connect/cursor-agent.ts
|
|
1418
|
-
import {
|
|
1428
|
+
import {
|
|
1429
|
+
Agent,
|
|
1430
|
+
CursorAgentError
|
|
1431
|
+
} from "@cursor/sdk";
|
|
1419
1432
|
import { setMaxListeners as setMaxListeners2 } from "node:events";
|
|
1420
|
-
import { resolve as
|
|
1433
|
+
import { resolve as resolve4 } from "path";
|
|
1421
1434
|
|
|
1422
1435
|
// src/session-utils.ts
|
|
1423
1436
|
var EventSession = class {
|
|
@@ -1577,6 +1590,58 @@ ${JSON.stringify(event, null, 2)}
|
|
|
1577
1590
|
\`\`\``;
|
|
1578
1591
|
}
|
|
1579
1592
|
|
|
1593
|
+
// src/commands/connect/agent-session-registry.ts
|
|
1594
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1595
|
+
import { dirname as dirname4, resolve as resolve3 } from "node:path";
|
|
1596
|
+
function registryPath(workdir, sessionId) {
|
|
1597
|
+
return resolve3(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
|
|
1598
|
+
}
|
|
1599
|
+
function readRegistry(path10) {
|
|
1600
|
+
if (!existsSync8(path10)) {
|
|
1601
|
+
return {};
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
const parsed = JSON.parse(readFileSync7(path10, "utf8"));
|
|
1605
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1606
|
+
const result = {};
|
|
1607
|
+
for (const [key, value] of Object.entries(
|
|
1608
|
+
parsed
|
|
1609
|
+
)) {
|
|
1610
|
+
if (typeof value === "string" && value.trim()) {
|
|
1611
|
+
result[key] = value.trim();
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return result;
|
|
1615
|
+
}
|
|
1616
|
+
} catch {
|
|
1617
|
+
}
|
|
1618
|
+
return {};
|
|
1619
|
+
}
|
|
1620
|
+
function writeRegistry(path10, registry) {
|
|
1621
|
+
mkdirSync5(dirname4(path10), { recursive: true });
|
|
1622
|
+
writeFileSync7(path10, `${JSON.stringify(registry, null, 2)}
|
|
1623
|
+
`, "utf8");
|
|
1624
|
+
}
|
|
1625
|
+
function loadSessionAgentId(workdir, sessionId, user) {
|
|
1626
|
+
const registry = readRegistry(registryPath(workdir, sessionId));
|
|
1627
|
+
return registry[user];
|
|
1628
|
+
}
|
|
1629
|
+
function saveSessionAgentId(workdir, sessionId, user, agentId) {
|
|
1630
|
+
const path10 = registryPath(workdir, sessionId);
|
|
1631
|
+
const registry = readRegistry(path10);
|
|
1632
|
+
registry[user] = agentId;
|
|
1633
|
+
writeRegistry(path10, registry);
|
|
1634
|
+
}
|
|
1635
|
+
function clearSessionAgentId(workdir, sessionId, user) {
|
|
1636
|
+
const path10 = registryPath(workdir, sessionId);
|
|
1637
|
+
const registry = readRegistry(path10);
|
|
1638
|
+
if (!(user in registry)) {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
delete registry[user];
|
|
1642
|
+
writeRegistry(path10, registry);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1580
1645
|
// src/commands/connect/cursor-message-log.ts
|
|
1581
1646
|
var CURSOR_MESSAGE_LOG_SYNC_INTERVAL_MS = 2e3;
|
|
1582
1647
|
function createThrottledCursorMessageLogSync(cfg, ctx, onError) {
|
|
@@ -1651,6 +1716,45 @@ async function syncCursorMessageLog(cfg, ctx, events) {
|
|
|
1651
1716
|
});
|
|
1652
1717
|
}
|
|
1653
1718
|
|
|
1719
|
+
// src/commands/connect/append-message-tool.ts
|
|
1720
|
+
function createAppendMessageCustomTools(cfg, messageId) {
|
|
1721
|
+
return {
|
|
1722
|
+
append_message: {
|
|
1723
|
+
description: "\u5411\u5F53\u524D\u4F1A\u8BDD\u6D88\u606F\u8FFD\u52A0\u56DE\u590D\u5185\u5BB9\u3002\u53EF\u591A\u6B21\u8C03\u7528\u8865\u5145\u8FDB\u5C55\uFF1B\u88AB @ \u65F6\u6536\u5230\u540E\u5E94\u5148\u7B80\u77ED\u786E\u8BA4\u518D\u6267\u884C\u4EFB\u52A1\u3002",
|
|
1724
|
+
inputSchema: {
|
|
1725
|
+
type: "object",
|
|
1726
|
+
properties: {
|
|
1727
|
+
content: {
|
|
1728
|
+
type: "string",
|
|
1729
|
+
description: "\u8981\u53D1\u9001\u5230\u7FA4\u91CC\u7684\u56DE\u590D\u5185\u5BB9"
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
required: ["content"]
|
|
1733
|
+
},
|
|
1734
|
+
execute: async (args) => {
|
|
1735
|
+
const content = typeof args.content === "string" ? args.content.trim() : "";
|
|
1736
|
+
if (!content) {
|
|
1737
|
+
return {
|
|
1738
|
+
content: [{ type: "text", text: "content \u4E0D\u80FD\u4E3A\u7A7A" }],
|
|
1739
|
+
isError: true
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
await appendMessageContent(cfg, messageId, content);
|
|
1744
|
+
console.log(`[apm] append_message \u5DF2\u8FFD\u52A0: messageId=${messageId}`);
|
|
1745
|
+
return "\u5DF2\u8FFD\u52A0\u6D88\u606F\u5185\u5BB9";
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1748
|
+
return {
|
|
1749
|
+
content: [{ type: "text", text: `\u8FFD\u52A0\u6D88\u606F\u5931\u8D25: ${detail}` }],
|
|
1750
|
+
isError: true
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1654
1758
|
// src/commands/connect/cursor-agent.ts
|
|
1655
1759
|
setMaxListeners2(50);
|
|
1656
1760
|
installAbortSignalDebug();
|
|
@@ -1672,6 +1776,36 @@ function formatCursorRunFailure(runId, options) {
|
|
|
1672
1776
|
}
|
|
1673
1777
|
return `Cursor run \u5931\u8D25: ${runId} \u2014 ${details.join("\uFF1B")}`;
|
|
1674
1778
|
}
|
|
1779
|
+
async function obtainAgent(ctx) {
|
|
1780
|
+
const agentOptions = {
|
|
1781
|
+
apiKey: ctx.apiKey,
|
|
1782
|
+
model: { id: ctx.model || "default" },
|
|
1783
|
+
local: {
|
|
1784
|
+
cwd: ctx.cwd
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
const savedAgentId = ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0;
|
|
1788
|
+
if (savedAgentId) {
|
|
1789
|
+
try {
|
|
1790
|
+
const agent2 = await Agent.resume(savedAgentId, agentOptions);
|
|
1791
|
+
console.log(
|
|
1792
|
+
`[apm] \u590D\u7528\u4F1A\u8BDD Agent user=${ctx.user} agentId=${savedAgentId}`
|
|
1793
|
+
);
|
|
1794
|
+
return { agent: agent2, resumed: true };
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
console.warn(
|
|
1797
|
+
`[apm] \u590D\u7528 Agent \u5931\u8D25\uFF08agentId=${savedAgentId}\uFF09\uFF0C\u56DE\u9000\u4E3A\u65B0\u5EFA:`,
|
|
1798
|
+
err instanceof Error ? err.message : err
|
|
1799
|
+
);
|
|
1800
|
+
clearSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const agent = await Agent.create(agentOptions);
|
|
1804
|
+
if (ctx.user) {
|
|
1805
|
+
saveSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user, agent.agentId);
|
|
1806
|
+
}
|
|
1807
|
+
return { agent, resumed: false };
|
|
1808
|
+
}
|
|
1675
1809
|
async function runCursorAgent(cfg, ctx, options) {
|
|
1676
1810
|
const signal = options?.signal;
|
|
1677
1811
|
logAbortSignalStats(signal, "runCursorAgent:start");
|
|
@@ -1682,18 +1816,18 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1682
1816
|
if (!apiKey) {
|
|
1683
1817
|
throw new Error("\u7F3A\u5C11 apiKey\uFF0C\u65E0\u6CD5\u8C03\u7528 Cursor SDK");
|
|
1684
1818
|
}
|
|
1685
|
-
const cwd =
|
|
1819
|
+
const cwd = resolve4(ctx.workdir);
|
|
1686
1820
|
const prompt = ctx.prompt;
|
|
1687
1821
|
console.log(
|
|
1688
1822
|
`[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${cwd}`
|
|
1689
1823
|
);
|
|
1690
|
-
const agent = await
|
|
1824
|
+
const { agent, resumed } = await obtainAgent({
|
|
1691
1825
|
apiKey,
|
|
1692
|
-
model:
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1826
|
+
model: ctx.model,
|
|
1827
|
+
cwd,
|
|
1828
|
+
workdir: ctx.workdir,
|
|
1829
|
+
sessionId: ctx.sessionId,
|
|
1830
|
+
user: ctx.user
|
|
1697
1831
|
});
|
|
1698
1832
|
const eventSession = new EventSession(prompt);
|
|
1699
1833
|
const remoteLogCtx = logCtx(ctx, agent.agentId);
|
|
@@ -1715,7 +1849,11 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1715
1849
|
signal?.addEventListener("abort", abortRun, { once: true });
|
|
1716
1850
|
logAbortSignalStats(signal, "runCursorAgent:after-addListener");
|
|
1717
1851
|
try {
|
|
1718
|
-
const run = await agent.send(prompt
|
|
1852
|
+
const run = await agent.send(prompt, {
|
|
1853
|
+
local: {
|
|
1854
|
+
customTools: createAppendMessageCustomTools(cfg, ctx.messageId)
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1719
1857
|
activeRun = run;
|
|
1720
1858
|
logAbortSignalStats(signal, "runCursorAgent:after-send");
|
|
1721
1859
|
console.log(`[apm] Cursor run id=${run.id} agentId=${agent.agentId}`);
|
|
@@ -1745,6 +1883,9 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1745
1883
|
resultText: result.result
|
|
1746
1884
|
});
|
|
1747
1885
|
console.error(`[apm] ${failureMessage}`);
|
|
1886
|
+
if (resumed) {
|
|
1887
|
+
clearSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user);
|
|
1888
|
+
}
|
|
1748
1889
|
throw new Error(failureMessage);
|
|
1749
1890
|
}
|
|
1750
1891
|
if (result.status === "cancelled") {
|
|
@@ -1753,6 +1894,9 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1753
1894
|
console.log(`[apm] Cursor Agent \u5B8C\u6210 messageId=${ctx.messageId}`);
|
|
1754
1895
|
} catch (err) {
|
|
1755
1896
|
if (err instanceof CursorAgentError) {
|
|
1897
|
+
if (resumed) {
|
|
1898
|
+
clearSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user);
|
|
1899
|
+
}
|
|
1756
1900
|
throw new Error(
|
|
1757
1901
|
`Cursor \u542F\u52A8\u5931\u8D25: ${err.message}${err.isRetryable ? "\uFF08\u53EF\u91CD\u8BD5\uFF09" : ""}`
|
|
1758
1902
|
);
|
|
@@ -1797,10 +1941,10 @@ function createRunSlotPool(maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
|
1797
1941
|
active += 1;
|
|
1798
1942
|
return Promise.resolve();
|
|
1799
1943
|
}
|
|
1800
|
-
return new Promise((
|
|
1944
|
+
return new Promise((resolve6) => {
|
|
1801
1945
|
waiters.push(() => {
|
|
1802
1946
|
active += 1;
|
|
1803
|
-
|
|
1947
|
+
resolve6();
|
|
1804
1948
|
});
|
|
1805
1949
|
});
|
|
1806
1950
|
};
|
|
@@ -1893,7 +2037,8 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
|
|
|
1893
2037
|
prompt: msg.content,
|
|
1894
2038
|
model: msg.model,
|
|
1895
2039
|
apiKey: msg.apiKey,
|
|
1896
|
-
workdir: msg.workdir
|
|
2040
|
+
workdir: msg.workdir,
|
|
2041
|
+
user: msg.user
|
|
1897
2042
|
},
|
|
1898
2043
|
{ signal }
|
|
1899
2044
|
)
|
|
@@ -1903,11 +2048,8 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
|
|
|
1903
2048
|
() => syncSessionDocuments(cfg, msg.sessionId, workspaceApmDir(msg.workdir))
|
|
1904
2049
|
);
|
|
1905
2050
|
await runStep(
|
|
1906
|
-
"commit-
|
|
1907
|
-
() => commitWorkingTreeIfDirty(
|
|
1908
|
-
msg.workdir,
|
|
1909
|
-
"chore(apm): sync session documents"
|
|
1910
|
-
)
|
|
2051
|
+
"commit-files",
|
|
2052
|
+
() => commitWorkingTreeIfDirty(msg.workdir, "chore(apm): commit working tree")
|
|
1911
2053
|
);
|
|
1912
2054
|
await runStep(
|
|
1913
2055
|
"status-success",
|
|
@@ -1967,7 +2109,7 @@ async function runConnect(options) {
|
|
|
1967
2109
|
}
|
|
1968
2110
|
const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
|
|
1969
2111
|
console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
|
|
1970
|
-
await new Promise((
|
|
2112
|
+
await new Promise((resolve6, reject) => {
|
|
1971
2113
|
const ws = new WebSocket(url);
|
|
1972
2114
|
let stopHeartbeat;
|
|
1973
2115
|
let shuttingDown = false;
|
|
@@ -1996,7 +2138,7 @@ async function runConnect(options) {
|
|
|
1996
2138
|
]);
|
|
1997
2139
|
} catch {
|
|
1998
2140
|
}
|
|
1999
|
-
|
|
2141
|
+
resolve6();
|
|
2000
2142
|
process.exit(code);
|
|
2001
2143
|
};
|
|
2002
2144
|
ws.on("open", () => {
|
|
@@ -2083,19 +2225,19 @@ async function runConnect(options) {
|
|
|
2083
2225
|
import path5 from "node:path";
|
|
2084
2226
|
|
|
2085
2227
|
// src/commands/deploy/internal/apm-config.ts
|
|
2086
|
-
import { existsSync as
|
|
2087
|
-
import { resolve as
|
|
2228
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
2229
|
+
import { resolve as resolve5 } from "node:path";
|
|
2088
2230
|
function loadApmConfig(options) {
|
|
2089
|
-
const p =
|
|
2231
|
+
const p = resolve5(
|
|
2090
2232
|
process.cwd(),
|
|
2091
|
-
options?.configPath ??
|
|
2233
|
+
options?.configPath ?? resolve5(workspaceApmDir(), "apm.config.json")
|
|
2092
2234
|
);
|
|
2093
|
-
if (!
|
|
2235
|
+
if (!existsSync9(p)) {
|
|
2094
2236
|
console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
|
|
2095
2237
|
process.exit(1);
|
|
2096
2238
|
}
|
|
2097
2239
|
try {
|
|
2098
|
-
const raw =
|
|
2240
|
+
const raw = readFileSync8(p, "utf8");
|
|
2099
2241
|
return JSON.parse(raw);
|
|
2100
2242
|
} catch (e) {
|
|
2101
2243
|
console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
|
|
@@ -2217,7 +2359,7 @@ import path4 from "node:path";
|
|
|
2217
2359
|
import Docker from "dockerode";
|
|
2218
2360
|
|
|
2219
2361
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
|
|
2220
|
-
import { existsSync as
|
|
2362
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
|
|
2221
2363
|
import path from "node:path";
|
|
2222
2364
|
function asOptionalTlsBuffer(value) {
|
|
2223
2365
|
if (typeof value !== "string") {
|
|
@@ -2229,8 +2371,8 @@ function asOptionalTlsBuffer(value) {
|
|
|
2229
2371
|
if (normalized === "") {
|
|
2230
2372
|
return void 0;
|
|
2231
2373
|
}
|
|
2232
|
-
if (
|
|
2233
|
-
return
|
|
2374
|
+
if (existsSync10(normalized)) {
|
|
2375
|
+
return readFileSync9(normalized);
|
|
2234
2376
|
}
|
|
2235
2377
|
const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
|
|
2236
2378
|
if (looksLikePath) {
|
|
@@ -2355,17 +2497,17 @@ var DockerodeClient = class {
|
|
|
2355
2497
|
await this.client.getImage(image).remove({ force: true });
|
|
2356
2498
|
}
|
|
2357
2499
|
async pullImage(image, auth) {
|
|
2358
|
-
const stream = await new Promise((
|
|
2500
|
+
const stream = await new Promise((resolve6, reject) => {
|
|
2359
2501
|
const pullOptions = auth ? { authconfig: auth } : void 0;
|
|
2360
2502
|
this.client.pull(image, pullOptions, (err, output) => {
|
|
2361
2503
|
if (err || !output) {
|
|
2362
2504
|
reject(err ?? new Error("docker pull \u8FD4\u56DE\u7A7A\u8F93\u51FA"));
|
|
2363
2505
|
return;
|
|
2364
2506
|
}
|
|
2365
|
-
|
|
2507
|
+
resolve6(output);
|
|
2366
2508
|
});
|
|
2367
2509
|
});
|
|
2368
|
-
await new Promise((
|
|
2510
|
+
await new Promise((resolve6, reject) => {
|
|
2369
2511
|
this.client.modem.followProgress(
|
|
2370
2512
|
stream,
|
|
2371
2513
|
(err) => {
|
|
@@ -2373,7 +2515,7 @@ var DockerodeClient = class {
|
|
|
2373
2515
|
reject(err);
|
|
2374
2516
|
return;
|
|
2375
2517
|
}
|
|
2376
|
-
|
|
2518
|
+
resolve6();
|
|
2377
2519
|
},
|
|
2378
2520
|
() => void 0
|
|
2379
2521
|
);
|
|
@@ -2440,7 +2582,7 @@ var DockerodeClient = class {
|
|
|
2440
2582
|
var createDockerodeClient = (config) => new DockerodeClient(config);
|
|
2441
2583
|
|
|
2442
2584
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
|
|
2443
|
-
import { existsSync as
|
|
2585
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
|
|
2444
2586
|
import path2 from "node:path";
|
|
2445
2587
|
function stripSurroundingQuotes(value) {
|
|
2446
2588
|
const t = value.trim();
|
|
@@ -2457,10 +2599,10 @@ function loadEnvFromFile(envFilePath) {
|
|
|
2457
2599
|
return {};
|
|
2458
2600
|
}
|
|
2459
2601
|
const targetPath = path2.resolve(envFilePath);
|
|
2460
|
-
if (!
|
|
2602
|
+
if (!existsSync11(targetPath) || !statSync4(targetPath).isFile()) {
|
|
2461
2603
|
return {};
|
|
2462
2604
|
}
|
|
2463
|
-
const raw =
|
|
2605
|
+
const raw = readFileSync10(targetPath, "utf-8");
|
|
2464
2606
|
const result = {};
|
|
2465
2607
|
for (const line of raw.split(/\r?\n/)) {
|
|
2466
2608
|
const normalized = line.trim();
|
|
@@ -2631,12 +2773,12 @@ function dockerPushImage(params, cwd) {
|
|
|
2631
2773
|
}
|
|
2632
2774
|
|
|
2633
2775
|
// src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
|
|
2634
|
-
import { existsSync as
|
|
2776
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
2635
2777
|
import path3 from "node:path";
|
|
2636
2778
|
function resolveDockerBuildPaths(cwd) {
|
|
2637
2779
|
const dockerfilePath = path3.join(cwd, "Dockerfile");
|
|
2638
2780
|
Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
|
|
2639
|
-
if (!
|
|
2781
|
+
if (!existsSync12(dockerfilePath)) {
|
|
2640
2782
|
throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
|
|
2641
2783
|
}
|
|
2642
2784
|
Logger.info("\u2713 Dockerfile \u5B58\u5728");
|
|
@@ -2864,14 +3006,14 @@ var MinioClient = class {
|
|
|
2864
3006
|
async deleteObjectsByPrefix(bucket, prefix) {
|
|
2865
3007
|
const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
|
|
2866
3008
|
const keys = [];
|
|
2867
|
-
await new Promise((
|
|
3009
|
+
await new Promise((resolve6, reject) => {
|
|
2868
3010
|
objectsStream.on("data", (obj) => {
|
|
2869
3011
|
if (obj.name) {
|
|
2870
3012
|
keys.push(obj.name);
|
|
2871
3013
|
}
|
|
2872
3014
|
});
|
|
2873
3015
|
objectsStream.on("error", reject);
|
|
2874
|
-
objectsStream.on("end",
|
|
3016
|
+
objectsStream.on("end", resolve6);
|
|
2875
3017
|
});
|
|
2876
3018
|
const chunkSize = 500;
|
|
2877
3019
|
for (let i = 0; i < keys.length; i += chunkSize) {
|
|
@@ -3109,7 +3251,7 @@ async function ensureRemoteDir(sftp, dir) {
|
|
|
3109
3251
|
}
|
|
3110
3252
|
}
|
|
3111
3253
|
function execCommand(client, command) {
|
|
3112
|
-
return new Promise((
|
|
3254
|
+
return new Promise((resolve6, reject) => {
|
|
3113
3255
|
client.exec(command, (err, stream) => {
|
|
3114
3256
|
if (err) return reject(err);
|
|
3115
3257
|
let stdout = "";
|
|
@@ -3119,7 +3261,7 @@ function execCommand(client, command) {
|
|
|
3119
3261
|
reject(new Error(`\u8FDC\u7A0B\u547D\u4EE4\u5931\u8D25 (${code}): ${stderr || stdout}`));
|
|
3120
3262
|
return;
|
|
3121
3263
|
}
|
|
3122
|
-
|
|
3264
|
+
resolve6(stdout);
|
|
3123
3265
|
}).on("data", (data) => {
|
|
3124
3266
|
stdout += data.toString();
|
|
3125
3267
|
});
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
1. 读取 `.apm/sessions/<会话ID>/session.yaml`
|
|
10
10
|
2. 读取 `.apm/rules/reply.md`
|
|
11
|
-
3. **立即**用 `
|
|
12
|
-
4. 按需阅读 `docs/`
|
|
11
|
+
3. **立即**用 `append_message` 工具回复(可先简短确认,再补充)
|
|
12
|
+
4. 按需阅读 `docs/` 下的文档,有进展继续调用 `append_message`
|
|
13
13
|
|
|
14
14
|
#### 重任务(开发、写方案、评审、部署)
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
4. 任务有阶段性进展或者任务完成后必须回复消息(具体见规则 `reply.md`)
|
|
20
20
|
5. 写工作日志(具体见规则 `write_doc.md`)
|
|
21
21
|
|
|
22
|
-
**禁止**在未回复前先读完所有 docs
|
|
22
|
+
**禁止**在未回复前先读完所有 docs。有进展就先调用 `append_message`。
|
|
23
23
|
|
|
24
24
|
### 目录指引
|
|
25
25
|
|
package/template/rules/reply.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
## 执行回复的方法
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
调用 `append_message` 工具,`content` 传入你要回复的消息内容。可多次调用补充进展。
|
|
11
11
|
|
|
12
|
-
示例:
|
|
12
|
+
示例: `append_message(content="收到,正在分析后端改动范围。")`
|
|
13
13
|
|
|
14
14
|
## 写文档的方法
|
|
15
15
|
|
|
@@ -16,13 +16,9 @@
|
|
|
16
16
|
保存位置: .apm/sessions/<会话 ID>/docs/<文件名>
|
|
17
17
|
文档内容格式: 根据你的主题来,不限制,禁止记流水账。
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## 文档同步
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
手动同步命令: `apm sync-document <会话 ID> --file=<文档名称>`
|
|
24
|
-
|
|
25
|
-
示例: `apm sync-document <会话ID> --file=张三-工作日志.md`
|
|
21
|
+
保存到 `docs/` 后,`apm connect` 会在每轮 Agent 结束时自动推送到平台,无需额外操作。
|
|
26
22
|
|
|
27
23
|
## 注意事项
|
|
28
24
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# apm-dev:按计划开发并发布测试环境
|
|
2
|
+
|
|
1
3
|
## 工作流程
|
|
2
4
|
|
|
3
|
-
### 步骤 1:
|
|
5
|
+
### 步骤 1: 获取实现计划与协作内容
|
|
4
6
|
|
|
5
|
-
1. 用 **Read**
|
|
6
|
-
2.
|
|
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 契约」章节为准,**不等后端部署完成**;契约没写清的字段 `@后端` 确认,禁止自行猜测。
|
|
9
|
+
3. 计划「假设」章节存在未确认项时,先 `@项目经理` 确认,确认前不开始开发。
|
|
7
10
|
|
|
8
11
|
### 步骤 2: 明确开发模式
|
|
9
12
|
|
|
@@ -11,33 +14,46 @@
|
|
|
11
14
|
|
|
12
15
|
- 影响范围局部:少量文件或单一层次(例如仅前端组件、或仅一个后端模块小改)。
|
|
13
16
|
- 无新表结构/大规模迁移/权限模型变更。
|
|
14
|
-
-
|
|
17
|
+
- 计划实现步骤清晰且数量少(经验上 **≤3** 条独立步骤)。
|
|
15
18
|
- 不需要跨多服务的架构裁定即可开工。
|
|
16
19
|
|
|
17
20
|
**Spec 开发**:(命中任一条即可):
|
|
18
21
|
|
|
19
22
|
- 前后端联动、多包改造或新公共抽象。
|
|
20
23
|
- 新数据模型、迁移、或安全/审计/权限相关。
|
|
21
|
-
-
|
|
24
|
+
- 计划范围大、条款多,或存在明显「待确认/多方案」需先规划。
|
|
22
25
|
- 评估认为不先产出 **proposal / design / specs / tasks** 不宜直接编码。
|
|
23
26
|
|
|
24
27
|
### 步骤 3: 如果前一步判定为 **Quick 开发** 才(在子 Agent 中)执行本步骤,否则执行下一步:
|
|
25
28
|
|
|
26
|
-
- 父 Agent 已通过 **Read**
|
|
29
|
+
- 父 Agent 已通过 **Read** 掌握本端计划;若启动新子 Agent,在委派提示中写明会话 ID、消息 ID、工作项路径、以及「实现须严格对照计划,**只允许改动计划『改动文件白名单』中列出的文件**;完成后若有代码改动须单独 `git commit`」。
|
|
27
30
|
- 使用 **Task** 工具,`subagent_type: generalPurpose`,**readonly: false**,委派子 Agent:
|
|
28
|
-
- 按需 **Read**
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
31
|
+
- 按需 **Read** 本端计划文档。
|
|
32
|
+
- 按计划直接改代码;遵守本仓库构建与依赖约定(AGENTS.md)。
|
|
33
|
+
- **白名单约束**:只改计划白名单内的文件。开发中确需新增文件或改动白名单外文件,先更新计划文档的白名单(写明原因),再动手;**禁止悄悄越界**。
|
|
34
|
+
- **Git**:实现与自洽验收通过后,若有代码改动,**立即 `git add` + `git commit` 一次**(Quick 通常为单次交付,**一次实现 = 一个 commit**;勿拆成无意义碎 commit)。提交信息建议包含 `sessionId`(可从 `session.yaml` 获取)与需求摘要。
|
|
35
|
+
- 完成后在返回中说明:改了哪些路径、与白名单的对账结果(逐文件列出)、是否通过本地可执行的检查(若子 Agent 跑了构建/测试则写明结果);若有 commit,写明 **short-sha** 与 **subject**,无代码改动则注明跳过 commit。
|
|
32
36
|
|
|
33
37
|
### 步骤 4: 如果前一步判定为 **Spec 开发** 才(在子 Agent 中)执行本步骤,否则执行下一步:
|
|
34
38
|
|
|
35
39
|
1. 父 Agent **Read** `.apm/skills/apm-propose/SKILL.md` 和 `.apm/skills/apm-apply-change/SKILL.md`
|
|
36
40
|
2. **子 Agent A(规划)**:Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-propose/SKILL.md` 并完整遵循:在 `plans/` 下生成 **proposal、design、specs、tasks** 等工件。
|
|
37
|
-
3. **子 Agent B(实现)**:待 A 成功落盘后,再 Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-apply-change/SKILL.md` 并完整遵循:按 **`plans/tasks.md`** 驱动实现与勾选;遵守该技能中的停止条件与 commit
|
|
41
|
+
3. **子 Agent B(实现)**:待 A 成功落盘后,再 Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-apply-change/SKILL.md` 并完整遵循:按 **`plans/tasks.md`** 驱动实现与勾选;遵守该技能中的停止条件与 commit 约定;同样遵守计划「改动文件白名单」。
|
|
38
42
|
4. 若 **apm-propose** 未产出可用 **`plans/tasks.md`**,不得强行进入 **apm-apply-change**;表格中标记阻塞原因。
|
|
39
43
|
|
|
40
44
|
### 步骤 5: 提交并 push 代码,保证工作区干净
|
|
41
45
|
|
|
42
46
|
- Quick / Spec 子 Agent 完成且本地已有 commit 时,父 Agent **立即 `git push`**(当前分支首次 push 用 `git push -u origin HEAD`)。
|
|
43
47
|
- 无本地 commit、无远程或未配置 upstream 时说明原因,勿强行 push。
|
|
48
|
+
|
|
49
|
+
### 步骤 6: 构建验证与发布测试环境(完成定义)
|
|
50
|
+
|
|
51
|
+
开发完成的定义是以下三项**全部满足**,缺一不可:
|
|
52
|
+
|
|
53
|
+
1. **构建通过**:执行本仓库的构建/检查命令(见 AGENTS.md 或部署文档),失败必须修复后重试。
|
|
54
|
+
2. **发布测试环境**:**Read** `.apm/skills/apm-deploy/SKILL.md` 并按其流程部署;部署涉及 SQL 变更时,在回复中明确写出待执行的 SQL 文件名。
|
|
55
|
+
3. **白名单对账**:在回复中逐文件列出本次改动与计划白名单的对应关系。
|
|
56
|
+
|
|
57
|
+
**注意:不做联调。** 前后端各自按 API 契约交付,接口对不上属于契约或实现问题,由 diff 评审与人工验收暴露后打回修复;禁止自行发起「联调」「接口实测」类的开放式动作。
|
|
58
|
+
|
|
59
|
+
完成后用 `append_message` 回复:改动概述 + 白名单对账 + 构建结果 + 测试环境地址,并 `@` 评审角色进行 diff 评审。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# apm-diff-review:开发完成后的代码改动评审
|
|
2
|
+
|
|
3
|
+
## 适用范围
|
|
4
|
+
|
|
5
|
+
开发完成、发布测试环境之后,由评审角色(可以是另一端工程师或专职评审智能体)检查 git 改动是否「只做了该做的事」。这是防止 AI 乱改代码的机器门禁:**通过才交人验收,不通过打回开发**。
|
|
6
|
+
|
|
7
|
+
评审对象是 **diff**,不是整个代码库;禁止借评审之机重构或顺手改代码。**本技能全程只读,不允许写任何代码。**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 工作流程
|
|
12
|
+
|
|
13
|
+
### 步骤 1:定位本次任务的改动
|
|
14
|
+
|
|
15
|
+
1. **Read** `.apm/sessions/<会话ID>/docs/` 下的 `BACKEND-PLAN.md` / `FRONTEND-PLAN.md`,拿到「改动文件白名单」。
|
|
16
|
+
2. 在工作目录执行 `git log --oneline -20`,找到本任务相关的 commit(提交信息中含会话 ID 或需求关键词)。
|
|
17
|
+
3. `git diff <基线>..HEAD --stat` 与 `git diff <基线>..HEAD` 查看完整改动。
|
|
18
|
+
|
|
19
|
+
### 步骤 2:三项检查
|
|
20
|
+
|
|
21
|
+
| 检查项 | 判定 |
|
|
22
|
+
| -------------- | -------------------------------------------------------------------- |
|
|
23
|
+
| **白名单对账** | diff 中出现白名单之外的文件,且计划未更新说明 → **不通过** |
|
|
24
|
+
| **需求相关性** | 存在与本需求无关的改动(顺手重构、改格式、动了无关逻辑)→ **不通过** |
|
|
25
|
+
| **计划落实** | 计划「实现步骤」中的关键点在 diff 中找不到对应实现 → **不通过** |
|
|
26
|
+
|
|
27
|
+
注意事项:
|
|
28
|
+
|
|
29
|
+
- 锚定计划和需求原文做判断,不要凭个人偏好挑剔代码风格。
|
|
30
|
+
- 改动是否「无关」拿不准时,倾向放过,但在回复中提示验收人留意。
|
|
31
|
+
|
|
32
|
+
### 步骤 3:输出结论
|
|
33
|
+
|
|
34
|
+
**通过**:用 `append_message` 回复评审结论,并 `@项目经理` 交人验收,回复中必须包含:
|
|
35
|
+
|
|
36
|
+
1. 「diff 评审通过」+ 一句话改动概述;
|
|
37
|
+
2. 测试环境地址(从开发的回复或工作日志中获取);
|
|
38
|
+
3. 提示按 `CHECKLIST.md` 逐条验收。
|
|
39
|
+
|
|
40
|
+
**不通过**:用 `append_message` 输出问题清单(每条注明文件 + 问题 + 依据哪条计划/需求),`@` 对应工程师打回修改。**禁止自己动手改。**
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 迭代上限
|
|
45
|
+
|
|
46
|
+
同一任务的 diff 评审**最多打回 3 次**。第 3 次仍不通过时,不再打回,改为 `@项目经理` 说明僵持原因,由人决策(人工修复 / 调整计划 / 放弃本次改动)。
|
|
47
|
+
|
|
48
|
+
## 何时使用
|
|
49
|
+
|
|
50
|
+
协作流程阶段 4(diff 评审);或用户要求评审代码改动。
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
后端工程师在需求评审通过后,编写两份文档:
|
|
4
4
|
|
|
5
|
-
| 文档
|
|
6
|
-
|
|
5
|
+
| 文档 | 路径 | 定位 |
|
|
6
|
+
| ------------ | ----------------- | ---------------------------------------- |
|
|
7
7
|
| `BACKEND.md` | `docs/BACKEND.md` | **Plan**:后端怎么改、分几步、动哪些文件 |
|
|
8
|
-
| `API.md`
|
|
8
|
+
| `API.md` | `docs/API.md` | **联调契约**:给前端看的 URL、参数、示例 |
|
|
9
9
|
|
|
10
10
|
两份文档禁止合并;`API.md` 不写 Service/SQL 等实现细节。
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
2. 按需调研代码库,确认现网接口与表结构。
|
|
18
18
|
3. **Read** `backend-template.md`,按模板 **Write** `docs/BACKEND.md`。
|
|
19
19
|
4. **Read** `api-template.md`,按模板 **Write** `docs/API.md`。
|
|
20
|
-
5.
|
|
20
|
+
5. @ 前端阅读 `API.md` 并编写 `FRONTEND.md`。
|
|
21
21
|
|
|
22
22
|
(模板路径:`.apm/skills/apm-write-backend-api/`)
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
## 写作要求
|
|
27
27
|
|
|
28
|
-
**BACKEND.md**(对齐 Cursor Plan,通常 **30~80 行**)
|
|
28
|
+
**BACKEND.md**(对齐 Cursor Plan,通常 **30 ~ 80 行**)
|
|
29
29
|
|
|
30
30
|
- 背景 → 实现步骤 → 涉及文件 → 数据与规则 → 验收
|
|
31
31
|
- 可写表名、关键字段;不要大段 SQL、不要完整参数表(那些放 `API.md`)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# apm-write-checklist:编写人工回归要点清单
|
|
2
|
+
|
|
3
|
+
## 适用范围
|
|
4
|
+
|
|
5
|
+
测试智能体在前后端计划(`BACKEND-PLAN.md` / `FRONTEND-PLAN.md`)就绪后,编写 `docs/CHECKLIST.md`。
|
|
6
|
+
|
|
7
|
+
这份清单是**给人做回归验收用的**,不是自动化测试用例:把需求的关键点过一遍即可,一条一行,人拿着它在测试环境点一遍就能判断需求做没做对。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 工作流程
|
|
12
|
+
|
|
13
|
+
### 步骤 1:读需求与计划
|
|
14
|
+
|
|
15
|
+
1. **Read** `.apm/sessions/<会话ID>/TASK.md` —— **以需求原文为准出题**,不要只从计划推导(计划理解错了,照着计划出的题也会跟着错)。
|
|
16
|
+
2. **Read** `docs/BACKEND-PLAN.md`、`docs/FRONTEND-PLAN.md`(存在哪份读哪份)。
|
|
17
|
+
|
|
18
|
+
### 步骤 2:交叉校验(这一步是打回机制)
|
|
19
|
+
|
|
20
|
+
逐条对照需求原文与计划:
|
|
21
|
+
|
|
22
|
+
- **计划与需求矛盾、或需求关键点在计划中没有覆盖**:不要写清单,先用 `append_message` 指出问题并 `@` 对应工程师打回,等计划修订后再写。
|
|
23
|
+
- **计划中的「假设」尚未被项目经理确认**:在回复中提醒,但可以先写清单(清单按需求原文出)。
|
|
24
|
+
- 计划覆盖完整:进入步骤 3。
|
|
25
|
+
|
|
26
|
+
### 步骤 3:按模板写清单
|
|
27
|
+
|
|
28
|
+
**Read** `.apm/skills/apm-write-checklist/checklist-template.md`,按模板 **Write** `docs/CHECKLIST.md`。
|
|
29
|
+
|
|
30
|
+
要求:
|
|
31
|
+
|
|
32
|
+
- **5 ~ 15 条**,关键点级别,不写入参出参细节、不写接口路径。
|
|
33
|
+
- 每条 = 在哪个页面、做什么操作、预期看到什么。
|
|
34
|
+
- 必须覆盖:需求的每个功能点至少 1 条、关键互斥/边界规则至少 1 条、对既有功能的回归至少 1 条(确认没改坏原有逻辑)。
|
|
35
|
+
|
|
36
|
+
### 步骤 4:回复
|
|
37
|
+
|
|
38
|
+
回复消息说明清单已就绪,可进入开发。
|
|
39
|
+
|
|
40
|
+
## 何时使用
|
|
41
|
+
|
|
42
|
+
协作流程阶段 2(测试要点);或用户要求写 `CHECKLIST.md`。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 回归要点清单模板
|
|
2
|
+
|
|
3
|
+
```markdown
|
|
4
|
+
# <需求名> · 人工回归要点
|
|
5
|
+
|
|
6
|
+
> 验收人按编号逐条验证;某条不通过时,直接在群里回复「第 N 条不通过,实际现象:xxx」即可。
|
|
7
|
+
|
|
8
|
+
## 新功能验证
|
|
9
|
+
|
|
10
|
+
| # | 页面 / 位置 | 操作 | 预期 |
|
|
11
|
+
|---|-------------|------|------|
|
|
12
|
+
| 1 | 检查项目维护 → 编辑 | 勾选「是否加分」 | 「是否扣分」自动取消,两者不可同时勾选 |
|
|
13
|
+
| 2 | 检查项目维护 → 保存 | 同时勾选加分与扣分后保存 | 保存被拦截并提示互斥 |
|
|
14
|
+
| 3 | 检查模板维护 → 新增 | 选择检查分类 | 「检查分类分值」自动带入,加分项不计入汇总 |
|
|
15
|
+
|
|
16
|
+
## 边界与兼容
|
|
17
|
+
|
|
18
|
+
| # | 页面 / 位置 | 操作 | 预期 |
|
|
19
|
+
|---|-------------|------|------|
|
|
20
|
+
| 4 | 检查录入 | 对加分项录入加分并保存 | 总分正确增加且不设上限,重新打开回显一致 |
|
|
21
|
+
| 5 | 检查录入 | 打开改造前创建的历史单据 | 正常显示,原扣分逻辑不变 |
|
|
22
|
+
|
|
23
|
+
## 既有功能回归
|
|
24
|
+
|
|
25
|
+
| # | 页面 / 位置 | 操作 | 预期 |
|
|
26
|
+
|---|-------------|------|------|
|
|
27
|
+
| 6 | 检查录入 | 普通扣分项的录入与保存 | 与改造前行为完全一致 |
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
写作要求:
|
|
31
|
+
|
|
32
|
+
- 表格三列固定:位置、操作、预期;每条一行,禁止嵌套步骤。
|
|
33
|
+
- 「预期」写人肉眼可判断的现象,不写「接口返回 200」这类机器口径。
|
|
34
|
+
- 总数控制在 5~15 条,超出说明粒度太细,合并。
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
1. **Read** `docs/PRD.md`、`docs/API.md`;`API.md` 不存在则退出并 @ 后端。
|
|
12
12
|
2. 按需调研代码库,确认改动入口。
|
|
13
13
|
3. **Read** `.apm/skills/apm-write-frontend-plan/plan-template.md`,按模板 **Write** `docs/FRONTEND.md`。
|
|
14
|
-
4.
|
|
14
|
+
4. 回复消息通知可进入开发。
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# apm-write-plan:直接基于需求写实现计划
|
|
2
|
+
|
|
3
|
+
## 适用范围
|
|
4
|
+
|
|
5
|
+
前端 / 后端工程师在任务启动后**直接读原始需求写实现计划**,不经过 PRD。
|
|
6
|
+
|
|
7
|
+
本技能合并了原 `apm-write-prd`、`apm-review`、`apm-write-frontend-plan`、`apm-write-backend-api` 四个技能的职能:评审(判断是否参与、发现口径缺口)和方案(怎么改、改哪些文件)一步完成。
|
|
8
|
+
|
|
9
|
+
| 角色 | 产出文档 | 路径 |
|
|
10
|
+
| ---- | --------------------------------------- | ----------------------- |
|
|
11
|
+
| 后端 | `BACKEND-PLAN.md`(含「API 契约」章节) | `docs/BACKEND-PLAN.md` |
|
|
12
|
+
| 前端 | `FRONTEND-PLAN.md` | `docs/FRONTEND-PLAN.md` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 工作流程
|
|
17
|
+
|
|
18
|
+
### 步骤 1:判断本端是否需要参与
|
|
19
|
+
|
|
20
|
+
**Read** `.apm/sessions/<会话ID>/TASK.md`(必要时结合群消息中项目经理的补充说明)。
|
|
21
|
+
|
|
22
|
+
- **不涉及本端改动**:立即用 `append_message` 回复「本需求与前端/后端无关,理由:xxx」(一句话说明理由),**流程到此结束,禁止写任何文档、禁止改任何代码**。
|
|
23
|
+
- **涉及本端改动**:进入步骤 2。
|
|
24
|
+
|
|
25
|
+
### 步骤 2:有限调研(必须遵守预算)
|
|
26
|
+
|
|
27
|
+
1. **优先读现成结论**:先读 `docs/` 下已有的模块档案、历史工作日志、其他成员已同步的文档,能复用就不要重新调研。
|
|
28
|
+
2. **再调研代码**:只看与需求直接相关的页面 / 接口 / 表。**调研预算:最多读 15 个代码文件**,禁止全库考古、禁止顺藤摸瓜阅读无关模块。
|
|
29
|
+
3. **口径不清不要自己拍板**:调研中发现需求没写清的业务口径(例如字段含义、互斥规则、历史数据兼容),一律记入计划的「依据与假设」章节,禁止编造业务规则。
|
|
30
|
+
|
|
31
|
+
### 步骤 3:按模板写计划
|
|
32
|
+
|
|
33
|
+
**Read** `.apm/skills/apm-write-plan/plan-template.md`,按模板 **Write** 对应的计划文档。三个章节为硬性要求,缺一不可:
|
|
34
|
+
|
|
35
|
+
1. **依据与假设**:每条关键口径标注来源(需求原文第几条 / 现有代码行为 / 已有文档);标不出来源的就是「假设」,单独列出。
|
|
36
|
+
2. **改动文件白名单**:本次允许改动的文件完整列表。后续开发与 diff 评审都以此为准,**开发时改了白名单之外的文件会被打回**。
|
|
37
|
+
3. **后端专属——API 契约**:给前端看的接口定义(Path、参数、响应示例、错误码)。前端开发以契约为准,不等后端部署完成。
|
|
38
|
+
|
|
39
|
+
### 步骤 4:回复
|
|
40
|
+
|
|
41
|
+
回复消息(遵守 `reply.md`):
|
|
42
|
+
|
|
43
|
+
- **「假设」章节非空**:把假设逐条列进回复内容,`@项目经理` 请其确认;明确说「以上假设确认前不开始开发」。
|
|
44
|
+
- **无假设**:回复计划已就绪,可进入测试要点编写。
|
|
45
|
+
- 前端计划依赖的接口后端契约还没出:在计划「期望接口」小节写出前端期望的接口形态,回复时 `@后端` 对齐,不要空等。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 写作要求
|
|
50
|
+
|
|
51
|
+
- 篇幅 **40 ~ 100 行**,宁可少写;不要伪代码、不要大段 SQL。
|
|
52
|
+
- 用产品语言描述行为,文件路径只出现在「改动文件白名单」。
|
|
53
|
+
- 前后端可同轮并行编写计划,不互相阻塞。
|
|
54
|
+
|
|
55
|
+
## 何时使用
|
|
56
|
+
|
|
57
|
+
协作流程阶段 1(实现计划);或用户要求写 `BACKEND-PLAN.md` / `FRONTEND-PLAN.md`。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 计划文档模板
|
|
2
|
+
|
|
3
|
+
> 后端写 `docs/BACKEND-PLAN.md`,前端写 `docs/FRONTEND-PLAN.md`。
|
|
4
|
+
> 「API 契约」章节仅后端需要;「期望接口」小节仅前端在后端契约未就绪时需要。
|
|
5
|
+
|
|
6
|
+
```markdown
|
|
7
|
+
# <需求名> · <前端|后端>实现计划
|
|
8
|
+
|
|
9
|
+
## 1. 需求理解
|
|
10
|
+
|
|
11
|
+
用 3~5 行复述本端要做什么(产品语言),不复制需求原文。
|
|
12
|
+
|
|
13
|
+
## 2. 依据与假设
|
|
14
|
+
|
|
15
|
+
### 依据(口径 + 来源)
|
|
16
|
+
|
|
17
|
+
| # | 口径 | 来源 |
|
|
18
|
+
|---|------|------|
|
|
19
|
+
| 1 | 「是否加分」与「是否扣分」互斥 | 需求原文第 1 条 |
|
|
20
|
+
| 2 | 加分项不汇总进分类分值 | 需求原文第 2 条 |
|
|
21
|
+
| 3 | 分值字段现状为 varchar | 代码现状:inspection_class 表 |
|
|
22
|
+
|
|
23
|
+
### 假设(待项目经理确认,确认前不开发)
|
|
24
|
+
|
|
25
|
+
- [ ] A1:xxx(为什么需要确认)
|
|
26
|
+
- [ ] A2:xxx
|
|
27
|
+
|
|
28
|
+
> 无假设时写「无,口径均有依据」。
|
|
29
|
+
|
|
30
|
+
## 3. 实现步骤
|
|
31
|
+
|
|
32
|
+
1. 第一步做什么(对应哪条口径)
|
|
33
|
+
2. 第二步做什么
|
|
34
|
+
3. ...(通常 3~6 步,不写代码细节)
|
|
35
|
+
|
|
36
|
+
## 4. 改动文件白名单
|
|
37
|
+
|
|
38
|
+
> 开发只允许改这些文件;diff 评审会逐一对账,越界会被打回。
|
|
39
|
+
> 开发中确需新增,先更新本清单并在群里说明原因。
|
|
40
|
+
|
|
41
|
+
- `src/views/inspection/InspectionProjectForm.vue` — 新增「是否加分」单选与互斥逻辑
|
|
42
|
+
- `src/api/inspection.ts` — 新增字段透传
|
|
43
|
+
- ...
|
|
44
|
+
|
|
45
|
+
## 5. API 契约(仅后端;前端开发以此为准)
|
|
46
|
+
|
|
47
|
+
### 5.1 <接口名>
|
|
48
|
+
|
|
49
|
+
- Path:`POST /inspection/project/save`
|
|
50
|
+
- 新增入参:
|
|
51
|
+
|
|
52
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
53
|
+
|------|------|------|------|
|
|
54
|
+
| isProjectAdd | string | 否 | "1"=加分项;与 isProjectDed 互斥 |
|
|
55
|
+
|
|
56
|
+
- 响应示例:
|
|
57
|
+
|
|
58
|
+
{ "success": true, "result": { "id": "xxx" } }
|
|
59
|
+
|
|
60
|
+
- 错误码:互斥冲突返回 `success=false, message="加分与扣分不可同时勾选"`
|
|
61
|
+
|
|
62
|
+
## 6. 期望接口(仅前端,后端契约未就绪时填写)
|
|
63
|
+
|
|
64
|
+
- 期望 `查询分类详情` 返回 `classScore` 字段,由后端确认。
|
|
65
|
+
```
|
|
@@ -12,7 +12,3 @@
|
|
|
12
12
|
**不管是第几版需求,都要当成第一版来看,禁止有历史版本或者修订版本或者第几版更新的字样**
|
|
13
13
|
|
|
14
14
|
**可读性**:信息完整保留,但避免整段只靠长句堆砌。对流程分支、状态条件、按钮对照、范围边界等多步骤/多条件内容,按模板中的「图示原则」**适当补充 Mermaid 图或简表**(每个需求点 0 ~ 1 张);图示用于快速扫读,**可验收细节仍以 bullet 为准**,不得因配图而删减文字要求。
|
|
15
|
-
|
|
16
|
-
### 步骤 3: 同步 PRD 到远程
|
|
17
|
-
|
|
18
|
-
执行命令:`apm sync-document <会话 ID> --file=PRD.md`
|