ai-project-manage-cli 6.0.93 → 6.0.95

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
@@ -55,7 +55,7 @@ apm daemon start
55
55
  apm daemon start -f
56
56
  ```
57
57
 
58
- PM2 已内置;同一时刻仅允许一个 connect。启动时会自动检测 CLI 更新并刷新守护配置。
58
+ 使用全局 PM2(未安装时会自动执行 `npm install -g pm2`);同一时刻仅允许一个 connect。启动时会自动检测 CLI 更新并刷新守护配置。
59
59
 
60
60
  `apm pull` 会自动将平台登记的**仓库项目文档**同步到 `.apm/project/`(含 `manifest.json`)。Agent 修改 `.apm/project/` 下文件后,`apm connect` 处理完消息会自动推回平台。
61
61
 
package/dist/index.js CHANGED
@@ -2029,9 +2029,169 @@ async function runSyncProjectDocuments(options) {
2029
2029
  }
2030
2030
 
2031
2031
  // src/commands/sync-document.ts
2032
- import { existsSync as existsSync10 } from "fs";
2032
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2033
2033
  import { basename as basename3 } from "path";
2034
2034
 
2035
+ // src/assumptions/local-validate.ts
2036
+ var NO_ASSUMPTIONS_RE = /无[,,]?\s*口径均有依据/;
2037
+ var BLOCK_HEADER_RE = /^####\s+(A\d+)\s*$/im;
2038
+ var DEV_PATTERNS = [
2039
+ { pattern: /status\s*=/i, message: "\u5305\u542B status= \u7B49\u6280\u672F\u679A\u4E3E" },
2040
+ { pattern: /\.(java|vue|ts|tsx|jsx|sql)\b/i, message: "\u5305\u542B\u6587\u4EF6\u8DEF\u5F84" },
2041
+ { pattern: /`[^`]+`/i, message: "\u5305\u542B\u4EE3\u7801\u53CD\u5F15\u53F7" },
2042
+ { pattern: /\bSELECT\b|\bINSERT\b|\bALTER\b/i, message: "\u5305\u542B SQL \u5173\u952E\u5B57" },
2043
+ { pattern: /\w+\.\w+/i, message: "\u5305\u542B\u7C7B\u540D\u6216\u8868\u5B57\u6BB5\u70B9\u53F7\u5199\u6CD5" },
2044
+ { pattern: /\b[a-z]+_[a-z0-9_]+\b/i, message: "\u5305\u542B snake_case \u5B57\u6BB5\u540D" }
2045
+ ];
2046
+ var BUSINESS_VERBS = /提交|展示|校验|驳回|审批|允许|禁止|筛选|列表|详情|暂存|删除|新增|修改|导出|导入/;
2047
+ function parseOptionsBlock(text) {
2048
+ const options = [];
2049
+ for (const line of text.split("\n")) {
2050
+ const trimmed = line.trim();
2051
+ const match = trimmed.match(/^-\s*([a-zA-Z0-9_]+)[::]\s*(.+)$/);
2052
+ if (match) {
2053
+ options.push({ id: match[1], label: match[2].trim() });
2054
+ }
2055
+ }
2056
+ return options;
2057
+ }
2058
+ function parseField(block, label) {
2059
+ const re = new RegExp(
2060
+ `-\\s*\\*\\*${label}\\*\\*(?:\\\uFF08[^\uFF09]*\\\uFF09)?[:\uFF1A]\\s*(.*)$`,
2061
+ "im"
2062
+ );
2063
+ const match = block.match(re);
2064
+ const value = match?.[1]?.trim();
2065
+ return value || void 0;
2066
+ }
2067
+ function parseStructuredBlock(code, block) {
2068
+ const pmQuestion = parseField(block, "\u95EE") ?? parseField(block, "prompt");
2069
+ if (!pmQuestion) return null;
2070
+ const optionsSection = block.match(
2071
+ /-\s*\*\*选项\*\*[::]?\s*\n([\s\S]*?)(?=\n-\s*\*\*|$)/i
2072
+ );
2073
+ const options = optionsSection ? parseOptionsBlock(optionsSection[1]) : [];
2074
+ return {
2075
+ code,
2076
+ questionId: code,
2077
+ pmQuestion,
2078
+ context: parseField(block, "\u573A\u666F"),
2079
+ options,
2080
+ technicalNote: parseField(block, "\u7814\u53D1\u5907\u6CE8"),
2081
+ reason: parseField(block, "\u539F\u56E0")
2082
+ };
2083
+ }
2084
+ function hasNoAssumptionsMarker(markdown) {
2085
+ return NO_ASSUMPTIONS_RE.test(markdown.replace(/\r\n/g, "\n"));
2086
+ }
2087
+ function countAssumptionMarkers(markdown) {
2088
+ const normalized = markdown.replace(/\r\n/g, "\n").trim();
2089
+ if (NO_ASSUMPTIONS_RE.test(normalized)) return 0;
2090
+ const codes = /* @__PURE__ */ new Set();
2091
+ for (const line of normalized.split("\n")) {
2092
+ const match = line.trim().match(/^####\s+(A\d+)\s*$/);
2093
+ if (match) codes.add(match[1]);
2094
+ }
2095
+ return codes.size;
2096
+ }
2097
+ function parseAssumptionsDocument(markdown, _source) {
2098
+ const normalized = markdown.replace(/\r\n/g, "\n").trim();
2099
+ if (!normalized || NO_ASSUMPTIONS_RE.test(normalized)) {
2100
+ return [];
2101
+ }
2102
+ const results = [];
2103
+ const blocks = normalized.split(/(?=^####\s+A\d+\s*$)/im);
2104
+ for (const block of blocks) {
2105
+ const headerMatch = block.match(BLOCK_HEADER_RE);
2106
+ if (!headerMatch) continue;
2107
+ const parsed = parseStructuredBlock(headerMatch[1], block);
2108
+ if (parsed) results.push(parsed);
2109
+ }
2110
+ return results;
2111
+ }
2112
+ function validateAssumptionForPm(assumption) {
2113
+ const issues = [];
2114
+ const questionForCheck = assumption.pmQuestion.replace(/`[^`]*`/g, "").trim();
2115
+ if (!assumption.pmQuestion?.trim()) {
2116
+ issues.push("\u7F3A\u5C11\u4E1A\u52A1\u95EE\u53E5");
2117
+ } else if (questionForCheck.length < 6) {
2118
+ issues.push("\u95EE\u53E5\u8FC7\u77ED");
2119
+ } else if (!BUSINESS_VERBS.test(questionForCheck)) {
2120
+ issues.push("\u95EE\u53E5\u7F3A\u5C11\u53EF\u7406\u89E3\u7684\u4E1A\u52A1\u52A8\u8BCD");
2121
+ }
2122
+ for (const { pattern, message } of DEV_PATTERNS) {
2123
+ if (pattern.test(questionForCheck)) {
2124
+ issues.push(message);
2125
+ }
2126
+ }
2127
+ if (!assumption.context?.trim()) {
2128
+ issues.push("\u7F3A\u5C11\u573A\u666F\u8BF4\u660E");
2129
+ }
2130
+ if (!assumption.options || assumption.options.length < 2) {
2131
+ issues.push("\u9009\u9879\u5C11\u4E8E 2 \u9879");
2132
+ }
2133
+ return { ok: issues.length === 0, issues };
2134
+ }
2135
+ function printAssumptionValidationResult(result) {
2136
+ for (const warning of result.warnings) {
2137
+ console.warn(`[apm] \u5047\u8BBE\u6821\u9A8C\u544A\u8B66: ${warning}`);
2138
+ }
2139
+ for (const item of result.issues) {
2140
+ console.error(
2141
+ `[apm] \u5047\u8BBE ${item.code} \u672A\u901A\u8FC7 PM \u6821\u9A8C: ${item.issues.join("\uFF1B")}`
2142
+ );
2143
+ }
2144
+ }
2145
+ function resolveAssumptionSourceFromFileName(fileName) {
2146
+ const key = fileName.replace(/\.md$/i, "").toUpperCase();
2147
+ if (key === "FRONTEND-ASSUMPTIONS") {
2148
+ return "FRONTEND_PLAN" /* FRONTEND_PLAN */;
2149
+ }
2150
+ if (key === "BACKEND-ASSUMPTIONS") {
2151
+ return "BACKEND_PLAN" /* BACKEND_PLAN */;
2152
+ }
2153
+ return null;
2154
+ }
2155
+ function validateAssumptionsMarkdown(markdown, source) {
2156
+ const markerCount = countAssumptionMarkers(markdown);
2157
+ const explicitEmpty = hasNoAssumptionsMarker(markdown);
2158
+ if (explicitEmpty && markerCount === 0) {
2159
+ return {
2160
+ ok: true,
2161
+ markerCount: 0,
2162
+ parsedCount: 0,
2163
+ issues: [],
2164
+ warnings: []
2165
+ };
2166
+ }
2167
+ const parsed = parseAssumptionsDocument(markdown, source);
2168
+ const issues = [];
2169
+ const warnings = [];
2170
+ for (const item of parsed) {
2171
+ const result = validateAssumptionForPm(item);
2172
+ if (!result.ok) {
2173
+ issues.push({ code: item.code, issues: result.issues });
2174
+ }
2175
+ }
2176
+ if (markerCount > 0 && parsed.length === 0) {
2177
+ warnings.push(
2178
+ `\u68C0\u6D4B\u5230 ${markerCount} \u6761\u5047\u8BBE\u6807\u8BB0\uFF0C\u4F46\u683C\u5F0F\u65E0\u6CD5\u89E3\u6790\u3002\u8BF7\u4F7F\u7528 #### A1 + \u95EE/\u573A\u666F/\u9009\u9879 \u7ED3\u6784\u3002`
2179
+ );
2180
+ } else if (markerCount > parsed.length) {
2181
+ warnings.push(
2182
+ `\u68C0\u6D4B\u5230 ${markerCount} \u6761\u5047\u8BBE\u6807\u8BB0\uFF0C\u4EC5\u89E3\u6790 ${parsed.length} \u6761\u3002`
2183
+ );
2184
+ }
2185
+ const ok = issues.length === 0 && warnings.length === 0 && (markerCount === 0 || parsed.length > 0);
2186
+ return {
2187
+ ok,
2188
+ markerCount,
2189
+ parsedCount: parsed.length,
2190
+ issues,
2191
+ warnings
2192
+ };
2193
+ }
2194
+
2035
2195
  // src/commands/sync-session-documents.ts
2036
2196
  import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
2037
2197
  import { join as join12 } from "path";
@@ -2116,6 +2276,19 @@ async function runSyncDocument(sessionId, options) {
2116
2276
  );
2117
2277
  process.exit(1);
2118
2278
  }
2279
+ const fileName = basename3(absPath);
2280
+ const assumptionSource = resolveAssumptionSourceFromFileName(fileName);
2281
+ if (assumptionSource) {
2282
+ const content = readFileSync9(absPath, "utf8");
2283
+ const validation = validateAssumptionsMarkdown(content, assumptionSource);
2284
+ if (!validation.ok) {
2285
+ printAssumptionValidationResult(validation);
2286
+ console.error(
2287
+ "[apm] \u5047\u8BBE sync \u672A\u901A\u8FC7\uFF0C\u5DF2\u4E2D\u6B62\u4E0A\u4F20\u3002\u8BF7\u6309 apm-write-assumptions \u6280\u80FD\u4FEE\u6B63\u540E\u91CD\u8BD5"
2288
+ );
2289
+ process.exit(1);
2290
+ }
2291
+ }
2119
2292
  const cfg = await ensureLoggedConfig();
2120
2293
  const api = createApmApiClient(cfg);
2121
2294
  const docsDir = sessionDocsDir(trimmedSessionId);
@@ -2216,6 +2389,7 @@ async function runUpdateMessageStatus(options) {
2216
2389
 
2217
2390
  // src/commands/connect.ts
2218
2391
  import WebSocket from "ws";
2392
+ import { setTimeout as delay2 } from "node:timers/promises";
2219
2393
 
2220
2394
  // src/ws/protocol.ts
2221
2395
  function nonEmptyString(v) {
@@ -3004,7 +3178,7 @@ ${JSON.stringify(event, null, 2)}
3004
3178
  }
3005
3179
 
3006
3180
  // src/commands/connect/agent-session-registry.ts
3007
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "node:fs";
3181
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
3008
3182
  import { dirname as dirname4, resolve as resolve3 } from "node:path";
3009
3183
  function registryPath(workdir, sessionId) {
3010
3184
  return resolve3(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
@@ -3014,7 +3188,7 @@ function readRegistry(path13) {
3014
3188
  return {};
3015
3189
  }
3016
3190
  try {
3017
- const parsed = JSON.parse(readFileSync9(path13, "utf8"));
3191
+ const parsed = JSON.parse(readFileSync10(path13, "utf8"));
3018
3192
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3019
3193
  const result = {};
3020
3194
  for (const [key, value] of Object.entries(
@@ -3482,7 +3656,7 @@ async function ensureMessageHasReply(cfg, sessionId, messageId, fallback) {
3482
3656
  }
3483
3657
 
3484
3658
  // src/commands/connect/cli-version-sync.ts
3485
- import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync11 } from "fs";
3659
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
3486
3660
  import { join as join13 } from "path";
3487
3661
  var CLI_VERSION_FILE = ".cli-version.json";
3488
3662
  function manifestPath(apmDir) {
@@ -3495,7 +3669,7 @@ function loadManifest3(apmDir) {
3495
3669
  }
3496
3670
  try {
3497
3671
  const parsed = JSON.parse(
3498
- readFileSync10(path13, "utf8")
3672
+ readFileSync11(path13, "utf8")
3499
3673
  );
3500
3674
  if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
3501
3675
  return parsed;
@@ -3584,7 +3758,7 @@ function createRunSlotPool(maxConcurrent = DEFAULT_MAX_CONCURRENT) {
3584
3758
  }
3585
3759
 
3586
3760
  // src/commands/connect-lock.ts
3587
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync12 } from "fs";
3761
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync12 } from "fs";
3588
3762
  import { join as join14 } from "path";
3589
3763
  var CONNECT_LOCK_PATH = join14(APM_CONFIG_DIR, "connect.lock");
3590
3764
  function isProcessAlive(pid) {
@@ -3599,7 +3773,7 @@ function isProcessAlive(pid) {
3599
3773
  function readConnectLock() {
3600
3774
  if (!existsSync13(CONNECT_LOCK_PATH)) return null;
3601
3775
  try {
3602
- const raw = readFileSync11(CONNECT_LOCK_PATH, "utf8");
3776
+ const raw = readFileSync12(CONNECT_LOCK_PATH, "utf8");
3603
3777
  const parsed = JSON.parse(raw);
3604
3778
  if (typeof parsed.pid !== "number" || parsed.mode !== "foreground" && parsed.mode !== "pm2" || typeof parsed.startedAt !== "string") {
3605
3779
  return null;
@@ -3654,7 +3828,7 @@ function forceReleaseConnectLock() {
3654
3828
 
3655
3829
  // src/commands/daemon.ts
3656
3830
  import { spawnSync as spawnSync2 } from "child_process";
3657
- import { createRequire } from "node:module";
3831
+ import { setTimeout as delay } from "node:timers/promises";
3658
3832
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync13 } from "fs";
3659
3833
  import { join as join15 } from "path";
3660
3834
  var PM2_APP_NAME = "apm-connect";
@@ -3682,10 +3856,12 @@ function buildConnectPm2Ecosystem(options) {
3682
3856
  args: options.connectArgs,
3683
3857
  autorestart: true,
3684
3858
  min_uptime: "10s",
3685
- max_restarts: 100,
3859
+ max_restarts: 0,
3686
3860
  restart_delay: 3e3,
3687
3861
  exp_backoff_restart_delay: 1e3,
3688
3862
  max_memory_restart: "1G",
3863
+ kill_timeout: 5e3,
3864
+ shutdown_with_message: true,
3689
3865
  env
3690
3866
  }
3691
3867
  ]
@@ -3695,11 +3871,69 @@ function formatConnectPm2EcosystemFile(ecosystem) {
3695
3871
  return `module.exports = ${JSON.stringify(ecosystem, null, 2)};
3696
3872
  `;
3697
3873
  }
3698
- function resolvePm2Bin() {
3699
- const require2 = createRequire(import.meta.url);
3700
- return require2.resolve("pm2/bin/pm2");
3701
- }
3702
3874
  var useNpmShell2 = process.platform === "win32";
3875
+ function runNpm2(args, options = {}) {
3876
+ return spawnSync2(useNpmShell2 ? "npm.cmd" : "npm", args, {
3877
+ ...options,
3878
+ shell: useNpmShell2
3879
+ });
3880
+ }
3881
+ function findGlobalPm2() {
3882
+ const whichResult = spawnSync2(useNpmShell2 ? "where" : "which", ["pm2"], {
3883
+ encoding: "utf8",
3884
+ shell: useNpmShell2,
3885
+ stdio: ["ignore", "pipe", "pipe"]
3886
+ });
3887
+ if (whichResult.status === 0) {
3888
+ const lines = whichResult.stdout?.toString().trim().split(/\r?\n/) ?? [];
3889
+ for (const line of lines) {
3890
+ const candidate = line.trim();
3891
+ if (candidate && existsSync14(candidate)) {
3892
+ return candidate;
3893
+ }
3894
+ }
3895
+ }
3896
+ const binResult = runNpm2(["bin", "-g"], {
3897
+ encoding: "utf8",
3898
+ stdio: ["ignore", "pipe", "pipe"]
3899
+ });
3900
+ if (binResult.status === 0) {
3901
+ const globalBin = binResult.stdout?.toString().trim();
3902
+ if (globalBin) {
3903
+ const candidate = join15(globalBin, useNpmShell2 ? "pm2.cmd" : "pm2");
3904
+ if (existsSync14(candidate)) {
3905
+ return candidate;
3906
+ }
3907
+ }
3908
+ }
3909
+ return null;
3910
+ }
3911
+ function installGlobalPm2() {
3912
+ console.log("[apm] \u672A\u68C0\u6D4B\u5230\u5168\u5C40 pm2\uFF0C\u6B63\u5728\u5B89\u88C5 npm install -g pm2 \u2026");
3913
+ const result = runNpm2(["install", "-g", "pm2"], {
3914
+ encoding: "utf8",
3915
+ stdio: "inherit"
3916
+ });
3917
+ if (result.status !== 0) {
3918
+ console.error("[apm] \u5B89\u88C5 pm2 \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: npm install -g pm2");
3919
+ process.exit(result.status ?? 1);
3920
+ }
3921
+ }
3922
+ function ensureGlobalPm2() {
3923
+ const existing = findGlobalPm2();
3924
+ if (existing) {
3925
+ return existing;
3926
+ }
3927
+ installGlobalPm2();
3928
+ const installed2 = findGlobalPm2();
3929
+ if (!installed2) {
3930
+ console.error(
3931
+ "[apm] \u5B89\u88C5 pm2 \u540E\u4ECD\u65E0\u6CD5\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: npm install -g pm2"
3932
+ );
3933
+ process.exit(1);
3934
+ }
3935
+ return installed2;
3936
+ }
3703
3937
  function resolveApmEntryPath(entryArg = process.argv[1]) {
3704
3938
  const fromArgv = entryArg?.trim();
3705
3939
  if (fromArgv && existsSync14(fromArgv)) {
@@ -3729,20 +3963,13 @@ function isRunningUnderPm2() {
3729
3963
  return process.env.name === PM2_APP_NAME && process.env.pm_id !== void 0;
3730
3964
  }
3731
3965
  function runPm2(args, options) {
3732
- let pm2Bin;
3733
- try {
3734
- pm2Bin = resolvePm2Bin();
3735
- } catch {
3736
- console.error(
3737
- "[apm] \u672A\u627E\u5230\u5185\u7F6E pm2\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5 apm CLI\uFF1Anpm install -g ai-project-manage-cli@latest"
3738
- );
3739
- process.exit(1);
3740
- }
3966
+ const pm2Bin = ensureGlobalPm2();
3741
3967
  const spawnOptions = {
3742
3968
  encoding: "utf8",
3743
- stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"]
3969
+ stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
3970
+ shell: useNpmShell2
3744
3971
  };
3745
- const result = spawnSync2(process.execPath, [pm2Bin, ...args], spawnOptions);
3972
+ const result = spawnSync2(pm2Bin, args, spawnOptions);
3746
3973
  if (result.error) {
3747
3974
  console.error("[apm] \u6267\u884C pm2 \u5931\u8D25:", result.error.message);
3748
3975
  process.exit(1);
@@ -3756,15 +3983,14 @@ function runPm2(args, options) {
3756
3983
  }
3757
3984
  }
3758
3985
  function runPm2Json(args) {
3759
- let pm2Bin;
3760
- try {
3761
- pm2Bin = resolvePm2Bin();
3762
- } catch {
3986
+ const pm2Bin = findGlobalPm2();
3987
+ if (!pm2Bin) {
3763
3988
  return "[]";
3764
3989
  }
3765
- const result = spawnSync2(process.execPath, [pm2Bin, ...args], {
3990
+ const result = spawnSync2(pm2Bin, args, {
3766
3991
  encoding: "utf8",
3767
- stdio: ["ignore", "pipe", "pipe"]
3992
+ stdio: ["ignore", "pipe", "pipe"],
3993
+ shell: useNpmShell2
3768
3994
  });
3769
3995
  if (result.status !== 0) return "[]";
3770
3996
  return result.stdout?.toString() ?? "[]";
@@ -3860,6 +4086,7 @@ async function prepareEcosystem(server) {
3860
4086
  }
3861
4087
  async function runDaemonStart(options) {
3862
4088
  assertConnectNotRunning();
4089
+ ensureGlobalPm2();
3863
4090
  await runUpdate();
3864
4091
  const cfg = await prepareEcosystem(options.server);
3865
4092
  runPm2(["start", PM2_ECOSYSTEM_PATH, "--update-env"], { inherit: true });
@@ -3878,10 +4105,44 @@ async function runDaemonStart(options) {
3878
4105
  console.log("[apm] \u505C\u6B62: apm daemon stop");
3879
4106
  }
3880
4107
  async function runDaemonStop() {
4108
+ pruneStaleConnectLock();
4109
+ if (!isPm2ConnectOnline()) {
4110
+ killConnectLockProcessIfAlive();
4111
+ forceReleaseConnectLock();
4112
+ console.log(`[apm] ${PM2_APP_NAME} \u672A\u5728\u8FD0\u884C`);
4113
+ return;
4114
+ }
3881
4115
  runPm2(["stop", PM2_APP_NAME], { inherit: true });
4116
+ for (let i = 0; i < 10; i += 1) {
4117
+ if (!isPm2ConnectOnline()) break;
4118
+ await delay(500);
4119
+ }
4120
+ if (isPm2ConnectOnline()) {
4121
+ console.log(`[apm] \u4F18\u96C5\u505C\u6B62\u8D85\u65F6\uFF0C\u5F3A\u5236\u79FB\u9664 ${PM2_APP_NAME}\u2026`);
4122
+ runPm2(["delete", PM2_APP_NAME], { inherit: true });
4123
+ }
4124
+ killConnectLockProcessIfAlive();
3882
4125
  forceReleaseConnectLock();
3883
4126
  console.log(`[apm] ${PM2_APP_NAME} \u5DF2\u505C\u6B62`);
3884
4127
  }
4128
+ function killConnectLockProcessIfAlive() {
4129
+ pruneStaleConnectLock();
4130
+ const lock = readConnectLock();
4131
+ if (!lock || !isProcessAlive(lock.pid)) return;
4132
+ try {
4133
+ process.kill(lock.pid, "SIGTERM");
4134
+ } catch {
4135
+ forceReleaseConnectLock();
4136
+ return;
4137
+ }
4138
+ if (isProcessAlive(lock.pid)) {
4139
+ try {
4140
+ process.kill(lock.pid, "SIGKILL");
4141
+ } catch {
4142
+ }
4143
+ }
4144
+ forceReleaseConnectLock();
4145
+ }
3885
4146
  async function runDaemonRestart(options) {
3886
4147
  pruneStaleConnectLock();
3887
4148
  const lock = readConnectLock();
@@ -3916,6 +4177,8 @@ async function runDaemonLogs(options) {
3916
4177
 
3917
4178
  // src/commands/connect.ts
3918
4179
  var HEARTBEAT_MS = 3e4;
4180
+ var RECONNECT_MIN_MS = 1e3;
4181
+ var RECONNECT_MAX_MS = 6e4;
3919
4182
  async function updateMessageStatus(cfg, messageId, status) {
3920
4183
  const api = createApmApiClient(cfg);
3921
4184
  await api.cli.updateMessageStatus({ id: messageId, status });
@@ -4068,6 +4331,17 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
4068
4331
  }
4069
4332
  }
4070
4333
  }
4334
+ function interruptibleSleep(ms, signal) {
4335
+ if (signal.aborted) return Promise.resolve();
4336
+ return new Promise((resolve5) => {
4337
+ const timer = setTimeout(resolve5, ms);
4338
+ const onAbort = () => {
4339
+ clearTimeout(timer);
4340
+ resolve5();
4341
+ };
4342
+ signal.addEventListener("abort", onAbort, { once: true });
4343
+ });
4344
+ }
4071
4345
  function startHeartbeat(ws, clientMachineId) {
4072
4346
  const send = () => {
4073
4347
  if (ws.readyState === WebSocket.OPEN) {
@@ -4083,182 +4357,274 @@ function startHeartbeat(ws, clientMachineId) {
4083
4357
  const timer = setInterval(send, HEARTBEAT_MS);
4084
4358
  return () => clearInterval(timer);
4085
4359
  }
4086
- async function runConnect(options) {
4087
- const { didUpdate } = await runUpdate();
4088
- if (didUpdate) {
4089
- await handleConnectAfterUpdate(options);
4090
- }
4091
- const cfg = await ensureLoggedConfig();
4092
- if (options.server?.trim()) {
4093
- cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4094
- }
4095
- const clientMachineId = resolveClientMachineId(cfg);
4096
- if (!clientMachineId) {
4097
- console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4098
- process.exit(1);
4099
- }
4100
- assertConnectNotRunning();
4101
- acquireConnectLock("foreground");
4102
- process.on("exit", releaseConnectLock);
4103
- const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4104
- console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4105
- await new Promise((resolve5, reject) => {
4106
- const ws = new WebSocket(url);
4107
- let stopHeartbeat;
4108
- let shuttingDown = false;
4109
- const shutdownAbort = new AbortController();
4110
- const runSlots = createRunSlotPool();
4111
- const activeTasks = /* @__PURE__ */ new Set();
4112
- const activeRuns = /* @__PURE__ */ new Map();
4113
- const pendingCancels = /* @__PURE__ */ new Set();
4114
- const shutdown = async (code = 0) => {
4115
- if (shuttingDown) return;
4116
- shuttingDown = true;
4117
- logAbortSignalStats(
4118
- shutdownAbort.signal,
4119
- "connect:shutdown-before-abort"
4120
- );
4121
- shutdownAbort.abort();
4122
- logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4123
- stopHeartbeat?.();
4124
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4125
- ws.terminate();
4126
- }
4127
- try {
4128
- await Promise.race([
4129
- Promise.all(activeTasks),
4130
- new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
4131
- ]);
4132
- } catch {
4133
- }
4134
- releaseConnectLock();
4135
- resolve5();
4136
- process.exit(code);
4137
- };
4138
- ws.on("open", () => {
4139
- console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4140
- stopHeartbeat = startHeartbeat(ws, clientMachineId);
4141
- });
4142
- ws.on("message", (data) => {
4143
- if (shuttingDown) return;
4144
- const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4145
- const parsed = parseAgentWsMessage(text);
4146
- if (parsed === null) {
4147
- console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4148
- return;
4149
- }
4150
- if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4151
- return;
4152
- }
4153
- const validated = validateAgentWsMessage(parsed, "outbound");
4154
- if (!validated.ok) {
4155
- console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4156
- return;
4157
- }
4158
- if (validated.data.type === "cancel") {
4159
- const { messageId } = validated.data;
4160
- pendingCancels.add(messageId);
4161
- activeRuns.get(messageId)?.abort();
4162
- return;
4163
- }
4164
- if (validated.data.type === "deploy") {
4165
- const msg2 = validated.data;
4166
- const perDeployController = new AbortController();
4167
- const signal2 = AbortSignal.any([
4168
- shutdownAbort.signal,
4169
- perDeployController.signal
4170
- ]);
4171
- const task2 = (async () => {
4172
- await runSlots.acquire();
4173
- try {
4174
- await handleInboundDeploy(cfg, msg2, signal2);
4175
- } finally {
4176
- runSlots.release();
4177
- }
4178
- })();
4179
- activeTasks.add(task2);
4180
- void task2.finally(() => {
4181
- activeTasks.delete(task2);
4182
- });
4183
- return;
4184
- }
4185
- if (validated.data.type !== "message") {
4186
- return;
4187
- }
4188
- const msg = validated.data;
4189
- const perMessageController = new AbortController();
4190
- activeRuns.set(msg.messageId, perMessageController);
4191
- if (pendingCancels.has(msg.messageId)) {
4192
- activeRuns.delete(msg.messageId);
4193
- pendingCancels.delete(msg.messageId);
4194
- return;
4195
- }
4196
- const signal = AbortSignal.any([
4360
+ function attachWsHandlers(ws, ctx, onOpen) {
4361
+ const {
4362
+ cfg,
4363
+ clientMachineId,
4364
+ shutdownAbort,
4365
+ runSlots,
4366
+ activeTasks,
4367
+ activeRuns,
4368
+ pendingCancels,
4369
+ isShuttingDown
4370
+ } = ctx;
4371
+ ws.on("open", () => {
4372
+ console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4373
+ onOpen();
4374
+ });
4375
+ ws.on("message", (data) => {
4376
+ if (isShuttingDown()) return;
4377
+ const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4378
+ const parsed = parseAgentWsMessage(text);
4379
+ if (parsed === null) {
4380
+ console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4381
+ return;
4382
+ }
4383
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4384
+ return;
4385
+ }
4386
+ const validated = validateAgentWsMessage(parsed, "outbound");
4387
+ if (!validated.ok) {
4388
+ console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4389
+ return;
4390
+ }
4391
+ if (validated.data.type === "cancel") {
4392
+ const { messageId } = validated.data;
4393
+ pendingCancels.add(messageId);
4394
+ activeRuns.get(messageId)?.abort();
4395
+ return;
4396
+ }
4397
+ if (validated.data.type === "deploy") {
4398
+ const msg2 = validated.data;
4399
+ const perDeployController = new AbortController();
4400
+ const signal2 = AbortSignal.any([
4197
4401
  shutdownAbort.signal,
4198
- perMessageController.signal
4402
+ perDeployController.signal
4199
4403
  ]);
4200
- const ctx = {
4201
- shutdownSignal: shutdownAbort.signal,
4202
- perMessageSignal: perMessageController.signal
4203
- };
4204
- const task = (async () => {
4404
+ const task2 = (async () => {
4205
4405
  await runSlots.acquire();
4206
4406
  try {
4207
- if (signal.aborted || isUserCancelled(ctx)) return;
4208
- try {
4209
- await updateMessageStatus(cfg, msg.messageId, "TYPING");
4210
- } catch (typingErr) {
4211
- if (isUserCancelled(ctx)) return;
4212
- console.error(
4213
- "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4214
- typingErr instanceof Error ? typingErr.message : typingErr
4215
- );
4216
- try {
4217
- await setMessageError(
4218
- cfg,
4219
- msg.messageId,
4220
- typingErr instanceof Error ? typingErr.message : String(typingErr)
4221
- );
4222
- await updateMessageStatus(cfg, msg.messageId, "FAILED");
4223
- } catch (statusErr) {
4224
- console.error(
4225
- "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4226
- statusErr instanceof Error ? statusErr.message : statusErr
4227
- );
4228
- }
4229
- return;
4230
- }
4231
- await handleInboundMessage(cfg, msg, signal, ctx);
4407
+ await handleInboundDeploy(cfg, msg2, signal2);
4232
4408
  } finally {
4233
4409
  runSlots.release();
4234
- activeRuns.delete(msg.messageId);
4235
- pendingCancels.delete(msg.messageId);
4236
4410
  }
4237
4411
  })();
4238
- activeTasks.add(task);
4239
- void task.finally(() => {
4240
- activeTasks.delete(task);
4412
+ activeTasks.add(task2);
4413
+ void task2.finally(() => {
4414
+ activeTasks.delete(task2);
4241
4415
  });
4416
+ return;
4417
+ }
4418
+ if (validated.data.type !== "message") {
4419
+ return;
4420
+ }
4421
+ const msg = validated.data;
4422
+ const perMessageController = new AbortController();
4423
+ activeRuns.set(msg.messageId, perMessageController);
4424
+ if (pendingCancels.has(msg.messageId)) {
4425
+ activeRuns.delete(msg.messageId);
4426
+ pendingCancels.delete(msg.messageId);
4427
+ return;
4428
+ }
4429
+ const signal = AbortSignal.any([
4430
+ shutdownAbort.signal,
4431
+ perMessageController.signal
4432
+ ]);
4433
+ const messageCtx = {
4434
+ shutdownSignal: shutdownAbort.signal,
4435
+ perMessageSignal: perMessageController.signal
4436
+ };
4437
+ const task = (async () => {
4438
+ await runSlots.acquire();
4439
+ try {
4440
+ if (signal.aborted || isUserCancelled(messageCtx)) return;
4441
+ try {
4442
+ await updateMessageStatus(cfg, msg.messageId, "TYPING");
4443
+ } catch (typingErr) {
4444
+ if (isUserCancelled(messageCtx)) return;
4445
+ console.error(
4446
+ "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4447
+ typingErr instanceof Error ? typingErr.message : typingErr
4448
+ );
4449
+ try {
4450
+ await setMessageError(
4451
+ cfg,
4452
+ msg.messageId,
4453
+ typingErr instanceof Error ? typingErr.message : String(typingErr)
4454
+ );
4455
+ await updateMessageStatus(cfg, msg.messageId, "FAILED");
4456
+ } catch (statusErr) {
4457
+ console.error(
4458
+ "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4459
+ statusErr instanceof Error ? statusErr.message : statusErr
4460
+ );
4461
+ }
4462
+ return;
4463
+ }
4464
+ await handleInboundMessage(cfg, msg, signal, messageCtx);
4465
+ } finally {
4466
+ runSlots.release();
4467
+ activeRuns.delete(msg.messageId);
4468
+ pendingCancels.delete(msg.messageId);
4469
+ }
4470
+ })();
4471
+ activeTasks.add(task);
4472
+ void task.finally(() => {
4473
+ activeTasks.delete(task);
4474
+ });
4475
+ });
4476
+ }
4477
+ function connectOnce(url, ctx, connectionAbort, onConnected) {
4478
+ return new Promise((resolve5, reject) => {
4479
+ const ws = new WebSocket(url);
4480
+ let stopHeartbeat;
4481
+ let settled = false;
4482
+ const finish = (fn) => {
4483
+ if (settled) return;
4484
+ settled = true;
4485
+ connectionAbort.removeEventListener("abort", onConnectionAbort);
4486
+ stopHeartbeat?.();
4487
+ fn();
4488
+ };
4489
+ const onConnectionAbort = () => {
4490
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4491
+ ws.terminate();
4492
+ }
4493
+ };
4494
+ connectionAbort.addEventListener("abort", onConnectionAbort);
4495
+ attachWsHandlers(ws, ctx, () => {
4496
+ stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
4497
+ onConnected();
4242
4498
  });
4243
4499
  ws.on("close", (code, reason) => {
4244
4500
  console.log(
4245
4501
  `[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
4246
4502
  );
4247
- void shutdown();
4503
+ if (ctx.isShuttingDown()) {
4504
+ finish(() => reject(new Error("shutdown")));
4505
+ return;
4506
+ }
4507
+ finish(resolve5);
4248
4508
  });
4249
4509
  ws.on("error", (err) => {
4250
4510
  console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
4251
- reject(err);
4252
- });
4253
- process.on("SIGINT", () => {
4254
- console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4255
- void shutdown();
4256
- });
4257
- process.on("SIGTERM", () => {
4258
- void shutdown();
4511
+ if (ctx.isShuttingDown()) {
4512
+ finish(() => reject(new Error("shutdown")));
4513
+ return;
4514
+ }
4515
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4516
+ ws.terminate();
4517
+ }
4259
4518
  });
4260
4519
  });
4261
4520
  }
4521
+ async function runConnect(options) {
4522
+ const { didUpdate } = await runUpdate();
4523
+ if (didUpdate) {
4524
+ await handleConnectAfterUpdate(options);
4525
+ }
4526
+ const cfg = await ensureLoggedConfig();
4527
+ if (options.server?.trim()) {
4528
+ cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4529
+ }
4530
+ const clientMachineId = resolveClientMachineId(cfg);
4531
+ if (!clientMachineId) {
4532
+ console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4533
+ process.exit(1);
4534
+ }
4535
+ assertConnectNotRunning();
4536
+ const lockMode = isRunningUnderPm2() ? "pm2" : "foreground";
4537
+ acquireConnectLock(lockMode);
4538
+ process.on("exit", releaseConnectLock);
4539
+ let shuttingDown = false;
4540
+ let shutdownPromise = null;
4541
+ const lifecycleAbort = new AbortController();
4542
+ const shutdownAbort = new AbortController();
4543
+ const runSlots = createRunSlotPool();
4544
+ const activeTasks = /* @__PURE__ */ new Set();
4545
+ const activeRuns = /* @__PURE__ */ new Map();
4546
+ const pendingCancels = /* @__PURE__ */ new Set();
4547
+ let currentConnectionAbort = null;
4548
+ const sessionCtx = {
4549
+ cfg,
4550
+ clientMachineId,
4551
+ shutdownAbort,
4552
+ runSlots,
4553
+ activeTasks,
4554
+ activeRuns,
4555
+ pendingCancels,
4556
+ isShuttingDown: () => shuttingDown
4557
+ };
4558
+ const drainAndExit = async (code = 0) => {
4559
+ const drainMs = isRunningUnderPm2() ? 500 : SHUTDOWN_DRAIN_MS;
4560
+ lifecycleAbort.abort();
4561
+ currentConnectionAbort?.abort();
4562
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-before-abort");
4563
+ shutdownAbort.abort();
4564
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4565
+ try {
4566
+ await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
4567
+ } catch {
4568
+ }
4569
+ releaseConnectLock();
4570
+ process.exit(code);
4571
+ };
4572
+ const shutdown = (code = 0) => {
4573
+ if (shuttingDown) return shutdownPromise ?? Promise.resolve();
4574
+ shuttingDown = true;
4575
+ shutdownPromise = drainAndExit(code);
4576
+ return shutdownPromise;
4577
+ };
4578
+ const onStopSignal = (code = 0) => {
4579
+ console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4580
+ void shutdown(code);
4581
+ };
4582
+ process.on("SIGINT", () => onStopSignal(0));
4583
+ process.on("SIGTERM", () => onStopSignal(0));
4584
+ if (isRunningUnderPm2()) {
4585
+ process.on("message", (msg) => {
4586
+ if (msg === "shutdown") onStopSignal(0);
4587
+ });
4588
+ }
4589
+ let reconnectDelay = RECONNECT_MIN_MS;
4590
+ while (!shuttingDown) {
4591
+ currentConnectionAbort = new AbortController();
4592
+ const connectionSignal = AbortSignal.any([
4593
+ lifecycleAbort.signal,
4594
+ currentConnectionAbort.signal
4595
+ ]);
4596
+ const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4597
+ console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4598
+ try {
4599
+ await connectOnce(url, sessionCtx, connectionSignal, () => {
4600
+ reconnectDelay = RECONNECT_MIN_MS;
4601
+ });
4602
+ if (shuttingDown) break;
4603
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4604
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4605
+ if (shuttingDown) break;
4606
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4607
+ } catch (err) {
4608
+ if (shuttingDown) break;
4609
+ const detail = err instanceof Error ? err.message : String(err);
4610
+ if (detail === "shutdown") break;
4611
+ console.error("[apm] \u8FDE\u63A5\u5931\u8D25:", detail);
4612
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4613
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4614
+ if (shuttingDown) break;
4615
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4616
+ } finally {
4617
+ currentConnectionAbort = null;
4618
+ }
4619
+ }
4620
+ if (!shuttingDown) {
4621
+ void shutdown(0);
4622
+ return;
4623
+ }
4624
+ if (shutdownPromise) {
4625
+ await shutdownPromise;
4626
+ }
4627
+ }
4262
4628
 
4263
4629
  // src/commands/create-pr-errors.ts
4264
4630
  import { ApiError as ApiError2 } from "listpage-http";
@@ -4391,7 +4757,7 @@ import { spawnSync as spawnSync5 } from "node:child_process";
4391
4757
  import path5 from "node:path";
4392
4758
 
4393
4759
  // src/commands/deploy/internal/apm-config.ts
4394
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
4760
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
4395
4761
  import { homedir as homedir2 } from "node:os";
4396
4762
  import { join as join16, resolve as resolve4 } from "node:path";
4397
4763
  function loadApmConfig(options) {
@@ -4404,7 +4770,7 @@ function loadApmConfig(options) {
4404
4770
  process.exit(1);
4405
4771
  }
4406
4772
  try {
4407
- const raw = readFileSync12(p, "utf8");
4773
+ const raw = readFileSync13(p, "utf8");
4408
4774
  return JSON.parse(raw);
4409
4775
  } catch (e) {
4410
4776
  console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
@@ -4593,7 +4959,7 @@ function readMavenLocalRepoFromSettings() {
4593
4959
  return null;
4594
4960
  }
4595
4961
  try {
4596
- const xml = readFileSync12(settingsPath, "utf8");
4962
+ const xml = readFileSync13(settingsPath, "utf8");
4597
4963
  const match = xml.match(
4598
4964
  /<localRepository>\s*([^<]+?)\s*<\/localRepository>/
4599
4965
  );
@@ -4735,7 +5101,7 @@ var DeployExecutionError = class extends Error {
4735
5101
  };
4736
5102
 
4737
5103
  // src/commands/deploy/internal/wisdom-auto-deploy.ts
4738
- import { existsSync as existsSync17, readFileSync as readFileSync14, statSync as statSync7 } from "node:fs";
5104
+ import { existsSync as existsSync17, readFileSync as readFileSync15, statSync as statSync7 } from "node:fs";
4739
5105
  import path4 from "node:path";
4740
5106
  import { spawnSync as spawnSync4 } from "node:child_process";
4741
5107
 
@@ -4744,7 +5110,7 @@ import {
4744
5110
  existsSync as existsSync16,
4745
5111
  mkdirSync as mkdirSync8,
4746
5112
  readdirSync as readdirSync5,
4747
- readFileSync as readFileSync13,
5113
+ readFileSync as readFileSync14,
4748
5114
  statSync as statSync6,
4749
5115
  writeFileSync as writeFileSync14
4750
5116
  } from "node:fs";
@@ -4788,6 +5154,56 @@ async function zipDirectory(distDir, zipPath) {
4788
5154
  console.error(`\u5DF2\u751F\u6210: ${zipPath} (${sizeMb} MB)`);
4789
5155
  return content.length;
4790
5156
  }
5157
+ var SFTP_UPLOAD_MAX_ATTEMPTS = 3;
5158
+ var SFTP_FAST_PUT_OPTIONS = {
5159
+ chunkSize: 64 * 1024,
5160
+ concurrency: 4
5161
+ };
5162
+ function buildSftpConnectOptions(settings) {
5163
+ return {
5164
+ host: settings.host,
5165
+ port: settings.port,
5166
+ username: settings.username,
5167
+ password: settings.password,
5168
+ readyTimeout: 3e4,
5169
+ tryKeyboard: true,
5170
+ keepaliveInterval: 1e4,
5171
+ keepaliveCountMax: 3
5172
+ };
5173
+ }
5174
+ async function sleep(ms) {
5175
+ await new Promise((resolve5) => setTimeout(resolve5, ms));
5176
+ }
5177
+ async function uploadZipWithRetry(settings, localZip, remoteZipPath) {
5178
+ let lastError;
5179
+ for (let attempt = 1; attempt <= SFTP_UPLOAD_MAX_ATTEMPTS; attempt++) {
5180
+ const sftp = new SftpClient();
5181
+ try {
5182
+ if (attempt > 1) {
5183
+ console.error(
5184
+ `SFTP \u4E0A\u4F20\u91CD\u8BD5 (${attempt}/${SFTP_UPLOAD_MAX_ATTEMPTS})...`
5185
+ );
5186
+ }
5187
+ await sftp.connect(buildSftpConnectOptions(settings));
5188
+ await ensureRemoteDir(sftp, settings.remotePath);
5189
+ await sftp.fastPut(localZip, remoteZipPath, SFTP_FAST_PUT_OPTIONS);
5190
+ return sftp;
5191
+ } catch (err) {
5192
+ lastError = err;
5193
+ try {
5194
+ await sftp.end();
5195
+ } catch {
5196
+ }
5197
+ if (attempt < SFTP_UPLOAD_MAX_ATTEMPTS) {
5198
+ const message = err instanceof Error ? err.message : String(err);
5199
+ const delaySec = attempt * 2;
5200
+ console.error(`SFTP \u4E0A\u4F20\u5931\u8D25 (${message})\uFF0C${delaySec}s \u540E\u91CD\u8BD5...`);
5201
+ await sleep(delaySec * 1e3);
5202
+ }
5203
+ }
5204
+ }
5205
+ throw lastError;
5206
+ }
4791
5207
  async function ensureRemoteDir(sftp, dir) {
4792
5208
  const parts = dir.replace(/\\/g, "/").split("/").filter(Boolean);
4793
5209
  let current = dir.startsWith("/") ? "" : ".";
@@ -4847,18 +5263,8 @@ async function uploadAndMaybeExtract(settings, localZip, extract) {
4847
5263
  console.error(
4848
5264
  `\u8FDE\u63A5 ${settings.username}@${settings.host}:${settings.port} ...`
4849
5265
  );
4850
- const sftp = new SftpClient();
5266
+ const sftp = await uploadZipWithRetry(settings, localZip, remoteZipPath);
4851
5267
  try {
4852
- await sftp.connect({
4853
- host: settings.host,
4854
- port: settings.port,
4855
- username: settings.username,
4856
- password: settings.password,
4857
- readyTimeout: 2e4,
4858
- tryKeyboard: true
4859
- });
4860
- await ensureRemoteDir(sftp, settings.remotePath);
4861
- await sftp.put(localZip, remoteZipPath);
4862
5268
  console.error(` \u2713 ${localZip} -> ${remoteZipPath}`);
4863
5269
  if (extract) {
4864
5270
  const target = settings.remotePath.replace(/\/$/, "");
@@ -4974,7 +5380,7 @@ function loadManifest4() {
4974
5380
  if (!existsSync16(manifestPath2)) {
4975
5381
  return {};
4976
5382
  }
4977
- return JSON.parse(readFileSync13(manifestPath2, "utf8"));
5383
+ return JSON.parse(readFileSync14(manifestPath2, "utf8"));
4978
5384
  }
4979
5385
  function saveManifest4(manifest) {
4980
5386
  const dir = deployCacheDir();
@@ -5040,7 +5446,7 @@ async function createUpdatePackage(entries, packageName) {
5040
5446
  log(`\u521B\u5EFA\u66F4\u65B0\u5305: ${path3.basename(zipPath)}\uFF08${entries.length} \u4E2A\u6587\u4EF6\uFF09`);
5041
5447
  const zip = new JSZip2();
5042
5448
  for (const entry of entries) {
5043
- const content = readFileSync13(entry.path);
5449
+ const content = readFileSync14(entry.path);
5044
5450
  zip.file(entry.arcname, content);
5045
5451
  log(` \u6253\u5305: ${entry.arcname} (${entry.reason})`);
5046
5452
  }
@@ -5606,7 +6012,7 @@ function readPackageScripts(cwd) {
5606
6012
  return {};
5607
6013
  }
5608
6014
  try {
5609
- const raw = readFileSync14(pkgPath, "utf8");
6015
+ const raw = readFileSync15(pkgPath, "utf8");
5610
6016
  const parsed = JSON.parse(raw);
5611
6017
  return parsed.scripts ?? {};
5612
6018
  } catch {
@@ -6130,7 +6536,7 @@ import path9 from "node:path";
6130
6536
  import Docker from "dockerode";
6131
6537
 
6132
6538
  // src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
6133
- import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
6539
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
6134
6540
  import path6 from "node:path";
6135
6541
  function asOptionalTlsBuffer(value) {
6136
6542
  if (typeof value !== "string") {
@@ -6143,7 +6549,7 @@ function asOptionalTlsBuffer(value) {
6143
6549
  return void 0;
6144
6550
  }
6145
6551
  if (existsSync18(normalized)) {
6146
- return readFileSync15(normalized);
6552
+ return readFileSync16(normalized);
6147
6553
  }
6148
6554
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
6149
6555
  if (looksLikePath) {
@@ -6353,7 +6759,7 @@ var DockerodeClient = class {
6353
6759
  var createDockerodeClient = (config) => new DockerodeClient(config);
6354
6760
 
6355
6761
  // src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
6356
- import { existsSync as existsSync19, readFileSync as readFileSync16, statSync as statSync8 } from "node:fs";
6762
+ import { existsSync as existsSync19, readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
6357
6763
  import path7 from "node:path";
6358
6764
  function stripSurroundingQuotes(value) {
6359
6765
  const t = value.trim();
@@ -6373,7 +6779,7 @@ function loadEnvFromFile(envFilePath) {
6373
6779
  if (!existsSync19(targetPath) || !statSync8(targetPath).isFile()) {
6374
6780
  return {};
6375
6781
  }
6376
- const raw = readFileSync16(targetPath, "utf-8");
6782
+ const raw = readFileSync17(targetPath, "utf-8");
6377
6783
  const result = {};
6378
6784
  for (const line of raw.split(/\r?\n/)) {
6379
6785
  const normalized = line.trim();
@@ -7034,7 +7440,7 @@ function buildProgram() {
7034
7440
  "\u8FDE\u63A5\u5E73\u53F0 WebSocket\uFF08/ws/agent\uFF09\uFF0C\u7EF4\u6301\u5FC3\u8DF3\u5E76\u5904\u7406\u4E0B\u884C message\uFF08TYPING \u2192 Cursor \u2192 SUCCESS/FAILED\uFF09\uFF1B\u542F\u52A8\u524D\u81EA\u52A8 apm update \u5230\u5F53\u524D\u5927\u7248\u672C\u6700\u65B0\u7248"
7035
7441
  ).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").option(
7036
7442
  "--daemon",
7037
- "\u540E\u53F0\u5B88\u62A4\u8FD0\u884C\uFF08\u5185\u7F6E PM2\uFF0C\u65AD\u7EBF\u6216\u9000\u51FA\u540E\u81EA\u52A8\u91CD\u542F\uFF1B\u63A8\u8350\u7528\u4E8E\u957F\u671F\u5728\u7EBF\uFF09"
7443
+ "\u540E\u53F0\u5B88\u62A4\u8FD0\u884C\uFF08\u5168\u5C40 PM2\uFF0C\u65AD\u7EBF\u6216\u9000\u51FA\u540E\u81EA\u52A8\u91CD\u542F\uFF1B\u63A8\u8350\u7528\u4E8E\u957F\u671F\u5728\u7EBF\uFF09"
7038
7444
  ).option("-f, --follow", "\u4E0E --daemon \u5408\u7528\uFF1A\u542F\u52A8\u540E\u5728\u5F53\u524D\u7EC8\u7AEF\u5B9E\u65F6\u8DDF\u8E2A\u65E5\u5FD7").action(
7039
7445
  async (opts) => {
7040
7446
  if (opts.follow && !opts.daemon) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.93",
3
+ "version": "6.0.95",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -44,7 +44,6 @@
44
44
  "dockerode": "~5.0.0",
45
45
  "ssh2": "~1.16.0",
46
46
  "ssh2-sftp-client": "~12.0.1",
47
- "jszip": "~3.10.1",
48
- "pm2": "~5.4.3"
47
+ "jszip": "~3.10.1"
49
48
  }
50
49
  }
@@ -0,0 +1,78 @@
1
+ # apm-write-assumptions:编写并同步待确认口径
2
+
3
+ ## 何时使用
4
+
5
+ `apm-write-plan` 步骤 3 编写 `*-ASSUMPTIONS.md` 时;或需重写/补写假设文档时。
6
+
7
+ | 角色 | 产出 |
8
+ | ---- | ------------------------------ |
9
+ | 前端 | `docs/FRONTEND-ASSUMPTIONS.md` |
10
+ | 后端 | `docs/BACKEND-ASSUMPTIONS.md` |
11
+
12
+ ---
13
+
14
+ ## 工作流程
15
+
16
+ ### 1. 按模板编写
17
+
18
+ 1. **Read** `.apm/skills/apm-write-assumptions/assumptions-template.md`
19
+ 2. **Write** 本端 `docs/*-ASSUMPTIONS.md`(写作时对照下方 **PM 校验规则**)
20
+ 3. 无假设时正文仅一行:`无,口径均有依据。`
21
+
22
+ ### 2. 直接 sync
23
+
24
+ ```bash
25
+ apm sync-document <sessionId> --file FRONTEND-ASSUMPTIONS.md
26
+ # 或 BACKEND-ASSUMPTIONS.md
27
+ ```
28
+
29
+ **成功**须看到:
30
+
31
+ ```text
32
+ [apm] 假设解析: 标记 N 条,入库 N 条(PM 可确认 N 条)
33
+ ```
34
+
35
+ 无假设时无 `假设解析` 行,属正常。
36
+
37
+ ### 3. 报错则改完重 sync
38
+
39
+ `sync-document` 失败时会打印具体假设编号与原因(如 `假设 A2 未通过 PM 校验: …`)。
40
+
41
+ 1. 按报错修正对应 `A*` 的 **问 / 场景 / 选项**(规则见下)
42
+ 2. 再次执行步骤 2
43
+ 3. **禁止**在未成功 sync 前 @ 项目经理、sync PLAN、或声称「请在 Web 待确认」
44
+
45
+ ---
46
+
47
+ ## PM 校验规则(与服务端一致)
48
+
49
+ 每条假设(`#### A1` …)必须包含:
50
+
51
+ | 字段 | 要求 |
52
+ | ------------ | --------------------------------------------------------------- |
53
+ | **问** | 业务语言;≥6 字;含下列业务动词之一;禁止字段名/路径/SQL/反引号 |
54
+ | **场景** | 非空 |
55
+ | **选项** | ≥2 项,格式 `- opt_id:说明` |
56
+ | **研发备注** | 可选;技术细节放这里,**不要**写进「问」 |
57
+
58
+ **问句须含以下动词之一**:
59
+
60
+ `提交` `展示` `校验` `驳回` `审批` `允许` `禁止` `筛选` `列表` `详情` `暂存` `删除` `新增` `修改` `导出` `导入`
61
+
62
+ **问句禁止**:`status=`、文件扩展名、反引号代码、SQL、`类.字段`、snake_case 字段名。
63
+
64
+ ### 正反例
65
+
66
+ | 假设 | 问句 | 结果 |
67
+ | ------- | ---------------------------------------------- | ---------------------- |
68
+ | A2 | 编辑已有记录时,「落实人员」应如何处理? | **失败**(无业务动词) |
69
+ | A2 修正 | 编辑已有记录时,是否**允许修改**「落实人员」? | 通过 |
70
+ | A3 | 「落实人员」可选择的范围是? | **失败** |
71
+ | A3 修正 | 「落实人员」下拉**列表**应**展示**哪些人员? | 通过 |
72
+
73
+ ---
74
+
75
+ ## 与其他技能
76
+
77
+ - 由 **`apm-write-plan`** 在 sync PLAN 前调用本技能
78
+ - PM 确认后由 **`apm-update-plan`** 更新 PLAN
@@ -2,9 +2,11 @@
2
2
 
3
3
  > 本文件由 apm sync-document 写入;PM 确认后服务端回写「状态/确认结果」。
4
4
  > **禁止**手动编辑;已确认条目 AI 禁止修改。
5
+ > 编写与校验规则见 `.apm/skills/apm-write-assumptions/SKILL.md`。
5
6
 
6
7
  #### A1
7
- - **问**:(业务语言问句,禁止字段名/文件路径)
8
+
9
+ - **问**:(业务语言问句;须含:提交/展示/校验/驳回/审批/允许/禁止/筛选/列表/详情/暂存/删除/新增/修改/导出/导入 之一)
8
10
  - **场景**:(需求未说明的背景)
9
11
  - **选项**:
10
12
  - opt_a:(选项说明)
@@ -4,11 +4,11 @@
4
4
 
5
5
  前端 / 后端工程师在任务启动后**直接读原始需求写实现计划**,不经过 PRD。
6
6
 
7
- | 角色 | 产出文档 | 路径 |
8
- | ---- | -------- | ---- |
9
- | 后端 | `BACKEND-PLAN.md` + `API.md` | `docs/BACKEND-PLAN.md`、`docs/API.md` |
7
+ | 角色 | 产出文档 | 路径 |
8
+ | ---- | ---------------------------------------------- | ------------------------------------------------------- |
9
+ | 后端 | `BACKEND-PLAN.md` + `API.md` | `docs/BACKEND-PLAN.md`、`docs/API.md` |
10
10
  | 前端 | `FRONTEND-PLAN.md` + `FRONTEND-ASSUMPTIONS.md` | `docs/FRONTEND-PLAN.md`、`docs/FRONTEND-ASSUMPTIONS.md` |
11
- | 后端 | 同上 | `docs/BACKEND-ASSUMPTIONS.md` |
11
+ | 后端 | 同上 | `docs/BACKEND-ASSUMPTIONS.md` |
12
12
 
13
13
  ---
14
14
 
@@ -27,27 +27,18 @@
27
27
  2. 代码调研预算:**最多 15 个文件**。
28
28
  3. 口径不清 → 记入 **ASSUMPTIONS 文件**,禁止编造。
29
29
 
30
- ### 步骤 3:按模板写计划与假设
30
+ ### 步骤 3:写计划与假设
31
31
 
32
32
  1. **Read** `.apm/skills/apm-write-plan/plan-template.md`,**Write** 本端 `*-PLAN.md`(**不含**假设章节)。
33
- 2. **Read** `.apm/skills/apm-write-plan/assumptions-template.md`,**Write** 本端 `*-ASSUMPTIONS.md`。
34
- 3. 无假设时在 ASSUMPTIONS 中写「无,口径均有依据」。
33
+ 2. **Read** 并完整执行 **`.apm/skills/apm-write-assumptions/SKILL.md`**(编写 `*-ASSUMPTIONS.md` → sync;报错则改完重 sync)。
35
34
 
36
- ### 步骤 3.5:假设 PM 友好自检(sync 前必做)
35
+ ### 步骤 4:同步 PLAN 与回复
37
36
 
38
- 每条假设必须含 **问 / 场景 / 选项(≥2 项,id + label)**:
37
+ 1. `apm sync-document --file FRONTEND-PLAN.md`(或 BACKEND)。
38
+ 2. **有假设且 ASSUMPTIONS 已成功 sync**:@项目经理 请其在 Web **「待您确认」** Panel 点选;说明「以上假设确认前不开始开发」。
39
+ 3. **无假设**:`append_message` 发送「本端需求理解」摘要并 @项目经理 请确认。
39
40
 
40
- - **问** 禁止:`status=`、字段名、表名、类路径、SQL。
41
- - **禁止多选**;复合口径由 PM 在 Web「其他」中填写。
42
- - 能自己查清的代码含义 → 移入 PLAN「依据」,删除假设。
43
- - 技术发现 → 写进 **研发备注**,问句用业务语言。
44
-
45
- ### 步骤 4:同步与回复
46
-
47
- 1. `apm sync-document --file FRONTEND-ASSUMPTIONS.md`(或 BACKEND)**先于** PLAN。
48
- 2. `apm sync-document --file FRONTEND-PLAN.md`(或 BACKEND)。
49
- 3. **有假设**:@项目经理 请其在 Web **「待您确认」** Panel 点选;说明「以上假设确认前不开始开发」。
50
- 4. **无假设**:`append_message` 发送「本端需求理解」摘要并 @项目经理 请确认。
41
+ > **禁止**在 ASSUMPTIONS sync 未成功时 @ 项目经理确认假设;PLAN 中「待确认 N 项」由服务端 sync ASSUMPTIONS 后自动更新,AI 不要手写具体数字。
51
42
 
52
43
  ### 假设确认后更新计划
53
44
 
@@ -1,26 +0,0 @@
1
- ## 工作流程
2
-
3
- ### 步骤 1: 获取当前版本 PRD 的内容
4
-
5
- 先用 **Read** 工具阅读 `.apm/sessions/<会话ID>/docs/PRD.md`,如果存在则这个目录下的内容为需求文档,否则用 **Read** 工具阅读 `.apm/sessions/<会话ID>/TASK.md`,并把这个文件里面的内容视为需求文档
6
-
7
- ### 步骤 2: 理解代码,给出评审意见,具体要求如下:
8
-
9
- #### 评审目标如下:
10
-
11
- 1. 梳理需求边界:发现文档中未写清的口径
12
- 2. 调研实现难度:在实现难度较高时给出提示
13
- 3. 禁止为了凑问题而提问,当 PRD 已足够清晰且无高难度改造时,可直接说此任务没问题
14
- 4. 你只需要站在自己的角度去评审,禁止站在全局思考问题。结合代码进行评审即可:
15
- - 前端关注 UI 交互还原,交互逻辑的各种边界,不考虑一些与后端接口之类的问题
16
- - 后端则关注数据表设计,需求对应的数据接口是否合理,能否获取
17
- - 全栈则关注前面两者,可以把这些问题合并起来提出来
18
-
19
- **难度高的定义**:预估改动的文件可能比较多,超过 10 个;或者改动逻辑比较复杂;
20
-
21
- #### 表达规范如下
22
-
23
- 1. **产品语言优先**:页面定位用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体。
24
- 2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
25
- 3. **锚定 PRD**:当需要引用到任务文档中的某一条时可以直接说行号,比如:需求原文[1-3]说 xxx;
26
- 4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点。已写清楚的规则不要重复质疑。
@@ -1,12 +0,0 @@
1
- # apm-rewrite-assumptions:重写 PM 不友好的假设问法
2
-
3
- ## 何时使用
4
-
5
- `apm sync-document` 同步 ASSUMPTIONS 失败(问法校验不通过),或计划被退回需重写「问 / 场景 / 选项」。
6
-
7
- ## 工作流程
8
-
9
- 1. **Read** 本端 `docs/FRONTEND-ASSUMPTIONS.md` 或 `docs/BACKEND-ASSUMPTIONS.md`(及 sync 报错中的 qualityIssues)。
10
- 2. 将研发术语移入 **研发备注**;**问** 改为 PM 可理解的业务句子;补全 **场景** 与 **≥2 个选项**(含 id + label)。
11
- 3. **禁止多选**;复合口径说明写在 Other 指引中。
12
- 4. 重新 **`apm sync-document --file *-ASSUMPTIONS.md`** 同步。