ai-project-manage-cli 6.0.32 → 6.0.34

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
@@ -500,11 +500,14 @@ import { join as join5 } from "path";
500
500
  import { stringify as yamlStringify } from "yaml";
501
501
 
502
502
  // src/session-messages-xml.ts
503
+ function asXmlText(value) {
504
+ return value ?? "";
505
+ }
503
506
  function escapeXmlAttr(value) {
504
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
507
+ return asXmlText(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
505
508
  }
506
509
  function wrapCdata(value) {
507
- return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
510
+ return `<![CDATA[${asXmlText(value).replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
508
511
  }
509
512
  function formatSessionMessagesXml(sessionId, messages) {
510
513
  const lines = [
@@ -1011,7 +1014,8 @@ var VALID_STATUSES = [
1011
1014
  "CREATED",
1012
1015
  "TYPING",
1013
1016
  "SUCCESS",
1014
- "FAILED"
1017
+ "FAILED",
1018
+ "CANCELLED"
1015
1019
  ];
1016
1020
  async function runUpdateMessageStatus(options) {
1017
1021
  const messageId = options.id?.trim();
@@ -1049,25 +1053,17 @@ function parseAgentWsMessage(raw) {
1049
1053
  function serializeAgentWsMessage(msg) {
1050
1054
  return JSON.stringify(msg);
1051
1055
  }
1052
- function validateAgentWsMessage(value, kind) {
1053
- if (typeof value !== "object" || value === null) {
1054
- return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
1056
+ function validateHeartbeat(o) {
1057
+ if (o.type !== "heartbeat") {
1058
+ return { ok: false, reason: "\u671F\u671B heartbeat" };
1055
1059
  }
1056
- const o = value;
1057
- const type = o.type;
1058
- if (type !== "heartbeat" && type !== "message") {
1059
- return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
1060
+ if (!nonEmptyString(o.userId)) {
1061
+ return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
1060
1062
  }
1061
- if (kind === "heartbeat" || type === "heartbeat") {
1062
- if (type !== "heartbeat") {
1063
- return { ok: false, reason: "\u671F\u671B heartbeat" };
1064
- }
1065
- if (!nonEmptyString(o.userId)) {
1066
- return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
1067
- }
1068
- return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
1069
- }
1070
- if (type !== "message") {
1063
+ return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
1064
+ }
1065
+ function validateMessagePush(o) {
1066
+ if (o.type !== "message") {
1071
1067
  return { ok: false, reason: "\u671F\u671B message" };
1072
1068
  }
1073
1069
  if (!nonEmptyString(o.messageId)) {
@@ -1105,6 +1101,35 @@ function validateAgentWsMessage(value, kind) {
1105
1101
  }
1106
1102
  };
1107
1103
  }
1104
+ function validateCancel(o) {
1105
+ if (o.type !== "cancel") {
1106
+ return { ok: false, reason: "\u671F\u671B cancel" };
1107
+ }
1108
+ if (!nonEmptyString(o.messageId)) {
1109
+ return { ok: false, reason: "cancel \u7F3A\u5C11 messageId" };
1110
+ }
1111
+ return {
1112
+ ok: true,
1113
+ data: { type: "cancel", messageId: o.messageId.trim() }
1114
+ };
1115
+ }
1116
+ function validateAgentWsMessage(value, kind) {
1117
+ if (typeof value !== "object" || value === null) {
1118
+ return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
1119
+ }
1120
+ const o = value;
1121
+ const type = o.type;
1122
+ if (type !== "heartbeat" && type !== "message" && type !== "cancel") {
1123
+ return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
1124
+ }
1125
+ if (kind === "heartbeat" || type === "heartbeat") {
1126
+ return validateHeartbeat(o);
1127
+ }
1128
+ if (type === "cancel") {
1129
+ return validateCancel(o);
1130
+ }
1131
+ return validateMessagePush(o);
1132
+ }
1108
1133
 
1109
1134
  // src/commands/connect/abort-signal-debug.ts
1110
1135
  import {
@@ -1506,62 +1531,84 @@ async function setMessageError(cfg, messageId, error) {
1506
1531
  console.log(`[apm] \u5DF2\u8BBE\u7F6E\u6D88\u606F\u9519\u8BEF: ${messageId}`);
1507
1532
  }
1508
1533
  var SHUTDOWN_DRAIN_MS = 3e3;
1509
- async function handleInboundMessage(cfg, raw, signal) {
1534
+ function isUserCancelled(ctx) {
1535
+ return ctx.perMessageSignal.aborted && !ctx.shutdownSignal.aborted;
1536
+ }
1537
+ async function handleInboundMessage(cfg, msg, signal, ctx) {
1538
+ if (isUserCancelled(ctx)) return;
1510
1539
  if (signal.aborted) return;
1511
- const parsed = parseAgentWsMessage(raw);
1512
- if (parsed === null) {
1513
- console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
1514
- return;
1515
- }
1516
- if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
1517
- return;
1518
- }
1519
- const validated = validateAgentWsMessage(parsed, "outbound");
1520
- if (!validated.ok) {
1521
- console.error(`[apm] \u6536\u5230\u65E0\u6548 message: ${validated.reason}`);
1522
- return;
1523
- }
1524
- if (validated.data.type !== "message") {
1525
- return;
1526
- }
1527
- const msg = validated.data;
1528
1540
  const messageId = msg.messageId;
1541
+ const runStep = async (step, fn) => {
1542
+ try {
1543
+ return await fn();
1544
+ } catch (err) {
1545
+ const detail = err instanceof Error ? err.message : String(err);
1546
+ throw new Error(`[${step}] ${detail}`);
1547
+ }
1548
+ };
1529
1549
  try {
1530
1550
  if (signal.aborted) return;
1531
- await runBranch(msg.sessionId, { cwd: msg.workdir });
1551
+ await runStep(
1552
+ "branch",
1553
+ () => runBranch(msg.sessionId, { cwd: msg.workdir })
1554
+ );
1532
1555
  if (signal.aborted) return;
1533
- await runPull(msg.sessionId, workspaceApmDir(msg.workdir));
1556
+ await runStep(
1557
+ "pull",
1558
+ () => runPull(msg.sessionId, workspaceApmDir(msg.workdir))
1559
+ );
1534
1560
  if (signal.aborted) return;
1535
- await commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull");
1561
+ await runStep(
1562
+ "commit-pull",
1563
+ () => commitWorkingTreeIfDirty(msg.workdir, "fix: apm pull")
1564
+ );
1536
1565
  if (signal.aborted) return;
1537
- await updateMessageStatus(cfg, messageId, "TYPING");
1538
- await runCursorAgent(
1539
- cfg,
1540
- {
1541
- messageId: msg.messageId,
1542
- sessionId: msg.sessionId,
1543
- prompt: msg.content,
1544
- model: msg.model,
1545
- apiKey: msg.apiKey,
1546
- workdir: msg.workdir
1547
- },
1548
- { signal }
1566
+ await runStep(
1567
+ "status-typing",
1568
+ () => updateMessageStatus(cfg, messageId, "TYPING")
1549
1569
  );
1550
- await syncSessionDocuments(
1551
- cfg,
1552
- msg.sessionId,
1553
- workspaceApmDir(msg.workdir)
1570
+ await runStep(
1571
+ "cursor-agent",
1572
+ () => runCursorAgent(
1573
+ cfg,
1574
+ {
1575
+ messageId: msg.messageId,
1576
+ sessionId: msg.sessionId,
1577
+ prompt: msg.content,
1578
+ model: msg.model,
1579
+ apiKey: msg.apiKey,
1580
+ workdir: msg.workdir
1581
+ },
1582
+ { signal }
1583
+ )
1554
1584
  );
1555
- await commitWorkingTreeIfDirty(
1556
- msg.workdir,
1557
- "chore(apm): sync session documents"
1585
+ await runStep(
1586
+ "sync-documents",
1587
+ () => syncSessionDocuments(cfg, msg.sessionId, workspaceApmDir(msg.workdir))
1588
+ );
1589
+ await runStep(
1590
+ "commit-documents",
1591
+ () => commitWorkingTreeIfDirty(
1592
+ msg.workdir,
1593
+ "chore(apm): sync session documents"
1594
+ )
1595
+ );
1596
+ await runStep(
1597
+ "status-success",
1598
+ () => updateMessageStatus(cfg, messageId, "SUCCESS")
1558
1599
  );
1559
- await updateMessageStatus(cfg, messageId, "SUCCESS");
1560
1600
  } catch (err) {
1601
+ if (isUserCancelled(ctx)) {
1602
+ console.log(`[apm] \u6D88\u606F\u5DF2\u7EC8\u6B62 messageId=${messageId}`);
1603
+ return;
1604
+ }
1561
1605
  console.error(
1562
1606
  "[apm] \u5904\u7406\u6D88\u606F\u5931\u8D25:",
1563
1607
  err instanceof Error ? err.message : err
1564
1608
  );
1609
+ if (err instanceof Error && err.stack) {
1610
+ console.error(err.stack);
1611
+ }
1565
1612
  try {
1566
1613
  await setMessageError(
1567
1614
  cfg,
@@ -1610,6 +1657,8 @@ async function runConnect(options) {
1610
1657
  let shuttingDown = false;
1611
1658
  const shutdownAbort = new AbortController();
1612
1659
  let inboundQueue = Promise.resolve();
1660
+ const activeRuns = /* @__PURE__ */ new Map();
1661
+ const pendingCancels = /* @__PURE__ */ new Set();
1613
1662
  const shutdown = async (code = 0) => {
1614
1663
  if (shuttingDown) return;
1615
1664
  shuttingDown = true;
@@ -1640,9 +1689,48 @@ async function runConnect(options) {
1640
1689
  ws.on("message", (data) => {
1641
1690
  if (shuttingDown) return;
1642
1691
  const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
1643
- inboundQueue = inboundQueue.then(
1644
- () => handleInboundMessage(cfg, text, shutdownAbort.signal)
1645
- );
1692
+ const parsed = parseAgentWsMessage(text);
1693
+ if (parsed === null) {
1694
+ console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
1695
+ return;
1696
+ }
1697
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
1698
+ return;
1699
+ }
1700
+ const validated = validateAgentWsMessage(parsed, "outbound");
1701
+ if (!validated.ok) {
1702
+ console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
1703
+ return;
1704
+ }
1705
+ if (validated.data.type === "cancel") {
1706
+ const { messageId } = validated.data;
1707
+ pendingCancels.add(messageId);
1708
+ activeRuns.get(messageId)?.abort();
1709
+ return;
1710
+ }
1711
+ if (validated.data.type !== "message") {
1712
+ return;
1713
+ }
1714
+ const msg = validated.data;
1715
+ const perMessageController = new AbortController();
1716
+ activeRuns.set(msg.messageId, perMessageController);
1717
+ if (pendingCancels.has(msg.messageId)) {
1718
+ activeRuns.delete(msg.messageId);
1719
+ pendingCancels.delete(msg.messageId);
1720
+ return;
1721
+ }
1722
+ const signal = AbortSignal.any([
1723
+ shutdownAbort.signal,
1724
+ perMessageController.signal
1725
+ ]);
1726
+ const ctx = {
1727
+ shutdownSignal: shutdownAbort.signal,
1728
+ perMessageSignal: perMessageController.signal
1729
+ };
1730
+ inboundQueue = inboundQueue.then(() => handleInboundMessage(cfg, msg, signal, ctx)).finally(() => {
1731
+ activeRuns.delete(msg.messageId);
1732
+ pendingCancels.delete(msg.messageId);
1733
+ });
1646
1734
  });
1647
1735
  ws.on("close", (code, reason) => {
1648
1736
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.32",
3
+ "version": "6.0.34",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -11,6 +11,8 @@
11
11
  内容要求如下:
12
12
  **不管是第几版需求,都要当成第一版来看,禁止有历史版本或者修订版本或者第几版更新的字样**
13
13
 
14
+ **可读性**:信息完整保留,但避免整段只靠长句堆砌。对流程分支、状态条件、按钮对照、范围边界等多步骤/多条件内容,按模板中的「图示原则」**适当补充 Mermaid 图或简表**(每个需求点 0 ~ 1 张);图示用于快速扫读,**可验收细节仍以 bullet 为准**,不得因配图而删减文字要求。
15
+
14
16
  ### 步骤 3: 同步 PRD 到远程
15
17
 
16
18
  执行命令:`apm sync-document <会话 ID> --file=PRD.md`
@@ -1,17 +1,43 @@
1
-
2
1
  ## 需求模板
2
+
3
+ ### 图示原则
4
+
5
+ 信息量保持完整,**文字与图示互补**:bullet 写清可验收细节,图示帮助读者快速建立整体印象。优先用 **Mermaid**(Markdown 内嵌,无需额外图片文件)。
6
+
7
+ | 场景 | 推荐图示 | 作用 |
8
+ | ------------------------------------------- | -------------------------------- | -------------------------- |
9
+ | 多步骤操作流程(审批、暂存/提交、办理路径) | `flowchart` | 一眼看清先后与分支 |
10
+ | 字段展示/必填随状态变化 | `flowchart` 或 `stateDiagram-v2` | 替代「当 A 且 B 时…」长句 |
11
+ | 弹窗/页面内区块与按钮布局 | `block-beta` 或 ASCII 框图 | 标出按钮位置与对照关系 |
12
+ | 多需求点关系、范围边界 | `mindmap` 或简表 | 开篇总览,再展开 bullet |
13
+ | 角色与系统交互时序 | `sequenceDiagram` | 仅步骤多、文字难扫读时使用 |
14
+
15
+ **图示写法**:
16
+
17
+ - 每个需求点 **0 ~ 1 张** 图即可;一张图只表达一个核心概念,节点用业务语言(如「一级审批」「暂存」)。
18
+ - 图 **不替代** bullet:图后仍保留 2 ~ 5 条可验收 bullet,图中未写清的细节(文案、边界、校验时机)必须在文字中写明。
19
+ - 图为辅助:若单条 bullet 已足够清楚,**不必强行配图**。
20
+
3
21
  ### 正文骨架
22
+
4
23
  ```markdown
5
24
  ## 背景
25
+
6
26
  (1 ~ 3 句:业务背景、要解决的问题、预期效果,如果没有可空着,不要强行编)
7
27
 
8
28
  ## 范围
29
+
9
30
  - **包含**:…
10
31
  - **不包含**:…
11
32
 
33
+ (可选:范围较大时,用 mindmap 或简表做一页总览)
34
+
12
35
  ## 需求说明
13
36
 
14
37
  ### 需求点 1:[简短名称]
38
+
39
+ (可选:流程/状态/布局类需求,在此放一张 Mermaid 图)
40
+
15
41
  - …
16
42
  - …
17
43
 
@@ -42,14 +68,67 @@
42
68
 
43
69
  ### 需求点 3:「科室医德考评小组人员名单」字段
44
70
 
71
+ (本需求点适合在 bullet 前加「字段展示与校验」流程图,见下方「图示示例」)
72
+
45
73
  - 原字段展示名 **「科室医德考评人员名单」** 统一改为 **「科室医德考评小组人员名单」**;凡界面、提示、校验文案等涉及该名称处**一并修改**。
46
74
  - 当流程处于 **一级审批** 且该字段**展示**时,须校验为**必填**(在展示场景下触发,而非所有保存入口一律校验)。
47
75
  ```
48
76
 
77
+ ### 图示示例(按需选用)
78
+
79
+ **字段展示与校验**(对应上文需求点 3):
80
+
81
+ ```mermaid
82
+ flowchart LR
83
+ A[进入表单] --> B{流程处于一级审批?}
84
+ B -->|是| C[展示该字段]
85
+ B -->|否| D[按原规则展示/隐藏]
86
+ C --> E{用户操作}
87
+ E -->|暂存或提交| F[校验必填]
88
+ E -->|仅浏览未保存| G[不触发必填校验]
89
+ ```
90
+
91
+ **流程分支**(暂存 vs 提交、审批节点):
92
+
93
+ ```mermaid
94
+ flowchart TD
95
+ A[用户打开新增/编辑弹窗] --> B[填写表单]
96
+ B --> C{点击底部按钮}
97
+ C -->|暂存| D[沿用原「确定」逻辑:校验 + 保存,不推进流程]
98
+ C -->|提交| E[沿用原「办理」逻辑:校验 + 保存并推进流程]
99
+ ```
100
+
101
+ **弹窗底部按钮布局**(对照既有按钮):
102
+
103
+ ```text
104
+ ┌─────────────────────────────────────┐
105
+ │ 新增 / 编辑弹窗 │
106
+ │ …表单字段… │
107
+ ├─────────────────────────────────────┤
108
+ │ [ 取消 ] [ 暂存 ] │ ← 暂存:原「确定」逻辑,仅改文案
109
+ └─────────────────────────────────────┘
110
+ ```
111
+
112
+ **范围总览**(需求点较多时,放在「范围」之后):
113
+
114
+ ```mermaid
115
+ mindmap
116
+ root((本需求))
117
+ 包含
118
+ 弹窗底部操作
119
+ 字段更名与校验
120
+ 不包含
121
+ 历史数据迁移
122
+ 导出能力
123
+ ```
124
+
49
125
  ### 自检列表
126
+
50
127
  - [ ] 含 **背景与目标、范围、需求说明** 三章,叙述完整
51
128
  - [ ] 2 分钟内可通读;每个需求点 2 ~ 5 条 bullet,一层列表
129
+ - [ ] 流程/状态/多分支类需求点有 **图示或简表** 辅助,且 bullet 仍覆盖图中未写清的细节
130
+ - [ ] 图示节点使用业务语言,与正文术语一致;无仅看图无法验收的遗漏
52
131
  - [ ] 场景、按钮行为、校验时机表述清楚,前后一致
53
132
  - [ ] 「范围」与需求点中的 **「不考虑」** 口径一致
54
133
  - [ ] 全文使用业务语言,术语统一
55
- - [ ] 仅材料明确涉及时才有「非功能」「待确认」
134
+ - [ ] 仅材料明确涉及时才有「非功能」「待确认」