@lark-apaas/openclaw-scripts-diagnose-cli 0.1.15-alpha.9 → 0.1.16-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +539 -129
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -52,7 +52,7 @@ node_assert = __toESM(node_assert);
|
|
|
52
52
|
* it terse and parseable.
|
|
53
53
|
*/
|
|
54
54
|
function getVersion() {
|
|
55
|
-
return "0.1.
|
|
55
|
+
return "0.1.16-alpha.0";
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/rule-engine/base.ts
|
|
@@ -1770,6 +1770,96 @@ SessionPersistenceRule = __decorate([Rule({
|
|
|
1770
1770
|
level: "silent"
|
|
1771
1771
|
})], SessionPersistenceRule);
|
|
1772
1772
|
//#endregion
|
|
1773
|
+
//#region src/rules/tools-allow-also-allow-conflict.ts
|
|
1774
|
+
/**
|
|
1775
|
+
* 检测 tools 配置中 allow 与 alsoAllow 同时非空的冲突。
|
|
1776
|
+
*
|
|
1777
|
+
* openclaw 的 Zod schema 在配置加载时会拒绝这种组合,原因是语义互斥:
|
|
1778
|
+
* - allow:完全替换式白名单,只允许列出的工具
|
|
1779
|
+
* - alsoAllow:追加式,在 profile 或默认基线上叠加额外工具
|
|
1780
|
+
* 两者同时存在会导致 openclaw 服务启动失败。
|
|
1781
|
+
*
|
|
1782
|
+
* 修复策略:将 alsoAllow 的条目去重合并进 allow,删除 alsoAllow。
|
|
1783
|
+
* 这是最保守的修复:保留原 allow 的精确白名单,不扩大权限范围。
|
|
1784
|
+
*
|
|
1785
|
+
* 检测范围:
|
|
1786
|
+
* - 顶层 tools
|
|
1787
|
+
* - tools.byProvider.*
|
|
1788
|
+
* - agents.list[].tools
|
|
1789
|
+
* - agents.list[].tools.byProvider.*
|
|
1790
|
+
*/
|
|
1791
|
+
let ToolsAllowAlsoAllowConflictRule = class ToolsAllowAlsoAllowConflictRule extends DiagnoseRule {
|
|
1792
|
+
validate(ctx) {
|
|
1793
|
+
const conflicts = [];
|
|
1794
|
+
visitAllScopes(ctx.config, (scope, label) => {
|
|
1795
|
+
if (hasConflict(scope)) conflicts.push(label);
|
|
1796
|
+
});
|
|
1797
|
+
if (conflicts.length === 0) return { pass: true };
|
|
1798
|
+
return {
|
|
1799
|
+
pass: false,
|
|
1800
|
+
message: `tools allow 与 alsoAllow 冲突(${conflicts.length} 处):${conflicts.join(";")}。修复方式:将 alsoAllow 合并进 allow 并删除 alsoAllow`
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
repair(ctx) {
|
|
1804
|
+
visitAllScopes(ctx.config, (scope) => mergeInScope(scope));
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
ToolsAllowAlsoAllowConflictRule = __decorate([Rule({
|
|
1808
|
+
key: "tools_allow_also_allow_conflict",
|
|
1809
|
+
description: "tools 配置中 allow 与 alsoAllow 同时设置会导致 openclaw 启动失败;自动将 alsoAllow 合并进 allow(实验性)",
|
|
1810
|
+
dependsOn: ["config_syntax_check"],
|
|
1811
|
+
repairMode: "standard",
|
|
1812
|
+
level: "critical",
|
|
1813
|
+
profile: "experimental"
|
|
1814
|
+
})], ToolsAllowAlsoAllowConflictRule);
|
|
1815
|
+
/**
|
|
1816
|
+
* 遍历所有 tools-policy scope,对每个 scope 调用 callback。
|
|
1817
|
+
* validate 和 repair 共用同一套遍历逻辑,确保两者覆盖的 scope 始终一致。
|
|
1818
|
+
*/
|
|
1819
|
+
function visitAllScopes(config, cb) {
|
|
1820
|
+
const topTools = asRecord(asRecord(config)?.tools);
|
|
1821
|
+
if (topTools) {
|
|
1822
|
+
cb(topTools, "tools");
|
|
1823
|
+
const byProvider = asRecord(topTools.byProvider);
|
|
1824
|
+
if (byProvider) for (const key of Object.keys(byProvider)) {
|
|
1825
|
+
const scope = asRecord(byProvider[key]);
|
|
1826
|
+
if (scope) cb(scope, `tools.byProvider.${key}`);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const agents = asRecord(asRecord(config)?.agents);
|
|
1830
|
+
const list = Array.isArray(agents?.list) ? agents.list : [];
|
|
1831
|
+
for (let i = 0; i < list.length; i++) {
|
|
1832
|
+
const agent = asRecord(list[i]);
|
|
1833
|
+
const agentId = typeof agent?.id === "string" ? agent.id : String(i);
|
|
1834
|
+
const tools = asRecord(agent?.tools);
|
|
1835
|
+
if (tools) {
|
|
1836
|
+
cb(tools, `agents.list[${agentId}].tools`);
|
|
1837
|
+
const byProvider = asRecord(tools.byProvider);
|
|
1838
|
+
if (byProvider) for (const key of Object.keys(byProvider)) {
|
|
1839
|
+
const scope = asRecord(byProvider[key]);
|
|
1840
|
+
if (scope) cb(scope, `agents.list[${agentId}].tools.byProvider.${key}`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/** scope 中 allow 与 alsoAllow 同时非空 */
|
|
1846
|
+
function hasConflict(scope) {
|
|
1847
|
+
const allow = scope.allow;
|
|
1848
|
+
const alsoAllow = scope.alsoAllow;
|
|
1849
|
+
return Array.isArray(allow) && allow.length > 0 && Array.isArray(alsoAllow) && alsoAllow.length > 0;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* 将 alsoAllow 去重合并进 allow,删除 alsoAllow。
|
|
1853
|
+
* 仅保留字符串元素,过滤掉格式异常的非字符串条目,避免写入损坏配置。
|
|
1854
|
+
*/
|
|
1855
|
+
function mergeInScope(scope) {
|
|
1856
|
+
if (!hasConflict(scope)) return;
|
|
1857
|
+
const allow = scope.allow.filter((v) => typeof v === "string");
|
|
1858
|
+
const alsoAllow = scope.alsoAllow.filter((v) => typeof v === "string");
|
|
1859
|
+
scope.allow = [...new Set([...allow, ...alsoAllow])];
|
|
1860
|
+
delete scope.alsoAllow;
|
|
1861
|
+
}
|
|
1862
|
+
//#endregion
|
|
1773
1863
|
//#region src/rules/feishu-default-account.ts
|
|
1774
1864
|
/**
|
|
1775
1865
|
* Owns the multi-agent feishu-channel migration: turns legacy v1/v2
|
|
@@ -2544,6 +2634,19 @@ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-s
|
|
|
2544
2634
|
/** Absolute path to the openclaw config JSON. */
|
|
2545
2635
|
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
2546
2636
|
/**
|
|
2637
|
+
* upgrade-lark 场景专属修复状态的信号文件目录。
|
|
2638
|
+
* fixStatus 有值时在此目录下创建同名文件(如 /tmp/event/PORT_FIX_READY),
|
|
2639
|
+
* 文件内容为完整的 UpgradeLarkResult JSON,供外部进程轮询感知升级结果。
|
|
2640
|
+
*/
|
|
2641
|
+
const FIX_EVENT_DIR = "/tmp/event";
|
|
2642
|
+
/**
|
|
2643
|
+
* 安装指令互斥锁文件路径。
|
|
2644
|
+
* upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker
|
|
2645
|
+
* 共享此锁,同一时刻只允许一个安装指令运行。
|
|
2646
|
+
* 锁文件内容:{ pid, command, startedAt }。
|
|
2647
|
+
*/
|
|
2648
|
+
const INSTALL_LOCK_FILE = `${DIAGNOSE_DIR}/install.lock`;
|
|
2649
|
+
/**
|
|
2547
2650
|
* upgrade-lark 每次运行的日志文件路径,含时间戳便于按时间排序定位。
|
|
2548
2651
|
* checkOnly=true 时文件名含 "-check" 后缀,便于与正式安装日志区分。
|
|
2549
2652
|
*/
|
|
@@ -3223,6 +3326,13 @@ function execCaptureErr(cmd) {
|
|
|
3223
3326
|
throw new Error(stderrStr ? `${base}\nstderr: ${stderrStr}` : base);
|
|
3224
3327
|
}
|
|
3225
3328
|
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Synchronous sleep using Atomics.wait on a shared buffer.
|
|
3331
|
+
* Works on the main thread (unlike setTimeout which requires an event loop tick).
|
|
3332
|
+
*/
|
|
3333
|
+
function sleepSync(ms) {
|
|
3334
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
3335
|
+
}
|
|
3226
3336
|
/** POSIX single-quote shell escape. Paths with embedded quotes are rare but
|
|
3227
3337
|
* the token-file path conventions in sandboxes don't guarantee cleanliness. */
|
|
3228
3338
|
function shellQuote(s) {
|
|
@@ -4070,9 +4180,10 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
|
|
|
4070
4180
|
};
|
|
4071
4181
|
}
|
|
4072
4182
|
/** Check a single bot entry (either an account object or the feishu channel itself).
|
|
4073
|
-
* appSecret is validated based on its current type:
|
|
4074
4183
|
* - object → must match canonical provider-ref
|
|
4075
4184
|
* - string → must match larkApps plaintext
|
|
4185
|
+
* - undefined/null → missing
|
|
4186
|
+
* - other → unexpected type
|
|
4076
4187
|
*/
|
|
4077
4188
|
checkBot(label, bot, larkApp, issues) {
|
|
4078
4189
|
const creatorOpenID = larkApp.creatorOpenID;
|
|
@@ -4085,7 +4196,8 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
|
|
|
4085
4196
|
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) issues.push(`${label} appSecret is a provider-ref but not the canonical one`);
|
|
4086
4197
|
} else if (typeof secret === "string") {
|
|
4087
4198
|
if (secret !== larkApp.appSecret) issues.push(`${label} appSecret plaintext mismatch`);
|
|
4088
|
-
} else issues.push(`${label} appSecret
|
|
4199
|
+
} else if (secret === void 0 || secret === null) issues.push(`${label} appSecret is missing`);
|
|
4200
|
+
else issues.push(`${label} appSecret has unexpected type ${typeof secret}`);
|
|
4089
4201
|
}
|
|
4090
4202
|
repair(ctx) {
|
|
4091
4203
|
const larkApps = ctx.vars.larkApps;
|
|
@@ -4109,9 +4221,8 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
|
|
|
4109
4221
|
}
|
|
4110
4222
|
}
|
|
4111
4223
|
/** Fix a single bot entry in-place.
|
|
4112
|
-
*
|
|
4113
|
-
* -
|
|
4114
|
-
* - string → fix to larkApps plaintext
|
|
4224
|
+
* - object (provider-ref) → fix to canonical
|
|
4225
|
+
* - otherwise → fix to larkApps plaintext
|
|
4115
4226
|
*/
|
|
4116
4227
|
fixBot(bot, larkApp) {
|
|
4117
4228
|
const creatorOpenID = larkApp.creatorOpenID;
|
|
@@ -4125,9 +4236,7 @@ let FeishuBotChannelConfigRule = class FeishuBotChannelConfigRule extends Diagno
|
|
|
4125
4236
|
const secret = bot.appSecret;
|
|
4126
4237
|
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
4127
4238
|
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
|
|
4128
|
-
} else if (
|
|
4129
|
-
if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
|
|
4130
|
-
}
|
|
4239
|
+
} else if (secret !== larkApp.appSecret) bot.appSecret = larkApp.appSecret;
|
|
4131
4240
|
}
|
|
4132
4241
|
};
|
|
4133
4242
|
FeishuBotChannelConfigRule = __decorate([Rule({
|
|
@@ -7013,45 +7122,56 @@ function fixBotChannelConfig(configPath, larkApps, log) {
|
|
|
7013
7122
|
return;
|
|
7014
7123
|
}
|
|
7015
7124
|
const config = loadJSON5().parse(node_fs.default.readFileSync(configPath, "utf-8"));
|
|
7016
|
-
const
|
|
7017
|
-
if (!
|
|
7018
|
-
log("no feishu
|
|
7125
|
+
const feishu = asRecord(getNestedMap(config, "channels", "feishu"));
|
|
7126
|
+
if (!feishu) {
|
|
7127
|
+
log("no feishu channel in config, skip bot channel config fix");
|
|
7019
7128
|
return;
|
|
7020
7129
|
}
|
|
7021
7130
|
let fixCount = 0;
|
|
7022
|
-
|
|
7131
|
+
const accounts = asRecord(feishu.accounts);
|
|
7132
|
+
if (accounts) for (const [, account] of Object.entries(accounts)) {
|
|
7023
7133
|
const bot = asRecord(account);
|
|
7024
7134
|
if (!bot) continue;
|
|
7025
7135
|
const appId = bot.appId;
|
|
7026
7136
|
if (typeof appId !== "string" || !appId.startsWith("cli_")) continue;
|
|
7027
7137
|
const larkApp = larkApps.find((e) => e.larkAppID === appId);
|
|
7028
7138
|
if (!larkApp) continue;
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
fixCount++;
|
|
7036
|
-
}
|
|
7037
|
-
}
|
|
7038
|
-
const secret = bot.appSecret;
|
|
7039
|
-
let needsFix = false;
|
|
7040
|
-
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
7041
|
-
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) needsFix = true;
|
|
7042
|
-
} else if (typeof secret === "string") {
|
|
7043
|
-
if (secret !== larkApp.appSecret) needsFix = true;
|
|
7044
|
-
} else needsFix = true;
|
|
7045
|
-
if (needsFix) {
|
|
7046
|
-
bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
|
|
7047
|
-
fixCount++;
|
|
7048
|
-
}
|
|
7139
|
+
fixCount += fixBot(bot, larkApp);
|
|
7140
|
+
}
|
|
7141
|
+
const singleAppId = feishu.appId;
|
|
7142
|
+
if (typeof singleAppId === "string" && singleAppId.startsWith("cli_") && !accounts) {
|
|
7143
|
+
const larkApp = larkApps.find((e) => e.larkAppID === singleAppId);
|
|
7144
|
+
if (larkApp) fixCount += fixBot(feishu, larkApp);
|
|
7049
7145
|
}
|
|
7050
7146
|
if (fixCount > 0) {
|
|
7051
7147
|
node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7052
7148
|
log(`fixed ${fixCount} bot channel config issue(s) (allowFrom/appSecret)`);
|
|
7053
7149
|
} else log("bot channel config ok, no fixes needed");
|
|
7054
7150
|
}
|
|
7151
|
+
/** Fix a single bot entry in-place. Returns number of fixes applied. */
|
|
7152
|
+
function fixBot(bot, larkApp) {
|
|
7153
|
+
let fixCount = 0;
|
|
7154
|
+
const creatorOpenID = larkApp.creatorOpenID;
|
|
7155
|
+
if (typeof creatorOpenID === "string" && creatorOpenID !== "") {
|
|
7156
|
+
const allowFrom = Array.isArray(bot.allowFrom) ? [...bot.allowFrom] : [];
|
|
7157
|
+
if (!allowFrom.includes(creatorOpenID)) {
|
|
7158
|
+
allowFrom.push(creatorOpenID);
|
|
7159
|
+
bot.allowFrom = allowFrom;
|
|
7160
|
+
fixCount++;
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
const secret = bot.appSecret;
|
|
7164
|
+
if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
|
|
7165
|
+
if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) {
|
|
7166
|
+
bot.appSecret = { ...DEFAULT_FEISHU_APP_SECRET };
|
|
7167
|
+
fixCount++;
|
|
7168
|
+
}
|
|
7169
|
+
} else if (secret !== larkApp.appSecret) {
|
|
7170
|
+
bot.appSecret = larkApp.appSecret;
|
|
7171
|
+
fixCount++;
|
|
7172
|
+
}
|
|
7173
|
+
return fixCount;
|
|
7174
|
+
}
|
|
7055
7175
|
/**
|
|
7056
7176
|
* Step 7: Verify startup scripts landed in configDir/scripts/.
|
|
7057
7177
|
*
|
|
@@ -7275,14 +7395,6 @@ function getResetTask(taskId) {
|
|
|
7275
7395
|
progress: "等待中..."
|
|
7276
7396
|
};
|
|
7277
7397
|
}
|
|
7278
|
-
/**
|
|
7279
|
-
* Synchronous sleep using Atomics.wait on a shared buffer.
|
|
7280
|
-
*/
|
|
7281
|
-
function sleepSync(ms) {
|
|
7282
|
-
const buf = new SharedArrayBuffer(4);
|
|
7283
|
-
const arr = new Int32Array(buf);
|
|
7284
|
-
Atomics.wait(arr, 0, 0, ms);
|
|
7285
|
-
}
|
|
7286
7398
|
//#endregion
|
|
7287
7399
|
//#region src/oss/resolveOssFileMap.ts
|
|
7288
7400
|
/**
|
|
@@ -10602,7 +10714,7 @@ async function reportCliRun(opts) {
|
|
|
10602
10714
|
//#region src/help.ts
|
|
10603
10715
|
const BIN = "mclaw-diagnose";
|
|
10604
10716
|
function versionBanner() {
|
|
10605
|
-
return `v0.1.
|
|
10717
|
+
return `v0.1.16-alpha.0`;
|
|
10606
10718
|
}
|
|
10607
10719
|
const COMMANDS = [
|
|
10608
10720
|
{
|
|
@@ -11140,12 +11252,28 @@ function planCtxPopulate(opts) {
|
|
|
11140
11252
|
//#endregion
|
|
11141
11253
|
//#region src/slardar/reporter.ts
|
|
11142
11254
|
let slardarInitialized = false;
|
|
11255
|
+
/**
|
|
11256
|
+
* 解析实际运行环境,映射到 Slardar 支持的 env 值。
|
|
11257
|
+
*
|
|
11258
|
+
* 仅处理 FORCE_FRAMEWORK_ENVIRONMENT(studio_server 在沙箱启动时注入):
|
|
11259
|
+
* boe / pre → "dev"(非生产环境,避免污染线上指标)
|
|
11260
|
+
* online → "online"
|
|
11261
|
+
* 未识别到有效值时返回 undefined,交由 openclaw-slardar 内部逻辑决定
|
|
11262
|
+
* (MIAODA_SLARDAR_ENV / 编译期常量 / 默认 online)。
|
|
11263
|
+
*/
|
|
11264
|
+
function resolveSlardarEnv() {
|
|
11265
|
+
const ffe = (process.env.FORCE_FRAMEWORK_ENVIRONMENT ?? "").trim().toLowerCase();
|
|
11266
|
+
if (ffe === "online") return "online";
|
|
11267
|
+
if (ffe === "boe" || ffe === "pre") return "dev";
|
|
11268
|
+
}
|
|
11143
11269
|
function initDiagnoseSlardar() {
|
|
11144
11270
|
if (slardarInitialized) return;
|
|
11145
11271
|
slardarInitialized = true;
|
|
11272
|
+
const env = resolveSlardarEnv();
|
|
11146
11273
|
initSlardar({
|
|
11147
11274
|
bid: "apaas_miaoda",
|
|
11148
|
-
release: getVersion()
|
|
11275
|
+
release: getVersion(),
|
|
11276
|
+
...env ? { env } : {}
|
|
11149
11277
|
});
|
|
11150
11278
|
process.on("beforeExit", flushDiagnoseSlardar);
|
|
11151
11279
|
}
|
|
@@ -11188,42 +11316,6 @@ function reportDoctorRunToSlardar(opts) {
|
|
|
11188
11316
|
}
|
|
11189
11317
|
});
|
|
11190
11318
|
}
|
|
11191
|
-
const LOG_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\](?:\s+\[run=[^\]]+\])?\s*/;
|
|
11192
|
-
/**
|
|
11193
|
-
* 读取日志文件末尾最多 maxBytes 字节,过滤噪音后返回纯内容。
|
|
11194
|
-
* - 剥除每行的时间戳 + run tag 前缀
|
|
11195
|
-
* - 过滤纯分隔符行(全为 '=')
|
|
11196
|
-
* 日志头部为固定 banner,有价值的内容集中在末尾,从尾部截取。
|
|
11197
|
-
*/
|
|
11198
|
-
function readLogFileTail(filePath, maxBytes = 4e3) {
|
|
11199
|
-
let fd = -1;
|
|
11200
|
-
try {
|
|
11201
|
-
fd = node_fs.default.openSync(filePath, "r");
|
|
11202
|
-
const { size } = node_fs.default.fstatSync(fd);
|
|
11203
|
-
const readOffset = size > maxBytes ? size - maxBytes : 0;
|
|
11204
|
-
const readSize = size - readOffset;
|
|
11205
|
-
const buf = Buffer.allocUnsafe(readSize);
|
|
11206
|
-
node_fs.default.readSync(fd, buf, 0, readSize, readOffset);
|
|
11207
|
-
let start = 0;
|
|
11208
|
-
while (start < buf.length && buf[start] !== 10) start++;
|
|
11209
|
-
return (start < buf.length ? buf.subarray(start + 1).toString("utf-8") : buf.toString("utf-8")).split("\n").map((line) => line.replace(LOG_PREFIX_RE, "")).filter((line) => !/^=+$/.test(line.trim())).join("\n").trim();
|
|
11210
|
-
} catch {
|
|
11211
|
-
return "";
|
|
11212
|
-
} finally {
|
|
11213
|
-
if (fd !== -1) try {
|
|
11214
|
-
node_fs.default.closeSync(fd);
|
|
11215
|
-
} catch {}
|
|
11216
|
-
}
|
|
11217
|
-
}
|
|
11218
|
-
/**
|
|
11219
|
-
* 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
|
|
11220
|
-
*
|
|
11221
|
-
* extraCategories 记录字符串维度:scene、exit_code、rollback_ok、
|
|
11222
|
-
* validation_error、error_msg、log_content(日志文件末尾 4000 字节,含关键结果)。
|
|
11223
|
-
*
|
|
11224
|
-
* extraMetrics 记录各阶段耗时(毫秒);未执行的阶段上报 -1 作为哨兵值,
|
|
11225
|
-
* 便于在 Slardar 查询时区分"未运行"和"运行了 0ms"。
|
|
11226
|
-
*/
|
|
11227
11319
|
/**
|
|
11228
11320
|
* 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
|
|
11229
11321
|
*
|
|
@@ -11234,13 +11326,17 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
|
|
|
11234
11326
|
* ## extraCategories(字符串维度)
|
|
11235
11327
|
* - scene:调用方标识(如 PageUpgradeLark)
|
|
11236
11328
|
* - check_only:是否为 --check 仅诊断模式
|
|
11237
|
-
* -
|
|
11238
|
-
* - skip_reason
|
|
11239
|
-
* -
|
|
11240
|
-
* -
|
|
11241
|
-
* -
|
|
11242
|
-
* -
|
|
11243
|
-
* -
|
|
11329
|
+
* - status:与 UpgradeLarkResult.status 一致(success / skipped / failed)
|
|
11330
|
+
* - skip_reason:跳过原因(对应 UpgradeLarkResult.skipReason)
|
|
11331
|
+
* - upgrade_needed:仅 --check 模式,是否检测到需要升级(对应 UpgradeLarkResult.upgradeNeeded)
|
|
11332
|
+
* - exit_code:npx 子进程退出码(对应 UpgradeLarkResult.exitCode,跳过安装时为空)
|
|
11333
|
+
* - rollback_ok:回滚是否成功(对应 UpgradeLarkResult.rollbackOk,未触发回滚时为空)
|
|
11334
|
+
* - validation_error:安装后校验失败信息(对应 UpgradeLarkResult.validationError)
|
|
11335
|
+
* - error:命令级错误信息(对应 UpgradeLarkResult.error)
|
|
11336
|
+
* - port_check_ok:端口存活检测结果(对应 UpgradeLarkResult.portCheckOk,未执行时为空)
|
|
11337
|
+
* - fix_status:场景专属修复状态(对应 UpgradeLarkResult.fixStatus,非场景命令时为空)
|
|
11338
|
+
* - result:执行结果一行摘要(派生字段,见 buildUpgradeLarkResultSummary)
|
|
11339
|
+
* - log_file:日志文件绝对路径(对应 UpgradeLarkResult.logFile)
|
|
11244
11340
|
*
|
|
11245
11341
|
* ## extraMetrics(数值指标,单位毫秒)
|
|
11246
11342
|
* 未执行的阶段上报 -1 作为哨兵值,便于与"运行了 0ms"区分。
|
|
@@ -11253,23 +11349,26 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
|
|
|
11253
11349
|
* 注:[7/7] 重启耗时写入日志但未单独上报,包含在 durationMs 总耗时中。
|
|
11254
11350
|
*/
|
|
11255
11351
|
function reportUpgradeLarkToSlardar(opts) {
|
|
11256
|
-
console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly}
|
|
11352
|
+
console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} status=${opts.resultStatus} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
|
|
11257
11353
|
const t = opts.timing ?? {};
|
|
11258
|
-
const logContent = readLogFileTail(opts.logFile);
|
|
11259
11354
|
reportTask({
|
|
11260
11355
|
eventName: "upgrade_lark_run",
|
|
11261
11356
|
durationMs: opts.durationMs,
|
|
11262
|
-
status: opts.
|
|
11357
|
+
status: opts.resultStatus === "failed" || opts.upgradeNeeded ? "failed" : "success",
|
|
11263
11358
|
extraCategories: {
|
|
11264
11359
|
scene: opts.scene ?? "",
|
|
11265
11360
|
check_only: String(opts.checkOnly),
|
|
11266
|
-
|
|
11361
|
+
status: opts.resultStatus,
|
|
11267
11362
|
skip_reason: opts.skipReason ?? "",
|
|
11363
|
+
upgrade_needed: String(opts.upgradeNeeded ?? ""),
|
|
11268
11364
|
exit_code: String(opts.exitCode ?? ""),
|
|
11269
|
-
rollback_ok:
|
|
11365
|
+
rollback_ok: String(opts.rollbackOk ?? ""),
|
|
11270
11366
|
validation_error: opts.validationError ?? "",
|
|
11271
|
-
|
|
11272
|
-
|
|
11367
|
+
error: opts.error ?? "",
|
|
11368
|
+
port_check_ok: String(opts.portCheckOk ?? ""),
|
|
11369
|
+
fix_status: opts.fixStatus ?? "",
|
|
11370
|
+
result: opts.resultSummary,
|
|
11371
|
+
log_file: opts.logFile
|
|
11273
11372
|
},
|
|
11274
11373
|
extraMetrics: {
|
|
11275
11374
|
pre_probe_ms: t.preProbeMs ?? -1,
|
|
@@ -11277,10 +11376,28 @@ function reportUpgradeLarkToSlardar(opts) {
|
|
|
11277
11376
|
backup_ms: t.backupMs ?? -1,
|
|
11278
11377
|
npx_install_ms: t.npxInstallMs ?? -1,
|
|
11279
11378
|
post_probe_ms: t.postProbeMs ?? -1,
|
|
11280
|
-
doctor_fix_ms: t.doctorFixMs ?? -1
|
|
11379
|
+
doctor_fix_ms: t.doctorFixMs ?? -1,
|
|
11380
|
+
port_check_ms: t.portCheckMs ?? -1
|
|
11281
11381
|
}
|
|
11282
11382
|
});
|
|
11283
11383
|
}
|
|
11384
|
+
/**
|
|
11385
|
+
* 将 upgrade-lark 运行结果归纳为一行摘要字符串,用于 Slardar result 字段。
|
|
11386
|
+
*
|
|
11387
|
+
* 格式:
|
|
11388
|
+
* "check: upgrade needed" / "check: no upgrade needed"
|
|
11389
|
+
* "skipped: <skipReason>"
|
|
11390
|
+
* "success: upgrade installed"
|
|
11391
|
+
* "failed: <error> (rolled back)" / "failed: <error> (rollback FAILED)"
|
|
11392
|
+
*/
|
|
11393
|
+
function buildUpgradeLarkResultSummary(result, checkOnly) {
|
|
11394
|
+
if (result.status === "skipped") {
|
|
11395
|
+
if (checkOnly) return result.upgradeNeeded ? "check: upgrade needed" : "check: no upgrade needed";
|
|
11396
|
+
return `skipped: ${result.skipReason ?? "pre-check gate"}`;
|
|
11397
|
+
}
|
|
11398
|
+
if (result.status === "success") return "success: upgrade installed";
|
|
11399
|
+
return `failed: ${result.error ?? result.validationError ?? "unknown error"}${result.rollbackOk === false ? " (rollback FAILED)" : result.rollbackOk ? " (rolled back)" : ""}`;
|
|
11400
|
+
}
|
|
11284
11401
|
//#endregion
|
|
11285
11402
|
//#region src/upgrade-lark.ts
|
|
11286
11403
|
/** 升级前需备份的 extensions/ 下的插件目录 */
|
|
@@ -11419,6 +11536,82 @@ function probeChannels(label, log, timeoutMs) {
|
|
|
11419
11536
|
};
|
|
11420
11537
|
}
|
|
11421
11538
|
}
|
|
11539
|
+
/**
|
|
11540
|
+
* 根据 scene、最终状态和端口检测结果,计算场景专属修复状态。
|
|
11541
|
+
* scene 不在已知场景列表时返回 undefined。
|
|
11542
|
+
*/
|
|
11543
|
+
function computeFixStatus(scene, status, portCheckOk = void 0) {
|
|
11544
|
+
if (scene === "FromPreviewFailed") return status === "success" && portCheckOk !== false ? "PORT_FIX_READY" : "PORT_FIX_FAILED";
|
|
11545
|
+
if (scene === "FromChannelFailed") return status === "success" ? "CHANNEL_FIX_READY" : "CHANNEL_FIX_FAILED";
|
|
11546
|
+
}
|
|
11547
|
+
/**
|
|
11548
|
+
* 轮询端口检测脚本,确认 openclaw-gateway 在重启后监听指定端口。
|
|
11549
|
+
*
|
|
11550
|
+
* 逻辑:
|
|
11551
|
+
* 1. 先等待 initialWaitMs(让服务有时间完成重启)
|
|
11552
|
+
* 2. 循环执行 `bash ${BUILTIN_PATH}/tool/port_check.sh <port>`,至多 maxAttempts 次:
|
|
11553
|
+
* - 输出为空 → 服务尚未就绪,等 intervalMs 后重试
|
|
11554
|
+
* - 输出含端口号 → 端口存活,返回 ok=true
|
|
11555
|
+
* - 输出非空但不含端口号 → 端口异常,立即返回 ok=false
|
|
11556
|
+
* 3. 超过 maxAttempts 仍无响应 → 返回 ok=false
|
|
11557
|
+
*
|
|
11558
|
+
* 注意:${BUILTIN_PATH} 是沙箱 shell 环境变量,通过 `bash -c` 让 shell 自行展开,
|
|
11559
|
+
* Node 侧不读取、不拼接该路径。
|
|
11560
|
+
*/
|
|
11561
|
+
function pollPortCheck(opts) {
|
|
11562
|
+
const { port, initialWaitMs, intervalMs, maxAttempts, log } = opts;
|
|
11563
|
+
const portStr = String(port);
|
|
11564
|
+
const cmd = "bash ${BUILTIN_PATH}/tool/port_check.sh " + portStr;
|
|
11565
|
+
log(` waiting ${initialWaitMs / 1e3}s before first port-check poll...`);
|
|
11566
|
+
if (initialWaitMs > 0) sleepSync(initialWaitMs);
|
|
11567
|
+
for (let i = 1; i <= maxAttempts; i++) {
|
|
11568
|
+
const r = (0, node_child_process.spawnSync)("bash", ["-c", cmd], {
|
|
11569
|
+
encoding: "utf-8",
|
|
11570
|
+
stdio: [
|
|
11571
|
+
"ignore",
|
|
11572
|
+
"pipe",
|
|
11573
|
+
"pipe"
|
|
11574
|
+
],
|
|
11575
|
+
timeout: 1e4
|
|
11576
|
+
});
|
|
11577
|
+
const output = (r.stdout ?? "").trim();
|
|
11578
|
+
const errout = (r.stderr ?? "").trim();
|
|
11579
|
+
log(` port-check [${i}/${maxAttempts}]: exit=${r.status ?? "null"} output=${JSON.stringify(output)}${errout ? ` stderr=${JSON.stringify(errout)}` : ""}`);
|
|
11580
|
+
if (!output) {
|
|
11581
|
+
sleepSync(intervalMs);
|
|
11582
|
+
continue;
|
|
11583
|
+
}
|
|
11584
|
+
if (output.includes(portStr)) {
|
|
11585
|
+
log(` port-check: SUCCESS — port ${port} confirmed in output`);
|
|
11586
|
+
return {
|
|
11587
|
+
ok: true,
|
|
11588
|
+
output
|
|
11589
|
+
};
|
|
11590
|
+
}
|
|
11591
|
+
log(` port-check: FAILED — output non-empty but port ${port} not found`);
|
|
11592
|
+
return {
|
|
11593
|
+
ok: false,
|
|
11594
|
+
output
|
|
11595
|
+
};
|
|
11596
|
+
}
|
|
11597
|
+
log(` port-check: FAILED — no response after ${maxAttempts} attempts`);
|
|
11598
|
+
return { ok: false };
|
|
11599
|
+
}
|
|
11600
|
+
/**
|
|
11601
|
+
* 将 fixStatus 信号文件写入 /tmp/event/<fixStatus>,内容为完整的 UpgradeLarkResult JSON。
|
|
11602
|
+
* 外部进程(如服务端诊断流程)通过轮询此文件感知升级结果。
|
|
11603
|
+
* 写入失败只记录日志,不影响主流程返回值。
|
|
11604
|
+
*/
|
|
11605
|
+
function writeFixStatusEvent(fixStatus, result, log) {
|
|
11606
|
+
const filePath = `${FIX_EVENT_DIR}/${fixStatus}`;
|
|
11607
|
+
try {
|
|
11608
|
+
node_fs.default.mkdirSync(FIX_EVENT_DIR, { recursive: true });
|
|
11609
|
+
node_fs.default.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
11610
|
+
log(`[fix-event] written: ${filePath}`);
|
|
11611
|
+
} catch (e) {
|
|
11612
|
+
log(`[fix-event] write failed: ${filePath} — ${e.message}`);
|
|
11613
|
+
}
|
|
11614
|
+
}
|
|
11422
11615
|
function runUpgradeLark(opts) {
|
|
11423
11616
|
const cwd = opts.cwd ?? "/home/gem/workspace/agent";
|
|
11424
11617
|
const configPath = opts.configPath ?? CONFIG_PATH;
|
|
@@ -11438,6 +11631,26 @@ function runUpgradeLark(opts) {
|
|
|
11438
11631
|
log(` configPath : ${configPath}`);
|
|
11439
11632
|
log(`${"=".repeat(60)}`);
|
|
11440
11633
|
const timing = {};
|
|
11634
|
+
for (const s of [
|
|
11635
|
+
"PORT_FIX_READY",
|
|
11636
|
+
"PORT_FIX_FAILED",
|
|
11637
|
+
"CHANNEL_FIX_READY",
|
|
11638
|
+
"CHANNEL_FIX_FAILED"
|
|
11639
|
+
]) {
|
|
11640
|
+
const f = `${FIX_EVENT_DIR}/${s}`;
|
|
11641
|
+
try {
|
|
11642
|
+
if (node_fs.default.existsSync(f)) {
|
|
11643
|
+
node_fs.default.rmSync(f);
|
|
11644
|
+
log(`[fix-event] cleared stale: ${f}`);
|
|
11645
|
+
}
|
|
11646
|
+
} catch (e) {
|
|
11647
|
+
log(`[fix-event] clear failed: ${f} — ${e.message}`);
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11650
|
+
const finalReturn = (r) => {
|
|
11651
|
+
if (r.fixStatus !== void 0) writeFixStatusEvent(r.fixStatus, r, log);
|
|
11652
|
+
return r;
|
|
11653
|
+
};
|
|
11441
11654
|
log("");
|
|
11442
11655
|
log("── [Pre-check A] channels probe(升级前)────────────────");
|
|
11443
11656
|
const t_preProbeStart = Date.now();
|
|
@@ -11476,14 +11689,14 @@ function runUpgradeLark(opts) {
|
|
|
11476
11689
|
log(`${"=".repeat(60)}`);
|
|
11477
11690
|
log("upgrade-lark skipped (pre-check gate)");
|
|
11478
11691
|
log(`${"=".repeat(60)}`);
|
|
11479
|
-
return {
|
|
11480
|
-
|
|
11481
|
-
skipped: true,
|
|
11692
|
+
return finalReturn({
|
|
11693
|
+
status: "skipped",
|
|
11482
11694
|
skipReason: reason,
|
|
11483
11695
|
upgradeNeeded: false,
|
|
11484
11696
|
timing,
|
|
11485
|
-
logFile
|
|
11486
|
-
|
|
11697
|
+
logFile,
|
|
11698
|
+
fixStatus: computeFixStatus(opts.scene, "skipped")
|
|
11699
|
+
});
|
|
11487
11700
|
}
|
|
11488
11701
|
if (beforeChannels.anyAccountWorking) {
|
|
11489
11702
|
const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
|
|
@@ -11491,14 +11704,14 @@ function runUpgradeLark(opts) {
|
|
|
11491
11704
|
log(`${"=".repeat(60)}`);
|
|
11492
11705
|
log("upgrade-lark skipped (pre-check gate)");
|
|
11493
11706
|
log(`${"=".repeat(60)}`);
|
|
11494
|
-
return {
|
|
11495
|
-
|
|
11496
|
-
skipped: true,
|
|
11707
|
+
return finalReturn({
|
|
11708
|
+
status: "skipped",
|
|
11497
11709
|
skipReason: reason,
|
|
11498
11710
|
upgradeNeeded: false,
|
|
11499
11711
|
timing,
|
|
11500
|
-
logFile
|
|
11501
|
-
|
|
11712
|
+
logFile,
|
|
11713
|
+
fixStatus: computeFixStatus(opts.scene, "skipped")
|
|
11714
|
+
});
|
|
11502
11715
|
}
|
|
11503
11716
|
log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
|
|
11504
11717
|
if (opts.checkOnly) {
|
|
@@ -11506,14 +11719,13 @@ function runUpgradeLark(opts) {
|
|
|
11506
11719
|
log(`${"=".repeat(60)}`);
|
|
11507
11720
|
log("upgrade-lark check complete");
|
|
11508
11721
|
log(`${"=".repeat(60)}`);
|
|
11509
|
-
return {
|
|
11510
|
-
|
|
11511
|
-
skipped: true,
|
|
11722
|
+
return finalReturn({
|
|
11723
|
+
status: "skipped",
|
|
11512
11724
|
skipReason: "check",
|
|
11513
11725
|
upgradeNeeded: true,
|
|
11514
11726
|
timing,
|
|
11515
11727
|
logFile
|
|
11516
|
-
};
|
|
11728
|
+
});
|
|
11517
11729
|
}
|
|
11518
11730
|
log("");
|
|
11519
11731
|
log("── [1/6] 文件备份 ────────────────────────────────────────");
|
|
@@ -11523,12 +11735,13 @@ function runUpgradeLark(opts) {
|
|
|
11523
11735
|
timing.backupMs = Date.now() - t_backupStart;
|
|
11524
11736
|
if (!backup.ok) {
|
|
11525
11737
|
log(`ERROR: ${backup.error}`);
|
|
11526
|
-
return {
|
|
11527
|
-
|
|
11738
|
+
return finalReturn({
|
|
11739
|
+
status: "failed",
|
|
11528
11740
|
error: backup.error,
|
|
11529
11741
|
timing,
|
|
11530
|
-
logFile
|
|
11531
|
-
|
|
11742
|
+
logFile,
|
|
11743
|
+
fixStatus: computeFixStatus(opts.scene, "failed")
|
|
11744
|
+
});
|
|
11532
11745
|
}
|
|
11533
11746
|
log("backup: ok");
|
|
11534
11747
|
logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
|
|
@@ -11569,15 +11782,15 @@ function runUpgradeLark(opts) {
|
|
|
11569
11782
|
if (statusCheckDelayMs > 0) {
|
|
11570
11783
|
log("");
|
|
11571
11784
|
log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
|
|
11572
|
-
|
|
11785
|
+
sleepSync(statusCheckDelayMs);
|
|
11573
11786
|
log("wait done");
|
|
11574
11787
|
}
|
|
11575
11788
|
const doRollback = (reason) => {
|
|
11576
11789
|
log(`ERROR: ${reason}`);
|
|
11577
11790
|
const rollbackOk = restoreFiles(fsOpts);
|
|
11578
11791
|
log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
|
|
11579
|
-
return {
|
|
11580
|
-
|
|
11792
|
+
return finalReturn({
|
|
11793
|
+
status: "failed",
|
|
11581
11794
|
error: reason,
|
|
11582
11795
|
validationError: reason,
|
|
11583
11796
|
stdout: npxStdout,
|
|
@@ -11585,8 +11798,9 @@ function runUpgradeLark(opts) {
|
|
|
11585
11798
|
exitCode: npxExitCode,
|
|
11586
11799
|
rollbackOk,
|
|
11587
11800
|
timing,
|
|
11588
|
-
logFile
|
|
11589
|
-
|
|
11801
|
+
logFile,
|
|
11802
|
+
fixStatus: computeFixStatus(opts.scene, "failed")
|
|
11803
|
+
});
|
|
11590
11804
|
};
|
|
11591
11805
|
log("");
|
|
11592
11806
|
log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
|
|
@@ -11612,7 +11826,7 @@ function runUpgradeLark(opts) {
|
|
|
11612
11826
|
timing.postProbeMs = Date.now() - t_postProbeStart;
|
|
11613
11827
|
log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
|
|
11614
11828
|
const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
|
|
11615
|
-
const isNewDefaultOnly = !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels)
|
|
11829
|
+
const isNewDefaultOnly = !afterVersionIncompatible && !afterChannels.feishuConfigInvalid && !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
|
|
11616
11830
|
log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
|
|
11617
11831
|
if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
|
|
11618
11832
|
if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
|
|
@@ -11638,8 +11852,9 @@ function runUpgradeLark(opts) {
|
|
|
11638
11852
|
if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
|
|
11639
11853
|
log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
|
|
11640
11854
|
log("");
|
|
11641
|
-
log("── [7/
|
|
11855
|
+
log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
|
|
11642
11856
|
const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
|
|
11857
|
+
let restartExecuted = false;
|
|
11643
11858
|
if (opts.skipRestart) log(" skipped: --skip-restart");
|
|
11644
11859
|
else if (node_fs.default.existsSync(restartScript)) {
|
|
11645
11860
|
const t_restart = Date.now();
|
|
@@ -11656,24 +11871,133 @@ function runUpgradeLark(opts) {
|
|
|
11656
11871
|
if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
|
|
11657
11872
|
if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
|
|
11658
11873
|
log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
|
|
11874
|
+
restartExecuted = true;
|
|
11659
11875
|
} else log(` skipped: ${restartScript} not found`);
|
|
11660
11876
|
log("");
|
|
11877
|
+
log("── [8/8] 端口存活检测 ────────────────────────────────────");
|
|
11878
|
+
let portCheckOk;
|
|
11879
|
+
if (!restartExecuted) log(" skipped: restart was not executed");
|
|
11880
|
+
else {
|
|
11881
|
+
const t_portCheck = Date.now();
|
|
11882
|
+
const pcResult = pollPortCheck({
|
|
11883
|
+
port: 18789,
|
|
11884
|
+
initialWaitMs: opts.portCheckInitialWaitMs ?? 3e3,
|
|
11885
|
+
intervalMs: opts.portCheckIntervalMs ?? 1e3,
|
|
11886
|
+
maxAttempts: opts.portCheckMaxAttempts ?? 30,
|
|
11887
|
+
log
|
|
11888
|
+
});
|
|
11889
|
+
timing.portCheckMs = Date.now() - t_portCheck;
|
|
11890
|
+
portCheckOk = pcResult.ok;
|
|
11891
|
+
log(` port-check result: ${portCheckOk ? "ok" : "FAILED"} (${timing.portCheckMs}ms)`);
|
|
11892
|
+
}
|
|
11893
|
+
log("");
|
|
11661
11894
|
log(`${"=".repeat(60)}`);
|
|
11662
11895
|
log("upgrade-lark completed successfully");
|
|
11663
11896
|
log(`${"=".repeat(60)}`);
|
|
11664
|
-
return {
|
|
11665
|
-
|
|
11897
|
+
return finalReturn({
|
|
11898
|
+
status: "success",
|
|
11666
11899
|
stdout: npxStdout,
|
|
11667
11900
|
stderr: npxStderr,
|
|
11668
11901
|
exitCode: npxExitCode,
|
|
11902
|
+
portCheckOk,
|
|
11669
11903
|
timing,
|
|
11670
|
-
logFile
|
|
11904
|
+
logFile,
|
|
11905
|
+
fixStatus: computeFixStatus(opts.scene, "success", portCheckOk)
|
|
11906
|
+
});
|
|
11907
|
+
}
|
|
11908
|
+
//#endregion
|
|
11909
|
+
//#region src/install-lock.ts
|
|
11910
|
+
/**
|
|
11911
|
+
* 检查指定 PID 是否仍在运行(向进程发 signal 0,不影响其运行状态)。
|
|
11912
|
+
* 跨平台:Linux/macOS 均支持;Windows 不在目标平台范围内。
|
|
11913
|
+
*/
|
|
11914
|
+
function isPidAlive(pid) {
|
|
11915
|
+
try {
|
|
11916
|
+
process.kill(pid, 0);
|
|
11917
|
+
return true;
|
|
11918
|
+
} catch (e) {
|
|
11919
|
+
if (e.code === "EPERM") return true;
|
|
11920
|
+
return false;
|
|
11921
|
+
}
|
|
11922
|
+
}
|
|
11923
|
+
/**
|
|
11924
|
+
* 尝试获取安装操作互斥锁。
|
|
11925
|
+
*
|
|
11926
|
+
* 利用 open(O_CREAT | O_EXCL) 的 POSIX 原子性确保同一时刻只有一个安装指令运行。
|
|
11927
|
+
* 覆盖的安装指令:upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker。
|
|
11928
|
+
*
|
|
11929
|
+
* 返回 null 表示成功获取锁;返回错误信息字符串表示锁被其他进程持有。
|
|
11930
|
+
* 与抛异常相比,返回值让调用方可以输出符合各自格式的 result,而不依赖 catch handler 统一处理。
|
|
11931
|
+
*
|
|
11932
|
+
* 成功获取后自动注册 process.once('exit') 释放锁,无需调用方手动清理。
|
|
11933
|
+
*/
|
|
11934
|
+
function acquireInstallLock(command) {
|
|
11935
|
+
node_fs.default.mkdirSync(DIAGNOSE_DIR, { recursive: true });
|
|
11936
|
+
const info = {
|
|
11937
|
+
pid: process.pid,
|
|
11938
|
+
command,
|
|
11939
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11671
11940
|
};
|
|
11941
|
+
const content = JSON.stringify(info);
|
|
11942
|
+
while (true) try {
|
|
11943
|
+
const fd = node_fs.default.openSync(INSTALL_LOCK_FILE, node_fs.default.constants.O_CREAT | node_fs.default.constants.O_EXCL | node_fs.default.constants.O_WRONLY, 420);
|
|
11944
|
+
node_fs.default.writeSync(fd, content);
|
|
11945
|
+
node_fs.default.closeSync(fd);
|
|
11946
|
+
process.once("exit", releaseInstallLock);
|
|
11947
|
+
console.error(`[install-lock] acquired command=${command} pid=${process.pid}`);
|
|
11948
|
+
return null;
|
|
11949
|
+
} catch (e) {
|
|
11950
|
+
if (e.code !== "EEXIST") throw e;
|
|
11951
|
+
let existing = null;
|
|
11952
|
+
try {
|
|
11953
|
+
existing = JSON.parse(node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8"));
|
|
11954
|
+
} catch {
|
|
11955
|
+
try {
|
|
11956
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11957
|
+
} catch {}
|
|
11958
|
+
continue;
|
|
11959
|
+
}
|
|
11960
|
+
if (!isPidAlive(existing.pid)) {
|
|
11961
|
+
console.error(`[install-lock] stale lock detected (pid=${existing.pid} command=${existing.command}), removing`);
|
|
11962
|
+
try {
|
|
11963
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11964
|
+
} catch {}
|
|
11965
|
+
continue;
|
|
11966
|
+
}
|
|
11967
|
+
const conflictMsg = `另一个安装指令正在运行,请等待其完成后重试。\n 占用指令: ${existing.command}\n 进程 PID : ${existing.pid}\n 开始时间: ${existing.startedAt}\n 锁文件 : ${INSTALL_LOCK_FILE}`;
|
|
11968
|
+
console.error(`[install-lock] conflict: command=${command} blocked by pid=${existing.pid} command=${existing.command}`);
|
|
11969
|
+
return conflictMsg;
|
|
11970
|
+
}
|
|
11971
|
+
}
|
|
11972
|
+
/**
|
|
11973
|
+
* 释放安装操作互斥锁。
|
|
11974
|
+
* 仅删除由本进程创建的锁文件,避免误删其他进程的锁。
|
|
11975
|
+
* 通常由 process.once('exit') 自动调用,不需要手动调用。
|
|
11976
|
+
*/
|
|
11977
|
+
function releaseInstallLock() {
|
|
11978
|
+
try {
|
|
11979
|
+
const raw = node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8");
|
|
11980
|
+
const info = JSON.parse(raw);
|
|
11981
|
+
if (info.pid === process.pid) {
|
|
11982
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11983
|
+
console.error(`[install-lock] released command=${info.command} pid=${process.pid}`);
|
|
11984
|
+
}
|
|
11985
|
+
} catch {}
|
|
11672
11986
|
}
|
|
11673
11987
|
//#endregion
|
|
11674
11988
|
//#region src/index.ts
|
|
11989
|
+
/** 锁冲突时各 install 命令的统一处理:输出 { ok: false } 并退出 */
|
|
11990
|
+
function handleInstallLockConflict(lockErr) {
|
|
11991
|
+
console.log(JSON.stringify({
|
|
11992
|
+
ok: false,
|
|
11993
|
+
error: lockErr
|
|
11994
|
+
}));
|
|
11995
|
+
node_process.default.exit(1);
|
|
11996
|
+
}
|
|
11675
11997
|
const args = node_process.default.argv.slice(2);
|
|
11676
11998
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
11999
|
+
const t0 = Date.now();
|
|
12000
|
+
const scene = getFlag(args, "scene");
|
|
11677
12001
|
/**
|
|
11678
12002
|
* Pull the first non-flag positional after the mode name.
|
|
11679
12003
|
* (The mode itself is args[0] in the filtered set, so we skip index 0.)
|
|
@@ -11757,13 +12081,11 @@ async function main() {
|
|
|
11757
12081
|
}
|
|
11758
12082
|
const caller = getFlag(args, "caller");
|
|
11759
12083
|
const traceId = getFlag(args, "trace-id");
|
|
11760
|
-
const scene = getFlag(args, "scene");
|
|
11761
12084
|
const profile = getFlag(args, "profile") === "experimental" ? "experimental" : "standard";
|
|
11762
12085
|
const rc = setRunContext({
|
|
11763
12086
|
caller,
|
|
11764
12087
|
traceId
|
|
11765
12088
|
});
|
|
11766
|
-
const t0 = Date.now();
|
|
11767
12089
|
console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
|
|
11768
12090
|
switch (mode) {
|
|
11769
12091
|
case "check": {
|
|
@@ -11871,6 +12193,17 @@ async function main() {
|
|
|
11871
12193
|
node_process.default.exit(1);
|
|
11872
12194
|
}
|
|
11873
12195
|
const resultFile = resetResultFile(taskId);
|
|
12196
|
+
const resetLockErr = acquireInstallLock("reset");
|
|
12197
|
+
if (resetLockErr) {
|
|
12198
|
+
try {
|
|
12199
|
+
node_fs.default.writeFileSync(resultFile, JSON.stringify({
|
|
12200
|
+
status: "failed",
|
|
12201
|
+
error: resetLockErr
|
|
12202
|
+
}));
|
|
12203
|
+
} catch {}
|
|
12204
|
+
node_process.default.exitCode = 1;
|
|
12205
|
+
return;
|
|
12206
|
+
}
|
|
11874
12207
|
const raw = await fetchCtxViaInnerApi({
|
|
11875
12208
|
populate: planCtxPopulate({ command: "reset" }),
|
|
11876
12209
|
caller,
|
|
@@ -11914,6 +12247,8 @@ async function main() {
|
|
|
11914
12247
|
console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
|
|
11915
12248
|
node_process.default.exit(1);
|
|
11916
12249
|
}
|
|
12250
|
+
const installOcLockErr = acquireInstallLock("install-openclaw");
|
|
12251
|
+
if (installOcLockErr) handleInstallLockConflict(installOcLockErr);
|
|
11917
12252
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
11918
12253
|
let installOssFileMap;
|
|
11919
12254
|
let rawForTelemetry;
|
|
@@ -11956,6 +12291,8 @@ async function main() {
|
|
|
11956
12291
|
console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
|
|
11957
12292
|
node_process.default.exit(1);
|
|
11958
12293
|
}
|
|
12294
|
+
const installExtLockErr = acquireInstallLock("install-extension");
|
|
12295
|
+
if (installExtLockErr) handleInstallLockConflict(installExtLockErr);
|
|
11959
12296
|
const all = args.includes("--all");
|
|
11960
12297
|
const names = getMultiFlag(args, "extension");
|
|
11961
12298
|
const homeBase = getFlag(args, "home_base");
|
|
@@ -12019,6 +12356,8 @@ async function main() {
|
|
|
12019
12356
|
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
|
|
12020
12357
|
node_process.default.exit(1);
|
|
12021
12358
|
}
|
|
12359
|
+
const installCliLockErr = acquireInstallLock("install-cli");
|
|
12360
|
+
if (installCliLockErr) handleInstallLockConflict(installCliLockErr);
|
|
12022
12361
|
const homeBase = getFlag(args, "home_base");
|
|
12023
12362
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
12024
12363
|
let installOssFileMap;
|
|
@@ -12162,6 +12501,32 @@ async function main() {
|
|
|
12162
12501
|
}
|
|
12163
12502
|
case "upgrade-lark": {
|
|
12164
12503
|
const checkOnly = args.includes("--check");
|
|
12504
|
+
if (!checkOnly) {
|
|
12505
|
+
const upgradeLockErr = acquireInstallLock("upgrade-lark");
|
|
12506
|
+
if (upgradeLockErr) {
|
|
12507
|
+
const fixStatus = computeFixStatus(scene, "failed");
|
|
12508
|
+
const failResult = {
|
|
12509
|
+
status: "failed",
|
|
12510
|
+
error: upgradeLockErr,
|
|
12511
|
+
logFile: "",
|
|
12512
|
+
fixStatus
|
|
12513
|
+
};
|
|
12514
|
+
if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
|
|
12515
|
+
console.log(JSON.stringify(failResult));
|
|
12516
|
+
reportUpgradeLarkToSlardar({
|
|
12517
|
+
scene,
|
|
12518
|
+
checkOnly,
|
|
12519
|
+
durationMs: Date.now() - t0,
|
|
12520
|
+
resultStatus: "failed",
|
|
12521
|
+
error: upgradeLockErr,
|
|
12522
|
+
logFile: "",
|
|
12523
|
+
fixStatus,
|
|
12524
|
+
resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
|
|
12525
|
+
});
|
|
12526
|
+
node_process.default.exitCode = 1;
|
|
12527
|
+
return;
|
|
12528
|
+
}
|
|
12529
|
+
}
|
|
12165
12530
|
const skipRestart = args.includes("--skip-restart");
|
|
12166
12531
|
const result = runUpgradeLark({
|
|
12167
12532
|
runId: rc.runId,
|
|
@@ -12175,14 +12540,17 @@ async function main() {
|
|
|
12175
12540
|
scene,
|
|
12176
12541
|
checkOnly,
|
|
12177
12542
|
durationMs: upgradeDurationMs,
|
|
12178
|
-
|
|
12179
|
-
|
|
12543
|
+
resultStatus: result.status,
|
|
12544
|
+
upgradeNeeded: result.upgradeNeeded,
|
|
12180
12545
|
skipReason: result.skipReason,
|
|
12181
12546
|
logFile: result.logFile,
|
|
12547
|
+
resultSummary: buildUpgradeLarkResultSummary(result, checkOnly),
|
|
12182
12548
|
exitCode: result.exitCode,
|
|
12183
12549
|
rollbackOk: result.rollbackOk,
|
|
12184
12550
|
validationError: result.validationError,
|
|
12185
12551
|
error: result.error,
|
|
12552
|
+
portCheckOk: result.portCheckOk,
|
|
12553
|
+
fixStatus: result.fixStatus,
|
|
12186
12554
|
timing: result.timing
|
|
12187
12555
|
});
|
|
12188
12556
|
try {
|
|
@@ -12194,14 +12562,14 @@ async function main() {
|
|
|
12194
12562
|
durationMs: upgradeDurationMs,
|
|
12195
12563
|
caller: rc.caller,
|
|
12196
12564
|
traceId: rc.traceId,
|
|
12197
|
-
success: result.
|
|
12565
|
+
success: result.status !== "failed",
|
|
12198
12566
|
result,
|
|
12199
|
-
error: result.
|
|
12567
|
+
error: result.status === "failed" ? { message: result.error ?? "upgrade-lark failed" } : void 0
|
|
12200
12568
|
});
|
|
12201
12569
|
} catch (e) {
|
|
12202
12570
|
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
12203
12571
|
}
|
|
12204
|
-
if (
|
|
12572
|
+
if (result.status === "failed" || checkOnly && result.upgradeNeeded) {
|
|
12205
12573
|
node_process.default.exitCode = 1;
|
|
12206
12574
|
return;
|
|
12207
12575
|
}
|
|
@@ -12225,6 +12593,48 @@ main().catch((err) => {
|
|
|
12225
12593
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12226
12594
|
console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
|
|
12227
12595
|
node_process.default.stderr.write(`Error: ${msg}\n`);
|
|
12596
|
+
if (mode) {
|
|
12597
|
+
const durationMs = Date.now() - t0;
|
|
12598
|
+
if (mode === "upgrade-lark") {
|
|
12599
|
+
const checkOnly = args.includes("--check");
|
|
12600
|
+
const fixStatus = computeFixStatus(scene, "failed");
|
|
12601
|
+
const failResult = {
|
|
12602
|
+
status: "failed",
|
|
12603
|
+
error: msg,
|
|
12604
|
+
logFile: "",
|
|
12605
|
+
fixStatus
|
|
12606
|
+
};
|
|
12607
|
+
if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
|
|
12608
|
+
console.log(JSON.stringify(failResult));
|
|
12609
|
+
reportUpgradeLarkToSlardar({
|
|
12610
|
+
scene,
|
|
12611
|
+
checkOnly,
|
|
12612
|
+
durationMs,
|
|
12613
|
+
resultStatus: "failed",
|
|
12614
|
+
error: msg,
|
|
12615
|
+
logFile: "",
|
|
12616
|
+
fixStatus,
|
|
12617
|
+
resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
|
|
12618
|
+
});
|
|
12619
|
+
} else if ([
|
|
12620
|
+
"doctor",
|
|
12621
|
+
"check",
|
|
12622
|
+
"repair",
|
|
12623
|
+
"install-openclaw",
|
|
12624
|
+
"install-extension",
|
|
12625
|
+
"install-cli",
|
|
12626
|
+
"download-resource",
|
|
12627
|
+
"reset",
|
|
12628
|
+
"lark-cli-init"
|
|
12629
|
+
].includes(mode)) reportDoctorRunToSlardar({
|
|
12630
|
+
command: mode,
|
|
12631
|
+
scene,
|
|
12632
|
+
profile: getFlag(args, "profile") === "experimental" ? "experimental" : "standard",
|
|
12633
|
+
fix: args.includes("--fix"),
|
|
12634
|
+
durationMs,
|
|
12635
|
+
success: false
|
|
12636
|
+
});
|
|
12637
|
+
}
|
|
12228
12638
|
node_process.default.exitCode = 1;
|
|
12229
12639
|
});
|
|
12230
12640
|
//#endregion
|
package/package.json
CHANGED