ai-project-manage-cli 7.1.13 → 7.1.15

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/project/`(含 `manifest.json`)。Agent 修改 `.apm/project/` 下文件后,`apm connect` 处理完消息会自动推回平台。
60
+ `apm pull` 会自动将平台登记的**项目文档**同步到 `.apm/docs/`(含 `manifest.json`)。Agent 修改 `.apm/docs/` 下文件后,`apm connect` 处理完消息会自动推回平台。
61
61
 
62
62
  详见仓库根目录 [docs/CLI.md](../../docs/CLI.md)。
package/dist/index.js CHANGED
@@ -212,10 +212,18 @@ var init_request_config = __esm({
212
212
  method: "PUT",
213
213
  path: "/cli/webide/terminals/meta"
214
214
  }),
215
+ acquireSteelBrowser: defineEndpoint({
216
+ method: "POST",
217
+ path: "/cli/steel-browser/acquire"
218
+ }),
215
219
  branchBaseline: defineEndpoint({
216
220
  method: "GET",
217
221
  path: "/cli/tasks/branch-baseline"
218
222
  }),
223
+ projectBaseBranch: defineEndpoint({
224
+ method: "GET",
225
+ path: "/cli/tasks/project-base-branch"
226
+ }),
219
227
  listSessionsForBranchCleanup: defineEndpoint({
220
228
  method: "GET",
221
229
  path: "/cli/sessions/branch-cleanup"
@@ -252,21 +260,21 @@ var init_request_config = __esm({
252
260
  method: "POST",
253
261
  path: "/cli/webide/pull-requests/merge"
254
262
  }),
255
- getRepositoryProjectDocumentManifest: defineEndpoint({
263
+ getProjectDocumentManifest: defineEndpoint({
256
264
  method: "GET",
257
- path: "/cli/repository-project-documents/manifest"
265
+ path: "/cli/project-documents/manifest"
258
266
  }),
259
- listRepositoryProjectDocuments: defineEndpoint({
267
+ listProjectDocuments: defineEndpoint({
260
268
  method: "GET",
261
- path: "/cli/repository-project-documents"
269
+ path: "/cli/project-documents"
262
270
  }),
263
- upsertRepositoryProjectDocument: defineEndpoint({
271
+ upsertProjectDocument: defineEndpoint({
264
272
  method: "PUT",
265
- path: "/cli/repository-project-documents/upsert"
273
+ path: "/cli/project-documents/upsert"
266
274
  }),
267
- removeRepositoryProjectDocument: defineEndpoint({
275
+ removeProjectDocument: defineEndpoint({
268
276
  method: "DELETE",
269
- path: "/cli/repository-project-documents"
277
+ path: "/cli/project-documents"
270
278
  }),
271
279
  createTaskDeployment: defineEndpoint({
272
280
  method: "POST",
@@ -648,11 +656,33 @@ function forceKillProcessTree(pid) {
648
656
  }
649
657
  }
650
658
  }
659
+ async function listProcessTreePids(rootPid) {
660
+ const pids = [rootPid];
661
+ const queue = [rootPid];
662
+ while (queue.length > 0) {
663
+ const parent = queue.shift();
664
+ try {
665
+ const { stdout } = await execFileAsync3("pgrep", ["-P", String(parent)], {
666
+ timeout: 2e3
667
+ });
668
+ for (const line of stdout.split("\n")) {
669
+ const child = Number(line.trim());
670
+ if (child > 0 && !pids.includes(child)) {
671
+ pids.push(child);
672
+ queue.push(child);
673
+ }
674
+ }
675
+ } catch {
676
+ }
677
+ }
678
+ return pids;
679
+ }
651
680
  async function probeListenPorts(pid) {
652
681
  try {
682
+ const treePids = await listProcessTreePids(pid);
653
683
  const { stdout } = await execFileAsync3(
654
684
  "lsof",
655
- ["-nP", `-p${pid}`, "-iTCP", "-sTCP:LISTEN"],
685
+ ["-nP", "-a", `-p${treePids.join(",")}`, "-iTCP", "-sTCP:LISTEN"],
656
686
  { timeout: 3e3, maxBuffer: 1024 * 1024 }
657
687
  );
658
688
  const ports = /* @__PURE__ */ new Set();
@@ -944,23 +974,16 @@ var init_webide_terminal_registry = __esm({
944
974
  if (!session.pid) return;
945
975
  const ports = await probeListenPorts(session.pid);
946
976
  if (ports.length === 0) return;
947
- let changed = false;
948
- for (const p of ports) {
949
- if (!session.ports.includes(p)) {
950
- session.ports.push(p);
951
- changed = true;
952
- }
977
+ const same = ports.length === session.ports.length && ports.every((p, i) => p === session.ports[i]);
978
+ if (same) return;
979
+ session.ports = ports;
980
+ if (!session.primaryUrl && session.ports[0]) {
981
+ session.primaryUrl = `http://127.0.0.1:${session.ports[0]}`;
953
982
  }
954
- if (changed) {
955
- session.ports.sort((a, b) => a - b);
956
- if (!session.primaryUrl && session.ports[0]) {
957
- session.primaryUrl = `http://127.0.0.1:${session.ports[0]}`;
958
- }
959
- if (session.status === "RUNNING" || session.status === "STARTING") {
960
- session.status = "READY";
961
- }
962
- void this.syncStatus(session);
983
+ if (session.status === "RUNNING" || session.status === "STARTING") {
984
+ session.status = "READY";
963
985
  }
986
+ void this.syncStatus(session);
964
987
  }
965
988
  async syncStatus(session) {
966
989
  const api = createApmApiClient(this.cfg);
@@ -1629,7 +1652,7 @@ ${diagnostic ?? ""}
1629
1652
  return { synced: true, repositoryId, configName: config.name };
1630
1653
  }
1631
1654
 
1632
- // src/repository-project-documents-sync.ts
1655
+ // src/project-documents-sync.ts
1633
1656
  init_client();
1634
1657
  init_config();
1635
1658
  import {
@@ -1643,7 +1666,7 @@ import { createHash } from "crypto";
1643
1666
  import { dirname as dirname2, join as join4, relative, sep } from "path";
1644
1667
  var MANIFEST_FILE = "manifest.json";
1645
1668
  function projectDocumentsDir(apmRoot) {
1646
- return join4(apmRoot ?? workspaceApmDir(), "project");
1669
+ return join4(apmRoot ?? workspaceApmDir(), "docs");
1647
1670
  }
1648
1671
  function projectDocumentLocalPath(apmRoot, documentPath) {
1649
1672
  const normalized = normalizeLocalDocumentPath(documentPath);
@@ -1720,41 +1743,65 @@ function diffManifestPaths(remote, local) {
1720
1743
  }
1721
1744
  return { download, deleteLocal };
1722
1745
  }
1723
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
1746
+ async function resolveProjectIdForSync(api, workdirPath) {
1747
+ try {
1748
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
1749
+ const projectId = baseline.projectId?.trim() || null;
1750
+ if (projectId) {
1751
+ return { projectId, diagnostic: null };
1752
+ }
1753
+ const projectIds = baseline.projectIds ?? [];
1754
+ if (projectIds.length === 0) {
1755
+ return {
1756
+ projectId: null,
1757
+ diagnostic: baseline.diagnostic?.message ?? `\u5DE5\u4F5C\u7A7A\u95F4\u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF08\u76EE\u5F55\uFF1A${workdirPath}\uFF09\u3002
1758
+ \u8BF7\u5728\u5E73\u53F0\u5C06\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u5230\u552F\u4E00\u9879\u76EE\u3002`
1759
+ };
1760
+ }
1761
+ return {
1762
+ projectId: null,
1763
+ diagnostic: `\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u4E86 ${projectIds.length} \u4E2A\u9879\u76EE\uFF0C\u65E0\u6CD5\u786E\u5B9A\u540C\u6B65\u76EE\u6807\u3002
1764
+ \u8BF7\u786E\u4FDD\u8BE5\u5DE5\u4F5C\u7A7A\u95F4\u53EA\u5173\u8054\u4E00\u4E2A\u9879\u76EE\u540E\u518D\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
1765
+ };
1766
+ } catch (err) {
1767
+ const detail = err instanceof Error ? err.message : String(err);
1768
+ return {
1769
+ projectId: null,
1770
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
1771
+ };
1772
+ }
1773
+ }
1774
+ async function syncProjectDocumentsPull(workdirPath, apmDir) {
1724
1775
  const empty = {
1725
1776
  synced: false,
1726
- repositoryId: null,
1777
+ projectId: null,
1727
1778
  downloaded: 0,
1728
1779
  deleted: 0
1729
1780
  };
1730
1781
  const cfg = await tryReadApmConfig();
1731
1782
  if (!cfg || !resolveApiKey(cfg)) {
1732
1783
  console.log(
1733
- "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
1784
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
1734
1785
  );
1735
1786
  return empty;
1736
1787
  }
1737
1788
  const api = createApmApiClient(cfg);
1738
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
1789
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
1739
1790
  api,
1740
1791
  workdirPath
1741
1792
  );
1742
- if (!repositoryId) {
1743
- console.log(
1744
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
1745
- ${diagnostic ?? ""}`
1746
- );
1793
+ if (!projectId) {
1794
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
1795
+ ${diagnostic ?? ""}`);
1747
1796
  return empty;
1748
1797
  }
1749
1798
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
1750
- const projectDir = projectDocumentsDir(targetApmDir);
1751
- await ensureDirExists(projectDir);
1752
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
1799
+ const docsDir = projectDocumentsDir(targetApmDir);
1800
+ await ensureDirExists(docsDir);
1801
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
1753
1802
  if (!remoteManifest) {
1754
- console.log(
1755
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
1756
- );
1757
- return { ...empty, repositoryId };
1803
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
1804
+ return { ...empty, projectId };
1758
1805
  }
1759
1806
  const localManifest = readLocalManifest(targetApmDir);
1760
1807
  const { download, deleteLocal } = diffManifestPaths(
@@ -1763,12 +1810,14 @@ ${diagnostic ?? ""}`
1763
1810
  );
1764
1811
  let downloaded = 0;
1765
1812
  if (download.length > 0) {
1766
- const { list } = await api.cli.listRepositoryProjectDocuments({
1767
- repositoryId,
1813
+ const { list } = await api.cli.listProjectDocuments({
1814
+ projectId,
1768
1815
  paths: download.join(",")
1769
1816
  });
1770
1817
  for (const doc of list) {
1771
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
1818
+ const absPath = toFsPath(
1819
+ projectDocumentLocalPath(targetApmDir, doc.path)
1820
+ );
1772
1821
  await ensureDirExists(dirname2(absPath));
1773
1822
  writeFileSync4(absPath, doc.content, "utf8");
1774
1823
  downloaded += 1;
@@ -1783,34 +1832,34 @@ ${diagnostic ?? ""}`
1783
1832
  }
1784
1833
  }
1785
1834
  writeFileSync4(
1786
- toFsPath(join4(projectDir, MANIFEST_FILE)),
1835
+ toFsPath(join4(docsDir, MANIFEST_FILE)),
1787
1836
  `${JSON.stringify(remoteManifest, null, 2)}
1788
1837
  `,
1789
1838
  "utf8"
1790
1839
  );
1791
1840
  console.log(
1792
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1841
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
1793
1842
  );
1794
1843
  return {
1795
1844
  synced: true,
1796
- repositoryId,
1845
+ projectId,
1797
1846
  downloaded,
1798
1847
  deleted
1799
1848
  };
1800
1849
  }
1801
- async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1850
+ async function syncProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1802
1851
  const api = createApmApiClient(cfg);
1803
- const { repositoryId } = await resolveRepositoryIdForSync(api, workdirPath);
1804
- if (!repositoryId) {
1852
+ const { projectId } = await resolveProjectIdForSync(api, workdirPath);
1853
+ if (!projectId) {
1805
1854
  return 0;
1806
1855
  }
1807
1856
  const targetApmDir = apmRoot ?? workspaceApmDir(workdirPath);
1808
1857
  const localPaths = listLocalDocumentPaths(targetApmDir);
1809
1858
  if (localPaths.length === 0) {
1810
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1859
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u672C\u5730\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1811
1860
  return 0;
1812
1861
  }
1813
- const remoteManifest = (await api.cli.getRepositoryProjectDocumentManifest({ repositoryId })).manifest ?? null;
1862
+ const remoteManifest = (await api.cli.getProjectDocumentManifest({ projectId })).manifest ?? null;
1814
1863
  const remoteHashByPath = new Map(
1815
1864
  (remoteManifest?.documents ?? []).map((doc) => [doc.path, doc.contentHash])
1816
1865
  );
@@ -1828,17 +1877,17 @@ async function syncRepositoryProjectDocumentsPush(cfg, workdirPath, apmRoot) {
1828
1877
  if (remoteHashByPath.get(path19) === contentHash) {
1829
1878
  continue;
1830
1879
  }
1831
- await api.cli.upsertRepositoryProjectDocument({
1832
- repositoryId,
1880
+ await api.cli.upsertProjectDocument({
1881
+ projectId,
1833
1882
  path: path19,
1834
1883
  content,
1835
1884
  description: remoteDescriptionByPath.get(path19) ?? void 0
1836
1885
  });
1837
1886
  synced += 1;
1838
- console.log(`[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: ${path19}`);
1887
+ console.log(`[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: ${path19}`);
1839
1888
  }
1840
1889
  if (synced === 0) {
1841
- console.log("[apm] \u4ED3\u5E93\u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1890
+ console.log("[apm] \u9879\u76EE\u6587\u6863\u65E0\u53D8\u5316\uFF0C\u8DF3\u8FC7\u63A8\u9001");
1842
1891
  }
1843
1892
  return synced;
1844
1893
  }
@@ -1859,7 +1908,7 @@ async function ensureWorkspaceInitialized(workdir, options) {
1859
1908
  const apmDir = workspaceApmDir(workdir);
1860
1909
  await copyTemplateFiles(apmDir, workdir);
1861
1910
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
1862
- await syncRepositoryProjectDocumentsPull(workdir, apmDir);
1911
+ await syncProjectDocumentsPull(workdir, apmDir);
1863
1912
  const trimmedName = options?.name?.trim();
1864
1913
  if (trimmedName) {
1865
1914
  const apmConfigPath = toFsPath(join5(apmDir, "apm.config.json"));
@@ -2773,7 +2822,7 @@ async function runPull(sessionId, remoteWorkdir) {
2773
2822
  await syncSessionAttachments(cfg, trimmedId, attachments, apmRoot);
2774
2823
  await syncPlatformRules(cfg, trimmedId, workdir, apmRoot);
2775
2824
  await syncRemoteDeploymentConfig(workdir, apmRoot);
2776
- await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
2825
+ await syncProjectDocumentsPull(workdir, apmRoot);
2777
2826
  console.log(`[apm] \u5DF2\u540C\u6B65\u4F1A\u8BDD\u5DE5\u4F5C\u533A: ${dir}`);
2778
2827
  return dir;
2779
2828
  }
@@ -3026,17 +3075,21 @@ async function runSyncDeployConfig() {
3026
3075
  }
3027
3076
 
3028
3077
  // src/commands/sync-project-documents.ts
3078
+ init_config();
3029
3079
  async function runSyncProjectDocuments(options) {
3030
- const pull = options?.pull ?? !options?.push;
3031
- const push = options?.push ?? false;
3032
3080
  const workdir = resolveWorkdirPath();
3033
3081
  const apmRoot = workspaceApmDir(workdir);
3034
- if (pull) {
3035
- await syncRepositoryProjectDocumentsPull(workdir, apmRoot);
3082
+ const doPush = Boolean(options.push);
3083
+ const doPull = Boolean(options.pull) || !doPush;
3084
+ if (doPull) {
3085
+ await syncProjectDocumentsPull(workdir, apmRoot);
3036
3086
  }
3037
- if (push) {
3038
- const cfg = await ensureLoggedConfig();
3039
- await syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot);
3087
+ if (doPush) {
3088
+ const cfg = await tryReadApmConfig();
3089
+ if (!cfg) {
3090
+ throw new Error("[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8BF7\u5148\u6267\u884C apm login");
3091
+ }
3092
+ await syncProjectDocumentsPush(cfg, workdir, apmRoot);
3040
3093
  }
3041
3094
  }
3042
3095
 
@@ -7491,7 +7544,7 @@ function createRecommendPlanTool(options) {
7491
7544
  function createUpsertWebIdePlanTool(options) {
7492
7545
  const { cfg, taskId } = options;
7493
7546
  return {
7494
- description: "Persist the implementation plan markdown for this WebIDE task and mark the plan as ready for human confirmation. Call once with the full plan document.",
7547
+ description: "Persist or replace the implementation plan markdown for this WebIDE task and mark it ready for human confirmation. Call with the full plan document (not a diff). Use after generating or revising the plan.",
7495
7548
  inputSchema: {
7496
7549
  type: "object",
7497
7550
  properties: {
@@ -7878,8 +7931,8 @@ async function obtainAgent(ctx) {
7878
7931
  settingSources: ["project"]
7879
7932
  } : {}
7880
7933
  },
7881
- ...ctx.mode ? { mode: ctx.mode } : {}
7882
- // mcpServers: createPlaywrightMcpServers(),
7934
+ ...ctx.mode ? { mode: ctx.mode } : {},
7935
+ ...ctx.mcpServers ? { mcpServers: ctx.mcpServers } : {}
7883
7936
  };
7884
7937
  const explicitAgentId = ctx.resumeAgentId?.trim();
7885
7938
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
@@ -7976,7 +8029,6 @@ async function runCursorAgent(cfg, ctx, options) {
7976
8029
  try {
7977
8030
  const run = await agent.send(prompt, {
7978
8031
  ...ctx.mode ? { mode: ctx.mode } : {},
7979
- // mcpServers: createPlaywrightMcpServers(),
7980
8032
  local: {
7981
8033
  ...options?.forceSend ? { force: true } : {},
7982
8034
  customTools
@@ -8409,7 +8461,7 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
8409
8461
  if (!pullRan) {
8410
8462
  await runStep(
8411
8463
  "sync-project-documents-pull",
8412
- () => syncRepositoryProjectDocumentsPull(workdir, apmRoot)
8464
+ () => syncProjectDocumentsPull(workdir, apmRoot)
8413
8465
  );
8414
8466
  }
8415
8467
  const agentResult = await runStep(
@@ -8442,7 +8494,7 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
8442
8494
  );
8443
8495
  await runStep(
8444
8496
  "sync-project-documents",
8445
- () => syncRepositoryProjectDocumentsPush(cfg, workdir, apmRoot)
8497
+ () => syncProjectDocumentsPush(cfg, workdir, apmRoot)
8446
8498
  );
8447
8499
  await runStep(
8448
8500
  "commit-files",
@@ -9979,12 +10031,12 @@ function buildProgram() {
9979
10031
  await runSyncDeployConfig();
9980
10032
  });
9981
10033
  program.command("sync-project-documents").description(
9982
- "\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\uFF1A\u9ED8\u8BA4 pull \u5230 .apm/project/\uFF1B\u52A0 --push \u5C06\u672C\u5730\u53D8\u66F4\u63A8\u9001\u5230\u5E73\u53F0"
9983
- ).option("--push", "\u63A8\u9001\u672C\u5730 .apm/project/ \u5230\u5E73\u53F0").option("--pull", "\u4ECE\u5E73\u53F0\u62C9\u53D6\u5230 .apm/project/").action(async (opts) => {
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) => {
9984
10036
  await runSyncProjectDocuments(opts);
9985
10037
  });
9986
10038
  program.command("pull").description(
9987
- "\u62C9\u53D6\u6C9F\u901A\u7FA4\u6570\u636E\u5230 .apm/sessions/<sessionId>/\uFF0C\u5E76\u540C\u6B65\u90E8\u7F72\u914D\u7F6E\u4E0E\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u5230 .apm/project/"
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/"
9988
10040
  ).argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async (sessionId) => {
9989
10041
  await runPull(sessionId);
9990
10042
  });
@@ -116,10 +116,18 @@ var requestConfig = {
116
116
  method: "PUT",
117
117
  path: "/cli/webide/terminals/meta"
118
118
  }),
119
+ acquireSteelBrowser: defineEndpoint({
120
+ method: "POST",
121
+ path: "/cli/steel-browser/acquire"
122
+ }),
119
123
  branchBaseline: defineEndpoint({
120
124
  method: "GET",
121
125
  path: "/cli/tasks/branch-baseline"
122
126
  }),
127
+ projectBaseBranch: defineEndpoint({
128
+ method: "GET",
129
+ path: "/cli/tasks/project-base-branch"
130
+ }),
123
131
  listSessionsForBranchCleanup: defineEndpoint({
124
132
  method: "GET",
125
133
  path: "/cli/sessions/branch-cleanup"
@@ -156,21 +164,21 @@ var requestConfig = {
156
164
  method: "POST",
157
165
  path: "/cli/webide/pull-requests/merge"
158
166
  }),
159
- getRepositoryProjectDocumentManifest: defineEndpoint({
167
+ getProjectDocumentManifest: defineEndpoint({
160
168
  method: "GET",
161
- path: "/cli/repository-project-documents/manifest"
169
+ path: "/cli/project-documents/manifest"
162
170
  }),
163
- listRepositoryProjectDocuments: defineEndpoint({
171
+ listProjectDocuments: defineEndpoint({
164
172
  method: "GET",
165
- path: "/cli/repository-project-documents"
173
+ path: "/cli/project-documents"
166
174
  }),
167
- upsertRepositoryProjectDocument: defineEndpoint({
175
+ upsertProjectDocument: defineEndpoint({
168
176
  method: "PUT",
169
- path: "/cli/repository-project-documents/upsert"
177
+ path: "/cli/project-documents/upsert"
170
178
  }),
171
- removeRepositoryProjectDocument: defineEndpoint({
179
+ removeProjectDocument: defineEndpoint({
172
180
  method: "DELETE",
173
- path: "/cli/repository-project-documents"
181
+ path: "/cli/project-documents"
174
182
  }),
175
183
  createTaskDeployment: defineEndpoint({
176
184
  method: "POST",
@@ -207,6 +215,7 @@ import { homedir } from "os";
207
215
  import { join } from "path";
208
216
  var APM_CONFIG_DIR = join(homedir(), ".config", "apm");
209
217
  var APM_CONFIG_PATH = join(APM_CONFIG_DIR, "config.json");
218
+ var DEFAULT_BASE_URL = "http://127.0.0.1:3000";
210
219
  function resolveClientMachineId(cfg) {
211
220
  return (cfg.clientMachineId ?? cfg.userId ?? "").trim();
212
221
  }
@@ -237,6 +246,33 @@ async function tryReadApmConfig() {
237
246
  return null;
238
247
  }
239
248
  }
249
+ function defaultApmConfig() {
250
+ return {
251
+ baseUrl: DEFAULT_BASE_URL,
252
+ clientMachineId: "",
253
+ apiKey: ""
254
+ };
255
+ }
256
+ async function ensureApmConfig() {
257
+ const existing = await tryReadApmConfig();
258
+ if (existing) return existing;
259
+ const defaults = defaultApmConfig();
260
+ await writeApmConfig(defaults);
261
+ return defaults;
262
+ }
263
+ async function writeApmConfig(cfg) {
264
+ const clientMachineId = resolveClientMachineId(cfg);
265
+ const apiKey = resolveApiKey(cfg);
266
+ const normalized = {
267
+ baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ""),
268
+ apiKey,
269
+ ...clientMachineId ? { clientMachineId } : {}
270
+ };
271
+ mkdirSync(APM_CONFIG_DIR, { recursive: true });
272
+ writeFileSync(APM_CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", {
273
+ mode: 384
274
+ });
275
+ }
240
276
 
241
277
  // src/api/client.ts
242
278
  function createApmApiClient(cfg) {
@@ -375,24 +411,6 @@ async function ensureRemoteBaselineBranch(cwd, baselineBranch) {
375
411
  `[apm] \u8FDC\u7A0B\u4E0D\u5B58\u5728\u57FA\u7EBF\u5206\u652F origin/${baselineBranch}\uFF0C\u8BF7\u786E\u8BA4\u4ED3\u5E93\u9ED8\u8BA4\u5206\u652F\u5DF2\u63A8\u9001\u5230 origin`
376
412
  );
377
413
  }
378
- async function resolveDefaultRemoteBranch(cwd) {
379
- try {
380
- const ref = (await execGit(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], true)).trim();
381
- const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
382
- if (match?.[1]) {
383
- return match[1];
384
- }
385
- } catch {
386
- }
387
- for (const candidate of ["main", "master"]) {
388
- if (await remoteBranchExists(cwd, candidate)) {
389
- return candidate;
390
- }
391
- }
392
- throw new Error(
393
- "[apm] \u65E0\u6CD5\u786E\u5B9A\u8FDC\u7A0B\u9ED8\u8BA4\u5206\u652F\uFF08origin/HEAD\u3001main\u3001master \u5747\u4E0D\u53EF\u7528\uFF09"
394
- );
395
- }
396
414
  async function hasUpstream(cwd) {
397
415
  try {
398
416
  await execGit(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"], true);
@@ -573,6 +591,16 @@ function assertApmGitignoredInRepo(workdir) {
573
591
  );
574
592
  }
575
593
  }
594
+ async function ensureLoggedConfig() {
595
+ const cfg = await ensureApmConfig();
596
+ if (!resolveApiKey(cfg)) {
597
+ console.error(
598
+ "[apm] \u672A\u68C0\u6D4B\u5230 API Key\uFF0C\u8BF7\u5148\u6267\u884C: apm login --api-key cm_xxx"
599
+ );
600
+ process.exit(1);
601
+ }
602
+ return cfg;
603
+ }
576
604
  async function ensureDirExists(dir) {
577
605
  mkdirSync2(dir, { recursive: true });
578
606
  }
@@ -977,6 +1005,17 @@ async function runTaskBranch(taskId, options = {}) {
977
1005
  if (!trimmedTaskId) {
978
1006
  throw new Error("[apm] taskId \u4E0D\u80FD\u4E3A\u7A7A");
979
1007
  }
1008
+ const cfg = await ensureLoggedConfig();
1009
+ const api = createApmApiClient(cfg);
1010
+ const projectBaseline = await api.cli.projectBaseBranch({
1011
+ taskId: trimmedTaskId
1012
+ });
1013
+ const baselineBranch = projectBaseline.baseBranch?.trim() ?? "";
1014
+ if (!baselineBranch) {
1015
+ throw new Error(
1016
+ "[apm] \u9879\u76EE\u672A\u914D\u7F6E\u57FA\u7840\u5206\u652F\uFF0C\u65E0\u6CD5\u521B\u5EFA WebIDE \u7279\u6027\u5206\u652F"
1017
+ );
1018
+ }
980
1019
  const cwd = options.cwd ?? process.cwd();
981
1020
  const workdirPath = resolveWorkdirPath(cwd);
982
1021
  const manifest = loadWorkspaceReposCache(workdirPath);
@@ -984,9 +1023,8 @@ async function runTaskBranch(taskId, options = {}) {
984
1023
  const branch = branchNameForTask(trimmedTaskId);
985
1024
  for (const gitRoot of repoRoots) {
986
1025
  const label = formatRepoLabel(workdirPath, gitRoot);
987
- const baselineBranch = await resolveDefaultRemoteBranch(gitRoot);
988
1026
  console.log(
989
- `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (baseline=${baselineBranch})`
1027
+ `[apm] \u4EFB\u52A1\u5206\u652F ${branch} @ ${label} (project baseline=${baselineBranch})`
990
1028
  );
991
1029
  await ensureFeatureBranch(branch, baselineBranch, {
992
1030
  ...options,
@@ -1643,7 +1681,7 @@ function createRecommendPlanTool(options) {
1643
1681
  function createUpsertWebIdePlanTool(options) {
1644
1682
  const { cfg, taskId } = options;
1645
1683
  return {
1646
- description: "Persist the implementation plan markdown for this WebIDE task and mark the plan as ready for human confirmation. Call once with the full plan document.",
1684
+ description: "Persist or replace the implementation plan markdown for this WebIDE task and mark it ready for human confirmation. Call with the full plan document (not a diff). Use after generating or revising the plan.",
1647
1685
  inputSchema: {
1648
1686
  type: "object",
1649
1687
  properties: {
@@ -2028,8 +2066,8 @@ async function obtainAgent(ctx) {
2028
2066
  settingSources: ["project"]
2029
2067
  } : {}
2030
2068
  },
2031
- ...ctx.mode ? { mode: ctx.mode } : {}
2032
- // mcpServers: createPlaywrightMcpServers(),
2069
+ ...ctx.mode ? { mode: ctx.mode } : {},
2070
+ ...ctx.mcpServers ? { mcpServers: ctx.mcpServers } : {}
2033
2071
  };
2034
2072
  const explicitAgentId = ctx.resumeAgentId?.trim();
2035
2073
  const savedAgentId = explicitAgentId || (ctx.user ? loadSessionAgentId(ctx.workdir, ctx.sessionId, ctx.user) : void 0);
@@ -2126,7 +2164,6 @@ async function runCursorAgent(cfg, ctx, options) {
2126
2164
  try {
2127
2165
  const run = await agent.send(prompt, {
2128
2166
  ...ctx.mode ? { mode: ctx.mode } : {},
2129
- // mcpServers: createPlaywrightMcpServers(),
2130
2167
  local: {
2131
2168
  ...options?.forceSend ? { force: true } : {},
2132
2169
  customTools
@@ -2622,7 +2659,7 @@ ${diagnostic ?? ""}
2622
2659
  return { synced: true, repositoryId, configName: config.name };
2623
2660
  }
2624
2661
 
2625
- // src/repository-project-documents-sync.ts
2662
+ // src/project-documents-sync.ts
2626
2663
  import {
2627
2664
  existsSync as existsSync5,
2628
2665
  readdirSync as readdirSync3,
@@ -2633,7 +2670,7 @@ import {
2633
2670
  import { dirname as dirname4, join as join6, relative as relative2, sep } from "path";
2634
2671
  var MANIFEST_FILE = "manifest.json";
2635
2672
  function projectDocumentsDir(apmRoot) {
2636
- return join6(apmRoot ?? workspaceApmDir(), "project");
2673
+ return join6(apmRoot ?? workspaceApmDir(), "docs");
2637
2674
  }
2638
2675
  function projectDocumentLocalPath(apmRoot, documentPath) {
2639
2676
  const normalized = normalizeLocalDocumentPath(documentPath);
@@ -2684,41 +2721,65 @@ function diffManifestPaths(remote, local) {
2684
2721
  }
2685
2722
  return { download, deleteLocal };
2686
2723
  }
2687
- async function syncRepositoryProjectDocumentsPull(workdirPath, apmDir) {
2724
+ async function resolveProjectIdForSync(api, workdirPath) {
2725
+ try {
2726
+ const baseline = await api.cli.workspaceBaseline({ workdirPath });
2727
+ const projectId = baseline.projectId?.trim() || null;
2728
+ if (projectId) {
2729
+ return { projectId, diagnostic: null };
2730
+ }
2731
+ const projectIds = baseline.projectIds ?? [];
2732
+ if (projectIds.length === 0) {
2733
+ return {
2734
+ projectId: null,
2735
+ diagnostic: baseline.diagnostic?.message ?? `\u5DE5\u4F5C\u7A7A\u95F4\u672A\u5173\u8054\u9879\u76EE\uFF0C\u65E0\u6CD5\u540C\u6B65\u9879\u76EE\u6587\u6863\uFF08\u76EE\u5F55\uFF1A${workdirPath}\uFF09\u3002
2736
+ \u8BF7\u5728\u5E73\u53F0\u5C06\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u5230\u552F\u4E00\u9879\u76EE\u3002`
2737
+ };
2738
+ }
2739
+ return {
2740
+ projectId: null,
2741
+ diagnostic: `\u5DE5\u4F5C\u7A7A\u95F4\u5173\u8054\u4E86 ${projectIds.length} \u4E2A\u9879\u76EE\uFF0C\u65E0\u6CD5\u786E\u5B9A\u540C\u6B65\u76EE\u6807\u3002
2742
+ \u8BF7\u786E\u4FDD\u8BE5\u5DE5\u4F5C\u7A7A\u95F4\u53EA\u5173\u8054\u4E00\u4E2A\u9879\u76EE\u540E\u518D\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002`
2743
+ };
2744
+ } catch (err) {
2745
+ const detail = err instanceof Error ? err.message : String(err);
2746
+ return {
2747
+ projectId: null,
2748
+ diagnostic: detail.replace(/^\[apm\]\s*/, "")
2749
+ };
2750
+ }
2751
+ }
2752
+ async function syncProjectDocumentsPull(workdirPath, apmDir) {
2688
2753
  const empty = {
2689
2754
  synced: false,
2690
- repositoryId: null,
2755
+ projectId: null,
2691
2756
  downloaded: 0,
2692
2757
  deleted: 0
2693
2758
  };
2694
2759
  const cfg = await tryReadApmConfig();
2695
2760
  if (!cfg || !resolveApiKey(cfg)) {
2696
2761
  console.log(
2697
- "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
2762
+ "[apm] \u672A\u68C0\u6D4B\u5230\u767B\u5F55\u4FE1\u606F\uFF0C\u8DF3\u8FC7\u9879\u76EE\u6587\u6863\u540C\u6B65\u3002\n[apm] \u8BF7\u5148\u6267\u884C apm login\u3002"
2698
2763
  );
2699
2764
  return empty;
2700
2765
  }
2701
2766
  const api = createApmApiClient(cfg);
2702
- const { repositoryId, diagnostic } = await resolveRepositoryIdForSync(
2767
+ const { projectId, diagnostic } = await resolveProjectIdForSync(
2703
2768
  api,
2704
2769
  workdirPath
2705
2770
  );
2706
- if (!repositoryId) {
2707
- console.log(
2708
- `[apm] \u672A\u80FD\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863\u3002
2709
- ${diagnostic ?? ""}`
2710
- );
2771
+ if (!projectId) {
2772
+ console.log(`[apm] \u672A\u80FD\u540C\u6B65\u9879\u76EE\u6587\u6863\u3002
2773
+ ${diagnostic ?? ""}`);
2711
2774
  return empty;
2712
2775
  }
2713
2776
  const targetApmDir = apmDir ?? workspaceApmDir(workdirPath);
2714
- const projectDir = projectDocumentsDir(targetApmDir);
2715
- await ensureDirExists(projectDir);
2716
- const { manifest: remoteManifest } = await api.cli.getRepositoryProjectDocumentManifest({ repositoryId });
2777
+ const docsDir = projectDocumentsDir(targetApmDir);
2778
+ await ensureDirExists(docsDir);
2779
+ const { manifest: remoteManifest } = await api.cli.getProjectDocumentManifest({ projectId });
2717
2780
  if (!remoteManifest) {
2718
- console.log(
2719
- `[apm] \u4ED3\u5E93 ${repositoryId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`
2720
- );
2721
- return { ...empty, repositoryId };
2781
+ console.log(`[apm] \u9879\u76EE ${projectId} \u65E0\u9879\u76EE\u6587\u6863 manifest\uFF0C\u8DF3\u8FC7\u540C\u6B65\u3002`);
2782
+ return { ...empty, projectId };
2722
2783
  }
2723
2784
  const localManifest = readLocalManifest(targetApmDir);
2724
2785
  const { download, deleteLocal } = diffManifestPaths(
@@ -2727,12 +2788,14 @@ ${diagnostic ?? ""}`
2727
2788
  );
2728
2789
  let downloaded = 0;
2729
2790
  if (download.length > 0) {
2730
- const { list } = await api.cli.listRepositoryProjectDocuments({
2731
- repositoryId,
2791
+ const { list } = await api.cli.listProjectDocuments({
2792
+ projectId,
2732
2793
  paths: download.join(",")
2733
2794
  });
2734
2795
  for (const doc of list) {
2735
- const absPath = toFsPath(projectDocumentLocalPath(targetApmDir, doc.path));
2796
+ const absPath = toFsPath(
2797
+ projectDocumentLocalPath(targetApmDir, doc.path)
2798
+ );
2736
2799
  await ensureDirExists(dirname4(absPath));
2737
2800
  writeFileSync7(absPath, doc.content, "utf8");
2738
2801
  downloaded += 1;
@@ -2747,17 +2810,17 @@ ${diagnostic ?? ""}`
2747
2810
  }
2748
2811
  }
2749
2812
  writeFileSync7(
2750
- toFsPath(join6(projectDir, MANIFEST_FILE)),
2813
+ toFsPath(join6(docsDir, MANIFEST_FILE)),
2751
2814
  `${JSON.stringify(remoteManifest, null, 2)}
2752
2815
  `,
2753
2816
  "utf8"
2754
2817
  );
2755
2818
  console.log(
2756
- `[apm] \u5DF2\u540C\u6B65\u4ED3\u5E93\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2819
+ `[apm] \u5DF2\u540C\u6B65\u9879\u76EE\u6587\u6863: \u4E0B\u8F7D ${downloaded}\uFF0C\u5220\u9664\u672C\u5730 ${deleted}`
2757
2820
  );
2758
2821
  return {
2759
2822
  synced: true,
2760
- repositoryId,
2823
+ projectId,
2761
2824
  downloaded,
2762
2825
  deleted
2763
2826
  };
@@ -2779,7 +2842,7 @@ async function ensureWorkspaceInitialized(workdir, options) {
2779
2842
  const apmDir = workspaceApmDir(workdir);
2780
2843
  await copyTemplateFiles(apmDir, workdir);
2781
2844
  const syncResult = await syncRemoteDeploymentConfig(workdir, apmDir);
2782
- await syncRepositoryProjectDocumentsPull(workdir, apmDir);
2845
+ await syncProjectDocumentsPull(workdir, apmDir);
2783
2846
  const trimmedName = options?.name?.trim();
2784
2847
  if (trimmedName) {
2785
2848
  const apmConfigPath = toFsPath(join7(apmDir, "apm.config.json"));
@@ -3214,7 +3277,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3214
3277
  id: messageId,
3215
3278
  content: fallback
3216
3279
  });
3217
- if (msg.action === "write-plan" && outcome.createPlan && outcome.createPlan.trim()) {
3280
+ if ((msg.action === "write-plan" || msg.action === "revise-plan") && outcome.createPlan && outcome.createPlan.trim()) {
3218
3281
  try {
3219
3282
  await api.cli.webideUpsertPlan({
3220
3283
  taskId,
@@ -3222,7 +3285,7 @@ async function handleWebIdeInboundMessage(cfg, msg, signal, options) {
3222
3285
  });
3223
3286
  } catch (err) {
3224
3287
  console.warn(
3225
- "[apm] write-plan \u8865\u5199\u8BA1\u5212\u5931\u8D25:",
3288
+ `[apm] ${msg.action} \u8865\u5199\u8BA1\u5212\u5931\u8D25:`,
3226
3289
  err instanceof Error ? err.message : err
3227
3290
  );
3228
3291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "7.1.13",
3
+ "version": "7.1.15",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,10 +33,10 @@
33
33
  - webide_testcase.md:WebIDE 生成测试用例时读取,按规范编写并提交用例
34
34
  - webide_merge.md:WebIDE 验收通过后读取;直接调用 MergeWebIdePullRequests 合并代码
35
35
 
36
- - 仓库项目上下文(`.apm/project/`):
36
+ - 项目文档(`.apm/docs/`):
37
37
 
38
- - 索引: `.apm/project/manifest.json` — 本仓库在平台登记的文档列表
39
- - 文档文件: `.apm/project/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
38
+ - 索引: `.apm/docs/manifest.json` — 本项目在平台登记的文档列表
39
+ - 文档文件: `.apm/docs/{path}` — 与 manifest 中 path 对应;`apm pull` 自动同步,`apm connect` 消息结束后自动推回平台
40
40
  - 任务涉及菜单名、路由、业务术语等时,**先 Read manifest 与相关文档**,禁止猜测路径
41
41
 
42
42
  - 本轮任务需要关注的文档指引(.apm/sessions/<会话 ID>/\*):
@@ -13,23 +13,25 @@
13
13
 
14
14
  ## 启动命令来源
15
15
 
16
- 必须 Read `.apm/project/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
16
+ 必须 Read `.apm/docs/deploy.md`,按文档中的 cwd / command / 服务划分调用 `StartWebIdeTerminal`。
17
17
 
18
18
  - 文档不存在或未写明启动方式 → 用 AppendMessage 说明无法启动本地环境,**禁止猜测命令**。
19
19
 
20
- ## 时机
20
+ ## 时机(打开本地 dev)
21
21
 
22
- 1. **首次**(`start-develop` / 跳过计划后的开发轮次):实现完成后需要本地环境时
23
- - 先 `ListWebIdeTerminals`
24
- - 若无活终端 → Read `deploy.md` → 按前后端分别 `StartWebIdeTerminal`
25
- - 等待 READY / 端口出现后,告知用户可在 WebIDE「终端」面板观察日志(勿粘贴整段 stdout)
26
- 2. **后续改码**(热更新):有活终端则**复用,不重起**
27
- 3. **显式重跑**(端口冲突、命令变更、或平台「重跑本地环境」指令):
28
- `StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start
29
- 不要在已死进程上糊弄;需要换命令时也是先停再建
22
+ 固定顺序:
23
+
24
+ 1. **`ListWebIdeTerminals`**
25
+ 2. 若无活终端 Read `deploy.md` 按前后端分别 `StartWebIdeTerminal`;已有活终端则复用
26
+ 3. 等待 READY / 端口即可
27
+ 4. AppendMessage:可在「终端」面板观察本地日志;「浏览器」面板由平台打开本客户机外网地址(勿粘贴整段 stdout;无需 Agent 打开浏览器)
28
+
29
+ 后续改码(热更新):有活终端则**复用,不重起**。
30
+
31
+ 显式重跑:`StopAllWebIdeTerminals` → 再 Read `deploy.md` → 重新 Start。
30
32
 
31
33
  ## 规范
32
34
 
33
35
  1. 禁止用 Shell / 后台进程方式起 `dev` / `serve` 等长驻命令。
34
- 2. 观察日志请用户看 WebIDE「终端」面板。
36
+ 2. 观察日志请用户看 WebIDE「终端」面板;页面请用户看「浏览器」面板(客户机外网地址)。
35
37
  3. 任务结束或不再需要时可 Stop;不要留下孤儿进程。