ai-project-manage-cli 6.0.33 → 6.0.35
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 +196 -59
- package/package.json +1 -1
- package/template/AGENTS.md +17 -4
- package/template/rules/reply.md +2 -2
package/dist/index.js
CHANGED
|
@@ -1012,9 +1012,11 @@ async function runAppendMessage(options) {
|
|
|
1012
1012
|
// src/commands/update-message-status.ts
|
|
1013
1013
|
var VALID_STATUSES = [
|
|
1014
1014
|
"CREATED",
|
|
1015
|
+
"QUEUED",
|
|
1015
1016
|
"TYPING",
|
|
1016
1017
|
"SUCCESS",
|
|
1017
|
-
"FAILED"
|
|
1018
|
+
"FAILED",
|
|
1019
|
+
"CANCELLED"
|
|
1018
1020
|
];
|
|
1019
1021
|
async function runUpdateMessageStatus(options) {
|
|
1020
1022
|
const messageId = options.id?.trim();
|
|
@@ -1052,25 +1054,17 @@ function parseAgentWsMessage(raw) {
|
|
|
1052
1054
|
function serializeAgentWsMessage(msg) {
|
|
1053
1055
|
return JSON.stringify(msg);
|
|
1054
1056
|
}
|
|
1055
|
-
function
|
|
1056
|
-
if (
|
|
1057
|
-
return { ok: false, reason: "\
|
|
1058
|
-
}
|
|
1059
|
-
const o = value;
|
|
1060
|
-
const type = o.type;
|
|
1061
|
-
if (type !== "heartbeat" && type !== "message") {
|
|
1062
|
-
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
1057
|
+
function validateHeartbeat(o) {
|
|
1058
|
+
if (o.type !== "heartbeat") {
|
|
1059
|
+
return { ok: false, reason: "\u671F\u671B heartbeat" };
|
|
1063
1060
|
}
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
return { ok: false, reason: "\u671F\u671B heartbeat" };
|
|
1067
|
-
}
|
|
1068
|
-
if (!nonEmptyString(o.userId)) {
|
|
1069
|
-
return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
|
|
1070
|
-
}
|
|
1071
|
-
return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
|
|
1061
|
+
if (!nonEmptyString(o.userId)) {
|
|
1062
|
+
return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
|
|
1072
1063
|
}
|
|
1073
|
-
|
|
1064
|
+
return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
|
|
1065
|
+
}
|
|
1066
|
+
function validateMessagePush(o) {
|
|
1067
|
+
if (o.type !== "message") {
|
|
1074
1068
|
return { ok: false, reason: "\u671F\u671B message" };
|
|
1075
1069
|
}
|
|
1076
1070
|
if (!nonEmptyString(o.messageId)) {
|
|
@@ -1108,6 +1102,35 @@ function validateAgentWsMessage(value, kind) {
|
|
|
1108
1102
|
}
|
|
1109
1103
|
};
|
|
1110
1104
|
}
|
|
1105
|
+
function validateCancel(o) {
|
|
1106
|
+
if (o.type !== "cancel") {
|
|
1107
|
+
return { ok: false, reason: "\u671F\u671B cancel" };
|
|
1108
|
+
}
|
|
1109
|
+
if (!nonEmptyString(o.messageId)) {
|
|
1110
|
+
return { ok: false, reason: "cancel \u7F3A\u5C11 messageId" };
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
ok: true,
|
|
1114
|
+
data: { type: "cancel", messageId: o.messageId.trim() }
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function validateAgentWsMessage(value, kind) {
|
|
1118
|
+
if (typeof value !== "object" || value === null) {
|
|
1119
|
+
return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
|
|
1120
|
+
}
|
|
1121
|
+
const o = value;
|
|
1122
|
+
const type = o.type;
|
|
1123
|
+
if (type !== "heartbeat" && type !== "message" && type !== "cancel") {
|
|
1124
|
+
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
1125
|
+
}
|
|
1126
|
+
if (kind === "heartbeat" || type === "heartbeat") {
|
|
1127
|
+
return validateHeartbeat(o);
|
|
1128
|
+
}
|
|
1129
|
+
if (type === "cancel") {
|
|
1130
|
+
return validateCancel(o);
|
|
1131
|
+
}
|
|
1132
|
+
return validateMessagePush(o);
|
|
1133
|
+
}
|
|
1111
1134
|
|
|
1112
1135
|
// src/commands/connect/abort-signal-debug.ts
|
|
1113
1136
|
import {
|
|
@@ -1496,6 +1519,54 @@ async function runCursorAgent(cfg, ctx, options) {
|
|
|
1496
1519
|
}
|
|
1497
1520
|
}
|
|
1498
1521
|
|
|
1522
|
+
// src/commands/connect/pre-step-cache.ts
|
|
1523
|
+
var PULL_TTL_MS = 3e4;
|
|
1524
|
+
var lastBranchSessionId = null;
|
|
1525
|
+
var lastPullAtBySession = /* @__PURE__ */ new Map();
|
|
1526
|
+
function shouldRunBranch(sessionId) {
|
|
1527
|
+
return lastBranchSessionId !== sessionId;
|
|
1528
|
+
}
|
|
1529
|
+
function markBranchDone(sessionId) {
|
|
1530
|
+
lastBranchSessionId = sessionId;
|
|
1531
|
+
}
|
|
1532
|
+
function shouldRunPull(sessionId) {
|
|
1533
|
+
const last = lastPullAtBySession.get(sessionId);
|
|
1534
|
+
if (last == null) {
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
return Date.now() - last >= PULL_TTL_MS;
|
|
1538
|
+
}
|
|
1539
|
+
function markPullDone(sessionId) {
|
|
1540
|
+
lastPullAtBySession.set(sessionId, Date.now());
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/commands/connect/run-slot-pool.ts
|
|
1544
|
+
var DEFAULT_MAX_CONCURRENT = 2;
|
|
1545
|
+
function createRunSlotPool(maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
1546
|
+
let active = 0;
|
|
1547
|
+
const waiters = [];
|
|
1548
|
+
const acquire = () => {
|
|
1549
|
+
if (active < maxConcurrent) {
|
|
1550
|
+
active += 1;
|
|
1551
|
+
return Promise.resolve();
|
|
1552
|
+
}
|
|
1553
|
+
return new Promise((resolve5) => {
|
|
1554
|
+
waiters.push(() => {
|
|
1555
|
+
active += 1;
|
|
1556
|
+
resolve5();
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
};
|
|
1560
|
+
const release = () => {
|
|
1561
|
+
active = Math.max(0, active - 1);
|
|
1562
|
+
const next = waiters.shift();
|
|
1563
|
+
if (next) {
|
|
1564
|
+
next();
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
return { acquire, release };
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1499
1570
|
// src/commands/connect.ts
|
|
1500
1571
|
var HEARTBEAT_MS = 3e4;
|
|
1501
1572
|
async function updateMessageStatus(cfg, messageId, status) {
|
|
@@ -1509,55 +1580,62 @@ async function setMessageError(cfg, messageId, error) {
|
|
|
1509
1580
|
console.log(`[apm] \u5DF2\u8BBE\u7F6E\u6D88\u606F\u9519\u8BEF: ${messageId}`);
|
|
1510
1581
|
}
|
|
1511
1582
|
var SHUTDOWN_DRAIN_MS = 3e3;
|
|
1512
|
-
|
|
1583
|
+
function isUserCancelled(ctx) {
|
|
1584
|
+
return ctx.perMessageSignal.aborted && !ctx.shutdownSignal.aborted;
|
|
1585
|
+
}
|
|
1586
|
+
async function handleInboundMessage(cfg, msg, signal, ctx) {
|
|
1587
|
+
if (isUserCancelled(ctx)) return;
|
|
1513
1588
|
if (signal.aborted) return;
|
|
1514
|
-
const parsed = parseAgentWsMessage(raw);
|
|
1515
|
-
if (parsed === null) {
|
|
1516
|
-
console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
const validated = validateAgentWsMessage(parsed, "outbound");
|
|
1523
|
-
if (!validated.ok) {
|
|
1524
|
-
console.error(`[apm] \u6536\u5230\u65E0\u6548 message: ${validated.reason}`);
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
if (validated.data.type !== "message") {
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
const msg = validated.data;
|
|
1531
1589
|
const messageId = msg.messageId;
|
|
1532
1590
|
const runStep = async (step, fn) => {
|
|
1591
|
+
const startedAt = Date.now();
|
|
1533
1592
|
try {
|
|
1534
|
-
|
|
1593
|
+
const result = await fn();
|
|
1594
|
+
console.log(`[apm] step=${step} elapsed=${Date.now() - startedAt}ms`);
|
|
1595
|
+
return result;
|
|
1535
1596
|
} catch (err) {
|
|
1536
1597
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1537
1598
|
throw new Error(`[${step}] ${detail}`);
|
|
1538
1599
|
}
|
|
1539
1600
|
};
|
|
1540
1601
|
try {
|
|
1541
|
-
if (signal.aborted) return;
|
|
1542
|
-
await runStep(
|
|
1543
|
-
"branch",
|
|
1544
|
-
() => runBranch(msg.sessionId, { cwd: msg.workdir })
|
|
1545
|
-
);
|
|
1546
|
-
if (signal.aborted) return;
|
|
1547
|
-
await runStep(
|
|
1548
|
-
"pull",
|
|
1549
|
-
() => runPull(msg.sessionId, workspaceApmDir(msg.workdir))
|
|
1550
|
-
);
|
|
1551
|
-
if (signal.aborted) return;
|
|
1552
|
-
await runStep(
|
|
1553
|
-
"commit-pull",
|
|
1554
|
-
() => commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull")
|
|
1555
|
-
);
|
|
1556
1602
|
if (signal.aborted) return;
|
|
1557
1603
|
await runStep(
|
|
1558
1604
|
"status-typing",
|
|
1559
1605
|
() => updateMessageStatus(cfg, messageId, "TYPING")
|
|
1560
1606
|
);
|
|
1607
|
+
if (shouldRunBranch(msg.sessionId)) {
|
|
1608
|
+
if (signal.aborted) return;
|
|
1609
|
+
await runStep(
|
|
1610
|
+
"branch",
|
|
1611
|
+
() => runBranch(msg.sessionId, { cwd: msg.workdir })
|
|
1612
|
+
);
|
|
1613
|
+
markBranchDone(msg.sessionId);
|
|
1614
|
+
} else {
|
|
1615
|
+
console.log(`[apm] step=branch skipped sessionId=${msg.sessionId}`);
|
|
1616
|
+
}
|
|
1617
|
+
let pullRan = false;
|
|
1618
|
+
if (shouldRunPull(msg.sessionId)) {
|
|
1619
|
+
if (signal.aborted) return;
|
|
1620
|
+
await runStep(
|
|
1621
|
+
"pull",
|
|
1622
|
+
() => runPull(msg.sessionId, workspaceApmDir(msg.workdir))
|
|
1623
|
+
);
|
|
1624
|
+
markPullDone(msg.sessionId);
|
|
1625
|
+
pullRan = true;
|
|
1626
|
+
} else {
|
|
1627
|
+
console.log(`[apm] step=pull skipped sessionId=${msg.sessionId}`);
|
|
1628
|
+
}
|
|
1629
|
+
if (pullRan) {
|
|
1630
|
+
if (signal.aborted) return;
|
|
1631
|
+
await runStep(
|
|
1632
|
+
"commit-pull",
|
|
1633
|
+
() => commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull")
|
|
1634
|
+
);
|
|
1635
|
+
} else {
|
|
1636
|
+
console.log(`[apm] step=commit-pull skipped sessionId=${msg.sessionId}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (signal.aborted) return;
|
|
1561
1639
|
await runStep(
|
|
1562
1640
|
"cursor-agent",
|
|
1563
1641
|
() => runCursorAgent(
|
|
@@ -1589,9 +1667,13 @@ async function handleInboundMessage(cfg, raw, signal) {
|
|
|
1589
1667
|
() => updateMessageStatus(cfg, messageId, "SUCCESS")
|
|
1590
1668
|
);
|
|
1591
1669
|
} catch (err) {
|
|
1670
|
+
if (isUserCancelled(ctx)) {
|
|
1671
|
+
console.log(`[apm] \u6D88\u606F\u5DF2\u7EC8\u6B62 messageId=${messageId}`);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1592
1674
|
console.error(
|
|
1593
1675
|
"[apm] \u5904\u7406\u6D88\u606F\u5931\u8D25:",
|
|
1594
|
-
err instanceof Error ? err.message : err
|
|
1676
|
+
err instanceof Error ? err.message : String(err)
|
|
1595
1677
|
);
|
|
1596
1678
|
if (err instanceof Error && err.stack) {
|
|
1597
1679
|
console.error(err.stack);
|
|
@@ -1643,7 +1725,10 @@ async function runConnect(options) {
|
|
|
1643
1725
|
let stopHeartbeat;
|
|
1644
1726
|
let shuttingDown = false;
|
|
1645
1727
|
const shutdownAbort = new AbortController();
|
|
1646
|
-
|
|
1728
|
+
const runSlots = createRunSlotPool();
|
|
1729
|
+
const activeTasks = /* @__PURE__ */ new Set();
|
|
1730
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
1731
|
+
const pendingCancels = /* @__PURE__ */ new Set();
|
|
1647
1732
|
const shutdown = async (code = 0) => {
|
|
1648
1733
|
if (shuttingDown) return;
|
|
1649
1734
|
shuttingDown = true;
|
|
@@ -1659,7 +1744,7 @@ async function runConnect(options) {
|
|
|
1659
1744
|
}
|
|
1660
1745
|
try {
|
|
1661
1746
|
await Promise.race([
|
|
1662
|
-
|
|
1747
|
+
Promise.all(activeTasks),
|
|
1663
1748
|
new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
|
|
1664
1749
|
]);
|
|
1665
1750
|
} catch {
|
|
@@ -1674,9 +1759,58 @@ async function runConnect(options) {
|
|
|
1674
1759
|
ws.on("message", (data) => {
|
|
1675
1760
|
if (shuttingDown) return;
|
|
1676
1761
|
const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1762
|
+
const parsed = parseAgentWsMessage(text);
|
|
1763
|
+
if (parsed === null) {
|
|
1764
|
+
console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const validated = validateAgentWsMessage(parsed, "outbound");
|
|
1771
|
+
if (!validated.ok) {
|
|
1772
|
+
console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (validated.data.type === "cancel") {
|
|
1776
|
+
const { messageId } = validated.data;
|
|
1777
|
+
pendingCancels.add(messageId);
|
|
1778
|
+
activeRuns.get(messageId)?.abort();
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (validated.data.type !== "message") {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const msg = validated.data;
|
|
1785
|
+
const perMessageController = new AbortController();
|
|
1786
|
+
activeRuns.set(msg.messageId, perMessageController);
|
|
1787
|
+
if (pendingCancels.has(msg.messageId)) {
|
|
1788
|
+
activeRuns.delete(msg.messageId);
|
|
1789
|
+
pendingCancels.delete(msg.messageId);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const signal = AbortSignal.any([
|
|
1793
|
+
shutdownAbort.signal,
|
|
1794
|
+
perMessageController.signal
|
|
1795
|
+
]);
|
|
1796
|
+
const ctx = {
|
|
1797
|
+
shutdownSignal: shutdownAbort.signal,
|
|
1798
|
+
perMessageSignal: perMessageController.signal
|
|
1799
|
+
};
|
|
1800
|
+
const task = (async () => {
|
|
1801
|
+
await runSlots.acquire();
|
|
1802
|
+
try {
|
|
1803
|
+
await handleInboundMessage(cfg, msg, signal, ctx);
|
|
1804
|
+
} finally {
|
|
1805
|
+
runSlots.release();
|
|
1806
|
+
activeRuns.delete(msg.messageId);
|
|
1807
|
+
pendingCancels.delete(msg.messageId);
|
|
1808
|
+
}
|
|
1809
|
+
})();
|
|
1810
|
+
activeTasks.add(task);
|
|
1811
|
+
void task.finally(() => {
|
|
1812
|
+
activeTasks.delete(task);
|
|
1813
|
+
});
|
|
1680
1814
|
});
|
|
1681
1815
|
ws.on("close", (code, reason) => {
|
|
1682
1816
|
console.log(
|
|
@@ -2891,7 +3025,10 @@ function buildProgram() {
|
|
|
2891
3025
|
program.command("append-message").description("\u5411\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u8FFD\u52A0\u5185\u5BB9\uFF08PUT /api/v1/cli/messages/content\uFF09").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption("--content <content>", "\u8981\u8FFD\u52A0\u7684\u6D88\u606F\u5185\u5BB9").action(async (opts) => {
|
|
2892
3026
|
await runAppendMessage(opts);
|
|
2893
3027
|
});
|
|
2894
|
-
program.command("update-message-status").description("\u66F4\u65B0\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u72B6\u6001").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption(
|
|
3028
|
+
program.command("update-message-status").description("\u66F4\u65B0\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u72B6\u6001").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption(
|
|
3029
|
+
"--status <status>",
|
|
3030
|
+
"CREATED | QUEUED | TYPING | SUCCESS | FAILED | CANCELLED"
|
|
3031
|
+
).action(async (opts) => {
|
|
2895
3032
|
await runUpdateMessageStatus(opts);
|
|
2896
3033
|
});
|
|
2897
3034
|
program.command("connect").description(
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
### 工作流程
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
按任务轻重选择路径:
|
|
6
|
+
|
|
7
|
+
#### 轻量回复(被 @ 询问、确认、同步进展)
|
|
8
|
+
|
|
9
|
+
1. 读取 `.apm/sessions/<会话ID>/session.yaml`
|
|
10
|
+
2. 读取 `.apm/rules/reply.md`
|
|
11
|
+
3. **立即**用 `apm append-message` 回复(可先简短确认,再补充)
|
|
12
|
+
4. 按需阅读 `docs/` 下的文档,有进展继续 append-message
|
|
13
|
+
|
|
14
|
+
#### 重任务(开发、写方案、评审、部署)
|
|
15
|
+
|
|
16
|
+
1. 读取 `.apm/sessions/<会话ID>/session.yaml`,必要时读取 `messages.xml` 了解历史
|
|
6
17
|
2. 根据你的名字从 `session.yaml` 的 `members` 中找到你对应的 **description**(智能体描述,非人设提示词)
|
|
7
18
|
3. 根据角色描述完成用户指定的任务
|
|
8
|
-
4.
|
|
9
|
-
5.
|
|
19
|
+
4. 任务有阶段性进展或者任务完成后必须回复消息(具体见规则 `reply.md`)
|
|
20
|
+
5. 写工作日志(具体见规则 `write_doc.md`)
|
|
21
|
+
|
|
22
|
+
**禁止**在未回复前先读完所有 docs。有进展就先 append-message。
|
|
10
23
|
|
|
11
24
|
### 目录指引
|
|
12
25
|
|
|
@@ -23,5 +36,5 @@
|
|
|
23
36
|
- 附件列表: `attachments/*`,当有文档中提及附件时,从这里查找
|
|
24
37
|
- 协作规则: `RULE.md`,在这里可以看到不同成员的协作规则,从而找其他成员协助你一起解决问题,按需阅读
|
|
25
38
|
- 协作 TODO: `TODO.md`,跨轮次任务清单(待办与已完成项),按需阅读
|
|
26
|
-
- 历史消息记录: `
|
|
39
|
+
- 历史消息记录: `messages.xml`,历史消息列表,数据量很大,按需阅读
|
|
27
40
|
- 初始目标: `TASK.md`,最初的目标,不一定具体,团队成员会有人负责让这个任务变得具体,仅供参考。如果你是相关的角色,则你需要先读取这个目标,然后规划接下来的动作。
|
package/template/rules/reply.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
1. 如果回复的内容包含文档地址,不要把前缀也写出来,直接用文档名称即可。例如: 我已经把待处理的内容更新到了 `PRD.md` 中,请查阅。
|
|
4
4
|
2. 回复的内容并非对你工作的总结,要尽可能简洁,不要长篇大论,如果同步的内容确实较多,你可以先写文档,然后指引用户去阅读你写的文档。
|
|
5
|
-
3.
|
|
5
|
+
3. 只要你有任何进展都可以调用下面的命令进行回复,你可以一直补充内容。**被 @ 提及时,收到后必须先回复一句简短确认(不超过 50 字),再开始读文档或执行任务。**
|
|
6
6
|
4. 如果有需要指定相关人,必须在消息内容中@指定出来
|
|
7
7
|
|
|
8
8
|
## 执行回复的方法
|
|
9
9
|
|
|
10
10
|
命令: `apm append-message --id=<消息ID> --content=<你要回复的消息内容>`
|
|
11
11
|
|
|
12
|
-
示例: apm append-message --id=xxxxx --content="
|
|
12
|
+
示例: apm append-message --id=xxxxx --content="收到,正在分析后端改动范围。"
|
|
13
13
|
|
|
14
14
|
## 写文档的方法
|
|
15
15
|
|