ai-project-manage-cli 7.1.15 → 7.1.17

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 CHANGED
@@ -57,6 +57,6 @@ apm daemon start -f
57
57
 
58
58
  使用全局 PM2(未安装时会自动执行 `npm install -g pm2`);同一时刻仅允许一个 connect。启动时会自动检测 CLI 更新并刷新守护配置。
59
59
 
60
- `apm pull` 会自动将平台登记的**项目文档**同步到 `.apm/docs/`(含 `manifest.json`)。Agent 修改 `.apm/docs/` 下文件后,`apm connect` 处理完消息会自动推回平台。
60
+ `apm pull` 会自动将平台登记的**项目文档**同步到 `.apm/project/<项目ID>/`(含 `manifest.json`)。Agent 修改该目录下文件后,`apm connect` 处理完消息会自动推回平台。
61
61
 
62
62
  详见仓库根目录 [docs/CLI.md](../../docs/CLI.md)。
package/dist/index.js CHANGED
@@ -184,6 +184,10 @@ var init_request_config = __esm({
184
184
  method: "GET",
185
185
  path: "/cli/webide/assumptions"
186
186
  }),
187
+ webideListAttachments: defineEndpoint({
188
+ method: "GET",
189
+ path: "/cli/webide/attachments"
190
+ }),
187
191
  webideRecommendPlan: defineEndpoint({
188
192
  method: "PUT",
189
193
  path: "/cli/webide/plan-recommendation"
@@ -220,10 +224,12 @@ var init_request_config = __esm({
220
224
  method: "GET",
221
225
  path: "/cli/tasks/branch-baseline"
222
226
  }),
223
- projectBaseBranch: defineEndpoint({
224
- method: "GET",
225
- path: "/cli/tasks/project-base-branch"
226
- }),
227
+ projectBaseBranch: defineEndpoint(
228
+ {
229
+ method: "GET",
230
+ path: "/cli/tasks/project-base-branch"
231
+ }
232
+ ),
227
233
  listSessionsForBranchCleanup: defineEndpoint({
228
234
  method: "GET",
229
235
  path: "/cli/sessions/branch-cleanup"
@@ -1665,12 +1671,26 @@ import {
1665
1671
  import { createHash } from "crypto";
1666
1672
  import { dirname as dirname2, join as join4, relative, sep } from "path";
1667
1673
  var MANIFEST_FILE = "manifest.json";
1668
- function projectDocumentsDir(apmRoot) {
1669
- return join4(apmRoot ?? workspaceApmDir(), "docs");
1674
+ function normalizeProjectIdForPath(projectId) {
1675
+ const id = projectId.trim();
1676
+ if (!id) {
1677
+ throw new Error("projectId \u4E0D\u80FD\u4E3A\u7A7A");
1678
+ }
1679
+ if (id.includes("..") || id.includes("/") || id.includes("\\") || id.includes("\0")) {
1680
+ throw new Error(`\u975E\u6CD5 projectId: ${projectId}`);
1681
+ }
1682
+ return id;
1683
+ }
1684
+ function projectDocumentsDir(apmRoot, projectId) {
1685
+ const id = normalizeProjectIdForPath(projectId);
1686
+ return join4(apmRoot ?? workspaceApmDir(), "project", id);
1670
1687
  }
1671
- function projectDocumentLocalPath(apmRoot, documentPath) {
1688
+ function projectDocumentLocalPath(apmRoot, projectId, documentPath) {
1672
1689
  const normalized = normalizeLocalDocumentPath(documentPath);
1673
- return join4(projectDocumentsDir(apmRoot), ...normalized.split("/"));
1690
+ return join4(
1691
+ projectDocumentsDir(apmRoot, projectId),
1692
+ ...normalized.split("/")
1693
+ );
1674
1694
  }
1675
1695
  function normalizeLocalDocumentPath(path19) {
1676
1696
  const trimmed = path19.trim().replace(/\\/g, "/");
@@ -1686,8 +1706,11 @@ function normalizeLocalDocumentPath(path19) {
1686
1706
  function hashLocalFileContent(content) {
1687
1707
  return createHash("sha256").update(content, "utf8").digest("hex");
1688
1708
  }
1689
- function readLocalManifest(apmRoot) {
1690
- const manifestPath2 = join4(projectDocumentsDir(apmRoot), MANIFEST_FILE);
1709
+ function readLocalManifest(apmRoot, projectId) {
1710
+ const manifestPath2 = join4(
1711
+ projectDocumentsDir(apmRoot, projectId),
1712
+ MANIFEST_FILE
1713
+ );
1691
1714
  if (!existsSync2(manifestPath2)) {
1692
1715
  return null;
1693
1716
  }
@@ -1699,8 +1722,8 @@ function readLocalManifest(apmRoot) {
1699
1722
  return null;
1700
1723
  }
1701
1724
  }
1702
- function listLocalDocumentPaths(apmRoot) {
1703
- const root = projectDocumentsDir(apmRoot);
1725
+ function listLocalDocumentPaths(apmRoot, projectId) {
1726
+ const root = projectDocumentsDir(apmRoot, projectId);
1704
1727
  if (!existsSync2(root)) {
1705
1728
  return [];
1706
1729
  }
@@ -1743,7 +1766,11 @@ function diffManifestPaths(remote, local) {
1743
1766
  }
1744
1767
  return { download, deleteLocal };
1745
1768
  }
1746
- async function resolveProjectIdForSync(api, workdirPath) {
1769
+ async function resolveProjectIdForSync(api, workdirPath, options) {
1770
+ const explicit = options?.projectId?.trim();
1771
+ if (explicit) {
1772
+ return { projectId: explicit, diagnostic: null };
1773
+ }
1747
1774
  try {
1748
1775
  const baseline = await api.cli.workspaceBaseline({ workdirPath });
1749
1776
  const projectId = baseline.projectId?.trim() || null;
@@ -1771,7 +1798,7 @@ async function resolveProjectIdForSync(api, workdirPath) {
1771
1798
  };
1772
1799
  }
1773
1800
  }
1774
- async function syncProjectDocumentsPull(workdirPath, apmDir) {
1801
+ async function syncProjectDocumentsPull(workdirPath, apmDir, options) {
1775
1802
  const empty = {
1776
1803
  synced: false,
1777
1804
  projectId: null,
@@ -1788,7 +1815,8 @@ async function syncProjectDocumentsPull(workdirPath, apmDir) {
1788
1815
  const api = createApmApiClient(cfg);
1789
1816
  const { projectId, diagnostic } = await resolveProjectIdForSync(
1790
1817
  api,
1791
- workdirPath
1818
+ workdirPath,
1819
+ options
1792
1820
  );
1793
1821
  if (!projectId) {
1794
1822
  console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
@@ -1796,14 +1824,16 @@ ${diagnostic ?? ""}`);
1796
1824
  return empty;
1797
1825
  }
1798
1826
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1799
- const docsDir = projectDocumentsDir(targetApmDir);
1827
+ const docsDir = projectDocumentsDir(targetApmDir, projectId);
1800
1828
  await ensureDirExists(docsDir);
1801
- const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
1829
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest(
1830
+ { projectId }
1831
+ );
1802
1832
  if (!remoteManifest) {
1803
1833
  console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
1804
1834
  return { ...empty, projectId };
1805
1835
  }
1806
- const localManifest = readLocalManifest(targetApmDir);
1836
+ const localManifest = readLocalManifest(targetApmDir, projectId);
1807
1837
  const { download, deleteLocal } = diffManifestPaths(
1808
1838
  remoteManifest,
1809
1839
  localManifest
@@ -1816,7 +1846,7 @@ ${diagnostic ?? ""}`);
1816
1846
  });
1817
1847
  for (const doc of list) {
1818
1848
  const absPath = toFsPath(
1819
- projectDocumentLocalPath(targetApmDir, doc.path)
1849
+ projectDocumentLocalPath(targetApmDir, projectId, doc.path)
1820
1850
  );
1821
1851
  await ensureDirExists(dirname2(absPath));
1822
1852
  writeFileSync4(absPath, doc.content, "utf8");
@@ -1825,7 +1855,9 @@ ${diagnostic ?? ""}`);
1825
1855
  }
1826
1856
  let deleted = 0;
1827
1857
  for (const path19 of deleteLocal) {
1828
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path19));
1858
+ const absPath = toFsPath(
1859
+ projectDocumentLocalPath(targetApmDir, projectId, path19)
1860
+ );
1829
1861
  if (existsSync2(absPath)) {
1830
1862
  rmSync(absPath, { force: true });
1831
1863
  deleted += 1;
@@ -1838,7 +1870,7 @@ ${diagnostic ?? ""}`);
1838
1870
  "utf8"
1839
1871
  );
1840
1872
  console.log(
1841
- `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1873
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: projectId=${projectId} \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted} \u2192 .apm/project/${projectId}/`
1842
1874
  );
1843
1875
  return {
1844
1876
  synced: true,
@@ -1847,14 +1879,18 @@ ${diagnostic ?? ""}`);
1847
1879
  deleted
1848
1880
  };
1849
1881
  }
1850
- async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1882
+ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot, options) {
1851
1883
  const api = createApmApiClient(cfg);
1852
- const { projectId } = await resolveProjectIdForSync(api, workdirPath);
1884
+ const { projectId } = await resolveProjectIdForSync(
1885
+ api,
1886
+ workdirPath,
1887
+ options
1888
+ );
1853
1889
  if (!projectId) {
1854
1890
  return 0;
1855
1891
  }
1856
1892
  const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1857
- const localPaths = listLocalDocumentPaths(targetApmDir);
1893
+ const localPaths = listLocalDocumentPaths(targetApmDir, projectId);
1858
1894
  if (localPaths.length === 0) {
1859
1895
  console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1860
1896
  return 0;
@@ -1864,14 +1900,13 @@ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1864
1900
  (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1865
1901
  );
1866
1902
  const remoteDescriptionByPath = new Map(
1867
- (remoteManifest?.documents ?? []).map((doc) => [
1868
- doc.path,
1869
- doc.description
1870
- ])
1903
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.description])
1871
1904
  );
1872
1905
  let synced = 0;
1873
1906
  for (const path19 of localPaths) {
1874
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path19));
1907
+ const absPath = toFsPath(
1908
+ projectDocumentLocalPath(targetApmDir, projectId, path19)
1909
+ );
1875
1910
  const content = readFileSync3(absPath, "utf8");
1876
1911
  const contentHash = hashLocalFileContent(content);
1877
1912
  if (remoteHashByPath.get(path19) === contentHash) {
@@ -2498,7 +2533,9 @@ import { join as join6 } from "path";
2498
2533
  var MANIFEST_FILE2 = ".sync-manifest.json";
2499
2534
  async function downloadAttachment(cfg, attachmentId) {
2500
2535
  const base = cfg.baseUrl.trim().replace(/\/+$/, "");
2501
- const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({ attachmentId })}`;
2536
+ const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({
2537
+ attachmentId
2538
+ })}`;
2502
2539
  const res = await fetch(url);
2503
2540
  if (!res.ok) {
2504
2541
  throw new Error(
@@ -2537,24 +2574,23 @@ function isAttachmentUpToDate(entry, item, dest) {
2537
2574
  const createdAt = item.createdAt ?? "";
2538
2575
  return entry.createdAt === createdAt;
2539
2576
  }
2540
- async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2541
- const dir = join6(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
2577
+ async function syncAttachmentsToDirectory(cfg, attachments, dir, logLabel) {
2542
2578
  await ensureDirExists(dir);
2543
2579
  if (attachments.length === 0) {
2544
2580
  saveManifest(dir, { version: 1, attachments: {} });
2545
- return;
2581
+ return [];
2546
2582
  }
2547
2583
  const manifest = loadManifest(dir);
2548
2584
  const nextManifest = { version: 1, attachments: {} };
2585
+ const names = [];
2549
2586
  for (const item of attachments) {
2550
2587
  const dest = join6(dir, item.name);
2551
2588
  const entry = manifest.attachments[item.id];
2552
2589
  const createdAt = item.createdAt ?? "";
2590
+ names.push(item.name);
2553
2591
  if (isAttachmentUpToDate(entry, item, dest)) {
2554
2592
  nextManifest.attachments[item.id] = entry;
2555
- console.log(
2556
- `[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${SESSION_ATTACHMENTS_SUBDIR}/${item.name}`
2557
- );
2593
+ console.log(`[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${logLabel}/${item.name}`);
2558
2594
  continue;
2559
2595
  }
2560
2596
  const buffer = await downloadAttachment(cfg, item.id);
@@ -2563,9 +2599,19 @@ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2563
2599
  name: item.name,
2564
2600
  createdAt
2565
2601
  };
2566
- console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${SESSION_ATTACHMENTS_SUBDIR}/${item.name}`);
2602
+ console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${logLabel}/${item.name}`);
2567
2603
  }
2568
2604
  saveManifest(dir, nextManifest);
2605
+ return names;
2606
+ }
2607
+ async function syncSessionAttachments(cfg, sessionId, attachments, apmRoot) {
2608
+ const dir = join6(sessionDir(sessionId, apmRoot), SESSION_ATTACHMENTS_SUBDIR);
2609
+ return syncAttachmentsToDirectory(
2610
+ cfg,
2611
+ attachments,
2612
+ dir,
2613
+ SESSION_ATTACHMENTS_SUBDIR
2614
+ );
2569
2615
  }
2570
2616
 
2571
2617
  // src/rules-sync.ts
@@ -10031,12 +10077,12 @@ function buildProgram() {
10031
10077
  await runSyncDeployConfig();
10032
10078
  });
10033
10079
  program.command("sync-project-documents").description(
10034
- "\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/docs/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
10035
- ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/docs/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/docs/").action(async (opts) => {
10080
+ "\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/project/<\u9879\u76EEID>/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
10081
+ ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/project/<\u9879\u76EEID>/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/project/<\u9879\u76EEID>/").action(async (opts) => {
10036
10082
  await runSyncProjectDocuments(opts);
10037
10083
  });
10038
10084
  program.command("pull").description(
10039
- "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u9879\u76EE\u6587\u6863\u5230 .apm/docs/"
10085
+ "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u9879\u76EE\u6587\u6863\u5230 .apm/project/<\u9879\u76EEID>/"
10040
10086
  ).argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async (sessionId) => {
10041
10087
  await runPull(sessionId);
10042
10088
  });
@@ -88,6 +88,10 @@ var requestConfig = {
88
88
  method: "GET",
89
89
  path: "/cli/webide/assumptions"
90
90
  }),
91
+ webideListAttachments: defineEndpoint({
92
+ method: "GET",
93
+ path: "/cli/webide/attachments"
94
+ }),
91
95
  webideRecommendPlan: defineEndpoint({
92
96
  method: "PUT",
93
97
  path: "/cli/webide/plan-recommendation"
@@ -124,10 +128,12 @@ var requestConfig = {
124
128
  method: "GET",
125
129
  path: "/cli/tasks/branch-baseline"
126
130
  }),
127
- projectBaseBranch: defineEndpoint({
128
- method: "GET",
129
- path: "/cli/tasks/project-base-branch"
130
- }),
131
+ projectBaseBranch: defineEndpoint(
132
+ {
133
+ method: "GET",
134
+ path: "/cli/tasks/project-base-branch"
135
+ }
136
+ ),
131
137
  listSessionsForBranchCleanup: defineEndpoint({
132
138
  method: "GET",
133
139
  path: "/cli/sessions/branch-cleanup"
@@ -591,6 +597,14 @@ function assertApmGitignoredInRepo(workdir) {
591
597
  );
592
598
  }
593
599
  }
600
+ var SESSION_ATTACHMENTS_SUBDIR = "attachments";
601
+ var WEBIDE_SUBDIR = "webide";
602
+ function webideTaskDir(taskId, workdir) {
603
+ return join2(workdir, ".apm", WEBIDE_SUBDIR, taskId);
604
+ }
605
+ function webideAttachmentsDir(taskId, workdir) {
606
+ return join2(webideTaskDir(taskId, workdir), SESSION_ATTACHMENTS_SUBDIR);
607
+ }
594
608
  async function ensureLoggedConfig() {
595
609
  const cfg = await ensureApmConfig();
596
610
  if (!resolveApiKey(cfg)) {
@@ -2667,14 +2681,29 @@ import {
2667
2681
  rmSync,
2668
2682
  writeFileSync as writeFileSync7
2669
2683
  } from "fs";
2684
+ import { createHash } from "crypto";
2670
2685
  import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
2671
2686
  var MANIFEST_FILE = "manifest.json";
2672
- function projectDocumentsDir(apmRoot) {
2673
- return join6(apmRoot ?? workspaceApmDir(), "docs");
2687
+ function normalizeProjectIdForPath(projectId) {
2688
+ const id = projectId.trim();
2689
+ if (!id) {
2690
+ throw new Error("projectId \u4E0D\u80FD\u4E3A\u7A7A");
2691
+ }
2692
+ if (id.includes("..") || id.includes("/") || id.includes("\\") || id.includes("\0")) {
2693
+ throw new Error(`\u975E\u6CD5 projectId: ${projectId}`);
2694
+ }
2695
+ return id;
2696
+ }
2697
+ function projectDocumentsDir(apmRoot, projectId) {
2698
+ const id = normalizeProjectIdForPath(projectId);
2699
+ return join6(apmRoot ?? workspaceApmDir(), "project", id);
2674
2700
  }
2675
- function projectDocumentLocalPath(apmRoot, documentPath) {
2701
+ function projectDocumentLocalPath(apmRoot, projectId, documentPath) {
2676
2702
  const normalized = normalizeLocalDocumentPath(documentPath);
2677
- return join6(projectDocumentsDir(apmRoot), ...normalized.split("/"));
2703
+ return join6(
2704
+ projectDocumentsDir(apmRoot, projectId),
2705
+ ...normalized.split("/")
2706
+ );
2678
2707
  }
2679
2708
  function normalizeLocalDocumentPath(path) {
2680
2709
  const trimmed = path.trim().replace(/\\/g, "/");
@@ -2687,8 +2716,14 @@ function normalizeLocalDocumentPath(path) {
2687
2716
  }
2688
2717
  return segments.join("/");
2689
2718
  }
2690
- function readLocalManifest(apmRoot) {
2691
- const manifestPath3 = join6(projectDocumentsDir(apmRoot), MANIFEST_FILE);
2719
+ function hashLocalFileContent(content) {
2720
+ return createHash("sha256").update(content, "utf8").digest("hex");
2721
+ }
2722
+ function readLocalManifest(apmRoot, projectId) {
2723
+ const manifestPath3 = join6(
2724
+ projectDocumentsDir(apmRoot, projectId),
2725
+ MANIFEST_FILE
2726
+ );
2692
2727
  if (!existsSync5(manifestPath3)) {
2693
2728
  return null;
2694
2729
  }
@@ -2700,6 +2735,29 @@ function readLocalManifest(apmRoot) {
2700
2735
  return null;
2701
2736
  }
2702
2737
  }
2738
+ function listLocalDocumentPaths(apmRoot, projectId) {
2739
+ const root = projectDocumentsDir(apmRoot, projectId);
2740
+ if (!existsSync5(root)) {
2741
+ return [];
2742
+ }
2743
+ const paths = [];
2744
+ const walk = (dir) => {
2745
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2746
+ const abs = join6(dir, entry.name);
2747
+ if (entry.isDirectory()) {
2748
+ walk(abs);
2749
+ continue;
2750
+ }
2751
+ if (entry.isFile() && entry.name === MANIFEST_FILE) {
2752
+ continue;
2753
+ }
2754
+ const rel = relative2(root, abs).split(sep).join("/");
2755
+ paths.push(rel);
2756
+ }
2757
+ };
2758
+ walk(root);
2759
+ return paths.sort();
2760
+ }
2703
2761
  function diffManifestPaths(remote, local) {
2704
2762
  const remoteMap = new Map(
2705
2763
  (remote?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
@@ -2721,7 +2779,11 @@ function diffManifestPaths(remote, local) {
2721
2779
  }
2722
2780
  return { download, deleteLocal };
2723
2781
  }
2724
- async function resolveProjectIdForSync(api, workdirPath) {
2782
+ async function resolveProjectIdForSync(api, workdirPath, options) {
2783
+ const explicit = options?.projectId?.trim();
2784
+ if (explicit) {
2785
+ return { projectId: explicit, diagnostic: null };
2786
+ }
2725
2787
  try {
2726
2788
  const baseline = await api.cli.workspaceBaseline({ workdirPath });
2727
2789
  const projectId = baseline.projectId?.trim() || null;
@@ -2749,7 +2811,7 @@ async function resolveProjectIdForSync(api, workdirPath) {
2749
2811
  };
2750
2812
  }
2751
2813
  }
2752
- async function syncProjectDocumentsPull(workdirPath, apmDir) {
2814
+ async function syncProjectDocumentsPull(workdirPath, apmDir, options) {
2753
2815
  const empty = {
2754
2816
  synced: false,
2755
2817
  projectId: null,
@@ -2766,7 +2828,8 @@ async function syncProjectDocumentsPull(workdirPath, apmDir) {
2766
2828
  const api = createApmApiClient(cfg);
2767
2829
  const { projectId, diagnostic } = await resolveProjectIdForSync(
2768
2830
  api,
2769
- workdirPath
2831
+ workdirPath,
2832
+ options
2770
2833
  );
2771
2834
  if (!projectId) {
2772
2835
  console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
@@ -2774,14 +2837,16 @@ ${diagnostic ?? ""}`);
2774
2837
  return empty;
2775
2838
  }
2776
2839
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
2777
- const docsDir = projectDocumentsDir(targetApmDir);
2840
+ const docsDir = projectDocumentsDir(targetApmDir, projectId);
2778
2841
  await ensureDirExists(docsDir);
2779
- const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
2842
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest(
2843
+ { projectId }
2844
+ );
2780
2845
  if (!remoteManifest) {
2781
2846
  console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
2782
2847
  return { ...empty, projectId };
2783
2848
  }
2784
- const localManifest = readLocalManifest(targetApmDir);
2849
+ const localManifest = readLocalManifest(targetApmDir, projectId);
2785
2850
  const { download, deleteLocal } = diffManifestPaths(
2786
2851
  remoteManifest,
2787
2852
  localManifest
@@ -2794,7 +2859,7 @@ ${diagnostic ?? ""}`);
2794
2859
  });
2795
2860
  for (const doc of list) {
2796
2861
  const absPath = toFsPath(
2797
- projectDocumentLocalPath(targetApmDir, doc.path)
2862
+ projectDocumentLocalPath(targetApmDir, projectId, doc.path)
2798
2863
  );
2799
2864
  await ensureDirExists(dirname4(absPath));
2800
2865
  writeFileSync7(absPath, doc.content, "utf8");
@@ -2803,7 +2868,9 @@ ${diagnostic ?? ""}`);
2803
2868
  }
2804
2869
  let deleted = 0;
2805
2870
  for (const path of deleteLocal) {
2806
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, path));
2871
+ const absPath = toFsPath(
2872
+ projectDocumentLocalPath(targetApmDir, projectId, path)
2873
+ );
2807
2874
  if (existsSync5(absPath)) {
2808
2875
  rmSync(absPath, { force: true });
2809
2876
  deleted += 1;
@@ -2816,7 +2883,7 @@ ${diagnostic ?? ""}`);
2816
2883
  "utf8"
2817
2884
  );
2818
2885
  console.log(
2819
- `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2886
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: projectId=${projectId} \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted} \u2192 .apm/project/${projectId}/`
2820
2887
  );
2821
2888
  return {
2822
2889
  synced: true,
@@ -2825,6 +2892,53 @@ ${diagnostic ?? ""}`);
2825
2892
  deleted
2826
2893
  };
2827
2894
  }
2895
+ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot, options) {
2896
+ const api = createApmApiClient(cfg);
2897
+ const { projectId } = await resolveProjectIdForSync(
2898
+ api,
2899
+ workdirPath,
2900
+ options
2901
+ );
2902
+ if (!projectId) {
2903
+ return 0;
2904
+ }
2905
+ const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
2906
+ const localPaths = listLocalDocumentPaths(targetApmDir, projectId);
2907
+ if (localPaths.length === 0) {
2908
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
2909
+ return 0;
2910
+ }
2911
+ const remoteManifest = (await api.cli.getProjectDocumentManifest({ projectId })).manifest ?? null;
2912
+ const remoteHashByPath = new Map(
2913
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
2914
+ );
2915
+ const remoteDescriptionByPath = new Map(
2916
+ (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.description])
2917
+ );
2918
+ let synced = 0;
2919
+ for (const path of localPaths) {
2920
+ const absPath = toFsPath(
2921
+ projectDocumentLocalPath(targetApmDir, projectId, path)
2922
+ );
2923
+ const content = readFileSync6(absPath, "utf8");
2924
+ const contentHash = hashLocalFileContent(content);
2925
+ if (remoteHashByPath.get(path) === contentHash) {
2926
+ continue;
2927
+ }
2928
+ await api.cli.upsertProjectDocument({
2929
+ projectId,
2930
+ path,
2931
+ content,
2932
+ description: remoteDescriptionByPath.get(path) ?? void 0
2933
+ });
2934
+ synced += 1;
2935
+ console.log(`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: ${path}`);
2936
+ }
2937
+ if (synced === 0) {
2938
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
2939
+ }
2940
+ return synced;
2941
+ }
2828
2942
 
2829
2943
  // src/commands/init.ts
2830
2944
  async function ensureWorkspaceInitialized(workdir, options) {
@@ -3087,21 +3201,108 @@ async function syncWorkspaceSkills(cfg, workdir) {
3087
3201
  );
3088
3202
  }
3089
3203
 
3090
- // src/commands/connect/cli-version-sync.ts
3204
+ // src/commands/sync-session-attachments.ts
3091
3205
  import { existsSync as existsSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
3092
3206
  import { join as join11 } from "path";
3207
+ var MANIFEST_FILE2 = ".sync-manifest.json";
3208
+ async function downloadAttachment(cfg, attachmentId) {
3209
+ const base = cfg.baseUrl.trim().replace(/\/+$/, "");
3210
+ const url = `${base}/api/v1/tasks/attachments/file?${new URLSearchParams({
3211
+ attachmentId
3212
+ })}`;
3213
+ const res = await fetch(url);
3214
+ if (!res.ok) {
3215
+ throw new Error(
3216
+ `[apm] \u4E0B\u8F7D\u9644\u4EF6\u5931\u8D25 (${res.status}): attachmentId=${attachmentId}`
3217
+ );
3218
+ }
3219
+ return Buffer.from(await res.arrayBuffer());
3220
+ }
3221
+ function loadManifest(dir) {
3222
+ const path = join11(dir, MANIFEST_FILE2);
3223
+ if (!existsSync8(path)) {
3224
+ return { version: 1, attachments: {} };
3225
+ }
3226
+ try {
3227
+ const parsed = JSON.parse(
3228
+ readFileSync9(path, "utf8")
3229
+ );
3230
+ if (parsed?.version === 1 && parsed.attachments && typeof parsed.attachments === "object") {
3231
+ return parsed;
3232
+ }
3233
+ } catch {
3234
+ }
3235
+ return { version: 1, attachments: {} };
3236
+ }
3237
+ function saveManifest(dir, manifest) {
3238
+ writeFileSync10(
3239
+ join11(dir, MANIFEST_FILE2),
3240
+ `${JSON.stringify(manifest, null, 2)}
3241
+ `,
3242
+ "utf8"
3243
+ );
3244
+ }
3245
+ function isAttachmentUpToDate(entry, item, dest) {
3246
+ if (!entry || !existsSync8(dest)) return false;
3247
+ if (entry.name !== item.name) return false;
3248
+ const createdAt = item.createdAt ?? "";
3249
+ return entry.createdAt === createdAt;
3250
+ }
3251
+ async function syncAttachmentsToDirectory(cfg, attachments, dir, logLabel) {
3252
+ await ensureDirExists(dir);
3253
+ if (attachments.length === 0) {
3254
+ saveManifest(dir, { version: 1, attachments: {} });
3255
+ return [];
3256
+ }
3257
+ const manifest = loadManifest(dir);
3258
+ const nextManifest = { version: 1, attachments: {} };
3259
+ const names = [];
3260
+ for (const item of attachments) {
3261
+ const dest = join11(dir, item.name);
3262
+ const entry = manifest.attachments[item.id];
3263
+ const createdAt = item.createdAt ?? "";
3264
+ names.push(item.name);
3265
+ if (isAttachmentUpToDate(entry, item, dest)) {
3266
+ nextManifest.attachments[item.id] = entry;
3267
+ console.log(`[apm] \u9644\u4EF6\u65E0\u53D8\u5316\uFF0C\u5DF2\u8DF3\u8FC7: ${logLabel}/${item.name}`);
3268
+ continue;
3269
+ }
3270
+ const buffer = await downloadAttachment(cfg, item.id);
3271
+ writeFileSync10(dest, buffer);
3272
+ nextManifest.attachments[item.id] = {
3273
+ name: item.name,
3274
+ createdAt
3275
+ };
3276
+ console.log(`[apm] \u5DF2\u4E0B\u8F7D\u9644\u4EF6: ${logLabel}/${item.name}`);
3277
+ }
3278
+ saveManifest(dir, nextManifest);
3279
+ return names;
3280
+ }
3281
+ async function syncWebIdeAttachments(cfg, taskId, workdir, attachments) {
3282
+ const dir = webideAttachmentsDir(taskId, workdir);
3283
+ return syncAttachmentsToDirectory(
3284
+ cfg,
3285
+ attachments,
3286
+ dir,
3287
+ `.apm/webide/${taskId}/attachments`
3288
+ );
3289
+ }
3290
+
3291
+ // src/commands/connect/cli-version-sync.ts
3292
+ import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync11 } from "fs";
3293
+ import { join as join12 } from "path";
3093
3294
  var CLI_VERSION_FILE = ".cli-version.json";
3094
3295
  function manifestPath2(apmDir) {
3095
- return join11(apmDir, CLI_VERSION_FILE);
3296
+ return join12(apmDir, CLI_VERSION_FILE);
3096
3297
  }
3097
- function loadManifest(apmDir) {
3298
+ function loadManifest2(apmDir) {
3098
3299
  const path = toFsPath(manifestPath2(apmDir));
3099
- if (!existsSync8(path)) {
3300
+ if (!existsSync9(path)) {
3100
3301
  return null;
3101
3302
  }
3102
3303
  try {
3103
3304
  const parsed = JSON.parse(
3104
- readFileSync9(path, "utf8")
3305
+ readFileSync10(path, "utf8")
3105
3306
  );
3106
3307
  if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
3107
3308
  return parsed;
@@ -3110,9 +3311,9 @@ function loadManifest(apmDir) {
3110
3311
  }
3111
3312
  return null;
3112
3313
  }
3113
- function saveManifest(apmDir, cliVersion) {
3314
+ function saveManifest2(apmDir, cliVersion) {
3114
3315
  const manifest = { version: 1, cliVersion };
3115
- writeFileSync10(
3316
+ writeFileSync11(
3116
3317
  toFsPath(manifestPath2(apmDir)),
3117
3318
  `${JSON.stringify(manifest, null, 2)}
3118
3319
  `,
@@ -3125,7 +3326,7 @@ function shouldSyncSkillsForCliVersion(workdir, currentVersion) {
3125
3326
  if (cached === currentVersion) {
3126
3327
  return false;
3127
3328
  }
3128
- const stored = loadManifest(workspaceApmDir(workdir));
3329
+ const stored = loadManifest2(workspaceApmDir(workdir));
3129
3330
  if (stored?.cliVersion === currentVersion) {
3130
3331
  syncedInSession.set(workdir, currentVersion);
3131
3332
  return false;
@@ -3133,7 +3334,7 @@ function shouldSyncSkillsForCliVersion(workdir, currentVersion) {
3133
3334
  return true;
3134
3335
  }
3135
3336
  function markSkillsSyncedForCliVersion(workdir, cliVersion) {
3136
- saveManifest(workspaceApmDir(workdir), cliVersion);
3337
+ saveManifest2(workspaceApmDir(workdir), cliVersion);
3137
3338
  syncedInSession.set(workdir, cliVersion);
3138
3339
  }
3139
3340
 
@@ -3206,6 +3407,51 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3206
3407
  if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3207
3408
  await ensureWebIdePullRequests(cfg, taskId, workdir);
3208
3409
  }
3410
+ if (signal.aborted) throw new Error("\u4EFB\u52A1\u5DF2\u53D6\u6D88");
3411
+ const api = createApmApiClient(cfg);
3412
+ let projectId = null;
3413
+ try {
3414
+ const baseline = await api.cli.projectBaseBranch({ taskId });
3415
+ projectId = baseline.projectId?.trim() || null;
3416
+ if (projectId) {
3417
+ const docs = await syncProjectDocumentsPull(workdir, void 0, {
3418
+ projectId
3419
+ });
3420
+ if (docs.synced) {
3421
+ console.log(
3422
+ `[apm] webide \u9879\u76EE\u6587\u6863\u5DF2\u5C31\u7EEA projectId=${projectId} downloaded=${docs.downloaded} deleted=${docs.deleted}`
3423
+ );
3424
+ }
3425
+ } else {
3426
+ console.warn(
3427
+ `[apm] WebIDE \u65E0\u6CD5\u89E3\u6790\u4EFB\u52A1\u6240\u5C5E\u9879\u76EE\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65 taskId=${taskId}`
3428
+ );
3429
+ }
3430
+ } catch (err) {
3431
+ console.warn(
3432
+ "[apm] WebIDE \u9879\u76EE\u6587\u6863\u540C\u6B65\u5931\u8D25:",
3433
+ err instanceof Error ? err.message : err
3434
+ );
3435
+ }
3436
+ try {
3437
+ const attachments = await api.cli.webideListAttachments({ taskId });
3438
+ const names = await syncWebIdeAttachments(
3439
+ cfg,
3440
+ taskId,
3441
+ workdir,
3442
+ attachments ?? []
3443
+ );
3444
+ if (names.length > 0) {
3445
+ console.log(
3446
+ `[apm] webide \u9644\u4EF6\u5DF2\u5C31\u7EEA count=${names.length} dir=.apm/webide/${taskId}/attachments`
3447
+ );
3448
+ }
3449
+ } catch (err) {
3450
+ console.warn(
3451
+ "[apm] WebIDE \u9644\u4EF6\u540C\u6B65\u5931\u8D25:",
3452
+ err instanceof Error ? err.message : err
3453
+ );
3454
+ }
3209
3455
  const savedAgentId = loadWebIdeAgentId(workdir, taskId);
3210
3456
  const logSyncRef = { current: null };
3211
3457
  const outcome = await runCursorAgent(
@@ -3272,7 +3518,6 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3272
3518
  result: outcome.result,
3273
3519
  createPlan: outcome.createPlan
3274
3520
  });
3275
- const api = createApmApiClient(cfg);
3276
3521
  await api.cli.webideEnsureMessageContent({
3277
3522
  id: messageId,
3278
3523
  content: fallback
@@ -3305,6 +3550,16 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3305
3550
  `[apm] webide \u81EA\u52A8 push action=${msg.action} repos=${pushed}`
3306
3551
  );
3307
3552
  }
3553
+ if (projectId) {
3554
+ try {
3555
+ await syncProjectDocumentsPush(cfg, workdir, void 0, { projectId });
3556
+ } catch (err) {
3557
+ console.warn(
3558
+ "[apm] WebIDE \u9879\u76EE\u6587\u6863\u63A8\u9001\u5931\u8D25:",
3559
+ err instanceof Error ? err.message : err
3560
+ );
3561
+ }
3562
+ }
3308
3563
  await logSyncRef.current?.markRun(outcome.runId, "finished");
3309
3564
  await updateStatus(cfg, messageId, "SUCCESS");
3310
3565
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "7.1.15",
3
+ "version": "7.1.17",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,13 +33,15 @@
33
33
  - webide_testcase.md:WebIDE 生成测试用例时读取,按规范编写并提交用例
34
34
  - webide_merge.md:WebIDE 验收通过后读取;直接调用 MergeWebIdePullRequests 合并代码
35
35
 
36
- - 项目文档(`.apm/docs/`):
36
+ - 项目文档(`.apm/project/<项目ID>/`):
37
37
 
38
- - 索引: `.apm/docs/manifest.json` — 本项目在平台登记的文档列表
39
- - 文档文件: `.apm/docs/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
38
+ - 索引: `.apm/project/<项目ID>/manifest.json` — 该项目在平台登记的文档列表
39
+ - 文档文件: `.apm/project/<项目ID>/{path}` — 与 manifest 中 path 对应;`apm pull` / Session `connect` 会同步;WebIDE 处理 `webide-message` 时按**任务所属项目**拉取到对应目录,结束后推回平台
40
+ - 部署/本地 dev:Read `.apm/project/<项目ID>/deploy.md`(完整路径以 prompt 为准)
40
41
  - 任务涉及菜单名、路由、业务术语等时,**先 Read manifest 与相关文档**,禁止猜测路径
41
42
 
42
43
  - 本轮任务需要关注的文档指引(.apm/sessions/<会话 ID>/\*):
44
+
43
45
  - 本轮会话状态: `session.yaml`,从这里可以看到每个成员的信息,找到可以协助你一起解决问题的人,可以结合 `RULE.md`一起看
44
46
  - 群文档: `docs/xxxx.md`,当需要相关上下文可以在这里查找,按需阅读
45
47
  - 附件列表: `attachments/*`,当有文档中提及附件时,从这里查找
@@ -47,3 +49,6 @@
47
49
  - 协作 TODO: `TODO.md`,跨轮次任务清单(待办与已完成项),按需阅读
48
50
  - 历史消息记录: `messages.xml`,历史消息列表,数据量很大,按需阅读
49
51
  - 初始目标: `TASK.md`,最初的目标,不一定具体,团队成员会有人负责让这个任务变得具体,仅供参考。如果你是相关的角色,则你需要先读取这个目标,然后规划接下来的动作。
52
+
53
+ - WebIDE 任务附件(`.apm/webide/<taskId>/attachments/`):
54
+ - `apm connect` 处理 webide-message 时会自动同步;prompt 中若出现「任务附件」指引,请按路径查看相关文件
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## 启动命令来源
15
15
 
16
- 必须 Read `.apm/docs/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
16
+ 必须 Read `.apm/project/<项目ID>/deploy.md`(完整路径以本轮 prompt 中给出的为准,勿省略项目 ID),按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
17
17
 
18
18
  - 文档不存在或未写明启动方式 → 用 AppendMessage 说明无法启动本地环境,**禁止猜测命令**。
19
19
 
@@ -22,13 +22,13 @@
22
22
  固定顺序:
23
23
 
24
24
  1. **`ListWebIdeTerminals`**
25
- 2. 若无活终端 → Read `deploy.md` → 按前后端分别 `StartWebIdeTerminal`;已有活终端则复用
25
+ 2. 若无活终端 → Read prompt 中的 `.apm/project/<项目ID>/deploy.md` 完整路径 → 按前后端分别 `StartWebIdeTerminal`;已有活终端则复用
26
26
  3. 等待 READY / 端口即可
27
27
  4. AppendMessage:可在「终端」面板观察本地日志;「浏览器」面板由平台打开本客户机外网地址(勿粘贴整段 stdout;无需 Agent 打开浏览器)
28
28
 
29
29
  后续改码(热更新):有活终端则**复用,不重起**。
30
30
 
31
- 显式重跑:`StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start。
31
+ 显式重跑:`StopAllWebIdeTerminals` → 再 Read `.apm/project/<项目ID>/deploy.md` → 重新 Start。
32
32
 
33
33
  ## 规范
34
34