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 +1 -1
- package/dist/index.js +616 -210
- package/package.json +2 -3
- package/template/skills/apm-write-assumptions/SKILL.md +78 -0
- package/template/skills/{apm-write-plan → apm-write-assumptions}/assumptions-template.md +3 -1
- package/template/skills/apm-write-plan/SKILL.md +11 -20
- package/template/skills/apm-review/SKILL.md +0 -26
- package/template/skills/apm-rewrite-assumptions/SKILL.md +0 -12
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ apm daemon start
|
|
|
55
55
|
apm daemon start -f
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
PM2
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3760
|
-
|
|
3761
|
-
pm2Bin = resolvePm2Bin();
|
|
3762
|
-
} catch {
|
|
3986
|
+
const pm2Bin = findGlobalPm2();
|
|
3987
|
+
if (!pm2Bin) {
|
|
3763
3988
|
return "[]";
|
|
3764
3989
|
}
|
|
3765
|
-
const result = spawnSync2(
|
|
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
|
-
|
|
4087
|
-
const {
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
const
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
);
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
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
|
-
|
|
4402
|
+
perDeployController.signal
|
|
4199
4403
|
]);
|
|
4200
|
-
const
|
|
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
|
-
|
|
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(
|
|
4239
|
-
void
|
|
4240
|
-
activeTasks.delete(
|
|
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
|
-
|
|
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
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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\
|
|
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.
|
|
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`
|
|
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
|
-
| 后端 | 同上
|
|
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**
|
|
34
|
-
3. 无假设时在 ASSUMPTIONS 中写「无,口径均有依据」。
|
|
33
|
+
2. **Read** 并完整执行 **`.apm/skills/apm-write-assumptions/SKILL.md`**(编写 `*-ASSUMPTIONS.md` → sync;报错则改完重 sync)。
|
|
35
34
|
|
|
36
|
-
### 步骤
|
|
35
|
+
### 步骤 4:同步 PLAN 与回复
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
1. `apm sync-document --file FRONTEND-PLAN.md`(或 BACKEND)。
|
|
38
|
+
2. **有假设且 ASSUMPTIONS 已成功 sync**:@项目经理 请其在 Web **「待您确认」** Panel 点选;说明「以上假设确认前不开始开发」。
|
|
39
|
+
3. **无假设**:`append_message` 发送「本端需求理解」摘要并 @项目经理 请确认。
|
|
39
40
|
|
|
40
|
-
|
|
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`** 同步。
|