@lark-apaas/openclaw-scripts-diagnose-cli 0.1.15-alpha.8 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +520 -119
- 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.15
|
|
55
|
+
return "0.1.15";
|
|
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.15
|
|
10717
|
+
return `v0.1.15`;
|
|
10606
10718
|
}
|
|
10607
10719
|
const COMMANDS = [
|
|
10608
10720
|
{
|
|
@@ -11188,32 +11300,6 @@ function reportDoctorRunToSlardar(opts) {
|
|
|
11188
11300
|
}
|
|
11189
11301
|
});
|
|
11190
11302
|
}
|
|
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
|
-
try {
|
|
11200
|
-
const buf = node_fs.default.readFileSync(filePath);
|
|
11201
|
-
let start = buf.length > maxBytes ? buf.length - maxBytes : 0;
|
|
11202
|
-
while (start < buf.length && buf[start] !== 10) start++;
|
|
11203
|
-
return buf.subarray(start > 0 ? start + 1 : 0).toString("utf-8").split("\n").map((line) => line.replace(LOG_PREFIX_RE, "")).filter((line) => !/^=+$/.test(line.trim())).join("\n").trim();
|
|
11204
|
-
} catch {
|
|
11205
|
-
return "";
|
|
11206
|
-
}
|
|
11207
|
-
}
|
|
11208
|
-
/**
|
|
11209
|
-
* 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
|
|
11210
|
-
*
|
|
11211
|
-
* extraCategories 记录字符串维度:scene、exit_code、rollback_ok、
|
|
11212
|
-
* validation_error、error_msg、log_content(日志文件末尾 4000 字节,含关键结果)。
|
|
11213
|
-
*
|
|
11214
|
-
* extraMetrics 记录各阶段耗时(毫秒);未执行的阶段上报 -1 作为哨兵值,
|
|
11215
|
-
* 便于在 Slardar 查询时区分"未运行"和"运行了 0ms"。
|
|
11216
|
-
*/
|
|
11217
11303
|
/**
|
|
11218
11304
|
* 向 Slardar 上报 upgrade-lark 运行结果(upgrade_lark_run 事件)。
|
|
11219
11305
|
*
|
|
@@ -11224,13 +11310,16 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
|
|
|
11224
11310
|
* ## extraCategories(字符串维度)
|
|
11225
11311
|
* - scene:调用方标识(如 PageUpgradeLark)
|
|
11226
11312
|
* - check_only:是否为 --check 仅诊断模式
|
|
11227
|
-
* -
|
|
11228
|
-
* - skip_reason
|
|
11229
|
-
* -
|
|
11230
|
-
* -
|
|
11231
|
-
* -
|
|
11232
|
-
* -
|
|
11233
|
-
* -
|
|
11313
|
+
* - status:与 UpgradeLarkResult.status 一致(success / skipped / failed)
|
|
11314
|
+
* - skip_reason:跳过原因(对应 UpgradeLarkResult.skipReason)
|
|
11315
|
+
* - upgrade_needed:仅 --check 模式,是否检测到需要升级(对应 UpgradeLarkResult.upgradeNeeded)
|
|
11316
|
+
* - exit_code:npx 子进程退出码(对应 UpgradeLarkResult.exitCode,跳过安装时为空)
|
|
11317
|
+
* - rollback_ok:回滚是否成功(对应 UpgradeLarkResult.rollbackOk,未触发回滚时为空)
|
|
11318
|
+
* - validation_error:安装后校验失败信息(对应 UpgradeLarkResult.validationError)
|
|
11319
|
+
* - error:命令级错误信息(对应 UpgradeLarkResult.error)
|
|
11320
|
+
* - port_check_ok:端口存活检测结果(对应 UpgradeLarkResult.portCheckOk,未执行时为空)
|
|
11321
|
+
* - result:执行结果一行摘要(派生字段,见 buildUpgradeLarkResultSummary)
|
|
11322
|
+
* - log_file:日志文件绝对路径(对应 UpgradeLarkResult.logFile)
|
|
11234
11323
|
*
|
|
11235
11324
|
* ## extraMetrics(数值指标,单位毫秒)
|
|
11236
11325
|
* 未执行的阶段上报 -1 作为哨兵值,便于与"运行了 0ms"区分。
|
|
@@ -11243,23 +11332,25 @@ function readLogFileTail(filePath, maxBytes = 4e3) {
|
|
|
11243
11332
|
* 注:[7/7] 重启耗时写入日志但未单独上报,包含在 durationMs 总耗时中。
|
|
11244
11333
|
*/
|
|
11245
11334
|
function reportUpgradeLarkToSlardar(opts) {
|
|
11246
|
-
console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly}
|
|
11335
|
+
console.error(`[slardar] upgrade_lark_run scene=${opts.scene ?? ""} checkOnly=${opts.checkOnly} status=${opts.resultStatus} exitCode=${opts.exitCode ?? ""} rollbackOk=${opts.rollbackOk ?? ""}`);
|
|
11247
11336
|
const t = opts.timing ?? {};
|
|
11248
|
-
const logContent = readLogFileTail(opts.logFile);
|
|
11249
11337
|
reportTask({
|
|
11250
11338
|
eventName: "upgrade_lark_run",
|
|
11251
11339
|
durationMs: opts.durationMs,
|
|
11252
|
-
status: opts.
|
|
11340
|
+
status: opts.resultStatus === "failed" || opts.upgradeNeeded ? "failed" : "success",
|
|
11253
11341
|
extraCategories: {
|
|
11254
11342
|
scene: opts.scene ?? "",
|
|
11255
11343
|
check_only: String(opts.checkOnly),
|
|
11256
|
-
|
|
11344
|
+
status: opts.resultStatus,
|
|
11257
11345
|
skip_reason: opts.skipReason ?? "",
|
|
11346
|
+
upgrade_needed: String(opts.upgradeNeeded ?? ""),
|
|
11258
11347
|
exit_code: String(opts.exitCode ?? ""),
|
|
11259
|
-
rollback_ok:
|
|
11348
|
+
rollback_ok: String(opts.rollbackOk ?? ""),
|
|
11260
11349
|
validation_error: opts.validationError ?? "",
|
|
11261
|
-
|
|
11262
|
-
|
|
11350
|
+
error: opts.error ?? "",
|
|
11351
|
+
port_check_ok: String(opts.portCheckOk ?? ""),
|
|
11352
|
+
result: opts.resultSummary,
|
|
11353
|
+
log_file: opts.logFile
|
|
11263
11354
|
},
|
|
11264
11355
|
extraMetrics: {
|
|
11265
11356
|
pre_probe_ms: t.preProbeMs ?? -1,
|
|
@@ -11267,10 +11358,28 @@ function reportUpgradeLarkToSlardar(opts) {
|
|
|
11267
11358
|
backup_ms: t.backupMs ?? -1,
|
|
11268
11359
|
npx_install_ms: t.npxInstallMs ?? -1,
|
|
11269
11360
|
post_probe_ms: t.postProbeMs ?? -1,
|
|
11270
|
-
doctor_fix_ms: t.doctorFixMs ?? -1
|
|
11361
|
+
doctor_fix_ms: t.doctorFixMs ?? -1,
|
|
11362
|
+
port_check_ms: t.portCheckMs ?? -1
|
|
11271
11363
|
}
|
|
11272
11364
|
});
|
|
11273
11365
|
}
|
|
11366
|
+
/**
|
|
11367
|
+
* 将 upgrade-lark 运行结果归纳为一行摘要字符串,用于 Slardar result 字段。
|
|
11368
|
+
*
|
|
11369
|
+
* 格式:
|
|
11370
|
+
* "check: upgrade needed" / "check: no upgrade needed"
|
|
11371
|
+
* "skipped: <skipReason>"
|
|
11372
|
+
* "success: upgrade installed"
|
|
11373
|
+
* "failed: <error> (rolled back)" / "failed: <error> (rollback FAILED)"
|
|
11374
|
+
*/
|
|
11375
|
+
function buildUpgradeLarkResultSummary(result, checkOnly) {
|
|
11376
|
+
if (result.status === "skipped") {
|
|
11377
|
+
if (checkOnly) return result.upgradeNeeded ? "check: upgrade needed" : "check: no upgrade needed";
|
|
11378
|
+
return `skipped: ${result.skipReason ?? "pre-check gate"}`;
|
|
11379
|
+
}
|
|
11380
|
+
if (result.status === "success") return "success: upgrade installed";
|
|
11381
|
+
return `failed: ${result.error ?? result.validationError ?? "unknown error"}${result.rollbackOk === false ? " (rollback FAILED)" : result.rollbackOk ? " (rolled back)" : ""}`;
|
|
11382
|
+
}
|
|
11274
11383
|
//#endregion
|
|
11275
11384
|
//#region src/upgrade-lark.ts
|
|
11276
11385
|
/** 升级前需备份的 extensions/ 下的插件目录 */
|
|
@@ -11409,6 +11518,82 @@ function probeChannels(label, log, timeoutMs) {
|
|
|
11409
11518
|
};
|
|
11410
11519
|
}
|
|
11411
11520
|
}
|
|
11521
|
+
/**
|
|
11522
|
+
* 根据 scene、最终状态和端口检测结果,计算场景专属修复状态。
|
|
11523
|
+
* scene 不在已知场景列表时返回 undefined。
|
|
11524
|
+
*/
|
|
11525
|
+
function computeFixStatus(scene, status, portCheckOk = void 0) {
|
|
11526
|
+
if (scene === "FromPreviewFailed") return status === "success" && portCheckOk !== false ? "PORT_FIX_READY" : "PORT_FIX_FAILED";
|
|
11527
|
+
if (scene === "FromChannelFailed") return status === "success" ? "CHANNEL_FIX_READY" : "CHANNEL_FIX_FAILED";
|
|
11528
|
+
}
|
|
11529
|
+
/**
|
|
11530
|
+
* 轮询端口检测脚本,确认 openclaw-gateway 在重启后监听指定端口。
|
|
11531
|
+
*
|
|
11532
|
+
* 逻辑:
|
|
11533
|
+
* 1. 先等待 initialWaitMs(让服务有时间完成重启)
|
|
11534
|
+
* 2. 循环执行 `bash ${BUILTIN_PATH}/tool/port_check.sh <port>`,至多 maxAttempts 次:
|
|
11535
|
+
* - 输出为空 → 服务尚未就绪,等 intervalMs 后重试
|
|
11536
|
+
* - 输出含端口号 → 端口存活,返回 ok=true
|
|
11537
|
+
* - 输出非空但不含端口号 → 端口异常,立即返回 ok=false
|
|
11538
|
+
* 3. 超过 maxAttempts 仍无响应 → 返回 ok=false
|
|
11539
|
+
*
|
|
11540
|
+
* 注意:${BUILTIN_PATH} 是沙箱 shell 环境变量,通过 `bash -c` 让 shell 自行展开,
|
|
11541
|
+
* Node 侧不读取、不拼接该路径。
|
|
11542
|
+
*/
|
|
11543
|
+
function pollPortCheck(opts) {
|
|
11544
|
+
const { port, initialWaitMs, intervalMs, maxAttempts, log } = opts;
|
|
11545
|
+
const portStr = String(port);
|
|
11546
|
+
const cmd = "bash ${BUILTIN_PATH}/tool/port_check.sh " + portStr;
|
|
11547
|
+
log(` waiting ${initialWaitMs / 1e3}s before first port-check poll...`);
|
|
11548
|
+
if (initialWaitMs > 0) sleepSync(initialWaitMs);
|
|
11549
|
+
for (let i = 1; i <= maxAttempts; i++) {
|
|
11550
|
+
const r = (0, node_child_process.spawnSync)("bash", ["-c", cmd], {
|
|
11551
|
+
encoding: "utf-8",
|
|
11552
|
+
stdio: [
|
|
11553
|
+
"ignore",
|
|
11554
|
+
"pipe",
|
|
11555
|
+
"pipe"
|
|
11556
|
+
],
|
|
11557
|
+
timeout: 1e4
|
|
11558
|
+
});
|
|
11559
|
+
const output = (r.stdout ?? "").trim();
|
|
11560
|
+
const errout = (r.stderr ?? "").trim();
|
|
11561
|
+
log(` port-check [${i}/${maxAttempts}]: exit=${r.status ?? "null"} output=${JSON.stringify(output)}${errout ? ` stderr=${JSON.stringify(errout)}` : ""}`);
|
|
11562
|
+
if (!output) {
|
|
11563
|
+
sleepSync(intervalMs);
|
|
11564
|
+
continue;
|
|
11565
|
+
}
|
|
11566
|
+
if (output.includes(portStr)) {
|
|
11567
|
+
log(` port-check: SUCCESS — port ${port} confirmed in output`);
|
|
11568
|
+
return {
|
|
11569
|
+
ok: true,
|
|
11570
|
+
output
|
|
11571
|
+
};
|
|
11572
|
+
}
|
|
11573
|
+
log(` port-check: FAILED — output non-empty but port ${port} not found`);
|
|
11574
|
+
return {
|
|
11575
|
+
ok: false,
|
|
11576
|
+
output
|
|
11577
|
+
};
|
|
11578
|
+
}
|
|
11579
|
+
log(` port-check: FAILED — no response after ${maxAttempts} attempts`);
|
|
11580
|
+
return { ok: false };
|
|
11581
|
+
}
|
|
11582
|
+
/**
|
|
11583
|
+
* 将 fixStatus 信号文件写入 /tmp/event/<fixStatus>,内容为完整的 UpgradeLarkResult JSON。
|
|
11584
|
+
* 外部进程(如服务端诊断流程)通过轮询此文件感知升级结果。
|
|
11585
|
+
* 写入失败只记录日志,不影响主流程返回值。
|
|
11586
|
+
*/
|
|
11587
|
+
function writeFixStatusEvent(fixStatus, result, log) {
|
|
11588
|
+
const filePath = `${FIX_EVENT_DIR}/${fixStatus}`;
|
|
11589
|
+
try {
|
|
11590
|
+
node_fs.default.mkdirSync(FIX_EVENT_DIR, { recursive: true });
|
|
11591
|
+
node_fs.default.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
11592
|
+
log(`[fix-event] written: ${filePath}`);
|
|
11593
|
+
} catch (e) {
|
|
11594
|
+
log(`[fix-event] write failed: ${filePath} — ${e.message}`);
|
|
11595
|
+
}
|
|
11596
|
+
}
|
|
11412
11597
|
function runUpgradeLark(opts) {
|
|
11413
11598
|
const cwd = opts.cwd ?? "/home/gem/workspace/agent";
|
|
11414
11599
|
const configPath = opts.configPath ?? CONFIG_PATH;
|
|
@@ -11428,6 +11613,26 @@ function runUpgradeLark(opts) {
|
|
|
11428
11613
|
log(` configPath : ${configPath}`);
|
|
11429
11614
|
log(`${"=".repeat(60)}`);
|
|
11430
11615
|
const timing = {};
|
|
11616
|
+
for (const s of [
|
|
11617
|
+
"PORT_FIX_READY",
|
|
11618
|
+
"PORT_FIX_FAILED",
|
|
11619
|
+
"CHANNEL_FIX_READY",
|
|
11620
|
+
"CHANNEL_FIX_FAILED"
|
|
11621
|
+
]) {
|
|
11622
|
+
const f = `${FIX_EVENT_DIR}/${s}`;
|
|
11623
|
+
try {
|
|
11624
|
+
if (node_fs.default.existsSync(f)) {
|
|
11625
|
+
node_fs.default.rmSync(f);
|
|
11626
|
+
log(`[fix-event] cleared stale: ${f}`);
|
|
11627
|
+
}
|
|
11628
|
+
} catch (e) {
|
|
11629
|
+
log(`[fix-event] clear failed: ${f} — ${e.message}`);
|
|
11630
|
+
}
|
|
11631
|
+
}
|
|
11632
|
+
const finalReturn = (r) => {
|
|
11633
|
+
if (r.fixStatus !== void 0) writeFixStatusEvent(r.fixStatus, r, log);
|
|
11634
|
+
return r;
|
|
11635
|
+
};
|
|
11431
11636
|
log("");
|
|
11432
11637
|
log("── [Pre-check A] channels probe(升级前)────────────────");
|
|
11433
11638
|
const t_preProbeStart = Date.now();
|
|
@@ -11466,14 +11671,14 @@ function runUpgradeLark(opts) {
|
|
|
11466
11671
|
log(`${"=".repeat(60)}`);
|
|
11467
11672
|
log("upgrade-lark skipped (pre-check gate)");
|
|
11468
11673
|
log(`${"=".repeat(60)}`);
|
|
11469
|
-
return {
|
|
11470
|
-
|
|
11471
|
-
skipped: true,
|
|
11674
|
+
return finalReturn({
|
|
11675
|
+
status: "skipped",
|
|
11472
11676
|
skipReason: reason,
|
|
11473
11677
|
upgradeNeeded: false,
|
|
11474
11678
|
timing,
|
|
11475
|
-
logFile
|
|
11476
|
-
|
|
11679
|
+
logFile,
|
|
11680
|
+
fixStatus: computeFixStatus(opts.scene, "skipped")
|
|
11681
|
+
});
|
|
11477
11682
|
}
|
|
11478
11683
|
if (beforeChannels.anyAccountWorking) {
|
|
11479
11684
|
const reason = "channels are working — upgrade not needed (issue detected but system is functional)";
|
|
@@ -11481,14 +11686,14 @@ function runUpgradeLark(opts) {
|
|
|
11481
11686
|
log(`${"=".repeat(60)}`);
|
|
11482
11687
|
log("upgrade-lark skipped (pre-check gate)");
|
|
11483
11688
|
log(`${"=".repeat(60)}`);
|
|
11484
|
-
return {
|
|
11485
|
-
|
|
11486
|
-
skipped: true,
|
|
11689
|
+
return finalReturn({
|
|
11690
|
+
status: "skipped",
|
|
11487
11691
|
skipReason: reason,
|
|
11488
11692
|
upgradeNeeded: false,
|
|
11489
11693
|
timing,
|
|
11490
|
-
logFile
|
|
11491
|
-
|
|
11694
|
+
logFile,
|
|
11695
|
+
fixStatus: computeFixStatus(opts.scene, "skipped")
|
|
11696
|
+
});
|
|
11492
11697
|
}
|
|
11493
11698
|
log(` PROCEED: requiresLarkUpgrade=true (version=${versionIncompatible}, feishuConfig=${feishuConfigInvalid}) AND channels not working → running upgrade`);
|
|
11494
11699
|
if (opts.checkOnly) {
|
|
@@ -11496,14 +11701,13 @@ function runUpgradeLark(opts) {
|
|
|
11496
11701
|
log(`${"=".repeat(60)}`);
|
|
11497
11702
|
log("upgrade-lark check complete");
|
|
11498
11703
|
log(`${"=".repeat(60)}`);
|
|
11499
|
-
return {
|
|
11500
|
-
|
|
11501
|
-
skipped: true,
|
|
11704
|
+
return finalReturn({
|
|
11705
|
+
status: "skipped",
|
|
11502
11706
|
skipReason: "check",
|
|
11503
11707
|
upgradeNeeded: true,
|
|
11504
11708
|
timing,
|
|
11505
11709
|
logFile
|
|
11506
|
-
};
|
|
11710
|
+
});
|
|
11507
11711
|
}
|
|
11508
11712
|
log("");
|
|
11509
11713
|
log("── [1/6] 文件备份 ────────────────────────────────────────");
|
|
@@ -11513,12 +11717,13 @@ function runUpgradeLark(opts) {
|
|
|
11513
11717
|
timing.backupMs = Date.now() - t_backupStart;
|
|
11514
11718
|
if (!backup.ok) {
|
|
11515
11719
|
log(`ERROR: ${backup.error}`);
|
|
11516
|
-
return {
|
|
11517
|
-
|
|
11720
|
+
return finalReturn({
|
|
11721
|
+
status: "failed",
|
|
11518
11722
|
error: backup.error,
|
|
11519
11723
|
timing,
|
|
11520
|
-
logFile
|
|
11521
|
-
|
|
11724
|
+
logFile,
|
|
11725
|
+
fixStatus: computeFixStatus(opts.scene, "failed")
|
|
11726
|
+
});
|
|
11522
11727
|
}
|
|
11523
11728
|
log("backup: ok");
|
|
11524
11729
|
logVersionSnapshot("before-versions", snapshotVersions(cwd, log), log);
|
|
@@ -11559,15 +11764,15 @@ function runUpgradeLark(opts) {
|
|
|
11559
11764
|
if (statusCheckDelayMs > 0) {
|
|
11560
11765
|
log("");
|
|
11561
11766
|
log(`── 等待 ${statusCheckDelayMs / 1e3}s(让 openclaw 服务完成重启) ─────────────`);
|
|
11562
|
-
|
|
11767
|
+
sleepSync(statusCheckDelayMs);
|
|
11563
11768
|
log("wait done");
|
|
11564
11769
|
}
|
|
11565
11770
|
const doRollback = (reason) => {
|
|
11566
11771
|
log(`ERROR: ${reason}`);
|
|
11567
11772
|
const rollbackOk = restoreFiles(fsOpts);
|
|
11568
11773
|
log(`rollback: ${rollbackOk ? "ok" : "FAILED"}`);
|
|
11569
|
-
return {
|
|
11570
|
-
|
|
11774
|
+
return finalReturn({
|
|
11775
|
+
status: "failed",
|
|
11571
11776
|
error: reason,
|
|
11572
11777
|
validationError: reason,
|
|
11573
11778
|
stdout: npxStdout,
|
|
@@ -11575,8 +11780,9 @@ function runUpgradeLark(opts) {
|
|
|
11575
11780
|
exitCode: npxExitCode,
|
|
11576
11781
|
rollbackOk,
|
|
11577
11782
|
timing,
|
|
11578
|
-
logFile
|
|
11579
|
-
|
|
11783
|
+
logFile,
|
|
11784
|
+
fixStatus: computeFixStatus(opts.scene, "failed")
|
|
11785
|
+
});
|
|
11580
11786
|
};
|
|
11581
11787
|
log("");
|
|
11582
11788
|
log("── [4/5] 安装后诊断校验 ─────────────────────────────────");
|
|
@@ -11602,7 +11808,7 @@ function runUpgradeLark(opts) {
|
|
|
11602
11808
|
timing.postProbeMs = Date.now() - t_postProbeStart;
|
|
11603
11809
|
log(` feishu config invalid after: ${afterChannels.feishuConfigInvalid}`);
|
|
11604
11810
|
const stillNeedsUpgrade = (afterVersionIncompatible || afterChannels.feishuConfigInvalid) && !afterChannels.anyAccountWorking;
|
|
11605
|
-
const isNewDefaultOnly = !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
|
|
11811
|
+
const isNewDefaultOnly = !afterVersionIncompatible && !afterChannels.feishuConfigInvalid && !beforeChannels.anyAccountWorking && isDefaultOnlyState(afterChannels);
|
|
11606
11812
|
log(` post-check: stillNeedsUpgrade=${stillNeedsUpgrade} (version=${afterVersionIncompatible}, feishuConfig=${afterChannels.feishuConfigInvalid}, channelsWorking=${afterChannels.anyAccountWorking}) isNewDefaultOnly=${isNewDefaultOnly}`);
|
|
11607
11813
|
if (stillNeedsUpgrade && !isNewDefaultOnly) return doRollback(`post-install diagnosis still shows anomaly: versionIncompatible=${afterVersionIncompatible}, feishuConfigInvalid=${afterChannels.feishuConfigInvalid}, anyAccountWorking=${afterChannels.anyAccountWorking}`);
|
|
11608
11814
|
if (isNewDefaultOnly) log(" post-install diagnosis: ok (new default account — plugin installed, awaiting configuration)");
|
|
@@ -11628,8 +11834,9 @@ function runUpgradeLark(opts) {
|
|
|
11628
11834
|
if (fixResult.stderr?.trim()) log(`doctor(fix) stderr:\n${fixResult.stderr.trim()}`);
|
|
11629
11835
|
log(`doctor(fix) exit: ${fixResult.status ?? "null"}${fixResult.error ? ` error: ${fixResult.error.message}` : ""}`);
|
|
11630
11836
|
log("");
|
|
11631
|
-
log("── [7/
|
|
11837
|
+
log("── [7/8] 重启 openclaw 服务 ──────────────────────────────");
|
|
11632
11838
|
const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
|
|
11839
|
+
let restartExecuted = false;
|
|
11633
11840
|
if (opts.skipRestart) log(" skipped: --skip-restart");
|
|
11634
11841
|
else if (node_fs.default.existsSync(restartScript)) {
|
|
11635
11842
|
const t_restart = Date.now();
|
|
@@ -11646,24 +11853,133 @@ function runUpgradeLark(opts) {
|
|
|
11646
11853
|
if (restartResult.stdout?.trim()) log(` restart stdout:\n${restartResult.stdout.trim()}`);
|
|
11647
11854
|
if (restartResult.stderr?.trim()) log(` restart stderr:\n${restartResult.stderr.trim()}`);
|
|
11648
11855
|
log(` restart.sh exit: ${restartResult.status ?? "null"} (${restartMs}ms)${restartResult.error ? ` error: ${restartResult.error.message}` : ""}`);
|
|
11856
|
+
restartExecuted = true;
|
|
11649
11857
|
} else log(` skipped: ${restartScript} not found`);
|
|
11650
11858
|
log("");
|
|
11859
|
+
log("── [8/8] 端口存活检测 ────────────────────────────────────");
|
|
11860
|
+
let portCheckOk;
|
|
11861
|
+
if (!restartExecuted) log(" skipped: restart was not executed");
|
|
11862
|
+
else {
|
|
11863
|
+
const t_portCheck = Date.now();
|
|
11864
|
+
const pcResult = pollPortCheck({
|
|
11865
|
+
port: 18789,
|
|
11866
|
+
initialWaitMs: opts.portCheckInitialWaitMs ?? 3e3,
|
|
11867
|
+
intervalMs: opts.portCheckIntervalMs ?? 1e3,
|
|
11868
|
+
maxAttempts: opts.portCheckMaxAttempts ?? 30,
|
|
11869
|
+
log
|
|
11870
|
+
});
|
|
11871
|
+
timing.portCheckMs = Date.now() - t_portCheck;
|
|
11872
|
+
portCheckOk = pcResult.ok;
|
|
11873
|
+
log(` port-check result: ${portCheckOk ? "ok" : "FAILED"} (${timing.portCheckMs}ms)`);
|
|
11874
|
+
}
|
|
11875
|
+
log("");
|
|
11651
11876
|
log(`${"=".repeat(60)}`);
|
|
11652
11877
|
log("upgrade-lark completed successfully");
|
|
11653
11878
|
log(`${"=".repeat(60)}`);
|
|
11654
|
-
return {
|
|
11655
|
-
|
|
11879
|
+
return finalReturn({
|
|
11880
|
+
status: "success",
|
|
11656
11881
|
stdout: npxStdout,
|
|
11657
11882
|
stderr: npxStderr,
|
|
11658
11883
|
exitCode: npxExitCode,
|
|
11884
|
+
portCheckOk,
|
|
11659
11885
|
timing,
|
|
11660
|
-
logFile
|
|
11886
|
+
logFile,
|
|
11887
|
+
fixStatus: computeFixStatus(opts.scene, "success", portCheckOk)
|
|
11888
|
+
});
|
|
11889
|
+
}
|
|
11890
|
+
//#endregion
|
|
11891
|
+
//#region src/install-lock.ts
|
|
11892
|
+
/**
|
|
11893
|
+
* 检查指定 PID 是否仍在运行(向进程发 signal 0,不影响其运行状态)。
|
|
11894
|
+
* 跨平台:Linux/macOS 均支持;Windows 不在目标平台范围内。
|
|
11895
|
+
*/
|
|
11896
|
+
function isPidAlive(pid) {
|
|
11897
|
+
try {
|
|
11898
|
+
process.kill(pid, 0);
|
|
11899
|
+
return true;
|
|
11900
|
+
} catch (e) {
|
|
11901
|
+
if (e.code === "EPERM") return true;
|
|
11902
|
+
return false;
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
/**
|
|
11906
|
+
* 尝试获取安装操作互斥锁。
|
|
11907
|
+
*
|
|
11908
|
+
* 利用 open(O_CREAT | O_EXCL) 的 POSIX 原子性确保同一时刻只有一个安装指令运行。
|
|
11909
|
+
* 覆盖的安装指令:upgrade-lark / install-openclaw / install-extension / install-cli / reset --worker。
|
|
11910
|
+
*
|
|
11911
|
+
* 返回 null 表示成功获取锁;返回错误信息字符串表示锁被其他进程持有。
|
|
11912
|
+
* 与抛异常相比,返回值让调用方可以输出符合各自格式的 result,而不依赖 catch handler 统一处理。
|
|
11913
|
+
*
|
|
11914
|
+
* 成功获取后自动注册 process.once('exit') 释放锁,无需调用方手动清理。
|
|
11915
|
+
*/
|
|
11916
|
+
function acquireInstallLock(command) {
|
|
11917
|
+
node_fs.default.mkdirSync(DIAGNOSE_DIR, { recursive: true });
|
|
11918
|
+
const info = {
|
|
11919
|
+
pid: process.pid,
|
|
11920
|
+
command,
|
|
11921
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11661
11922
|
};
|
|
11923
|
+
const content = JSON.stringify(info);
|
|
11924
|
+
while (true) try {
|
|
11925
|
+
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);
|
|
11926
|
+
node_fs.default.writeSync(fd, content);
|
|
11927
|
+
node_fs.default.closeSync(fd);
|
|
11928
|
+
process.once("exit", releaseInstallLock);
|
|
11929
|
+
console.error(`[install-lock] acquired command=${command} pid=${process.pid}`);
|
|
11930
|
+
return null;
|
|
11931
|
+
} catch (e) {
|
|
11932
|
+
if (e.code !== "EEXIST") throw e;
|
|
11933
|
+
let existing = null;
|
|
11934
|
+
try {
|
|
11935
|
+
existing = JSON.parse(node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8"));
|
|
11936
|
+
} catch {
|
|
11937
|
+
try {
|
|
11938
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11939
|
+
} catch {}
|
|
11940
|
+
continue;
|
|
11941
|
+
}
|
|
11942
|
+
if (!isPidAlive(existing.pid)) {
|
|
11943
|
+
console.error(`[install-lock] stale lock detected (pid=${existing.pid} command=${existing.command}), removing`);
|
|
11944
|
+
try {
|
|
11945
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11946
|
+
} catch {}
|
|
11947
|
+
continue;
|
|
11948
|
+
}
|
|
11949
|
+
const conflictMsg = `另一个安装指令正在运行,请等待其完成后重试。\n 占用指令: ${existing.command}\n 进程 PID : ${existing.pid}\n 开始时间: ${existing.startedAt}\n 锁文件 : ${INSTALL_LOCK_FILE}`;
|
|
11950
|
+
console.error(`[install-lock] conflict: command=${command} blocked by pid=${existing.pid} command=${existing.command}`);
|
|
11951
|
+
return conflictMsg;
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
/**
|
|
11955
|
+
* 释放安装操作互斥锁。
|
|
11956
|
+
* 仅删除由本进程创建的锁文件,避免误删其他进程的锁。
|
|
11957
|
+
* 通常由 process.once('exit') 自动调用,不需要手动调用。
|
|
11958
|
+
*/
|
|
11959
|
+
function releaseInstallLock() {
|
|
11960
|
+
try {
|
|
11961
|
+
const raw = node_fs.default.readFileSync(INSTALL_LOCK_FILE, "utf-8");
|
|
11962
|
+
const info = JSON.parse(raw);
|
|
11963
|
+
if (info.pid === process.pid) {
|
|
11964
|
+
node_fs.default.rmSync(INSTALL_LOCK_FILE, { force: true });
|
|
11965
|
+
console.error(`[install-lock] released command=${info.command} pid=${process.pid}`);
|
|
11966
|
+
}
|
|
11967
|
+
} catch {}
|
|
11662
11968
|
}
|
|
11663
11969
|
//#endregion
|
|
11664
11970
|
//#region src/index.ts
|
|
11971
|
+
/** 锁冲突时各 install 命令的统一处理:输出 { ok: false } 并退出 */
|
|
11972
|
+
function handleInstallLockConflict(lockErr) {
|
|
11973
|
+
console.log(JSON.stringify({
|
|
11974
|
+
ok: false,
|
|
11975
|
+
error: lockErr
|
|
11976
|
+
}));
|
|
11977
|
+
node_process.default.exit(1);
|
|
11978
|
+
}
|
|
11665
11979
|
const args = node_process.default.argv.slice(2);
|
|
11666
11980
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
11981
|
+
const t0 = Date.now();
|
|
11982
|
+
const scene = getFlag(args, "scene");
|
|
11667
11983
|
/**
|
|
11668
11984
|
* Pull the first non-flag positional after the mode name.
|
|
11669
11985
|
* (The mode itself is args[0] in the filtered set, so we skip index 0.)
|
|
@@ -11747,13 +12063,11 @@ async function main() {
|
|
|
11747
12063
|
}
|
|
11748
12064
|
const caller = getFlag(args, "caller");
|
|
11749
12065
|
const traceId = getFlag(args, "trace-id");
|
|
11750
|
-
const scene = getFlag(args, "scene");
|
|
11751
12066
|
const profile = getFlag(args, "profile") === "experimental" ? "experimental" : "standard";
|
|
11752
12067
|
const rc = setRunContext({
|
|
11753
12068
|
caller,
|
|
11754
12069
|
traceId
|
|
11755
12070
|
});
|
|
11756
|
-
const t0 = Date.now();
|
|
11757
12071
|
console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
|
|
11758
12072
|
switch (mode) {
|
|
11759
12073
|
case "check": {
|
|
@@ -11861,6 +12175,17 @@ async function main() {
|
|
|
11861
12175
|
node_process.default.exit(1);
|
|
11862
12176
|
}
|
|
11863
12177
|
const resultFile = resetResultFile(taskId);
|
|
12178
|
+
const resetLockErr = acquireInstallLock("reset");
|
|
12179
|
+
if (resetLockErr) {
|
|
12180
|
+
try {
|
|
12181
|
+
node_fs.default.writeFileSync(resultFile, JSON.stringify({
|
|
12182
|
+
status: "failed",
|
|
12183
|
+
error: resetLockErr
|
|
12184
|
+
}));
|
|
12185
|
+
} catch {}
|
|
12186
|
+
node_process.default.exitCode = 1;
|
|
12187
|
+
return;
|
|
12188
|
+
}
|
|
11864
12189
|
const raw = await fetchCtxViaInnerApi({
|
|
11865
12190
|
populate: planCtxPopulate({ command: "reset" }),
|
|
11866
12191
|
caller,
|
|
@@ -11904,6 +12229,8 @@ async function main() {
|
|
|
11904
12229
|
console.error("Usage: install-openclaw <tag> [--oss_file_map=<base64>]");
|
|
11905
12230
|
node_process.default.exit(1);
|
|
11906
12231
|
}
|
|
12232
|
+
const installOcLockErr = acquireInstallLock("install-openclaw");
|
|
12233
|
+
if (installOcLockErr) handleInstallLockConflict(installOcLockErr);
|
|
11907
12234
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
11908
12235
|
let installOssFileMap;
|
|
11909
12236
|
let rawForTelemetry;
|
|
@@ -11946,6 +12273,8 @@ async function main() {
|
|
|
11946
12273
|
console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--oss_file_map=<base64>]");
|
|
11947
12274
|
node_process.default.exit(1);
|
|
11948
12275
|
}
|
|
12276
|
+
const installExtLockErr = acquireInstallLock("install-extension");
|
|
12277
|
+
if (installExtLockErr) handleInstallLockConflict(installExtLockErr);
|
|
11949
12278
|
const all = args.includes("--all");
|
|
11950
12279
|
const names = getMultiFlag(args, "extension");
|
|
11951
12280
|
const homeBase = getFlag(args, "home_base");
|
|
@@ -12009,6 +12338,8 @@ async function main() {
|
|
|
12009
12338
|
console.error("Usage: install-cli <tag> --cli=<name>... [--home_base=<dir>] [--oss_file_map=<base64>]");
|
|
12010
12339
|
node_process.default.exit(1);
|
|
12011
12340
|
}
|
|
12341
|
+
const installCliLockErr = acquireInstallLock("install-cli");
|
|
12342
|
+
if (installCliLockErr) handleInstallLockConflict(installCliLockErr);
|
|
12012
12343
|
const homeBase = getFlag(args, "home_base");
|
|
12013
12344
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
12014
12345
|
let installOssFileMap;
|
|
@@ -12152,6 +12483,31 @@ async function main() {
|
|
|
12152
12483
|
}
|
|
12153
12484
|
case "upgrade-lark": {
|
|
12154
12485
|
const checkOnly = args.includes("--check");
|
|
12486
|
+
if (!checkOnly) {
|
|
12487
|
+
const upgradeLockErr = acquireInstallLock("upgrade-lark");
|
|
12488
|
+
if (upgradeLockErr) {
|
|
12489
|
+
const fixStatus = computeFixStatus(scene, "failed");
|
|
12490
|
+
const failResult = {
|
|
12491
|
+
status: "failed",
|
|
12492
|
+
error: upgradeLockErr,
|
|
12493
|
+
logFile: "",
|
|
12494
|
+
fixStatus
|
|
12495
|
+
};
|
|
12496
|
+
if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
|
|
12497
|
+
console.log(JSON.stringify(failResult));
|
|
12498
|
+
reportUpgradeLarkToSlardar({
|
|
12499
|
+
scene,
|
|
12500
|
+
checkOnly,
|
|
12501
|
+
durationMs: Date.now() - t0,
|
|
12502
|
+
resultStatus: "failed",
|
|
12503
|
+
error: upgradeLockErr,
|
|
12504
|
+
logFile: "",
|
|
12505
|
+
resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
|
|
12506
|
+
});
|
|
12507
|
+
node_process.default.exitCode = 1;
|
|
12508
|
+
return;
|
|
12509
|
+
}
|
|
12510
|
+
}
|
|
12155
12511
|
const skipRestart = args.includes("--skip-restart");
|
|
12156
12512
|
const result = runUpgradeLark({
|
|
12157
12513
|
runId: rc.runId,
|
|
@@ -12165,14 +12521,16 @@ async function main() {
|
|
|
12165
12521
|
scene,
|
|
12166
12522
|
checkOnly,
|
|
12167
12523
|
durationMs: upgradeDurationMs,
|
|
12168
|
-
|
|
12169
|
-
|
|
12524
|
+
resultStatus: result.status,
|
|
12525
|
+
upgradeNeeded: result.upgradeNeeded,
|
|
12170
12526
|
skipReason: result.skipReason,
|
|
12171
12527
|
logFile: result.logFile,
|
|
12528
|
+
resultSummary: buildUpgradeLarkResultSummary(result, checkOnly),
|
|
12172
12529
|
exitCode: result.exitCode,
|
|
12173
12530
|
rollbackOk: result.rollbackOk,
|
|
12174
12531
|
validationError: result.validationError,
|
|
12175
12532
|
error: result.error,
|
|
12533
|
+
portCheckOk: result.portCheckOk,
|
|
12176
12534
|
timing: result.timing
|
|
12177
12535
|
});
|
|
12178
12536
|
try {
|
|
@@ -12184,21 +12542,23 @@ async function main() {
|
|
|
12184
12542
|
durationMs: upgradeDurationMs,
|
|
12185
12543
|
caller: rc.caller,
|
|
12186
12544
|
traceId: rc.traceId,
|
|
12187
|
-
success: result.
|
|
12545
|
+
success: result.status !== "failed",
|
|
12188
12546
|
result,
|
|
12189
|
-
error: result.
|
|
12547
|
+
error: result.status === "failed" ? { message: result.error ?? "upgrade-lark failed" } : void 0
|
|
12190
12548
|
});
|
|
12191
12549
|
} catch (e) {
|
|
12192
12550
|
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
12193
12551
|
}
|
|
12194
|
-
if (
|
|
12552
|
+
if (result.status === "failed" || checkOnly && result.upgradeNeeded) {
|
|
12195
12553
|
node_process.default.exitCode = 1;
|
|
12196
12554
|
return;
|
|
12197
12555
|
}
|
|
12198
12556
|
break;
|
|
12199
12557
|
}
|
|
12200
12558
|
case "channels-probe": {
|
|
12201
|
-
const
|
|
12559
|
+
const timeoutRaw = getFlag(args, "timeout");
|
|
12560
|
+
const parsed = timeoutRaw != null ? Number(timeoutRaw) : NaN;
|
|
12561
|
+
const result = runChannelsProbe(timeoutRaw != null ? Number.isNaN(parsed) ? 6e4 : Math.max(1e3, parsed) : void 0);
|
|
12202
12562
|
console.log(JSON.stringify(result));
|
|
12203
12563
|
break;
|
|
12204
12564
|
}
|
|
@@ -12213,6 +12573,47 @@ main().catch((err) => {
|
|
|
12213
12573
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12214
12574
|
console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
|
|
12215
12575
|
node_process.default.stderr.write(`Error: ${msg}\n`);
|
|
12576
|
+
if (mode) {
|
|
12577
|
+
const durationMs = Date.now() - t0;
|
|
12578
|
+
if (mode === "upgrade-lark") {
|
|
12579
|
+
const checkOnly = args.includes("--check");
|
|
12580
|
+
const fixStatus = computeFixStatus(scene, "failed");
|
|
12581
|
+
const failResult = {
|
|
12582
|
+
status: "failed",
|
|
12583
|
+
error: msg,
|
|
12584
|
+
logFile: "",
|
|
12585
|
+
fixStatus
|
|
12586
|
+
};
|
|
12587
|
+
if (fixStatus !== void 0) writeFixStatusEvent(fixStatus, failResult, console.error);
|
|
12588
|
+
console.log(JSON.stringify(failResult));
|
|
12589
|
+
reportUpgradeLarkToSlardar({
|
|
12590
|
+
scene,
|
|
12591
|
+
checkOnly,
|
|
12592
|
+
durationMs,
|
|
12593
|
+
resultStatus: "failed",
|
|
12594
|
+
error: msg,
|
|
12595
|
+
logFile: "",
|
|
12596
|
+
resultSummary: buildUpgradeLarkResultSummary(failResult, checkOnly)
|
|
12597
|
+
});
|
|
12598
|
+
} else if ([
|
|
12599
|
+
"doctor",
|
|
12600
|
+
"check",
|
|
12601
|
+
"repair",
|
|
12602
|
+
"install-openclaw",
|
|
12603
|
+
"install-extension",
|
|
12604
|
+
"install-cli",
|
|
12605
|
+
"download-resource",
|
|
12606
|
+
"reset",
|
|
12607
|
+
"lark-cli-init"
|
|
12608
|
+
].includes(mode)) reportDoctorRunToSlardar({
|
|
12609
|
+
command: mode,
|
|
12610
|
+
scene,
|
|
12611
|
+
profile: getFlag(args, "profile") === "experimental" ? "experimental" : "standard",
|
|
12612
|
+
fix: args.includes("--fix"),
|
|
12613
|
+
durationMs,
|
|
12614
|
+
success: false
|
|
12615
|
+
});
|
|
12616
|
+
}
|
|
12216
12617
|
node_process.default.exitCode = 1;
|
|
12217
12618
|
});
|
|
12218
12619
|
//#endregion
|